From ff67161f946c515a3b0a719ce80531fa14a06a8f Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Sat, 17 Jan 2015 15:13:01 +0100 Subject: Find instructions that would cause an IllegalAccessError when inlined Some instructions would cause an IllegalAccessError if they are inlined into a different class. Based on Miguel's implementation in 6efc0528c6. --- .../scala/tools/nsc/backend/jvm/BTypes.scala | 43 +++++++- .../tools/nsc/backend/jvm/BTypesFromSymbols.scala | 4 +- .../nsc/backend/jvm/opt/ByteCodeRepository.scala | 16 +-- .../scala/tools/nsc/backend/jvm/opt/Inliner.scala | 118 +++++++++++++++++++++ 4 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala (limited to 'src/compiler') diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala index ec26ef9b48..ff30631c10 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala @@ -6,10 +6,11 @@ package scala.tools.nsc package backend.jvm +import scala.annotation.switch import scala.tools.asm import asm.Opcodes import scala.tools.asm.tree.{InnerClassNode, ClassNode} -import opt.ByteCodeRepository +import opt.{ByteCodeRepository, Inliner} import scala.collection.convert.decorateAsScala._ /** @@ -34,6 +35,8 @@ abstract class BTypes { */ val byteCodeRepository: ByteCodeRepository + val inliner: Inliner[this.type] + // 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 @@ -50,6 +53,31 @@ abstract class BTypes { */ val classBTypeFromInternalName: collection.concurrent.Map[InternalName, ClassBType] = recordPerRunCache(collection.concurrent.TrieMap.empty[InternalName, ClassBType]) + /** + * 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]]. */ @@ -725,6 +753,19 @@ abstract class BTypes { 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 isPublic = (info.flags & asm.Opcodes.ACC_PUBLIC) != 0 + def isNestedClass = info.nestedInfo.isDefined def enclosingNestedClassesChain: List[ClassBType] = diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala index 2af665d31c..117b377622 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala @@ -7,9 +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 scala.tools.nsc.backend.jvm.opt.{Inliner, ByteCodeRepository} import BTypes.InternalName /** @@ -38,6 +38,8 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { val byteCodeRepository = new ByteCodeRepository(global.classPath, recordPerRunCache(collection.concurrent.TrieMap.empty[InternalName, (ClassNode, Source)])) + val inliner: Inliner[this.type] = new Inliner(this) + final def initializeCoreBTypes(): Unit = { coreBTypes.setBTypes(new CoreBTypes[this.type](this)) } 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..9e56f25888 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala @@ -59,12 +59,16 @@ class ByteCodeRepository(val classPath: ClassFileLookup[AbstractFile], val class * * @return The [[MethodNode]] of the requested method and the [[InternalName]] of its declaring class. */ - 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): Option[(MethodNode, InternalName)] = { + // In a MethodInsnNode, the `owner` field may be an array descriptor, for exmple when invoking `clone`. + if (ownerInternalNameOrArrayDescriptor.charAt(0) == '[') None + else { + val c = classNode(ownerInternalNameOrArrayDescriptor) + c.methods.asScala.find(m => m.name == name && m.desc == descriptor).map((_, ownerInternalNameOrArrayDescriptor)) orElse { + val parents = Option(c.superName) ++ c.interfaces.asScala + // `view` to stop at the first result + parents.view.flatMap(methodNode(_, name, descriptor)).headOption + } } } 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..6e5e03f730 --- /dev/null +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala @@ -0,0 +1,118 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2014 LAMP/EPFL + * @author Martin Odersky + */ + +package scala.tools.nsc +package backend.jvm +package opt + +import scala.tools.asm +import asm.Opcodes +import asm.tree._ +import scala.collection.convert.decorateAsScala._ +import OptimizerReporting._ + +class Inliner[BT <: BTypes](val btypes: BT) { + import btypes._ + import btypes.byteCodeRepository + + def findIllegalAccess(instructions: InsnList, destinationClass: ClassBType): Option[AbstractInsnNode] = { + + /** + * 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): 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 || c.packageInternalName == from.packageInternalName + case a: ArrayBType => classIsAccessible(a.elementType, from) + case _: PrimitiveBType => 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): 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 = (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE) & memberFlags + key match { + case Opcodes.ACC_PUBLIC => // B1 + true + + case Opcodes.ACC_PROTECTED => // B2 + val condB2 = destinationClass.isSubtypeOf(memberDeclClass) && { + val isStatic = (Opcodes.ACC_STATIC & memberFlags) != 0 + isStatic || memberRefClass.isSubtypeOf(destinationClass) || destinationClass.isSubtypeOf(memberRefClass) + } + condB2 || samePackageAsDestination // B3 (protected) + + case 0 => // B3 (default access) + samePackageAsDestination + + case Opcodes.ACC_PRIVATE => // B4 + memberDeclClass == destinationClass + } + } + + def isLegal(instruction: AbstractInsnNode): 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) + val (fieldNode, fieldDeclClass) = byteCodeRepository.fieldNode(fieldRefClass.internalName, fi.name, fi.desc).get + memberIsAccessible(fieldNode.access, classBTypeFromParsedClassfile(fieldDeclClass), fieldRefClass) + + case mi: MethodInsnNode => + if (mi.owner.charAt(0) == '[') true // array methods are accessible + else { + val methodRefClass = classBTypeFromParsedClassfile(mi.owner) + val (methodNode, methodDeclClass) = byteCodeRepository.methodNode(methodRefClass.internalName, mi.name, mi.desc).get + memberIsAccessible(methodNode.access, classBTypeFromParsedClassfile(methodDeclClass), methodRefClass) + } + + case ivd: InvokeDynamicInsnNode => + // TODO @lry check necessary conditions to inline an indy, instead of giving up + false + + case ci: LdcInsnNode => ci.cst match { + case t: asm.Type => classIsAccessible(bTypeForDescriptorOrInternalNameFromClassfile(t.getInternalName)) + case _ => true + } + + case _ => true + } + + instructions.iterator.asScala.find(!isLegal(_)) + } +} -- cgit v1.2.3 From b34a452c0683d260ffb1644575a0e970559cae87 Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Sat, 17 Jan 2015 15:29:49 +0100 Subject: Tools to perform inlining. The method Inliner.inline clones the bytecode of a method and copies the new instructions to the callsite with the necessary modifications. See comments in the code. More tests are added in a later commit which integrates the inliner into the backend - tests are easier to write after that. --- .../tools/nsc/backend/jvm/BCodeBodyBuilder.scala | 3 +- .../scala/tools/nsc/backend/jvm/BCodeHelpers.scala | 1 + .../tools/nsc/backend/jvm/BCodeIdiomatic.scala | 4 +- .../tools/nsc/backend/jvm/BCodeSkelBuilder.scala | 1 + .../scala/tools/nsc/backend/jvm/BTypes.scala | 5 +- .../scala/tools/nsc/backend/jvm/GenBCode.scala | 3 + .../nsc/backend/jvm/opt/ByteCodeRepository.scala | 11 +- .../tools/nsc/backend/jvm/opt/BytecodeUtils.scala | 125 ++++++++++++- .../scala/tools/nsc/backend/jvm/opt/Inliner.scala | 203 ++++++++++++++++++++- .../scala/tools/nsc/backend/jvm/opt/LocalOpt.scala | 27 +-- .../nsc/backend/jvm/opt/OptimizerReporting.scala | 10 +- .../tools/nsc/backend/jvm/opt/InlinerTest.scala | 193 ++++++++++++++++++++ 12 files changed, 534 insertions(+), 52 deletions(-) create mode 100644 test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala (limited to 'src/compiler') diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala index 062daa4eac..1b3f124dd8 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala @@ -12,6 +12,7 @@ package jvm import scala.annotation.switch import scala.tools.asm +import GenBCode._ /* * @@ -613,7 +614,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 diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala index ccee230191..e366bbabb8 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala @@ -10,6 +10,7 @@ package backend.jvm import scala.tools.asm import scala.collection.mutable import scala.tools.nsc.io.AbstractFile +import GenBCode._ /* * Traits encapsulating functionality to convert Scala AST Trees into ASM ClassNodes. diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeIdiomatic.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeIdiomatic.scala index c3db28151b..c743ebd16f 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeIdiomatic.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeIdiomatic.scala @@ -10,6 +10,7 @@ package backend.jvm import scala.tools.asm import scala.annotation.switch import scala.collection.mutable +import GenBCode._ /* * A high-level facade to the ASM API for bytecode generation. @@ -42,9 +43,6 @@ abstract class BCodeIdiomatic extends SubComponent { val StringBuilderClassName = "scala/collection/mutable/StringBuilder" - val CLASS_CONSTRUCTOR_NAME = "" - val INSTANCE_CONSTRUCTOR_NAME = "" - val EMPTY_STRING_ARRAY = Array.empty[String] val EMPTY_INT_ARRAY = Array.empty[Int] val EMPTY_LABEL_ARRAY = Array.empty[asm.Label] diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala index 142c901c21..48df4e1121 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala @@ -16,6 +16,7 @@ import scala.annotation.switch import scala.tools.asm import scala.tools.asm.util.{TraceMethodVisitor, ASMifier} import java.io.PrintWriter +import GenBCode._ /* * diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala index ff30631c10..fe4c4794a9 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala @@ -11,6 +11,7 @@ import scala.tools.asm import asm.Opcodes import scala.tools.asm.tree.{InnerClassNode, ClassNode} import opt.{ByteCodeRepository, Inliner} +import OptimizerReporting._ import scala.collection.convert.decorateAsScala._ /** @@ -273,7 +274,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") } /** @@ -364,7 +365,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 diff --git a/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala b/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala index d5e95c47cf..9b3bd7648d 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala @@ -410,4 +410,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 = "" + val INSTANCE_CONSTRUCTOR_NAME = "" } 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 9e56f25888..b3ac06877b 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala @@ -13,6 +13,7 @@ import scala.collection.convert.decorateAsScala._ import scala.tools.nsc.io.AbstractFile import scala.tools.nsc.util.ClassFileLookup import OptimizerReporting._ +import BytecodeUtils._ import ByteCodeRepository._ import BTypes.InternalName @@ -93,16 +94,6 @@ class ByteCodeRepository(val classPath: ClassFileLookup[AbstractFile], val class 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 _ => - } - } - } } object ByteCodeRepository { 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..4cff92d38b 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,16 @@ 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 nextExecutableInstruction(instruction: AbstractInsnNode, alsoKeep: AbstractInsnNode => Boolean = Set()): Option[AbstractInsnNode] = { var result = instruction do { result = result.getNext } @@ -181,4 +196,112 @@ 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 + } + + class BasicAnalyzer(methodNode: MethodNode, classInternalName: InternalName) { + val analyzer = new Analyzer[BasicValue](new BasicInterpreter) + analyzer.analyze(classInternalName, methodNode) + def frameAt(instruction: AbstractInsnNode): Frame[BasicValue] = 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/Inliner.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala index 6e5e03f730..f964b5b25d 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala @@ -8,15 +8,206 @@ package backend.jvm package opt import scala.tools.asm -import asm.Opcodes +import asm.Opcodes._ import asm.tree._ import scala.collection.convert.decorateAsScala._ +import scala.collection.convert.decorateAsJava._ +import AsmUtils._ +import BytecodeUtils._ import OptimizerReporting._ +import scala.tools.asm.tree.analysis._ class Inliner[BT <: BTypes](val btypes: BT) { import btypes._ import btypes.byteCodeRepository + /** + * 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[String] = { + 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 BasicAnalyzer(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) + + 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[String] = { + + 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(s"Method ${methodSignature(calleeDeclarationClass.internalName, callee)} is not inlined because it is synchronized") + } else if (!callee.tryCatchBlocks.isEmpty && stackHasNonParameters) { + Some( + s"""The operand stack at the callsite in ${methodSignature(callsiteClass.internalName, callsiteMethod)} contains more values than the + |arguments expected by the callee ${methodSignature(calleeDeclarationClass.internalName, callee)}. These values would be discarded + |when entering an exception handler declared in the inlined method.""".stripMargin + ) + } else findIllegalAccess(callee.instructions, callsiteClass) map { + case illegalAccessIns => + s"""The callee ${methodSignature(calleeDeclarationClass.internalName, callee)} contains the instruction ${AsmUtils.textify(illegalAccessIns)} + |that would cause an IllegalAccessError when inlined into class ${callsiteClass.internalName}""".stripMargin + } + } + def findIllegalAccess(instructions: InsnList, destinationClass: ClassBType): Option[AbstractInsnNode] = { /** @@ -57,14 +248,14 @@ class Inliner[BT <: BTypes](val btypes: BT) { // 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 = (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE) & memberFlags + val key = (ACC_PUBLIC | ACC_PROTECTED | ACC_PRIVATE) & memberFlags key match { - case Opcodes.ACC_PUBLIC => // B1 + case ACC_PUBLIC => // B1 true - case Opcodes.ACC_PROTECTED => // B2 + case ACC_PROTECTED => // B2 val condB2 = destinationClass.isSubtypeOf(memberDeclClass) && { - val isStatic = (Opcodes.ACC_STATIC & memberFlags) != 0 + val isStatic = (ACC_STATIC & memberFlags) != 0 isStatic || memberRefClass.isSubtypeOf(destinationClass) || destinationClass.isSubtypeOf(memberRefClass) } condB2 || samePackageAsDestination // B3 (protected) @@ -72,7 +263,7 @@ class Inliner[BT <: BTypes](val btypes: BT) { case 0 => // B3 (default access) samePackageAsDestination - case Opcodes.ACC_PRIVATE => // B4 + case ACC_PRIVATE => // B4 memberDeclClass == destinationClass } } 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..3a7250031a 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -8,7 +8,7 @@ package backend.jvm package opt import scala.annotation.switch -import scala.tools.asm.{Opcodes, MethodWriter, ClassWriter} +import scala.tools.asm.Opcodes import scala.tools.asm.tree.analysis.{Analyzer, BasicValue, BasicInterpreter} import scala.tools.asm.tree._ import scala.collection.convert.decorateAsScala._ @@ -73,7 +73,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: String): 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. @@ -318,29 +318,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. * diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/OptimizerReporting.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/OptimizerReporting.scala index 7002e43d98..a918e13534 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/OptimizerReporting.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/OptimizerReporting.scala @@ -8,17 +8,19 @@ package backend.jvm import scala.tools.asm import asm.tree._ +import scala.tools.nsc.backend.jvm.BTypes.InternalName /** * Reporting utilities used in the optimizer. + * + * TODO: move out of opt package, rename: it's already used outside the optimizer. + * Centralize backend reporting here. */ object OptimizerReporting { - def methodSignature(className: String, methodName: String, methodDescriptor: String): String = { - className + "::" + methodName + methodDescriptor + def methodSignature(classInternalName: InternalName, method: MethodNode): String = { + classInternalName + "::" + method.name + method.desc } - 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/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..1aa7bd1391 --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -0,0 +1,193 @@ +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.asm.tree.analysis._ +import scala.tools.nsc.backend.jvm.opt.BytecodeUtils.BasicAnalyzer +import scala.tools.testing.AssertUtil._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import AsmUtils._ + +import scala.collection.convert.decorateAsScala._ + +@RunWith(classOf[JUnit4]) +class InlinerTest { + val compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:none") + import compiler.genBCode.bTypes._ + def addToRepo(cls: List[ClassNode]): List[ClassNode] = { + for (c <- cls) byteCodeRepository.classes(c.name) = (c, ByteCodeRepository.Classfile) + cls + } + + // inline first invocation of f into g in class C + def inlineTest(code: String, mod: ClassNode => Unit = _ => ()): (MethodNode, Option[String]) = { + val List(cls) = addToRepo(compileClasses(compiler)(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 BasicAnalyzer(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 contains "synchronized", 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 contains "operand stack at the callsite", 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) = addToRepo(compileClasses(compiler)(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 BasicAnalyzer(h, dTp.internalName) + + val r = inliner.inline( + gCall, + analyzer.frameAt(gCall).getStackSize, + h, + dTp, + g, + cTp, + receiverKnownNotNull = true, + keepLineNumbers = true) + + assert(r.get contains "would cause an IllegalAccessError", r) + } +} -- cgit v1.2.3 From 0d8b32469ec655f35a31843e1843b8a580e772d1 Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Sat, 21 Feb 2015 16:04:21 +0100 Subject: Build a call graph for inlining decisions Inlining decisions will be taken by analyzing the ASM bytecode. This commit adds tools to build a call graph representation that can be used for these decisions. The call graph is currently built by considering method descriptors of callsite instructions. It will become more precise by using data flow analyses. --- .../scala/tools/nsc/backend/jvm/BCodeHelpers.scala | 127 +++---------- .../tools/nsc/backend/jvm/BCodeSkelBuilder.scala | 8 +- .../scala/tools/nsc/backend/jvm/BTypes.scala | 34 +++- .../tools/nsc/backend/jvm/BTypesFromSymbols.scala | 203 +++++++++++++++++++-- .../tools/nsc/backend/jvm/opt/BytecodeUtils.scala | 2 +- .../tools/nsc/backend/jvm/opt/CallGraph.scala | 114 ++++++++++++ .../scala/tools/nsc/backend/jvm/opt/Inliner.scala | 25 ++- .../scala/tools/nsc/backend/jvm/opt/LocalOpt.scala | 4 +- .../scala/tools/nsc/settings/ScalaSettings.scala | 10 +- .../backend/jvm/opt/BTypesFromClassfileTest.scala | 9 + .../tools/nsc/backend/jvm/opt/CallGraphTest.scala | 146 +++++++++++++++ .../tools/nsc/backend/jvm/opt/InlinerTest.scala | 8 +- 12 files changed, 553 insertions(+), 137 deletions(-) create mode 100644 src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala create mode 100644 test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala (limited to 'src/compiler') diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala index e366bbabb8..246d565987 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala @@ -332,125 +332,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) 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 => + 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 => innerClassBufferASM += c + case _ => + } + r } /** diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala index 48df4e1121..b4de5cf52f 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala @@ -10,6 +10,7 @@ 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 @@ -126,9 +127,12 @@ abstract class BCodeSkelBuilder extends BCodeHelpers { 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.classes(cnode.name) = (cnode, ByteCodeRepository.CompilationUnit) + } + assert(cd.symbol == claszSymbol, "Someone messed up BCodePhase.claszSymbol during genPlainClass().") } // end of method genPlainClass() /* diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala index fe4c4794a9..c93496fb49 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala @@ -10,7 +10,8 @@ import scala.annotation.switch import scala.tools.asm import asm.Opcodes import scala.tools.asm.tree.{InnerClassNode, ClassNode} -import opt.{ByteCodeRepository, Inliner} +import scala.tools.nsc.backend.jvm.BTypes.{MethodInlineInfo, InlineInfo} +import scala.tools.nsc.backend.jvm.opt.{CallGraph, ByteCodeRepository, Inliner} import OptimizerReporting._ import scala.collection.convert.decorateAsScala._ @@ -38,9 +39,14 @@ abstract class BTypes { val inliner: Inliner[this.type] + val callGraph: CallGraph[this.type] + // 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 + /** * A map from internal names to ClassBTypes. Every ClassBType is added to this map on its * construction. @@ -54,6 +60,23 @@ abstract class BTypes { */ val classBTypeFromInternalName: collection.concurrent.Map[InternalName, ClassBType] = recordPerRunCache(collection.concurrent.TrieMap.empty[InternalName, ClassBType]) + /** + * Build the [[InlineInfo]] for the methods of a class, given its internal name. + * + * The InlineInfo is part of the ClassBType's [[ClassInfo]]. Note that there are two ways to build + * a ClassBType: from a class symbol (methods in [[BTypesFromSymbols]]) or from a [[ClassNode]]. + * The InlineInfo however contains information that can only be retrieved from the symbol of + * the class (e.g., is a method annotated @inline). + * + * This method (implemented in [[BTypesFromSymbols]]) looks up the class symbol in the symbol + * table, using the classfile name of the class. + * + * The method tries to undo some of the name mangling, but the lookup does not succeed for all + * classes. In case it fails, the resulting ClassBType will simply not have an InlineInfo, and + * we won't be able to inline its methods. + */ + def inlineInfosFromSymbolLookup(internalName: InternalName): Map[String, MethodInlineInfo] + /** * Obtain the BType for a type descriptor or internal name. For class descriptors, the ClassBType * is constructed by parsing the corresponding classfile. @@ -145,7 +168,8 @@ 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) + + classBType.info = ClassInfo(superClass, interfaces, flags, nestedClasses, nestedInfo, inlineInfosFromSymbolLookup(classBType.internalName)) classBType } @@ -901,9 +925,13 @@ abstract class BTypes { * @param nestedClasses Classes nested in this class. Those need to be added to the * InnerClass table, see the InnerClass spec summary above. * @param nestedInfo If this describes a nested class, information for the InnerClass table. + * @param inlineInfos The [[InlineInfo]]s for the methods declared in this class. The map is + * indexed by the string s"$name$descriptor" (to disambiguate overloads). + * Entries may be missing, see comment on [[inlineInfosFromSymbolLookup]]. */ final case class ClassInfo(superClass: Option[ClassBType], interfaces: List[ClassBType], flags: Int, - nestedClasses: List[ClassBType], nestedInfo: Option[NestedInfo]) + nestedClasses: List[ClassBType], nestedInfo: Option[NestedInfo], + inlineInfos: Map[String, MethodInlineInfo]) /** * Information required to add a class to an InnerClass table. diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala index 117b377622..9ed7b3174b 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala @@ -9,8 +9,8 @@ package backend.jvm import scala.tools.asm import scala.tools.asm.tree.ClassNode import scala.tools.nsc.backend.jvm.opt.ByteCodeRepository.Source -import scala.tools.nsc.backend.jvm.opt.{Inliner, ByteCodeRepository} -import BTypes.InternalName +import scala.tools.nsc.backend.jvm.opt.{CallGraph, Inliner, ByteCodeRepository} +import scala.tools.nsc.backend.jvm.BTypes.{MethodInlineInfo, InlineInfo, InternalName} /** * This class mainly contains the method classBTypeFromSymbol, which extracts the necessary @@ -40,12 +40,58 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { val inliner: Inliner[this.type] = new Inliner(this) + val callGraph: CallGraph[this.type] = new CallGraph(this) + + /** + * See doc in [[BTypes.inlineInfosFromSymbolLookup]]. + * TODO: once the optimzier uses parallelism, lock before symbol table accesses + */ + def inlineInfosFromSymbolLookup(internalName: InternalName): Map[String, MethodInlineInfo] = { + val name = internalName.replace('/', '.') + + // TODO: de-mangle more class names + + def inEmptyPackage = name.indexOf('.') == -1 + def isModule = name.endsWith("$") + def isTopLevel = { + // TODO: this is conservative, there's also $'s introduced by name mangling, e.g., $colon$colon + // for this, use NameTransformer.decode + if (isModule) name.indexOf('$') == (name.length - 1) + else name.indexOf('$') == -1 + } + + val lookupName = { + if (isModule) newTermName(name.substring(0, name.length - 1)) + else newTypeName(name) + } + + // for now we only try classes that look like top-level + val classSym = if (!isTopLevel) NoSymbol else { + val member = { + if (inEmptyPackage) { + // rootMirror.getClassIfDefined fails for classes / modules in the empty package. + // maybe that should be fixed. + rootMirror.EmptyPackageClass.info.member(lookupName) + } else { + if (isModule) rootMirror.getModuleIfDefined(lookupName) + else rootMirror.getClassIfDefined(lookupName) + } + } + if (isModule) member.moduleClass else member + } + + if (classSym == NoSymbol) Map.empty + else buildInlineInfos(classSym) + } + final def initializeCoreBTypes(): Unit = { coreBTypes.setBTypes(new CoreBTypes[this.type](this)) } def recordPerRunCache[T <: collection.generic.Clearable](cache: T): T = perRunCaches.recordCache(cache) + def inlineGlobalEnabled: Boolean = settings.YoptInlineGlobal + // helpers that need access to global. // TODO @lry create a separate component, they don't belong to BTypesFromSymbols @@ -78,22 +124,125 @@ 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. + setClassInfo(classSym, ClassBType(internalName)) + }) + } + } - 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)) + } + } + } + + 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 = { @@ -217,7 +366,9 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { val nestedInfo = buildNestedInfo(classSym) - classBType.info = ClassInfo(superClass, interfaces, flags, nestedClasses, nestedInfo) + val inlineInfos = buildInlineInfos(classSym) + + classBType.info = ClassInfo(superClass, interfaces, flags, nestedClasses, nestedInfo, inlineInfos) classBType } @@ -272,6 +423,27 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { } } + private def buildInlineInfos(classSym: Symbol): Map[String, MethodInlineInfo] = { + if (!settings.YoptInlinerEnabled) Map.empty + else { + // Primitve methods cannot be inlined, so there's no point in building an InlineInfo. Also, some + // primitive methods (e.g., `isInstanceOf`) have non-erased types, which confuses [[typeToBType]]. + classSym.info.decls.iterator.filter(m => m.isMethod && !scalaPrimitives.isPrimitive(m)).map({ + case methodSym => + val methodBType = methodBTypeFromSymbol(methodSym) + val name = methodSym.javaSimpleName.toString // same as in genDefDef + val signature = name + methodBType.descriptor + val info = MethodInlineInfo( + effectivelyFinal = methodSym.isEffectivelyFinalOrNotOverridden, + traitMethodWithStaticImplementation = false, // temporary, fixed in future commit + annotatedInline = methodSym.hasAnnotation(ScalaInlineClass), + annotatedNoInline = methodSym.hasAnnotation(ScalaNoInlineClass) + ) + (signature, info) + }).toMap + } + } + /** * 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 @@ -289,7 +461,8 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { interfaces = Nil, flags = asm.Opcodes.ACC_SUPER | asm.Opcodes.ACC_PUBLIC | asm.Opcodes.ACC_FINAL, nestedClasses = nested, - nestedInfo = None + nestedInfo = None, + Map.empty // no InlineInfo needed, scala never invokes methods on the mirror class ) c }) 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 4cff92d38b..74f46d04f9 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala @@ -293,7 +293,7 @@ object BytecodeUtils { } class BasicAnalyzer(methodNode: MethodNode, classInternalName: InternalName) { - val analyzer = new Analyzer[BasicValue](new BasicInterpreter) + val analyzer = new Analyzer(new BasicInterpreter) analyzer.analyze(classInternalName, methodNode) def frameAt(instruction: AbstractInsnNode): Frame[BasicValue] = analyzer.getFrames()(methodNode.instructions.indexOf(instruction)) } 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..bfaa67004c --- /dev/null +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala @@ -0,0 +1,114 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2014 LAMP/EPFL + * @author Martin Odersky + */ + +package scala.tools.nsc +package backend.jvm +package opt + +import scala.tools.asm.tree._ +import scala.collection.convert.decorateAsScala._ +import scala.tools.nsc.backend.jvm.opt.BytecodeUtils.BasicAnalyzer + +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] = { + // 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 BasicAnalyzer(methodNode, definingClass.internalName) + + methodNode.instructions.iterator.asScala.collect({ + case call: MethodInsnNode => + // TODO: log an inliner warning if the callee method cannot be found in the code repo? eg it's not on the classpath. + val callee = byteCodeRepository.methodNode(call.owner, call.name, call.desc) map { + case (method, declarationClass) => + val (declarationClassNode, source) = byteCodeRepository.classNodeAndSource(declarationClass) + val declarationClassBType = classBTypeFromClassNode(declarationClassNode) + val methodSignature = method.name + method.desc + val (safeToInline, annotatedInline, annotatedNoInline) = declarationClassBType.info.inlineInfos.get(methodSignature) match { + case Some(inlineInfo) => + val canInlineFromSource = inlineGlobalEnabled || source == ByteCodeRepository.CompilationUnit + // TODO: for now, we consider a callee safeToInline only if it's final + // type analysis can render more calls safeToInline (e.g. when the precise receiver type is known) + (canInlineFromSource && inlineInfo.effectivelyFinal, Some(inlineInfo.annotatedInline), Some(inlineInfo.annotatedNoInline)) + case None => + (false, None, None) + } + Callee( + callee = method, + calleeDeclarationClass = declarationClassBType, + safeToInline = safeToInline, + annotatedInline = annotatedInline, + annotatedNoInline = annotatedNoInline + ) + } + + val argInfos = if (callee.isEmpty) 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 + ) + }).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. For virtual calls, an override of the callee might be invoked. + * @param argInfos Information about the invocation receiver and arguments + * @param callsiteStackHeight The stack height at the callsite, required by the inliner + */ + final case class Callsite(callsiteInstruction: MethodInsnNode, callsiteMethod: MethodNode, callsiteClass: ClassBType, + callee: Option[Callee], argInfos: List[ArgInfo], + callsiteStackHeight: Int) { + override def toString = s"Invocation of ${callsiteInstruction.name + callsiteInstruction.desc}@${callsiteMethod.instructions.indexOf(callsiteInstruction)} in ${callsiteMethod.name}" + } + + /** + * 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 called method. For virtual calls, an override may actually be invoked. + * @param calleeDeclarationClass The class in which the callee is declared + * @param safeToInline True if the callee can be safely inlined: it cannot be overridden, + * and the inliner settings (project / global) allow inlining it. + * @param annotatedInline Defined if it is known whether the callee is annotated @inline + * @param annotatedNoInline Defined if it is known whether the callee is annotated @noinline + */ + final case class Callee(callee: MethodNode, calleeDeclarationClass: ClassBType, + safeToInline: Boolean, + annotatedInline: Option[Boolean], annotatedNoInline: Option[Boolean]) +} diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala index f964b5b25d..7527491e9b 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala @@ -19,7 +19,7 @@ import scala.tools.asm.tree.analysis._ class Inliner[BT <: BTypes](val btypes: BT) { import btypes._ - import btypes.byteCodeRepository + import callGraph._ /** * Copy and adapt the instructions of a method to a callsite. @@ -152,6 +152,29 @@ class Inliner[BT <: BTypes](val btypes: BT) { 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 + ) + + 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) 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 3a7250031a..23c8daa046 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -9,7 +9,7 @@ package opt import scala.annotation.switch import scala.tools.asm.Opcodes -import scala.tools.asm.tree.analysis.{Analyzer, BasicValue, BasicInterpreter} +import scala.tools.asm.tree.analysis.{Analyzer, BasicInterpreter} import scala.tools.asm.tree._ import scala.collection.convert.decorateAsScala._ import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ @@ -149,7 +149,7 @@ class LocalOpt(settings: ScalaSettings) { def removeUnreachableCodeImpl(method: MethodNode, ownerClassName: String): (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 diff --git a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala index d7f4cca615..9674b4cfae 100644 --- a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala @@ -222,6 +222,8 @@ trait ScalaSettings extends AbsScalaSettings 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.") @@ -231,10 +233,10 @@ trait ScalaSettings extends AbsScalaSettings private val methodChoices = List(unreachableCode, simplifyJumps, recurseUnreachableJumps, 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) } @@ -252,6 +254,10 @@ trait ScalaSettings extends AbsScalaSettings 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 + 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/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala index 4481fcd6be..65c96226ff 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala @@ -59,6 +59,15 @@ class BTypesFromClassfileTest { else (fromSym.flags | ACC_PRIVATE | ACC_PUBLIC) == (fromClassfile.flags | ACC_PRIVATE | ACC_PUBLIC) }, s"class flags differ\n$fromSym\n$fromClassfile") + // when parsing from classfile, the inline infos are obtained through the classSymbol, which + // is searched based on the classfile name. this lookup can fail. + assert(fromSym.inlineInfos.size == fromClassfile.inlineInfos.size || fromClassfile.inlineInfos.isEmpty, + s"wrong # of inline infos:\n${fromSym.inlineInfos.keys.toList.sorted}\n${fromClassfile.inlineInfos.keys.toList.sorted}") + fromClassfile.inlineInfos foreach { + case (signature, inlineInfo) => + assert(fromSym.inlineInfos(signature) == inlineInfo, s"inline infos differ for $signature:\n$inlineInfo\n${fromClassfile.inlineInfos(signature)}") + } + val chk1 = sameBTypes(fromSym.superClass, fromClassfile.superClass, checked) val chk2 = sameBTypes(fromSym.interfaces, fromClassfile.interfaces, chk1) 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..400fb6b00a --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.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.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.BasicAnalyzer +import scala.tools.testing.AssertUtil._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import AsmUtils._ + +import scala.collection.convert.decorateAsScala._ + +@RunWith(classOf[JUnit4]) +class CallGraphTest { + // no need to move the compiler instance to a companion: there's a single test method, so only a + // single instance created. + val compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:inline-global") + 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): List[ClassNode] = { + notPerRun.foreach(_.clear()) + compileClasses(compiler)(code) + } + + 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 clss @ List(cCls, cMod, dCls, testCls) = compile(code).map(c => byteCodeRepository.classNode(c.name)) + clss.foreach(cls => { + // add classes to the call graph manually, the compiler doesn't do it yet. the next commit removes these lines. + cls.methods.asScala foreach BytecodeUtils.computeMaxLocalsMaxStack + callGraph.addClass(cls) + }) + + + 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.get == atInline) + assert(callee.annotatedNoInline.get == 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/InlinerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala index 2ec6853f13..6cd89e1323 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -26,7 +26,7 @@ object InlinerTest extends ClearAfterClass.Clearable { var compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:project") // allows inspecting the caches after a compilation run - def notPerRun: List[Clearable] = List(compiler.genBCode.bTypes.classBTypeFromInternalName, compiler.genBCode.bTypes.byteCodeRepository.classes) + 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 } @@ -41,11 +41,7 @@ class InlinerTest extends ClearAfterClass { def compile(code: String): List[ClassNode] = { InlinerTest.notPerRun.foreach(_.clear()) - val cls = compileClasses(compiler)(code) - // the compiler doesn't add classes being compiled to the code repo yet, so we do it manually. - // this line is removed in the next commit. - for (c <- cls) byteCodeRepository.classes(c.name) = (c, ByteCodeRepository.Classfile) - cls + compileClasses(compiler)(code) } // inline first invocation of f into g in class C -- cgit v1.2.3 From 37f7b76710c72360577250f07bd8b5cf55e527cc Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Mon, 19 Jan 2015 10:00:04 +0100 Subject: Integrate the inliner into the backend pipeline The current heuristics are simple: attempt to inline every method annotated `@inline`. Cycles in the inline request graph are broken in a determinisitc manner. Inlining is then performed starting at the leaves of the inline request graph, i.e., with those callsites where the target method has no callsites to inline. This expansion strategy can make a method grow arbitrarily. We will most likely have to add some thresholds and / or other measures to prevent size issues. --- .../scala/tools/nsc/backend/jvm/GenBCode.scala | 22 ++- .../scala/tools/nsc/backend/jvm/opt/Inliner.scala | 122 ++++++++++++ .../scala/tools/nsc/backend/jvm/opt/LocalOpt.scala | 38 +++- .../tools/nsc/backend/jvm/opt/CallGraphTest.scala | 10 +- .../tools/nsc/backend/jvm/opt/InlinerTest.scala | 211 ++++++++++++++++++++- 5 files changed, 389 insertions(+), 14 deletions(-) (limited to 'src/compiler') diff --git a/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala b/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala index 9b3bd7648d..173aa0ca30 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`, diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala index 7527491e9b..b74c7ba86c 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala @@ -7,6 +7,7 @@ package scala.tools.nsc package backend.jvm package opt +import scala.annotation.tailrec import scala.tools.asm import asm.Opcodes._ import asm.tree._ @@ -16,11 +17,132 @@ import AsmUtils._ import BytecodeUtils._ import OptimizerReporting._ import scala.tools.asm.tree.analysis._ +import collection.mutable class Inliner[BT <: BTypes](val btypes: BT) { import btypes._ import callGraph._ + def runInliner(): Unit = { + for (request <- collectAndOrderInlineRequests) { + val Some(callee) = request.callee + inline(request.callsiteInstruction, request.callsiteStackHeight, request.callsiteMethod, request.callsiteClass, + callee.callee, callee.calleeDeclarationClass, + receiverKnownNotNull = false, keepLineNumbers = false) + } + } + + /** + * 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.iterator.filter({ + case (_, callsite) => callsite.callee match { + case Some(Callee(callee, _, safeToInline, Some(annotatedInline), _)) => + // TODO: fix inlining from traits. + // For trait methods the callee is abstract: "trait T { @inline final def f = 1}". + // A callsite (t: T).f is `safeToInline` (effectivelyFinal is true), but the callee is the + // abstract method in the interface. + !isAbstractMethod(callee) && safeToInline && annotatedInline + case _ => false + } + case _ => false + }).map(_._2).toList + } + + /** + * 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. * 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 23c8daa046..8c5a31658c 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -12,6 +12,7 @@ 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 @@ -47,6 +48,37 @@ import scala.tools.nsc.settings.ScalaSettings * - eliminate labels that are not referenced, merge sequences of label definitions. */ 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 + * depends on this property. + */ + def minimalRemoveUnreachableCode(method: MethodNode, ownerClassName: InternalName): Boolean = { + if (method.instructions.size == 0) return false // fast path for abstract methods + + val (codeRemoved, _) = removeUnreachableCodeImpl(method, ownerClassName) + if (codeRemoved) { + // Required for correctness, see comment in class LocalOpt + removeEmptyExceptionHandlers(method) + removeUnusedLocalVariableNodes(method)() + } + codeRemoved + } + /** * Remove unreachable instructions from all (non-abstract) methods and apply various other * cleanups to the bytecode. @@ -73,7 +105,7 @@ class LocalOpt(settings: ScalaSettings) { * * Returns `true` if the bytecode of `method` was changed. */ - 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. @@ -124,7 +156,7 @@ class LocalOpt(settings: ScalaSettings) { // (*) 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,7 +178,7 @@ 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(new BasicInterpreter) diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala index 400fb6b00a..5946f50f0c 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala @@ -23,8 +23,6 @@ import scala.collection.convert.decorateAsScala._ @RunWith(classOf[JUnit4]) class CallGraphTest { - // no need to move the compiler instance to a companion: there's a single test method, so only a - // single instance created. val compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:inline-global") import compiler.genBCode.bTypes._ @@ -72,13 +70,7 @@ class CallGraphTest { // 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 clss @ List(cCls, cMod, dCls, testCls) = compile(code).map(c => byteCodeRepository.classNode(c.name)) - clss.foreach(cls => { - // add classes to the call graph manually, the compiler doesn't do it yet. the next commit removes these lines. - cls.methods.asScala foreach BytecodeUtils.computeMaxLocalsMaxStack - callGraph.addClass(cls) - }) - + val List(cCls, cMod, dCls, testCls) = compile(code).map(c => byteCodeRepository.classNode(c.name)) 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) diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala index 6cd89e1323..819252841e 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -23,7 +23,7 @@ import scala.collection.convert.decorateAsScala._ import scala.tools.testing.ClearAfterClass object InlinerTest extends ClearAfterClass.Clearable { - var compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:project") + var compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:classpath") // 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) @@ -44,6 +44,15 @@ class InlinerTest extends ClearAfterClass { compileClasses(compiler)(code) } + def checkCallsite(callsite: callGraph.Callsite, callee: MethodNode) = { + assert(callsite.callsiteMethod.instructions.contains(callsite.callsiteInstruction), instructionsFromMethod(callsite.callsiteMethod)) + + val callsiteClassNode = byteCodeRepository.classNode(callsite.callsiteClass.internalName) + 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[String]) = { val List(cls) = compile(code) @@ -205,4 +214,204 @@ class InlinerTest extends ClearAfterClass { assert(r.get contains "would cause an IllegalAccessError", 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 = instructionsFromMethod(cCls.methods.asScala.find(_.name == "test").get) + assert(instructions.contains(Op(ICONST_0)), instructions mkString "\n") + 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 = + """class C { + | def f(src: AnyRef, srcPos: Int, dest: AnyRef, destPos: Int, length: Int): Unit = { + | compat.Platform.arraycopy(src, srcPos, dest, destPos, length) + | } + |} + """.stripMargin + val List(c) = compile(code) + val ins = instructionsFromMethod(c.methods.asScala.find(_.name == "f").get) + val invokeSysArraycopy = Invoke(INVOKESTATIC, "java/lang/System", "arraycopy", "(Ljava/lang/Object;ILjava/lang/Object;II)V", false) + assert(ins contains invokeSysArraycopy, ins mkString "\n") + } + + @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 atInlineInTraitDoesNotCrash(): 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) + val ins = instructionsFromMethod(c.methods.asScala.find(_.name == "g").get) + val invokeF = Invoke(INVOKEINTERFACE, "T", "f", "()I", true) + // no inlining yet + assert(ins contains invokeF, ins mkString "\n") + } + + @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) + val ins = instructionsFromMethod(c.methods.asScala.find(_.name == "g").get) + println(ins) + // no more invoke, f is inlined + assert(ins.count(_.isInstanceOf[Invoke]) == 0, ins mkString "\n") + } + + @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 BasicAnalyzer(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 + assert(ins.count(_.isInstanceOf[Invoke]) == 0, ins mkString "\n") + + // no null check when inlining a static method + ins foreach { + case Jump(IFNONNULL, _) => assert(false, ins mkString "\n") + 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}") + } } -- cgit v1.2.3 From ea10434ff3bf24ac61dd4f65edcf931b7e988c0a Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Mon, 19 Jan 2015 12:11:33 +0100 Subject: Looking up the ClassNode for an InternalName returns an Option The `ByteCodeRepository.classNode(InternalName)` method now returns an option. Concretely, in mixed compilation, the compiler does not create a ClassNode for Java classes, and they may not exist on the classpath either. --- .../tools/nsc/backend/jvm/BCodeSkelBuilder.scala | 2 +- .../scala/tools/nsc/backend/jvm/BTypes.scala | 16 +++++++-- .../tools/nsc/backend/jvm/BTypesFromSymbols.scala | 2 +- .../nsc/backend/jvm/opt/ByteCodeRepository.scala | 34 +++++++++--------- .../tools/nsc/backend/jvm/opt/CallGraph.scala | 41 ++++++++++++---------- .../nsc/backend/jvm/opt/OptimizerReporting.scala | 1 + .../tools/nsc/backend/jvm/opt/CallGraphTest.scala | 2 +- .../backend/jvm/opt/InlinerIllegalAccessTest.scala | 2 +- .../tools/nsc/backend/jvm/opt/InlinerTest.scala | 2 +- 9 files changed, 58 insertions(+), 44 deletions(-) (limited to 'src/compiler') diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala index b4de5cf52f..32a421c570 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala @@ -129,7 +129,7 @@ abstract class BCodeSkelBuilder extends BCodeHelpers { if (settings.YoptInlinerEnabled) { // The inliner needs to find all classes in the code repo, also those being compiled - byteCodeRepository.classes(cnode.name) = (cnode, ByteCodeRepository.CompilationUnit) + byteCodeRepository.classes(cnode.name) = Some((cnode, ByteCodeRepository.CompilationUnit)) } assert(cd.symbol == claszSymbol, "Someone messed up BCodePhase.claszSymbol during genPlainClass().") diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala index c93496fb49..f07c7b7764 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala @@ -106,7 +106,13 @@ abstract class BTypes { * Parse the classfile for `internalName` and construct the [[ClassBType]]. */ def classBTypeFromParsedClassfile(internalName: InternalName): ClassBType = { - classBTypeFromClassNode(byteCodeRepository.classNode(internalName)) + val classNode = byteCodeRepository.classNode(internalName) getOrElse { + // There's no way out, we need the ClassBType. I (lry) only know one case byteCodeRepository.classNode + // returns None: for Java classes in mixed compilation. In this case we should not end up here, + // because there exists a symbol for that Java class, the ClassBType should be built from the symbol. + assertionError(s"Could not find bytecode for class $internalName") + } + classBTypeFromClassNode(classNode) } /** @@ -141,11 +147,15 @@ 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) getOrElse { + assertionError(s"Could not find bytecode for class ${innerClassNode.name}") + } + classNodeForInnerClass.outerClass == classNode.name + }) } val nestedClasses: List[ClassBType] = classNode.innerClasses.asScala.collect({ diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala index 9ed7b3174b..d94bd77851 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala @@ -36,7 +36,7 @@ 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, recordPerRunCache(collection.concurrent.TrieMap.empty)) val inliner: Inliner[this.type] = new Inliner(this) 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 b3ac06877b..ea4dd0c032 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala @@ -25,21 +25,23 @@ 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. + * For Java classes in mixed compilation, the map contains `None`: there is no + * ClassNode 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 classes: collection.concurrent.Map[InternalName, Option[(ClassNode, Source)]]) { /** * 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): Option[(ClassNode, Source)] = { + classes.getOrElseUpdate(internalName, parseClass(internalName).map((_, Classfile))) } /** * 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): Option[ClassNode] = classNodeAndSource(internalName).map(_._1) /** * The field node for a field matching `name` and `descriptor`, accessed in class `classInternalName`. @@ -48,10 +50,10 @@ class ByteCodeRepository(val classPath: ClassFileLookup[AbstractFile], val class * @return The [[FieldNode]] of the requested field and the [[InternalName]] of its declaring class. */ 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)) - } + classNode(classInternalName).flatMap(c => + c.fields.asScala.find(f => f.name == name && f.desc == descriptor).map((_, classInternalName)) orElse { + Option(c.superName).flatMap(n => fieldNode(n, name, descriptor)) + }) } /** @@ -64,16 +66,16 @@ class ByteCodeRepository(val classPath: ClassFileLookup[AbstractFile], val class // In a MethodInsnNode, the `owner` field may be an array descriptor, for exmple when invoking `clone`. if (ownerInternalNameOrArrayDescriptor.charAt(0) == '[') None else { - val c = classNode(ownerInternalNameOrArrayDescriptor) - c.methods.asScala.find(m => m.name == name && m.desc == descriptor).map((_, ownerInternalNameOrArrayDescriptor)) orElse { - val parents = Option(c.superName) ++ c.interfaces.asScala - // `view` to stop at the first result - parents.view.flatMap(methodNode(_, name, descriptor)).headOption - } + classNode(ownerInternalNameOrArrayDescriptor).flatMap(c => + c.methods.asScala.find(m => m.name == name && m.desc == descriptor).map((_, ownerInternalNameOrArrayDescriptor)) orElse { + val parents = Option(c.superName) ++ c.interfaces.asScala + // `view` to stop at the first result + parents.view.flatMap(methodNode(_, name, descriptor)).headOption + }) } } - private def parseClass(internalName: InternalName): ClassNode = { + private def parseClass(internalName: InternalName): Option[ClassNode] = { val fullName = internalName.replace('/', '.') classPath.findClassFile(fullName) map { classFile => val classNode = new asm.tree.ClassNode() @@ -90,8 +92,6 @@ 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.") } } } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala index bfaa67004c..ac40ab8904 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala @@ -34,27 +34,30 @@ class CallGraph[BT <: BTypes](val btypes: BT) { methodNode.instructions.iterator.asScala.collect({ case call: MethodInsnNode => // TODO: log an inliner warning if the callee method cannot be found in the code repo? eg it's not on the classpath. - val callee = byteCodeRepository.methodNode(call.owner, call.name, call.desc) map { + val callee = byteCodeRepository.methodNode(call.owner, call.name, call.desc) flatMap { case (method, declarationClass) => - val (declarationClassNode, source) = byteCodeRepository.classNodeAndSource(declarationClass) - val declarationClassBType = classBTypeFromClassNode(declarationClassNode) - val methodSignature = method.name + method.desc - val (safeToInline, annotatedInline, annotatedNoInline) = declarationClassBType.info.inlineInfos.get(methodSignature) match { - case Some(inlineInfo) => - val canInlineFromSource = inlineGlobalEnabled || source == ByteCodeRepository.CompilationUnit - // TODO: for now, we consider a callee safeToInline only if it's final - // type analysis can render more calls safeToInline (e.g. when the precise receiver type is known) - (canInlineFromSource && inlineInfo.effectivelyFinal, Some(inlineInfo.annotatedInline), Some(inlineInfo.annotatedNoInline)) - case None => - (false, None, None) + // TODO: log inliner warning if callee decl class cannot be found? + byteCodeRepository.classNodeAndSource(declarationClass) map { + case (declarationClassNode, source) => + val declarationClassBType = classBTypeFromClassNode(declarationClassNode) + val methodSignature = method.name + method.desc + val (safeToInline, annotatedInline, annotatedNoInline) = declarationClassBType.info.inlineInfos.get(methodSignature) match { + case Some(inlineInfo) => + val canInlineFromSource = inlineGlobalEnabled || source == ByteCodeRepository.CompilationUnit + // TODO: for now, we consider a callee safeToInline only if it's final + // type analysis can render more calls safeToInline (e.g. when the precise receiver type is known) + (canInlineFromSource && inlineInfo.effectivelyFinal, Some(inlineInfo.annotatedInline), Some(inlineInfo.annotatedNoInline)) + case None => + (false, None, None) + } + Callee( + callee = method, + calleeDeclarationClass = declarationClassBType, + safeToInline = safeToInline, + annotatedInline = annotatedInline, + annotatedNoInline = annotatedNoInline + ) } - Callee( - callee = method, - calleeDeclarationClass = declarationClassBType, - safeToInline = safeToInline, - annotatedInline = annotatedInline, - annotatedNoInline = annotatedNoInline - ) } val argInfos = if (callee.isEmpty) Nil else { diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/OptimizerReporting.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/OptimizerReporting.scala index a918e13534..53c00c7724 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/OptimizerReporting.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/OptimizerReporting.scala @@ -21,6 +21,7 @@ object OptimizerReporting { classInternalName + "::" + method.name + method.desc } + // TODO: clean up reporting of the inliner, test inline failure warnings, etc def inlineFailure(reason: String): Nothing = MissingRequirementError.signal(reason) def assertionError(message: String): Nothing = throw new AssertionError(message) } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala index 5946f50f0c..69bd92b4ba 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala @@ -70,7 +70,7 @@ class CallGraphTest { // 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 List(cCls, cMod, dCls, testCls) = compile(code).map(c => byteCodeRepository.classNode(c.name)) + val List(cCls, cMod, dCls, testCls) = compile(code).map(c => byteCodeRepository.classNode(c.name).get) 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) diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala index 36f297767e..40dc990c0f 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala @@ -31,7 +31,7 @@ class InlinerIllegalAccessTest extends ClearAfterClass { val compiler = InlinerIllegalAccessTest.compiler import compiler.genBCode.bTypes._ - def addToRepo(cls: List[ClassNode]): Unit = for (c <- cls) byteCodeRepository.classes(c.name) = (c, ByteCodeRepository.Classfile) + def addToRepo(cls: List[ClassNode]): Unit = for (c <- cls) byteCodeRepository.classes(c.name) = Some((c, ByteCodeRepository.Classfile)) def assertEmpty(ins: Option[AbstractInsnNode]) = for (i <- ins) throw new AssertionError(textify(i)) @Test diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala index 819252841e..240d106f5c 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -47,7 +47,7 @@ class InlinerTest extends ClearAfterClass { def checkCallsite(callsite: callGraph.Callsite, callee: MethodNode) = { assert(callsite.callsiteMethod.instructions.contains(callsite.callsiteInstruction), instructionsFromMethod(callsite.callsiteMethod)) - val callsiteClassNode = byteCodeRepository.classNode(callsite.callsiteClass.internalName) + 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) -- cgit v1.2.3 From 37c91654433a12249ae125b9454ba17cef103327 Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Mon, 19 Jan 2015 15:16:23 +0100 Subject: After removing a live handler, need to re-run unreachableCode Building the call graph and running the inliner require all code to be reachable (in terms of ASM's data flow analysis / interpreter). After removing unreachable code, empty exception handlers need to be removed for correctness (see comment in LocalOpt.methodOptimizations). Removing a live exception handler renders its handler unreachable and therefore requires running another round of removing unreachable code. --- .../scala/tools/nsc/backend/jvm/opt/LocalOpt.scala | 33 ++++++++++++++-------- .../scala/tools/nsc/settings/ScalaSettings.scala | 4 +-- 2 files changed, 22 insertions(+), 15 deletions(-) (limited to 'src/compiler') 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 8c5a31658c..f6cfc5598b 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -65,17 +65,25 @@ class LocalOpt(settings: ScalaSettings) { * * 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 - * depends on this property. + * 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 - val (codeRemoved, _) = removeUnreachableCodeImpl(method, ownerClassName) - if (codeRemoved) { - // Required for correctness, see comment in class LocalOpt - removeEmptyExceptionHandlers(method) - removeUnusedLocalVariableNodes(method)() + // 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 } @@ -134,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) @@ -148,12 +154,15 @@ 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) // also removes unused diff --git a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala index 9674b4cfae..43b634eee1 100644 --- a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala @@ -218,7 +218,6 @@ 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.") @@ -230,7 +229,7 @@ trait ScalaSettings extends AbsScalaSettings 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, inlineProject) @@ -249,7 +248,6 @@ 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) -- cgit v1.2.3 From 4e982451decdc3821febfe975e1b8e406a3741e8 Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Tue, 20 Jan 2015 23:20:26 +0100 Subject: Don't crash the inliner in mixed compilation In mixed compilation, the bytecode of Java classes is not availalbe: the Scala compiler does not produce any, and there are no classfiles yet. When inlining a (Scala defined) method that contains an invocation to a Java method, we need the Java method's bytecode in order to check whether that invocation can be transplanted to the new location without causing an IllegalAccessError. If the bytecode cannot be found, inlining won't be allowed. --- .../scala/tools/nsc/backend/jvm/BTypes.scala | 60 ++++++++++++---------- .../scala/tools/nsc/backend/jvm/opt/Inliner.scala | 41 +++++++++++---- test/files/run/bcodeInlinerMixed.flags | 1 + test/files/run/bcodeInlinerMixed/A_1.java | 3 ++ test/files/run/bcodeInlinerMixed/B_1.scala | 20 ++++++++ test/files/run/bcodeInlinerMixed/Test.scala | 16 ++++++ .../backend/jvm/opt/BTypesFromClassfileTest.scala | 2 +- .../backend/jvm/opt/InlinerIllegalAccessTest.scala | 4 +- .../tools/nsc/backend/jvm/opt/InlinerTest.scala | 43 ++++++++++++++-- 9 files changed, 146 insertions(+), 44 deletions(-) create mode 100644 test/files/run/bcodeInlinerMixed.flags create mode 100644 test/files/run/bcodeInlinerMixed/A_1.java create mode 100644 test/files/run/bcodeInlinerMixed/B_1.scala create mode 100644 test/files/run/bcodeInlinerMixed/Test.scala (limited to 'src/compiler') diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala index f07c7b7764..e617c86b23 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala @@ -87,32 +87,29 @@ abstract class BTypes { * * 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))) + def bTypeForDescriptorOrInternalNameFromClassfile(desc: String): Option[BType] = (desc(0): @switch) match { + case 'V' => Some(UNIT) + case 'Z' => Some(BOOL) + case 'C' => Some(CHAR) + case 'B' => Some(BYTE) + case 'S' => Some(SHORT) + case 'I' => Some(INT) + case 'F' => Some(FLOAT) + case 'J' => Some(LONG) + case 'D' => Some(DOUBLE) + case '[' => bTypeForDescriptorOrInternalNameFromClassfile(desc.substring(1)) map ArrayBType 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]]. Returns `None` if the + * classfile cannot be found in the `byteCodeRepository`. */ - def classBTypeFromParsedClassfile(internalName: InternalName): ClassBType = { - val classNode = byteCodeRepository.classNode(internalName) getOrElse { - // There's no way out, we need the ClassBType. I (lry) only know one case byteCodeRepository.classNode - // returns None: for Java classes in mixed compilation. In this case we should not end up here, - // because there exists a symbol for that Java class, the ClassBType should be built from the symbol. - assertionError(s"Could not find bytecode for class $internalName") + def classBTypeFromParsedClassfile(internalName: InternalName): Option[ClassBType] = { + classBTypeFromInternalName.get(internalName) orElse { + byteCodeRepository.classNode(internalName) map classBTypeFromClassNode } - classBTypeFromClassNode(classNode) } /** @@ -120,20 +117,31 @@ 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 = { + def ensureClassBTypeFromParsedClassfile(internalName: InternalName): ClassBType = { + classBTypeFromParsedClassfile(internalName) getOrElse { + // When building a ClassBType from a parsed classfile, we need the ClassBTypes for all + // referenced types. + // TODO: make this more robust with respect to incomplete classpaths. + // Maybe not those parts of the ClassBType that require the missing class are not actually + // queried during the backend, so every part of a ClassBType that requires parsing a + // (potentially missing) classfile should be computed lazily. + assertionError(s"Could not find bytecode for class $internalName") + } + } val superClass = classNode.superName match { case null => assert(classNode.name == ObjectReference.internalName, s"class with missing super type: ${classNode.name}") None case superName => - Some(classBTypeFromParsedClassfile(superName)) + Some(ensureClassBTypeFromParsedClassfile(superName)) } - val interfaces: List[ClassBType] = classNode.interfaces.asScala.map(classBTypeFromParsedClassfile)(collection.breakOut) + val interfaces: List[ClassBType] = classNode.interfaces.asScala.map(ensureClassBTypeFromParsedClassfile)(collection.breakOut) val flags = classNode.access @@ -159,7 +167,7 @@ abstract class BTypes { } val nestedClasses: List[ClassBType] = classNode.innerClasses.asScala.collect({ - case i if nestedInCurrentClass(i) => classBTypeFromParsedClassfile(i.name) + case i if nestedInCurrentClass(i) => ensureClassBTypeFromParsedClassfile(i.name) })(collection.breakOut) // if classNode is a nested class, it has an innerClass attribute for itself. in this @@ -169,11 +177,11 @@ abstract class BTypes { val enclosingClass = if (innerEntry.outerName != null) { // if classNode is a member class, the outerName is non-null - classBTypeFromParsedClassfile(innerEntry.outerName) + ensureClassBTypeFromParsedClassfile(innerEntry.outerName) } else { // for anonymous or local classes, the outerName is null, but the enclosing class is // stored in the EnclosingMethod attribute (which ASM encodes in classNode.outerClass). - classBTypeFromParsedClassfile(classNode.outerClass) + ensureClassBTypeFromParsedClassfile(classNode.outerClass) } val staticFlag = (innerEntry.access & Opcodes.ACC_STATIC) != 0 NestedInfo(enclosingClass, Option(innerEntry.outerName), Option(innerEntry.innerName), staticFlag) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala index b74c7ba86c..2ca8e8b8c4 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala @@ -353,6 +353,11 @@ class Inliner[BT <: BTypes](val btypes: BT) { } } + /** + * Returns the first instruction in the `instructions` list that would cause a + * [[java.lang.IllegalAccessError]] when inlined into the `destinationClass`. Returns `None` if + * all instructions can be legally transplanted. + */ def findIllegalAccess(instructions: InsnList, destinationClass: ClassBType): Option[AbstractInsnNode] = { /** @@ -413,36 +418,50 @@ class Inliner[BT <: BTypes](val btypes: BT) { } } + /** + * 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. + */ def isLegal(instruction: AbstractInsnNode): 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)) + bTypeForDescriptorOrInternalNameFromClassfile(ti.desc).exists(classIsAccessible(_)) case ma: MultiANewArrayInsnNode => // "a symbolic reference to a class, array, or interface type" - classIsAccessible(bTypeForDescriptorOrInternalNameFromClassfile(ma.desc)) + bTypeForDescriptorOrInternalNameFromClassfile(ma.desc).exists(classIsAccessible(_)) case fi: FieldInsnNode => - val fieldRefClass = classBTypeFromParsedClassfile(fi.owner) - val (fieldNode, fieldDeclClass) = byteCodeRepository.fieldNode(fieldRefClass.internalName, fi.name, fi.desc).get - memberIsAccessible(fieldNode.access, classBTypeFromParsedClassfile(fieldDeclClass), fieldRefClass) + (for { + fieldRefClass <- classBTypeFromParsedClassfile(fi.owner) + (fieldNode, fieldDeclClassNode) <- byteCodeRepository.fieldNode(fieldRefClass.internalName, fi.name, fi.desc) + fieldDeclClass <- classBTypeFromParsedClassfile(fieldDeclClassNode) + } yield { + memberIsAccessible(fieldNode.access, fieldDeclClass, fieldRefClass) + }) getOrElse false case mi: MethodInsnNode => if (mi.owner.charAt(0) == '[') true // array methods are accessible - else { - val methodRefClass = classBTypeFromParsedClassfile(mi.owner) - val (methodNode, methodDeclClass) = byteCodeRepository.methodNode(methodRefClass.internalName, mi.name, mi.desc).get - memberIsAccessible(methodNode.access, classBTypeFromParsedClassfile(methodDeclClass), methodRefClass) - } + else (for { + methodRefClass <- classBTypeFromParsedClassfile(mi.owner) + (methodNode, methodDeclClassNode) <- byteCodeRepository.methodNode(methodRefClass.internalName, mi.name, mi.desc) + methodDeclClass <- classBTypeFromParsedClassfile(methodDeclClassNode) + } yield { + memberIsAccessible(methodNode.access, methodDeclClass, methodRefClass) + }) getOrElse false case ivd: InvokeDynamicInsnNode => // TODO @lry check necessary conditions to inline an indy, instead of giving up false case ci: LdcInsnNode => ci.cst match { - case t: asm.Type => classIsAccessible(bTypeForDescriptorOrInternalNameFromClassfile(t.getInternalName)) + case t: asm.Type => bTypeForDescriptorOrInternalNameFromClassfile(t.getInternalName).exists(classIsAccessible(_)) case _ => true } 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/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala index 65c96226ff..f7c9cab284 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala @@ -90,7 +90,7 @@ class BTypesFromClassfileTest { clearCache() val fromSymbol = classBTypeFromSymbol(classSym) clearCache() - val fromClassfile = bTypes.classBTypeFromParsedClassfile(fromSymbol.internalName) + val fromClassfile = bTypes.classBTypeFromParsedClassfile(fromSymbol.internalName).get sameBType(fromSymbol, fromClassfile) } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala index 40dc990c0f..ef0f6bcd77 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala @@ -59,7 +59,7 @@ class InlinerIllegalAccessTest extends ClearAfterClass { def check(classNode: ClassNode, test: Option[AbstractInsnNode] => Unit) = { for (m <- methods) - test(inliner.findIllegalAccess(m.instructions, classBTypeFromParsedClassfile(classNode.name))) + test(inliner.findIllegalAccess(m.instructions, classBTypeFromParsedClassfile(classNode.name).get)) } check(cClass, assertEmpty) @@ -153,7 +153,7 @@ class InlinerIllegalAccessTest extends ClearAfterClass { val List(rbD, rcD, rfD, rgD) = dCl.methods.asScala.toList.filter(_.name(0) == 'r').sortBy(_.name) def check(method: MethodNode, dest: ClassNode, test: Option[AbstractInsnNode] => Unit): Unit = { - test(inliner.findIllegalAccess(method.instructions, classBTypeFromParsedClassfile(dest.name))) + test(inliner.findIllegalAccess(method.instructions, classBTypeFromParsedClassfile(dest.name).get)) } val cOrDOwner = (_: Option[AbstractInsnNode] @unchecked) match { diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala index 240d106f5c..4e7a2399a2 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -6,12 +6,15 @@ 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.BasicAnalyzer +import scala.tools.nsc.io._ import scala.tools.testing.AssertUtil._ import CodeGenTools._ @@ -57,7 +60,7 @@ class InlinerTest extends ClearAfterClass { def inlineTest(code: String, mod: ClassNode => Unit = _ => ()): (MethodNode, Option[String]) = { val List(cls) = compile(code) mod(cls) - val clsBType = classBTypeFromParsedClassfile(cls.name) + val clsBType = classBTypeFromParsedClassfile(cls.name).get 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() @@ -191,8 +194,8 @@ class InlinerTest extends ClearAfterClass { val List(c, d) = compile(code) - val cTp = classBTypeFromParsedClassfile(c.name) - val dTp = classBTypeFromParsedClassfile(d.name) + val cTp = classBTypeFromParsedClassfile(c.name).get + val dTp = classBTypeFromParsedClassfile(d.name).get val g = c.methods.asScala.find(_.name == "g").get val h = d.methods.asScala.find(_.name == "h").get @@ -350,7 +353,7 @@ class InlinerTest extends ClearAfterClass { 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 clsBType = classBTypeFromParsedClassfile(c.name).get val analyzer = new BasicAnalyzer(f, clsBType.internalName) val integerClassBType = classBTypeFromInternalName("java/lang/Integer") @@ -414,4 +417,36 @@ class InlinerTest extends ClearAfterClass { // 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 + + InlinerTest.notPerRun.foreach(_.clear()) + compiler.reporter.reset() + compiler.settings.outputDirs.setSingleOutput(new VirtualDirectory("(memory)", None)) + val run = new compiler.Run() + run.compileSources(List(new BatchSourceFile("A.java", javaCode), new BatchSourceFile("B.scala", scalaCode))) + val outDir = compiler.settings.outputDirs.getSingleOutput.get + + val List(b) = outDir.iterator.map(f => AsmUtils.readClass(f.toByteArray)).toList.sortBy(_.name) + val ins = getSingleMethod(b, "g").instructions + val invokeFlop = Invoke(INVOKEVIRTUAL, "B", "flop", "()I", false) + assert(ins contains invokeFlop, ins mkString "\n") + } } -- cgit v1.2.3 From 027e97981d9b6a3783e9ab247cc898017b3de821 Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Wed, 11 Mar 2015 11:34:08 -0700 Subject: Workaround for SI-9111 The inliner forces some method symbols to complete that would not be completed otherwise. This triggers SI-9111, in which the completer of a valid Java method definition reports an error in mixed compilation. The workaround disables error reporting while completing lazy method and class symbols in the backend. --- .../scala/tools/nsc/backend/jvm/BTypes.scala | 6 +- .../tools/nsc/backend/jvm/BTypesFromSymbols.scala | 69 +++++++++++++++++----- .../nsc/backend/jvm/opt/OptimizerReporting.scala | 1 + test/files/pos/t9111-inliner-workaround.flags | 1 + test/files/pos/t9111-inliner-workaround/A_1.java | 13 ++++ .../pos/t9111-inliner-workaround/Test_1.scala | 10 ++++ 6 files changed, 84 insertions(+), 16 deletions(-) create mode 100644 test/files/pos/t9111-inliner-workaround.flags create mode 100644 test/files/pos/t9111-inliner-workaround/A_1.java create mode 100644 test/files/pos/t9111-inliner-workaround/Test_1.scala (limited to 'src/compiler') diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala index e617c86b23..81d8adb7de 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala @@ -12,7 +12,7 @@ import asm.Opcodes import scala.tools.asm.tree.{InnerClassNode, ClassNode} import scala.tools.nsc.backend.jvm.BTypes.{MethodInlineInfo, InlineInfo} import scala.tools.nsc.backend.jvm.opt.{CallGraph, ByteCodeRepository, Inliner} -import OptimizerReporting._ +import opt.OptimizerReporting._ import scala.collection.convert.decorateAsScala._ /** @@ -736,8 +736,10 @@ 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. + * + * Currently non-final due to SI-9111 */ - final case class ClassBType(internalName: InternalName) extends RefBType { + /*final*/ case class ClassBType(internalName: InternalName) extends RefBType { /** * Write-once variable allows initializing a cyclic graph of infos. This is required for * nested classes. Example: for the definition `class A { class B }` we have diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala index d94bd77851..9fdb92b47c 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala @@ -146,13 +146,48 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { 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. - setClassInfo(classSym, ClassBType(internalName)) + if (completeSilentlyAndCheckErroneous(classSym)) { + new ErroneousClassBType(internalName) + } else { + // 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)) + } }) } } + /** + * Part of the workaround for SI-9111. Makes sure that the compiler only fails if the ClassInfo + * of the symbol that could not be completed is actually required. + */ + private class ErroneousClassBType(internalName: InternalName) extends ClassBType(internalName) { + def msg = s"The class info for $internalName could not be completed due to SI-9111." + override def info: ClassInfo = opt.OptimizerReporting.assertionError(msg) + override def info_=(i: ClassInfo): Unit = opt.OptimizerReporting.assertionError(msg) + } + + /** + * This is a hack to work around SI-9111. The completer of `methodSym` may report type errors. We + * cannot change the typer context of the completer at this point and make it silent: the context + * captured when creating the completer in the namer. However, we can temporarily replace + * global.reporter (it's a var) to store errors. + */ + def completeSilentlyAndCheckErroneous(sym: Symbol): Boolean = { + if (sym.rawInfo.isComplete) false + else { + val originalReporter = global.reporter + val storeReporter = new reporters.StoreReporter() + try { + global.reporter = storeReporter + sym.info + } finally { + global.reporter = originalReporter + } + storeReporter.infos.exists(_.severity == storeReporter.ERROR) + } + } + /** * Builds a [[MethodBType]] for a method symbol. */ @@ -428,18 +463,24 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { else { // Primitve methods cannot be inlined, so there's no point in building an InlineInfo. Also, some // primitive methods (e.g., `isInstanceOf`) have non-erased types, which confuses [[typeToBType]]. - classSym.info.decls.iterator.filter(m => m.isMethod && !scalaPrimitives.isPrimitive(m)).map({ + classSym.info.decls.iterator.filter(m => m.isMethod && !scalaPrimitives.isPrimitive(m)).flatMap({ case methodSym => - val methodBType = methodBTypeFromSymbol(methodSym) - val name = methodSym.javaSimpleName.toString // same as in genDefDef - val signature = name + methodBType.descriptor - val info = MethodInlineInfo( - effectivelyFinal = methodSym.isEffectivelyFinalOrNotOverridden, - traitMethodWithStaticImplementation = false, // temporary, fixed in future commit - annotatedInline = methodSym.hasAnnotation(ScalaInlineClass), - annotatedNoInline = methodSym.hasAnnotation(ScalaNoInlineClass) - ) - (signature, info) + if (completeSilentlyAndCheckErroneous(methodSym)) { + // Happens due to SI-9111. Just don't provide any InlineInfo for that method, we don't + // need fail the compiler. + None + } else { + val methodBType = methodBTypeFromSymbol(methodSym) + val name = methodSym.javaSimpleName.toString // same as in genDefDef + val signature = name + methodBType.descriptor + val info = MethodInlineInfo( + effectivelyFinal = methodSym.isEffectivelyFinalOrNotOverridden, + traitMethodWithStaticImplementation = false, // temporary, fixed in future commit + annotatedInline = methodSym.hasAnnotation(ScalaInlineClass), + annotatedNoInline = methodSym.hasAnnotation(ScalaNoInlineClass) + ) + Some((signature, info)) + } }).toMap } } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/OptimizerReporting.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/OptimizerReporting.scala index 53c00c7724..5b47bc88c2 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/OptimizerReporting.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/OptimizerReporting.scala @@ -5,6 +5,7 @@ package scala.tools.nsc package backend.jvm +package opt import scala.tools.asm import asm.tree._ 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() +} -- cgit v1.2.3 From f8bb3d5289e5eb84ccd94386e5c3df1bdf8b91bc Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Wed, 11 Mar 2015 12:09:21 -0700 Subject: Inline final methods defined in traits In order to inline a final trait method, callsites of such methods are first re-written from interface calls to static calls of the trait's implementation class. Then inlining proceeds as ususal. One problem that came up during development was that mixin methods are added to class symbols only for classes being compiled, but not for others. In order to inline a mixin method, we need the InlineInfo, which so far was built using the class (and method) symbols. So we had a problem with separate compilation. Looking up the symbol from a given classfile name was already known to be brittle (it's also one of the weak points of the current inliner), so we changed the strategy. Now the InlineInfo for every class is encoded in a new classfile attribute. This classfile attribute is relatively small, because all strings it references (class internal names, method names, method descriptors) would exist anyway in the constant pool, so it just adds a few references. When building the InlineInfo for a class symbol, we only look at the symbol properties for symbols being compiled in the current run. For unpickled symbols, we build the InlineInfo by reading the classfile attribute. This change also adds delambdafy:method classes to currentRun.symSource. Otherwise, currentRun.compiles(lambdaClass) is false. --- .../tools/nsc/backend/jvm/BCodeSkelBuilder.scala | 6 +- .../scala/tools/nsc/backend/jvm/BTypes.scala | 76 +++-- .../tools/nsc/backend/jvm/BTypesFromSymbols.scala | 134 +++------ .../nsc/backend/jvm/opt/ByteCodeRepository.scala | 8 +- .../tools/nsc/backend/jvm/opt/BytecodeUtils.scala | 4 + .../tools/nsc/backend/jvm/opt/CallGraph.scala | 69 ++++- .../scala/tools/nsc/backend/jvm/opt/Inliner.scala | 81 ++++- .../scala/tools/nsc/transform/Delambdafy.scala | 2 + .../scala/tools/nsc/backend/jvm/CodeGenTools.scala | 64 +++- .../tools/nsc/backend/jvm/DirectCompileTest.scala | 13 + .../backend/jvm/opt/BTypesFromClassfileTest.scala | 13 +- .../tools/nsc/backend/jvm/opt/CallGraphTest.scala | 4 +- .../jvm/opt/CompactLocalVariablesTest.scala | 4 +- .../tools/nsc/backend/jvm/opt/InlineInfoTest.scala | 65 +++++ .../jvm/opt/InlinerSeparateCompilationTest.scala | 114 ++++++++ .../tools/nsc/backend/jvm/opt/InlinerTest.scala | 325 +++++++++++++++++++-- .../nsc/backend/jvm/opt/UnreachableCodeTest.scala | 4 +- test/junit/scala/tools/testing/TempDir.scala | 18 ++ 18 files changed, 805 insertions(+), 199 deletions(-) create mode 100644 test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala create mode 100644 test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala create mode 100644 test/junit/scala/tools/testing/TempDir.scala (limited to 'src/compiler') diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala index 32a421c570..e40e928761 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala @@ -103,6 +103,8 @@ abstract class BCodeSkelBuilder extends BCodeHelpers { isCZRemote = isRemote(claszSymbol) thisName = internalName(claszSymbol) + val classBType = classBTypeFromSymbol(claszSymbol) + cnode = new asm.tree.ClassNode() initJClass(cnode) @@ -120,10 +122,12 @@ abstract class BCodeSkelBuilder extends BCodeHelpers { addClassFields() - innerClassBufferASM ++= classBTypeFromSymbol(claszSymbol).info.nestedClasses + innerClassBufferASM ++= classBType.info.nestedClasses gen(cd.impl) addInnerClassesASM(cnode, innerClassBufferASM.toList) + cnode.visitAttribute(classBType.inlineInfoAttribute) + if (AsmUtils.traceClassEnabled && cnode.name.contains(AsmUtils.traceClassPattern)) AsmUtils.traceClass(cnode) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala index 81d8adb7de..51a17b7fe4 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala @@ -10,8 +10,8 @@ import scala.annotation.switch import scala.tools.asm import asm.Opcodes import scala.tools.asm.tree.{InnerClassNode, ClassNode} -import scala.tools.nsc.backend.jvm.BTypes.{MethodInlineInfo, InlineInfo} -import scala.tools.nsc.backend.jvm.opt.{CallGraph, ByteCodeRepository, Inliner} +import scala.tools.nsc.backend.jvm.BTypes.{InlineInfo, MethodInlineInfo} +import scala.tools.nsc.backend.jvm.opt._ import opt.OptimizerReporting._ import scala.collection.convert.decorateAsScala._ @@ -47,6 +47,9 @@ abstract class BTypes { // When building the call graph, we need to know if global inlining is allowed (the component doesn't have a global) def inlineGlobalEnabled: Boolean + // When the inliner is not enabled, there's no point in adding InlineInfos to all ClassBTypes + def inlinerEnabled: Boolean + /** * A map from internal names to ClassBTypes. Every ClassBType is added to this map on its * construction. @@ -60,23 +63,6 @@ abstract class BTypes { */ val classBTypeFromInternalName: collection.concurrent.Map[InternalName, ClassBType] = recordPerRunCache(collection.concurrent.TrieMap.empty[InternalName, ClassBType]) - /** - * Build the [[InlineInfo]] for the methods of a class, given its internal name. - * - * The InlineInfo is part of the ClassBType's [[ClassInfo]]. Note that there are two ways to build - * a ClassBType: from a class symbol (methods in [[BTypesFromSymbols]]) or from a [[ClassNode]]. - * The InlineInfo however contains information that can only be retrieved from the symbol of - * the class (e.g., is a method annotated @inline). - * - * This method (implemented in [[BTypesFromSymbols]]) looks up the class symbol in the symbol - * table, using the classfile name of the class. - * - * The method tries to undo some of the name mangling, but the lookup does not succeed for all - * classes. In case it fails, the resulting ClassBType will simply not have an InlineInfo, and - * we won't be able to inline its methods. - */ - def inlineInfosFromSymbolLookup(internalName: InternalName): Map[String, MethodInlineInfo] - /** * Obtain the BType for a type descriptor or internal name. For class descriptors, the ClassBType * is constructed by parsing the corresponding classfile. @@ -187,10 +173,52 @@ abstract class BTypes { NestedInfo(enclosingClass, Option(innerEntry.outerName), Option(innerEntry.innerName), staticFlag) } - classBType.info = ClassInfo(superClass, interfaces, flags, nestedClasses, nestedInfo, inlineInfosFromSymbolLookup(classBType.internalName)) + val inlineInfo = inlineInfoFromClassfile(classNode) + + classBType.info = ClassInfo(superClass, interfaces, flags, nestedClasses, nestedInfo, inlineInfo) classBType } + /** + * Build the InlineInfo for a class. For Scala classes, the information is stored in the + * ScalaInlineInfo attribute. If the attribute is missing, the InlineInfo is built using the + * metadata available in the classfile (ACC_FINAL flags, etc). + */ + def inlineInfoFromClassfile(classNode: ClassNode): InlineInfo = { + def fromClassfileAttribute: Option[InlineInfo] = { + // TODO: if this is a scala class and there's no attribute, emit an inliner warning if the InlineInfo is used + if (classNode.attrs == null) None + else classNode.attrs.asScala.collect({ case a: InlineInfoAttribute => a}).headOption.map(_.inlineInfo) + } + + def fromClassfileWithoutAttribute = { + // when building MethodInlineInfos for the members of a ClassSymbol, we exclude those methods + // in scalaPrimitives. This is necessary because some of them have non-erased types, which would + // require special handling. Excluding is OK because they are never inlined. + // Here we are parsing from a classfile and we don't need to do anything special. Many of these + // primitives don't even exist, for example Any.isInstanceOf. + val methodInfos = classNode.methods.asScala.map(methodNode => { + val info = MethodInlineInfo( + effectivelyFinal = BytecodeUtils.isFinalMethod(methodNode), + traitMethodWithStaticImplementation = false, + annotatedInline = false, + annotatedNoInline = false) + (methodNode.name + methodNode.desc, info) + }).toMap + InlineInfo( + traitImplClassSelfType = None, + isEffectivelyFinal = BytecodeUtils.isFinalClass(classNode), + methodInfos = methodInfos, + warning = None) + } + + // The InlineInfo is built from the classfile (not from the symbol) for all classes that are NOT + // being compiled. For those classes, the info is only needed if the inliner is enabled, othewise + // we can save the memory. + if (!inlinerEnabled) BTypes.EmptyInlineInfo + else fromClassfileAttribute getOrElse fromClassfileWithoutAttribute + } + /** * A BType is either a primitive type, a ClassBType, an ArrayBType of one of these, or a MethodType * referring to BTypes. @@ -831,6 +859,8 @@ abstract class BTypes { ) } + def inlineInfoAttribute: InlineInfoAttribute = InlineInfoAttribute(info.inlineInfo) + def isSubtypeOf(other: ClassBType): Boolean = { if (this == other) return true @@ -945,13 +975,11 @@ abstract class BTypes { * @param nestedClasses Classes nested in this class. Those need to be added to the * InnerClass table, see the InnerClass spec summary above. * @param nestedInfo If this describes a nested class, information for the InnerClass table. - * @param inlineInfos The [[InlineInfo]]s for the methods declared in this class. The map is - * indexed by the string s"$name$descriptor" (to disambiguate overloads). - * Entries may be missing, see comment on [[inlineInfosFromSymbolLookup]]. + * @param inlineInfo Information about this class for the inliner. */ final case class ClassInfo(superClass: Option[ClassBType], interfaces: List[ClassBType], flags: Int, nestedClasses: List[ClassBType], nestedInfo: Option[NestedInfo], - inlineInfos: Map[String, MethodInlineInfo]) + inlineInfo: InlineInfo) /** * Information required to add a class to an InnerClass table. diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala index 9fdb92b47c..a217e54ed8 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala @@ -7,10 +7,8 @@ package scala.tools.nsc package backend.jvm import scala.tools.asm -import scala.tools.asm.tree.ClassNode -import scala.tools.nsc.backend.jvm.opt.ByteCodeRepository.Source import scala.tools.nsc.backend.jvm.opt.{CallGraph, Inliner, ByteCodeRepository} -import scala.tools.nsc.backend.jvm.BTypes.{MethodInlineInfo, InlineInfo, InternalName} +import scala.tools.nsc.backend.jvm.BTypes.{InlineInfo, MethodInlineInfo, InternalName} /** * This class mainly contains the method classBTypeFromSymbol, which extracts the necessary @@ -42,48 +40,6 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { val callGraph: CallGraph[this.type] = new CallGraph(this) - /** - * See doc in [[BTypes.inlineInfosFromSymbolLookup]]. - * TODO: once the optimzier uses parallelism, lock before symbol table accesses - */ - def inlineInfosFromSymbolLookup(internalName: InternalName): Map[String, MethodInlineInfo] = { - val name = internalName.replace('/', '.') - - // TODO: de-mangle more class names - - def inEmptyPackage = name.indexOf('.') == -1 - def isModule = name.endsWith("$") - def isTopLevel = { - // TODO: this is conservative, there's also $'s introduced by name mangling, e.g., $colon$colon - // for this, use NameTransformer.decode - if (isModule) name.indexOf('$') == (name.length - 1) - else name.indexOf('$') == -1 - } - - val lookupName = { - if (isModule) newTermName(name.substring(0, name.length - 1)) - else newTypeName(name) - } - - // for now we only try classes that look like top-level - val classSym = if (!isTopLevel) NoSymbol else { - val member = { - if (inEmptyPackage) { - // rootMirror.getClassIfDefined fails for classes / modules in the empty package. - // maybe that should be fixed. - rootMirror.EmptyPackageClass.info.member(lookupName) - } else { - if (isModule) rootMirror.getModuleIfDefined(lookupName) - else rootMirror.getClassIfDefined(lookupName) - } - } - if (isModule) member.moduleClass else member - } - - if (classSym == NoSymbol) Map.empty - else buildInlineInfos(classSym) - } - final def initializeCoreBTypes(): Unit = { coreBTypes.setBTypes(new CoreBTypes[this.type](this)) } @@ -92,6 +48,8 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { def inlineGlobalEnabled: Boolean = settings.YoptInlineGlobal + def inlinerEnabled: Boolean = settings.YoptInlinerEnabled + // helpers that need access to global. // TODO @lry create a separate component, they don't belong to BTypesFromSymbols @@ -167,27 +125,6 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { override def info_=(i: ClassInfo): Unit = opt.OptimizerReporting.assertionError(msg) } - /** - * This is a hack to work around SI-9111. The completer of `methodSym` may report type errors. We - * cannot change the typer context of the completer at this point and make it silent: the context - * captured when creating the completer in the namer. However, we can temporarily replace - * global.reporter (it's a var) to store errors. - */ - def completeSilentlyAndCheckErroneous(sym: Symbol): Boolean = { - if (sym.rawInfo.isComplete) false - else { - val originalReporter = global.reporter - val storeReporter = new reporters.StoreReporter() - try { - global.reporter = storeReporter - sym.info - } finally { - global.reporter = originalReporter - } - storeReporter.infos.exists(_.severity == storeReporter.ERROR) - } - } - /** * Builds a [[MethodBType]] for a method symbol. */ @@ -401,9 +338,9 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { val nestedInfo = buildNestedInfo(classSym) - val inlineInfos = buildInlineInfos(classSym) + val inlineInfo = buildInlineInfo(classSym, classBType.internalName) - classBType.info = ClassInfo(superClass, interfaces, flags, nestedClasses, nestedInfo, inlineInfos) + classBType.info = ClassInfo(superClass, interfaces, flags, nestedClasses, nestedInfo, inlineInfo) classBType } @@ -458,30 +395,39 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { } } - private def buildInlineInfos(classSym: Symbol): Map[String, MethodInlineInfo] = { - if (!settings.YoptInlinerEnabled) Map.empty + /** + * Build the InlineInfo for a ClassBType from the class symbol. + * + * Note that the InlineInfo is only built from the symbolic information for classes that are being + * compiled. For all other classes we delegate to inlineInfoFromClassfile. The reason is that + * mixed-in methods are only added to class symbols being compiled, but not to other classes + * extending traits. Creating the InlineInfo from the symbol would prevent these mixins from being + * inlined. + * + * So for classes being compiled, the InlineInfo is created here and stored in the ScalaInlineInfo + * classfile attribute. + */ + private def buildInlineInfo(classSym: Symbol, internalName: InternalName): InlineInfo = { + def buildFromSymbol = buildInlineInfoFromClassSymbol(classSym, classBTypeFromSymbol(_).internalName, methodBTypeFromSymbol(_).descriptor) + + // phase travel required, see implementation of `compiles`. for nested classes, it checks if the + // enclosingTopLevelClass is being compiled. after flatten, all classes are considered top-level, + // so `compiles` would return `false`. + if (exitingPickler(currentRun.compiles(classSym))) buildFromSymbol else { - // Primitve methods cannot be inlined, so there's no point in building an InlineInfo. Also, some - // primitive methods (e.g., `isInstanceOf`) have non-erased types, which confuses [[typeToBType]]. - classSym.info.decls.iterator.filter(m => m.isMethod && !scalaPrimitives.isPrimitive(m)).flatMap({ - case methodSym => - if (completeSilentlyAndCheckErroneous(methodSym)) { - // Happens due to SI-9111. Just don't provide any InlineInfo for that method, we don't - // need fail the compiler. - None - } else { - val methodBType = methodBTypeFromSymbol(methodSym) - val name = methodSym.javaSimpleName.toString // same as in genDefDef - val signature = name + methodBType.descriptor - val info = MethodInlineInfo( - effectivelyFinal = methodSym.isEffectivelyFinalOrNotOverridden, - traitMethodWithStaticImplementation = false, // temporary, fixed in future commit - annotatedInline = methodSym.hasAnnotation(ScalaInlineClass), - annotatedNoInline = methodSym.hasAnnotation(ScalaNoInlineClass) - ) - Some((signature, info)) - } - }).toMap + // For classes not being compiled, the InlineInfo is read from the classfile attribute. This + // fixes an issue with mixed-in methods: the mixin phase enters mixin methods only to class + // symbols being compiled. For non-compiled classes, we could not build MethodInlineInfos + // for those mixin members, which prevents inlining. + byteCodeRepository.classNode(internalName) match { + case Some(classNode) => + inlineInfoFromClassfile(classNode) + case None => + // TODO: inliner warning if the InlineInfo for that class is being used + // We can still use the inline information built from the symbol, even though mixin + // members will be missing. + buildFromSymbol + } } } @@ -503,7 +449,7 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { flags = asm.Opcodes.ACC_SUPER | asm.Opcodes.ACC_PUBLIC | asm.Opcodes.ACC_FINAL, nestedClasses = nested, nestedInfo = None, - Map.empty // no InlineInfo needed, scala never invokes methods on the mirror class + InlineInfo(None, true, Map.empty, None) // no InlineInfo needed, scala never invokes methods on the mirror class ) c }) @@ -538,8 +484,8 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { } // legacy, to be removed when the @remote annotation gets removed - final def isRemote(s: Symbol) = (s hasAnnotation definitions.RemoteAttr) - final def hasPublicBitSet(flags: Int) = ((flags & asm.Opcodes.ACC_PUBLIC) != 0) + final def isRemote(s: Symbol) = s hasAnnotation definitions.RemoteAttr + final def hasPublicBitSet(flags: Int) = (flags & asm.Opcodes.ACC_PUBLIC) != 0 /** * Return the Java modifiers for the given symbol. diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala index ea4dd0c032..fb58f1b189 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala @@ -10,6 +10,7 @@ package opt import scala.tools.asm import asm.tree._ import scala.collection.convert.decorateAsScala._ +import scala.tools.asm.Attribute import scala.tools.nsc.io.AbstractFile import scala.tools.nsc.util.ClassFileLookup import OptimizerReporting._ @@ -64,6 +65,7 @@ class ByteCodeRepository(val classPath: ClassFileLookup[AbstractFile], val class */ def methodNode(ownerInternalNameOrArrayDescriptor: String, name: String, descriptor: String): Option[(MethodNode, InternalName)] = { // In a MethodInsnNode, the `owner` field may be an array descriptor, for exmple when invoking `clone`. + // We don't inline array methods (they are native anyway), so just return None. if (ownerInternalNameOrArrayDescriptor.charAt(0) == '[') None else { classNode(ownerInternalNameOrArrayDescriptor).flatMap(c => @@ -80,9 +82,13 @@ class ByteCodeRepository(val classPath: ClassFileLookup[AbstractFile], val class classPath.findClassFile(fullName) map { classFile => val classNode = new asm.tree.ClassNode() val classReader = new asm.ClassReader(classFile.toByteArray) + + // Passing the InlineInfoAttributePrototype makes the ClassReader invoke the specific `read` + // method of the InlineInfoAttribute class, instead of putting the byte array into a generic + // Attribute. // We don't need frames when inlining, but we want to keep the local variable table, so we // don't use SKIP_DEBUG. - classReader.accept(classNode, asm.ClassReader.SKIP_FRAMES) + classReader.accept(classNode, Array[Attribute](InlineInfoAttributePrototype), asm.ClassReader.SKIP_FRAMES) // SKIP_FRAMES leaves line number nodes. Remove them because they are not correct after // inlining. // TODO: we need to remove them also for classes that are not parsed from classfiles, why not simplify and do it once when inlining? diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala index 74f46d04f9..e221eef636 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala @@ -83,6 +83,10 @@ object BytecodeUtils { def isSynchronizedMethod(methodNode: MethodNode): Boolean = (methodNode.access & Opcodes.ACC_SYNCHRONIZED) != 0 + def isFinalClass(classNode: ClassNode): Boolean = (classNode.access & Opcodes.ACC_FINAL) != 0 + + def isFinalMethod(methodNode: MethodNode): Boolean = (methodNode.access & (Opcodes.ACC_FINAL | Opcodes.ACC_PRIVATE)) != 0 + def nextExecutableInstruction(instruction: AbstractInsnNode, alsoKeep: AbstractInsnNode => Boolean = Set()): Option[AbstractInsnNode] = { var result = instruction do { result = result.getNext } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala index ac40ab8904..020db738e8 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala @@ -9,7 +9,9 @@ package opt import scala.tools.asm.tree._ import scala.collection.convert.decorateAsScala._ +import scala.tools.nsc.backend.jvm.BTypes.InternalName import scala.tools.nsc.backend.jvm.opt.BytecodeUtils.BasicAnalyzer +import ByteCodeRepository.{Source, CompilationUnit} class CallGraph[BT <: BTypes](val btypes: BT) { import btypes._ @@ -22,6 +24,43 @@ class CallGraph[BT <: BTypes](val btypes: BT) { } def analyzeCallsites(methodNode: MethodNode, definingClass: ClassBType): List[Callsite] = { + + /** + * Analyze a callsite and gather meta-data that can be used for inlining decisions. + * + * @return Three booleans indicating whether + * 1. the callsite can be safely inlined + * 2. the callee is annotated `@inline` + * 3. the callee is annotated `@noinline` + */ + def analyzeCallsite(calleeMethodNode: MethodNode, calleeDeclarationClassBType: ClassBType, receiverTypeInternalName: InternalName, calleeSource: Source): (Boolean, Boolean, Boolean) = { + val methodSignature = calleeMethodNode.name + calleeMethodNode.desc + + // The inlineInfo.methodInfos of a ClassBType holds an InlineInfo for each method *declared* + // within a class (not for inherited methods). Since we already have the classBType of the + // callee, we only check there for the methodInlineInfo, we should find it there. + calleeDeclarationClassBType.info.inlineInfo.methodInfos.find(_._1 == methodSignature) match { + case Some((_, methodInlineInfo)) => + val canInlineFromSource = inlineGlobalEnabled || calleeSource == CompilationUnit + // A non-final method can be inline if the receiver type is a final subclass. Example: + // class A { @inline def f = 1 }; object B extends A; B.f // can be inlined + def isStaticallyResolved: Boolean = { + // TODO: type analysis can render more calls statically resolved + // Example: `new A.f` can be inlined, the receiver type is known to be exactly A. + methodInlineInfo.effectivelyFinal || { + // TODO: inline warning when the receiver class cannot be found on the classpath + classBTypeFromParsedClassfile(receiverTypeInternalName).exists(_.info.inlineInfo.isEffectivelyFinal) + } + } + + (canInlineFromSource && isStaticallyResolved, methodInlineInfo.annotatedInline, methodInlineInfo.annotatedNoInline) + + case None => + // TODO: issue inliner warning + (false, false, false) + } + } + // TODO: run dataflow analyses to make the call graph more precise // - producers to get forwarded parameters (ForwardedParam) // - typeAnalysis for more precise argument types, more precise callee @@ -40,16 +79,7 @@ class CallGraph[BT <: BTypes](val btypes: BT) { byteCodeRepository.classNodeAndSource(declarationClass) map { case (declarationClassNode, source) => val declarationClassBType = classBTypeFromClassNode(declarationClassNode) - val methodSignature = method.name + method.desc - val (safeToInline, annotatedInline, annotatedNoInline) = declarationClassBType.info.inlineInfos.get(methodSignature) match { - case Some(inlineInfo) => - val canInlineFromSource = inlineGlobalEnabled || source == ByteCodeRepository.CompilationUnit - // TODO: for now, we consider a callee safeToInline only if it's final - // type analysis can render more calls safeToInline (e.g. when the precise receiver type is known) - (canInlineFromSource && inlineInfo.effectivelyFinal, Some(inlineInfo.annotatedInline), Some(inlineInfo.annotatedNoInline)) - case None => - (false, None, None) - } + val (safeToInline, annotatedInline, annotatedNoInline) = analyzeCallsite(method, declarationClassBType, call.owner, source) Callee( callee = method, calleeDeclarationClass = declarationClassBType, @@ -81,17 +111,21 @@ class CallGraph[BT <: BTypes](val btypes: BT) { /** * A callsite in the call graph. + * * @param callsiteInstruction The invocation instruction * @param callsiteMethod The method containing the callsite * @param callsiteClass The class containing the callsite - * @param callee The callee. For virtual calls, an override of the callee might be invoked. + * @param callee The callee, as it appears in the invocation instruction. For virtual + * calls, an override of the callee might be invoked. Also, the callee + * can be abstract. `None` if the callee MethodNode cannot be found in + * the bytecode repository. * @param argInfos Information about the invocation receiver and arguments * @param callsiteStackHeight The stack height at the callsite, required by the inliner */ final case class Callsite(callsiteInstruction: MethodInsnNode, callsiteMethod: MethodNode, callsiteClass: ClassBType, callee: Option[Callee], argInfos: List[ArgInfo], callsiteStackHeight: Int) { - override def toString = s"Invocation of ${callsiteInstruction.name + callsiteInstruction.desc}@${callsiteMethod.instructions.indexOf(callsiteInstruction)} in ${callsiteMethod.name}" + override def toString = s"Invocation of ${callee.map(_.calleeDeclarationClass.internalName).getOrElse("?")}.${callsiteInstruction.name + callsiteInstruction.desc}@${callsiteMethod.instructions.indexOf(callsiteInstruction)} in ${callsiteClass.internalName}.${callsiteMethod.name}" } /** @@ -104,14 +138,17 @@ class CallGraph[BT <: BTypes](val btypes: BT) { /** * A callee in the call graph. - * @param callee The called method. For virtual calls, an override may actually be invoked. + * + * @param callee The callee, as it appears in the invocation instruction. For + * virtual calls, an override of the callee might be invoked. Also, + * the callee can be abstract. * @param calleeDeclarationClass The class in which the callee is declared * @param safeToInline True if the callee can be safely inlined: it cannot be overridden, * and the inliner settings (project / global) allow inlining it. - * @param annotatedInline Defined if it is known whether the callee is annotated @inline - * @param annotatedNoInline Defined if it is known whether the callee is annotated @noinline + * @param annotatedInline True if the callee is annotated @inline + * @param annotatedNoInline True if the callee is annotated @noinline */ final case class Callee(callee: MethodNode, calleeDeclarationClass: ClassBType, safeToInline: Boolean, - annotatedInline: Option[Boolean], annotatedNoInline: Option[Boolean]) + annotatedInline: Boolean, annotatedNoInline: Boolean) } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala index 2ca8e8b8c4..970cc6803a 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala @@ -16,7 +16,6 @@ import scala.collection.convert.decorateAsJava._ import AsmUtils._ import BytecodeUtils._ import OptimizerReporting._ -import scala.tools.asm.tree.analysis._ import collection.mutable class Inliner[BT <: BTypes](val btypes: BT) { @@ -24,6 +23,8 @@ class Inliner[BT <: BTypes](val btypes: BT) { import callGraph._ def runInliner(): Unit = { + rewriteFinalTraitMethodInvocations() + for (request <- collectAndOrderInlineRequests) { val Some(callee) = request.callee inline(request.callsiteInstruction, request.callsiteStackHeight, request.callsiteMethod, request.callsiteClass, @@ -58,18 +59,74 @@ class Inliner[BT <: BTypes](val btypes: BT) { * requests is allowed to have cycles, and the callsites can appear in any order. */ def selectCallsitesForInlining: List[Callsite] = { - callsites.iterator.filter({ - case (_, callsite) => callsite.callee match { - case Some(Callee(callee, _, safeToInline, Some(annotatedInline), _)) => - // TODO: fix inlining from traits. - // For trait methods the callee is abstract: "trait T { @inline final def f = 1}". - // A callsite (t: T).f is `safeToInline` (effectivelyFinal is true), but the callee is the - // abstract method in the interface. - !isAbstractMethod(callee) && safeToInline && annotatedInline - case _ => false - } + callsites.valuesIterator.filter({ + case Callsite(_, _, _, Some(Callee(callee, _, safeToInline, annotatedInline, _)), _, _) => + // For trait methods the callee is abstract: "trait T { @inline final def f = 1}". + // A callsite (t: T).f is `safeToInline` (effectivelyFinal is true), but the callee is the + // abstract method in the interface. + // Even though we such invocations are re-written using `rewriteFinalTraitMethodInvocation`, + // the guard is kept here for the cases where the rewrite fails. + !isAbstractMethod(callee) && safeToInline && annotatedInline + case _ => false - }).map(_._2).toList + }).toList + } + + def rewriteFinalTraitMethodInvocations(): Unit = { + // Rewriting final trait method callsites to the implementation class enables inlining. + // We cannot just iterate over the values of the `callsites` map because the rewrite changes the + // map. Therefore we first copy the values to a list. + callsites.values.toList.foreach(rewriteFinalTraitMethodInvocation) + } + + /** + * Rewrite the INVOKEINTERFACE callsite of a final trait method invocation to INVOKESTATIC of the + * corresponding method in the implementation class. This enables inlining final trait methods. + * + * In a final trait method callsite, the callee is safeToInline and the callee method is abstract + * (the receiver type is the interface, so the method is abstract). + */ + def rewriteFinalTraitMethodInvocation(callsite: Callsite): Unit = callsite.callee match { + case Some(Callee(callee, calleeDeclarationClass, true, true, annotatedNoInline)) if isAbstractMethod(callee) => + assert(calleeDeclarationClass.isInterface, s"expected interface call (final trait method) when inlining abstract method: $callsite") + + val traitMethodArgumentTypes = asm.Type.getArgumentTypes(callee.desc) + + val selfParamTypeName = calleeDeclarationClass.info.inlineInfo.traitImplClassSelfType.getOrElse(calleeDeclarationClass.internalName) + val selfParamType = asm.Type.getObjectType(selfParamTypeName) + + val implClassMethodDescriptor = asm.Type.getMethodDescriptor(asm.Type.getReturnType(callee.desc), selfParamType +: traitMethodArgumentTypes: _*) + val implClassInternalName = calleeDeclarationClass.internalName + "$class" + + // The rewrite reading the implementation class and the implementation method from the bytecode + // repository. If either of the two fails, the rewrite is not performed. + for { + // TODO: inline warnings if impl class or method cannot be found + (implClassMethod, _) <- byteCodeRepository.methodNode(implClassInternalName, callee.name, implClassMethodDescriptor) + implClassBType <- classBTypeFromParsedClassfile(implClassInternalName) + } yield { + val newCallsiteInstruction = new MethodInsnNode(INVOKESTATIC, implClassInternalName, callee.name, implClassMethodDescriptor, false) + callsite.callsiteMethod.instructions.insert(callsite.callsiteInstruction, newCallsiteInstruction) + callsite.callsiteMethod.instructions.remove(callsite.callsiteInstruction) + + callGraph.callsites.remove(callsite.callsiteInstruction) + val staticCallsite = Callsite( + callsiteInstruction = newCallsiteInstruction, + callsiteMethod = callsite.callsiteMethod, + callsiteClass = callsite.callsiteClass, + callee = Some(Callee( + callee = implClassMethod, + calleeDeclarationClass = implClassBType, + safeToInline = true, + annotatedInline = true, + annotatedNoInline = annotatedNoInline)), + argInfos = Nil, + callsiteStackHeight = callsite.callsiteStackHeight + ) + callGraph.callsites(newCallsiteInstruction) = staticCallsite + } + + case _ => } /** diff --git a/src/compiler/scala/tools/nsc/transform/Delambdafy.scala b/src/compiler/scala/tools/nsc/transform/Delambdafy.scala index 1f832ba81e..94e88589f5 100644 --- a/src/compiler/scala/tools/nsc/transform/Delambdafy.scala +++ b/src/compiler/scala/tools/nsc/transform/Delambdafy.scala @@ -255,6 +255,8 @@ abstract class Delambdafy extends Transform with TypingTransformers with ast.Tre val name = unit.freshTypeName(s"$oldClassPart$suffix".replace("$anon", "$nestedInAnon")) val lambdaClass = pkg newClassSymbol(name, originalFunction.pos, FINAL | SYNTHETIC) addAnnotation SerialVersionUIDAnnotation + // make sure currentRun.compiles(lambdaClass) is true (AddInterfaces does the same for trait impl classes) + currentRun.symSource(lambdaClass) = funOwner.sourceFile lambdaClass setInfo ClassInfoType(parents, newScope, lambdaClass) assert(!lambdaClass.isAnonymousClass && !lambdaClass.isAnonymousFunction, "anonymous class name: "+ lambdaClass.name) assert(lambdaClass.isDelambdafyFunction, "not lambda class name: " + lambdaClass.name) diff --git a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala index e94f33db3d..c64f6e7f10 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala @@ -14,6 +14,7 @@ import scala.tools.nsc.settings.{MutableSettings, ScalaSettings} import scala.tools.nsc.{Settings, Global} import scala.tools.partest.ASMConverters import scala.collection.JavaConverters._ +import scala.tools.testing.TempDir object CodeGenTools { import ASMConverters._ @@ -42,20 +43,27 @@ object CodeGenTools { } def newCompiler(defaultArgs: String = "-usejavacp", extraArgs: String = ""): Global = { + val compiler = newCompilerWithoutVirtualOutdir(defaultArgs, extraArgs) + resetOutput(compiler) + compiler + } + + def newCompilerWithoutVirtualOutdir(defaultArgs: String = "-usejavacp", extraArgs: String = ""): Global = { val settings = new Settings() val args = (CommandLineParser tokenize defaultArgs) ++ (CommandLineParser tokenize extraArgs) settings.processArguments(args, processAll = true) - val compiler = new Global(settings) - resetOutput(compiler) - compiler + new Global(settings) } - def compile(compiler: Global)(code: String): List[(String, Array[Byte])] = { + def newRun(compiler: Global): compiler.Run = { compiler.reporter.reset() resetOutput(compiler) - val run = new compiler.Run() - run.compileSources(List(new BatchSourceFile("unitTestSource.scala", code))) - val outDir = compiler.settings.outputDirs.getSingleOutput.get + new compiler.Run() + } + + def makeSourceFile(code: String, filename: String): BatchSourceFile = new BatchSourceFile(filename, code) + + def getGeneratedClassfiles(outDir: AbstractFile): List[(String, Array[Byte])] = { def files(dir: AbstractFile): List[(String, Array[Byte])] = { val res = ListBuffer.empty[(String, Array[Byte])] for (f <- dir.iterator) { @@ -67,8 +75,46 @@ object CodeGenTools { files(outDir) } - def compileClasses(compiler: Global)(code: String): List[ClassNode] = { - compile(compiler)(code).map(p => AsmUtils.readClass(p._2)).sortBy(_.name) + def compile(compiler: Global)(scalaCode: String, javaCode: List[(String, String)] = Nil): List[(String, Array[Byte])] = { + val run = newRun(compiler) + run.compileSources(makeSourceFile(scalaCode, "unitTestSource.scala") :: javaCode.map(p => makeSourceFile(p._1, p._2))) + getGeneratedClassfiles(compiler.settings.outputDirs.getSingleOutput.get) + } + + /** + * Compile multiple Scala files separately into a single output directory. + * + * Note that a new compiler instance is created for compiling each file because symbols survive + * across runs. This makes separate compilation slower. + * + * The output directory is a physical directory, I have not figured out if / how it's possible to + * add a VirtualDirectory to the classpath of a compiler. + */ + def compileSeparately(codes: List[String], extraArgs: String = ""): List[(String, Array[Byte])] = { + val outDir = AbstractFile.getDirectory(TempDir.createTempDir()) + val outDirPath = outDir.canonicalPath + val argsWithOutDir = extraArgs + s" -d $outDirPath -cp $outDirPath" + + for (code <- codes) { + val compiler = newCompilerWithoutVirtualOutdir(extraArgs = argsWithOutDir) + new compiler.Run().compileSources(List(makeSourceFile(code, "unitTestSource.scala"))) + } + + val classfiles = getGeneratedClassfiles(outDir) + outDir.delete() + classfiles + } + + def compileClassesSeparately(codes: List[String], extraArgs: String = "") = { + readAsmClasses(compileSeparately(codes, extraArgs)) + } + + def readAsmClasses(classfiles: List[(String, Array[Byte])]) = { + classfiles.map(p => AsmUtils.readClass(p._2)).sortBy(_.name) + } + + def compileClasses(compiler: Global)(code: String, javaCode: List[(String, String)] = Nil): List[ClassNode] = { + readAsmClasses(compile(compiler)(code, javaCode)) } def compileMethods(compiler: Global)(code: String): List[MethodNode] = { diff --git a/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala b/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala index 3b1b009037..94877fb037 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala @@ -78,4 +78,17 @@ class DirectCompileTest extends ClearAfterClass { Label(11) )) } + + @Test + def testSeparateCompilation(): Unit = { + val codeA = "class A { def f = 1 }" + val codeB = "class B extends A { def g = f }" + val List(a, b) = compileClassesSeparately(List(codeA, codeB)) + val ins = getSingleMethod(b, "g").instructions + assert(ins exists { + case Invoke(_, "B", "f", _, _) => true + case _ => false + }, ins) + + } } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala index f7c9cab284..761f214f82 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala @@ -59,17 +59,12 @@ class BTypesFromClassfileTest { else (fromSym.flags | ACC_PRIVATE | ACC_PUBLIC) == (fromClassfile.flags | ACC_PRIVATE | ACC_PUBLIC) }, s"class flags differ\n$fromSym\n$fromClassfile") - // when parsing from classfile, the inline infos are obtained through the classSymbol, which - // is searched based on the classfile name. this lookup can fail. - assert(fromSym.inlineInfos.size == fromClassfile.inlineInfos.size || fromClassfile.inlineInfos.isEmpty, - s"wrong # of inline infos:\n${fromSym.inlineInfos.keys.toList.sorted}\n${fromClassfile.inlineInfos.keys.toList.sorted}") - fromClassfile.inlineInfos foreach { - case (signature, inlineInfo) => - assert(fromSym.inlineInfos(signature) == inlineInfo, s"inline infos differ for $signature:\n$inlineInfo\n${fromClassfile.inlineInfos(signature)}") - } + // we don't compare InlineInfos in this test: in both cases (from symbol and from classfile) they + // are actually created by looking at the classfile members, not the symbol's. InlineInfos are only + // built from symbols for classes that are being compiled, which is not the case here. Instead + // there's a separate InlineInfoTest. val chk1 = sameBTypes(fromSym.superClass, fromClassfile.superClass, checked) - val chk2 = sameBTypes(fromSym.interfaces, fromClassfile.interfaces, chk1) // The fromSym info has only member classes, no local or anonymous. The symbol is read from the diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala index 69bd92b4ba..16f09db189 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala @@ -89,8 +89,8 @@ class CallGraphTest { assert(callee.callee == target) assert(callee.calleeDeclarationClass == calleeDeclClass) assert(callee.safeToInline == safeToInline) - assert(callee.annotatedInline.get == atInline) - assert(callee.annotatedNoInline.get == atNoInline) + assert(callee.annotatedInline == atInline) + assert(callee.annotatedNoInline == atNoInline) assert(callsite.argInfos == List()) // not defined yet } catch { diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala index fc748196d0..76492cfa23 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala @@ -17,8 +17,8 @@ class CompactLocalVariablesTest { // recurse-unreachable-jumps is required for eliminating catch blocks, in the first dce round they // are still live.only after eliminating the empty handler the catch blocks become unreachable. - val methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code,recurse-unreachable-jumps,compact-locals") - val noCompactVarsCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code,recurse-unreachable-jumps") + val methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code,compact-locals") + val noCompactVarsCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code") @Test def compactUnused(): Unit = { diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala new file mode 100644 index 0000000000..4e12ed757e --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala @@ -0,0 +1,65 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import scala.collection.generic.Clearable +import org.junit.Assert._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import AsmUtils._ +import scala.tools.testing.ClearAfterClass + +import scala.collection.convert.decorateAsScala._ + +object InlineInfoTest extends ClearAfterClass.Clearable { + var compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:classpath") + def clear(): Unit = { compiler = null } + + def notPerRun: List[Clearable] = List(compiler.genBCode.bTypes.classBTypeFromInternalName, compiler.genBCode.bTypes.byteCodeRepository.classes) + notPerRun foreach compiler.perRunCaches.unrecordCache +} + +@RunWith(classOf[JUnit4]) +class InlineInfoTest { + val compiler = InlineInfoTest.compiler + + def compile(code: String) = { + InlineInfoTest.notPerRun.foreach(_.clear()) + compileClasses(compiler)(code) + } + + @Test + def inlineInfosFromSymbolAndAttribute(): Unit = { + val code = + """trait T { + | @inline def f: Int + | @noinline final def g = 0 + |} + |trait U { self: T => + | @inline def f = 0 + | final def h = 0 + | final class K { + | @inline def i = 0 + | } + |} + |sealed trait V { + | @inline def j = 0 + |} + |class C extends T with U + """.stripMargin + val classes = compile(code) + val fromSyms = classes.map(c => compiler.genBCode.bTypes.classBTypeFromInternalName(c.name).info.inlineInfo) + + val fromAttrs = classes.map(c => { + assert(c.attrs.asScala.exists(_.isInstanceOf[InlineInfoAttribute]), c.attrs) + compiler.genBCode.bTypes.inlineInfoFromClassfile(c) + }) + + assert(fromSyms == fromAttrs) + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala new file mode 100644 index 0000000000..58a262c401 --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala @@ -0,0 +1,114 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import scala.tools.asm.Opcodes._ +import org.junit.Assert._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import AsmUtils._ + +import scala.collection.convert.decorateAsScala._ + +object InlinerSeparateCompilationTest { + val args = "-Ybackend:GenBCode -Yopt:l:classpath" +} + +@RunWith(classOf[JUnit4]) +class InlinerSeparateCompilationTest { + import InlinerSeparateCompilationTest._ + import InlinerTest.{listStringLines, assertInvoke, assertNoInvoke} + + @Test + def inlnieMixedinMember(): Unit = { + val codeA = + """trait T { + | @inline def f = 0 + |} + |object O extends T { + | @inline def g = 1 + |} + """.stripMargin + + val codeB = + """class C { + | def t1(t: T) = t.f + | def t2 = O.f + | def t3 = O.g + |} + """.stripMargin + + val List(c, o, oMod, t, tCls) = compileClassesSeparately(List(codeA, codeB), args) + assertInvoke(getSingleMethod(c, "t1"), "T", "f") + assertNoInvoke(getSingleMethod(c, "t2")) + assertNoInvoke(getSingleMethod(c, "t3")) + } + + @Test + def inlineSealedMember(): Unit = { + val codeA = + """sealed trait T { + | @inline def f = 1 + |} + """.stripMargin + + val codeB = + """class C { + | def t1(t: T) = t.f + |} + """.stripMargin + + val List(c, t, tCls) = compileClassesSeparately(List(codeA, codeB), args) + assertNoInvoke(getSingleMethod(c, "t1")) + } + + @Test + def inlineInheritedMember(): Unit = { + val codeA = + """trait T { + | @inline final def f = 1 + |} + |trait U extends T { + | @inline final def g = f + |} + """.stripMargin + + val codeB = + """class C extends U { + | def t1 = this.f + | def t2 = this.g + | def t3(t: T) = t.f + |} + """.stripMargin + + val List(c, t, tCls, u, uCls) = compileClassesSeparately(List(codeA, codeB), args) + for (m <- List("t1", "t2", "t3")) assertNoInvoke(getSingleMethod(c, m)) + } + + @Test + def inlineWithSelfType(): Unit = { + val assembly = + """trait Assembly extends T { + | @inline final def g = 1 + | @inline final def n = m + |} + """.stripMargin + + val codeA = + s"""trait T { self: Assembly => + | @inline final def f = g + | @inline final def m = 1 + |} + |$assembly + """.stripMargin + + val List(a, aCls, t, tCls) = compileClassesSeparately(List(codeA, assembly), args) + assertNoInvoke(getSingleMethod(tCls, "f")) + assertNoInvoke(getSingleMethod(aCls, "n")) + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala index 4e7a2399a2..694dff8dee 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -33,18 +33,37 @@ object InlinerTest extends ClearAfterClass.Clearable { notPerRun foreach compiler.perRunCaches.unrecordCache def clear(): Unit = { compiler = null } + + implicit class listStringLines[T](val l: List[T]) extends AnyVal { + def stringLines = l.mkString("\n") + } + + def assertNoInvoke(m: Method): Unit = assertNoInvoke(m.instructions) + def assertNoInvoke(ins: List[Instruction]): Unit = { + assert(!ins.exists(_.isInstanceOf[Invoke]), ins.stringLines) + } + + def assertInvoke(m: Method, receiver: String, method: String): Unit = assertInvoke(m.instructions, receiver, method) + def assertInvoke(l: List[Instruction], receiver: String, method: String): Unit = { + assert(l.exists { + case Invoke(_, `receiver`, `method`, _, _) => true + case _ => false + }, l.stringLines) + } } @RunWith(classOf[JUnit4]) class InlinerTest extends ClearAfterClass { ClearAfterClass.stateToClear = InlinerTest + import InlinerTest.{listStringLines, assertInvoke, assertNoInvoke} + val compiler = InlinerTest.compiler import compiler.genBCode.bTypes._ - def compile(code: String): List[ClassNode] = { + def compile(scalaCode: String, javaCode: List[(String, String)] = Nil): List[ClassNode] = { InlinerTest.notPerRun.foreach(_.clear()) - compileClasses(compiler)(code) + compileClasses(compiler)(scalaCode, javaCode) } def checkCallsite(callsite: callGraph.Callsite, callee: MethodNode) = { @@ -229,8 +248,8 @@ class InlinerTest extends ClearAfterClass { |} """.stripMargin val List(cCls) = compile(code) - val instructions = instructionsFromMethod(cCls.methods.asScala.find(_.name == "test").get) - assert(instructions.contains(Op(ICONST_0)), instructions mkString "\n") + val instructions = getSingleMethod(cCls, "test").instructions + assert(instructions.contains(Op(ICONST_0)), instructions.stringLines) assert(!instructions.contains(Op(ICONST_1)), instructions) } @@ -282,16 +301,22 @@ class InlinerTest extends ClearAfterClass { def arraycopy(): Unit = { // also tests inlining of a void-returning method (no return value on the stack) val code = - """class C { + """// can't use the `compat.Platform.arraycopy` from the std lib for now, because the classfile doesn't have a ScalaInlineInfo attribute + |object Platform { + | @inline def arraycopy(src: AnyRef, srcPos: Int, dest: AnyRef, destPos: Int, length: Int) { + | System.arraycopy(src, srcPos, dest, destPos, length) + | } + |} + |class C { | def f(src: AnyRef, srcPos: Int, dest: AnyRef, destPos: Int, length: Int): Unit = { - | compat.Platform.arraycopy(src, srcPos, dest, destPos, length) + | Platform.arraycopy(src, srcPos, dest, destPos, length) | } |} """.stripMargin - val List(c) = compile(code) - val ins = instructionsFromMethod(c.methods.asScala.find(_.name == "f").get) + val List(c, _, _) = compile(code) + val ins = getSingleMethod(c, "f").instructions val invokeSysArraycopy = Invoke(INVOKESTATIC, "java/lang/System", "arraycopy", "(Ljava/lang/Object;ILjava/lang/Object;II)V", false) - assert(ins contains invokeSysArraycopy, ins mkString "\n") + assert(ins contains invokeSysArraycopy, ins.stringLines) } @Test @@ -311,7 +336,7 @@ class InlinerTest extends ClearAfterClass { } @Test - def atInlineInTraitDoesNotCrash(): Unit = { + def atInlineInTrait(): Unit = { val code = """trait T { | @inline final def f = 0 @@ -321,10 +346,7 @@ class InlinerTest extends ClearAfterClass { |} """.stripMargin val List(c, t, tClass) = compile(code) - val ins = instructionsFromMethod(c.methods.asScala.find(_.name == "g").get) - val invokeF = Invoke(INVOKEINTERFACE, "T", "f", "()I", true) - // no inlining yet - assert(ins contains invokeF, ins mkString "\n") + assertNoInvoke(getSingleMethod(c, "g")) } @Test @@ -336,10 +358,8 @@ class InlinerTest extends ClearAfterClass { |} """.stripMargin val List(c) = compile(code) - val ins = instructionsFromMethod(c.methods.asScala.find(_.name == "g").get) - println(ins) // no more invoke, f is inlined - assert(ins.count(_.isInstanceOf[Invoke]) == 0, ins mkString "\n") + assertNoInvoke(getSingleMethod(c, "g")) } @Test @@ -373,11 +393,11 @@ class InlinerTest extends ClearAfterClass { val ins = instructionsFromMethod(f) // no invocations, lowestOneBit is inlined - assert(ins.count(_.isInstanceOf[Invoke]) == 0, ins mkString "\n") + assertNoInvoke(ins) // no null check when inlining a static method ins foreach { - case Jump(IFNONNULL, _) => assert(false, ins mkString "\n") + case Jump(IFNONNULL, _) => assert(false, ins.stringLines) case _ => } } @@ -437,16 +457,267 @@ class InlinerTest extends ClearAfterClass { |} """.stripMargin - InlinerTest.notPerRun.foreach(_.clear()) - compiler.reporter.reset() - compiler.settings.outputDirs.setSingleOutput(new VirtualDirectory("(memory)", None)) - val run = new compiler.Run() - run.compileSources(List(new BatchSourceFile("A.java", javaCode), new BatchSourceFile("B.scala", scalaCode))) - val outDir = compiler.settings.outputDirs.getSingleOutput.get - val List(b) = outDir.iterator.map(f => AsmUtils.readClass(f.toByteArray)).toList.sortBy(_.name) + val List(b) = compile(scalaCode, List((javaCode, "A.java"))) val ins = getSingleMethod(b, "g").instructions val invokeFlop = Invoke(INVOKEVIRTUAL, "B", "flop", "()I", false) - assert(ins contains invokeFlop, ins mkString "\n") + assert(ins contains invokeFlop, ins.stringLines) + } + + @Test + def inlineFromTraits(): Unit = { + val code = + """trait T { + | @inline final def f = g + | @inline final def g = 1 + |} + | + |class C extends T { + | def t1(t: T) = t.f + | def t2(c: C) = c.f + |} + """.stripMargin + val List(c, t, tClass) = compile(code) + // both are just `return 1`, no more calls + assertNoInvoke(getSingleMethod(c, "t1")) + assertNoInvoke(getSingleMethod(c, "t2")) + } + + @Test + def inlineMixinMethods(): Unit = { + val code = + """trait T { + | @inline final def f = 1 + |} + |class C extends T + """.stripMargin + val List(c, t, tClass) = compile(code) + // the static implementaiton method is inlined into the mixin, so there's no invocation in the mixin + assertNoInvoke(getSingleMethod(c, "f")) + } + + @Test + def inlineTraitInherited(): Unit = { + val code = + """trait T { + | @inline final def f = 1 + |} + |trait U extends T { + | @inline final def g = f + |} + |class C extends U { + | def t1 = f + | def t2 = g + |} + """.stripMargin + val List(c, t, tClass, u, uClass) = compile(code) + assertNoInvoke(getSingleMethod(c, "t1")) + assertNoInvoke(getSingleMethod(c, "t2")) + } + + @Test + def virtualTraitNoInline(): Unit = { + val code = + """trait T { + | @inline def f = 1 + |} + |class C extends T { + | def t1(t: T) = t.f + | def t2 = this.f + |} + """.stripMargin + val List(c, t, tClass) = compile(code) + assertInvoke(getSingleMethod(c, "t1"), "T", "f") + assertInvoke(getSingleMethod(c, "t2"), "C", "f") + } + + @Test + def sealedTraitInline(): Unit = { + val code = + """sealed trait T { + | @inline def f = 1 + |} + |class C { + | def t1(t: T) = t.f + |} + """.stripMargin + val List(c, t, tClass) = compile(code) + assertNoInvoke(getSingleMethod(c, "t1")) + } + + @Test + def inlineFromObject(): Unit = { + val code = + """trait T { + | @inline def f = 0 + |} + |object O extends T { + | @inline def g = 1 + | // mixin generates `def f = T$class.f(this)`, which is inlined here (we get ICONST_0) + |} + |class C { + | def t1 = O.f // the mixin method of O is inlined, so we directly get the ICONST_0 + | def t2 = O.g // object members are inlined + | def t3(t: T) = t.f // no inlining here + |} + """.stripMargin + val List(c, oMirror, oModule, t, tClass) = compile(code) + + assertNoInvoke(getSingleMethod(oModule, "f")) + + assertNoInvoke(getSingleMethod(c, "t1")) + assertNoInvoke(getSingleMethod(c, "t2")) + assertInvoke(getSingleMethod(c, "t3"), "T", "f") + } + + @Test + def selfTypeInline(): Unit = { + val code = + """trait T { self: Assembly => + | @inline final def f = g + | @inline final def m = 1 + |} + |trait Assembly extends T { + | @inline final def g = 1 + | @inline final def n = m // inlined. (*) + | // (*) the declaration class of m is T. the signature of T$class.m is m(LAssembly;)I. so we need the self type to build the + | // signature. then we can look up the MethodNode of T$class.m and then rewrite the INVOKEINTERFACE to INVOKESTATIC. + |} + |class C { + | def t1(a: Assembly) = a.f // like above, decl class is T, need self-type of T to rewrite the interface call to static. + | def t2(a: Assembly) = a.n + |} + """.stripMargin + + val List(assembly, assemblyClass, c, t, tClass) = compile(code) + + assertNoInvoke(getSingleMethod(tClass, "f")) + + assertNoInvoke(getSingleMethod(assemblyClass, "n")) + + assertNoInvoke(getSingleMethod(c, "t1")) + assertNoInvoke(getSingleMethod(c, "t2")) + } + + @Test + def selfTypeInline2(): Unit = { + // There are some interesting things going on here with the self types. Here's a short version: + // + // trait T1 { def f = 1 } + // trait T2a { self: T1 with T2a => // self type in the backend: T1 + // def f = 2 + // def g = f // resolved to T2a.f + // } + // trait T2b { self: T2b with T1 => // self type in the backend: T2b + // def f = 2 + // def g = f // resolved to T1.f + // } + // + // scala> val t = typeOf[T2a]; exitingMixin(t.typeOfThis.typeSymbol) // self type of T2a is T1 + // res28: $r.intp.global.Symbol = trait T1 + // + // scala> typeOf[T2a].typeOfThis.member(newTermName("f")).owner // f in T2a is resolved as T2a.f + // res29: $r.intp.global.Symbol = trait T2a + // + // scala> val t = typeOf[T2b]; exitingMixin(t.typeOfThis.typeSymbol) // self type of T2b is T1 + // res30: $r.intp.global.Symbol = trait T2b + // + // scala> typeOf[T2b].typeOfThis.member(newTermName("f")).owner // f in T2b is resolved as T1.f + // res31: $r.intp.global.Symbol = trait T1 + + val code = + """trait T1 { + | @inline def f: Int = 0 + | @inline def g1 = f // not inlined: f not final, so T1$class.g1 has an interface call T1.f + |} + | + |// erased self-type (used in impl class for `self` parameter): T1 + |trait T2a { self: T1 with T2a => + | @inline override final def f = 1 + | @inline def g2a = f // inlined: resolved as T2a.f, which is re-written to T2a$class.f, so T2a$class.g2a has ICONST_1 + |} + | + |final class Ca extends T1 with T2a { + | // mixin generates accessors like `def g1 = T1$class.g1`, the impl class method call is inlined into the accessor. + | + | def m1a = g1 // call to accessor, inlined, we get the interface call T1.f + | def m2a = g2a // call to accessor, inlined, we get ICONST_1 + | def m3a = f // call to accessor, inlined, we get ICONST_1 + | + | def m4a(t: T1) = t.f // T1.f is not final, so not inlined, interface call to T1.f + | def m5a(t: T2a) = t.f // re-written to T2a$class.f, inlined, ICONST_1 + |} + | + |// erased self-type: T2b + |trait T2b { self: T2b with T1 => + | @inline override final def f = 1 + | @inline def g2b = f // not inlined: resolved as T1.f, so T2b$class.g2b has an interface call T1.f + |} + | + |final class Cb extends T1 with T2b { + | def m1b = g1 // inlined, we get the interface call to T1.f + | def m2b = g2b // inlined, we get the interface call to T1.f + | def m3b = f // inlined, we get ICONST_1 + | + | def m4b(t: T1) = t.f // T1.f is not final, so not inlined, interface call to T1.f + | def m5b(t: T2b) = t.f // re-written to T2b$class.f, inlined, ICONST_1 + |} + """.stripMargin + val List(ca, cb, t1, t1C, t2a, t2aC, t2b, t2bC) = compile(code) + + val t2aCfDesc = t2aC.methods.asScala.find(_.name == "f").get.desc + assert(t2aCfDesc == "(LT1;)I", t2aCfDesc) // self-type of T2a is T1 + + val t2bCfDesc = t2bC.methods.asScala.find(_.name == "f").get.desc + assert(t2bCfDesc == "(LT2b;)I", t2bCfDesc) // self-type of T2b is T2b + + assertNoInvoke(getSingleMethod(t2aC, "g2a")) + assertInvoke(getSingleMethod(t2bC, "g2b"), "T1", "f") + + assertInvoke(getSingleMethod(ca, "m1a"), "T1", "f") + assertNoInvoke(getSingleMethod(ca, "m2a")) // no invoke, see comment on def g2a + assertNoInvoke(getSingleMethod(ca, "m3a")) + assertInvoke(getSingleMethod(ca, "m4a"), "T1", "f") + assertNoInvoke(getSingleMethod(ca, "m5a")) + + assertInvoke(getSingleMethod(cb, "m1b"), "T1", "f") + assertInvoke(getSingleMethod(cb, "m2b"), "T1", "f") // invoke, see comment on def g2b + assertNoInvoke(getSingleMethod(cb, "m3b")) + assertInvoke(getSingleMethod(cb, "m4b"), "T1", "f") + assertNoInvoke(getSingleMethod(cb, "m5b")) + } + + @Test + def finalSubclassInline(): Unit = { + val code = + """class C { + | @inline def f = 0 + | @inline final def g = 1 + |} + |final class D extends C + |object E extends C + |class T { + | def t1(d: D) = d.f + d.g + E.f + E.g // d.f can be inlined because the reciever type is D, which is final. + |} // so d.f can be resolved statically. same for E.f + """.stripMargin + val List(c, d, e, eModule, t) = compile(code) + assertNoInvoke(getSingleMethod(t, "t1")) + } + + @Test + def inlineFromNestedClasses(): Unit = { + val code = + """class C { + | trait T { @inline final def f = 1 } + | class D extends T{ + | def m(t: T) = t.f + | } + | + | def m(d: D) = d.f + |} + """.stripMargin + val List(c, d, t, tC) = compile(code) + assertNoInvoke(getSingleMethod(d, "m")) + assertNoInvoke(getSingleMethod(c, "m")) } } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala index 7c9636b8b7..c2e2a1b883 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala @@ -22,8 +22,8 @@ object UnreachableCodeTest extends ClearAfterClass.Clearable { var dceCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code") var noOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:none") - // jvm-1.5 disables computing stack map frames, and it emits dead code as-is. - var noOptNoFramesCompiler = newCompiler(extraArgs = "-target:jvm-1.5 -Ybackend:GenBCode -Yopt:l:none") + // jvm-1.5 disables computing stack map frames, and it emits dead code as-is. note that this flag triggers a deprecation warning + var noOptNoFramesCompiler = newCompiler(extraArgs = "-target:jvm-1.5 -Ybackend:GenBCode -Yopt:l:none -deprecation") def clear(): Unit = { methodOptCompiler = null diff --git a/test/junit/scala/tools/testing/TempDir.scala b/test/junit/scala/tools/testing/TempDir.scala new file mode 100644 index 0000000000..475de8c4a2 --- /dev/null +++ b/test/junit/scala/tools/testing/TempDir.scala @@ -0,0 +1,18 @@ +package scala.tools.testing + +import java.io.{IOException, File} + +object TempDir { + final val TEMP_DIR_ATTEMPTS = 10000 + def createTempDir(): File = { + val baseDir = new File(System.getProperty("java.io.tmpdir")) + val baseName = System.currentTimeMillis() + "-" + var c = 0 + while (c < TEMP_DIR_ATTEMPTS) { + val tempDir = new File(baseDir, baseName + c) + if (tempDir.mkdir()) return tempDir + c += 1 + } + throw new IOException(s"Failed to create directory") + } +} -- cgit v1.2.3 From a4e71b188fe8069b4de3a0753defb624b8b1eb8c Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Mon, 9 Feb 2015 19:33:20 +0100 Subject: Cast receiver if necessary when rewriting trait calls to impl method 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. --- .../scala/tools/nsc/backend/jvm/AsmUtils.scala | 18 ++++++++++-- .../scala/tools/nsc/backend/jvm/BTypes.scala | 4 +-- .../tools/nsc/backend/jvm/opt/BytecodeUtils.scala | 9 ++++-- .../tools/nsc/backend/jvm/opt/CallGraph.scala | 4 +-- .../scala/tools/nsc/backend/jvm/opt/Inliner.scala | 34 +++++++++++++++++----- .../tools/nsc/backend/jvm/opt/CallGraphTest.scala | 1 - .../tools/nsc/backend/jvm/opt/InlinerTest.scala | 25 +++++++++++++--- 7 files changed, 74 insertions(+), 21 deletions(-) (limited to 'src/compiler') 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/BTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala index 51a17b7fe4..872d1cc522 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala @@ -16,11 +16,11 @@ import opt.OptimizerReporting._ 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 { 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 e221eef636..d2658bcd2a 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala @@ -296,10 +296,13 @@ object BytecodeUtils { )).toList } - class BasicAnalyzer(methodNode: MethodNode, classInternalName: InternalName) { - val analyzer = new Analyzer(new BasicInterpreter) + /** + * 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[BasicValue] = analyzer.getFrames()(methodNode.instructions.indexOf(instruction)) + def frameAt(instruction: AbstractInsnNode): Frame[V] = analyzer.getFrames()(methodNode.instructions.indexOf(instruction)) } implicit class `frame extensions`[V <: Value](val frame: Frame[V]) extends AnyVal { diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala index 020db738e8..18b95184e5 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala @@ -10,7 +10,7 @@ package opt import scala.tools.asm.tree._ import scala.collection.convert.decorateAsScala._ import scala.tools.nsc.backend.jvm.BTypes.InternalName -import scala.tools.nsc.backend.jvm.opt.BytecodeUtils.BasicAnalyzer +import scala.tools.nsc.backend.jvm.opt.BytecodeUtils.AsmAnalyzer import ByteCodeRepository.{Source, CompilationUnit} class CallGraph[BT <: BTypes](val btypes: BT) { @@ -68,7 +68,7 @@ class CallGraph[BT <: BTypes](val btypes: BT) { // 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 BasicAnalyzer(methodNode, definingClass.internalName) + val analyzer = new AsmAnalyzer(methodNode, definingClass.internalName) methodNode.instructions.iterator.asScala.collect({ case call: MethodInsnNode => diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala index 970cc6803a..b2459862ea 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala @@ -17,6 +17,7 @@ import AsmUtils._ import BytecodeUtils._ import OptimizerReporting._ import collection.mutable +import scala.tools.asm.tree.analysis.{SourceInterpreter, Analyzer} class Inliner[BT <: BTypes](val btypes: BT) { import btypes._ @@ -92,19 +93,38 @@ class Inliner[BT <: BTypes](val btypes: BT) { val traitMethodArgumentTypes = asm.Type.getArgumentTypes(callee.desc) - val selfParamTypeName = calleeDeclarationClass.info.inlineInfo.traitImplClassSelfType.getOrElse(calleeDeclarationClass.internalName) - val selfParamType = asm.Type.getObjectType(selfParamTypeName) + val selfParamType = calleeDeclarationClass.info.inlineInfo.traitImplClassSelfType match { + case Some(internalName) => classBTypeFromParsedClassfile(internalName) + case None => Some(calleeDeclarationClass) + } - val implClassMethodDescriptor = asm.Type.getMethodDescriptor(asm.Type.getReturnType(callee.desc), selfParamType +: traitMethodArgumentTypes: _*) val implClassInternalName = calleeDeclarationClass.internalName + "$class" // The rewrite reading the implementation class and the implementation method from the bytecode // repository. If either of the two fails, the rewrite is not performed. for { - // TODO: inline warnings if impl class or method cannot be found - (implClassMethod, _) <- byteCodeRepository.methodNode(implClassInternalName, callee.name, implClassMethodDescriptor) - implClassBType <- classBTypeFromParsedClassfile(implClassInternalName) + // TODO: inline warnings if selfClassType, impl class or impl method cannot be found + selfType <- selfParamType + implClassMethodDescriptor = asm.Type.getMethodDescriptor(asm.Type.getReturnType(callee.desc), selfType.toASMType +: traitMethodArgumentTypes: _*) + (implClassMethod, _) <- byteCodeRepository.methodNode(implClassInternalName, callee.name, implClassMethodDescriptor) + implClassBType <- classBTypeFromParsedClassfile(implClassInternalName) } 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 (!calleeDeclarationClass.isSubtypeOf(selfType)) { + 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, selfType.internalName) + callsite.callsiteMethod.instructions.insert(i, cast) + } + } + val newCallsiteInstruction = new MethodInsnNode(INVOKESTATIC, implClassInternalName, callee.name, implClassMethodDescriptor, false) callsite.callsiteMethod.instructions.insert(callsite.callsiteInstruction, newCallsiteInstruction) callsite.callsiteMethod.instructions.remove(callsite.callsiteInstruction) @@ -291,7 +311,7 @@ class Inliner[BT <: BTypes](val btypes: BT) { // 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 BasicAnalyzer(callee, calleeDeclarationClass.internalName) + val analyzer = new AsmAnalyzer(callee, calleeDeclarationClass.internalName) for (originalReturn <- callee.instructions.iterator().asScala if isReturn(originalReturn)) { val frame = analyzer.frameAt(originalReturn) diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala index 16f09db189..d7344ae61f 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala @@ -11,7 +11,6 @@ import org.junit.Assert._ import scala.tools.asm.tree._ import scala.tools.asm.tree.analysis._ -import scala.tools.nsc.backend.jvm.opt.BytecodeUtils.BasicAnalyzer import scala.tools.testing.AssertUtil._ import CodeGenTools._ diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala index 694dff8dee..7f58f77b15 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -13,7 +13,7 @@ import org.junit.Assert._ import scala.tools.asm.tree._ import scala.tools.asm.tree.analysis._ -import scala.tools.nsc.backend.jvm.opt.BytecodeUtils.BasicAnalyzer +import scala.tools.nsc.backend.jvm.opt.BytecodeUtils.AsmAnalyzer import scala.tools.nsc.io._ import scala.tools.testing.AssertUtil._ @@ -84,7 +84,7 @@ class InlinerTest extends ClearAfterClass { 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 BasicAnalyzer(g, clsBType.internalName) + val analyzer = new AsmAnalyzer(g, clsBType.internalName) val r = inliner.inline( fCall, @@ -222,7 +222,7 @@ class InlinerTest extends ClearAfterClass { case m: MethodInsnNode if m.name == "g" => m }).next() - val analyzer = new BasicAnalyzer(h, dTp.internalName) + val analyzer = new AsmAnalyzer(h, dTp.internalName) val r = inliner.inline( gCall, @@ -374,7 +374,7 @@ class InlinerTest extends ClearAfterClass { 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).get - val analyzer = new BasicAnalyzer(f, clsBType.internalName) + val analyzer = new AsmAnalyzer(f, clsBType.internalName) val integerClassBType = classBTypeFromInternalName("java/lang/Integer") val lowestOneBitMethod = byteCodeRepository.methodNode(integerClassBType.internalName, "lowestOneBit", "(I)I").get._1 @@ -720,4 +720,21 @@ class InlinerTest extends ClearAfterClass { 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)) + } } -- cgit v1.2.3 From 57c07204ca452564b930085cfa9e8b099e45b2a9 Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Fri, 6 Feb 2015 13:55:49 +0100 Subject: Limit the size of the ByteCodeRepository cache I observed cases (eg Scaladoc tests) where we end up with 17k+ ClassNodes, which makes 500 MB. --- .../tools/nsc/backend/jvm/BCodeSkelBuilder.scala | 2 +- .../tools/nsc/backend/jvm/BTypesFromSymbols.scala | 3 +- .../nsc/backend/jvm/opt/ByteCodeRepository.scala | 37 ++++++++++++++++++++-- .../backend/jvm/opt/InlinerIllegalAccessTest.scala | 2 +- 4 files changed, 39 insertions(+), 5 deletions(-) (limited to 'src/compiler') diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala index e40e928761..61606419bd 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala @@ -133,7 +133,7 @@ abstract class BCodeSkelBuilder extends BCodeHelpers { if (settings.YoptInlinerEnabled) { // The inliner needs to find all classes in the code repo, also those being compiled - byteCodeRepository.classes(cnode.name) = Some((cnode, ByteCodeRepository.CompilationUnit)) + byteCodeRepository.add(cnode, ByteCodeRepository.CompilationUnit) } assert(cd.symbol == claszSymbol, "Someone messed up BCodePhase.claszSymbol during genPlainClass().") diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala index a217e54ed8..b90030dd8c 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala @@ -413,7 +413,8 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { // 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 + 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 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 fb58f1b189..0958601d73 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala @@ -17,6 +17,7 @@ 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 @@ -26,16 +27,48 @@ 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 `None`: there is no * ClassNode generated by the backend and also no classfile that could be parsed. */ -class ByteCodeRepository(val classPath: ClassFileLookup[AbstractFile], val classes: collection.concurrent.Map[InternalName, Option[(ClassNode, Source)]]) { +class ByteCodeRepository(val classPath: ClassFileLookup[AbstractFile], val classes: collection.concurrent.Map[InternalName, Option[(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.isDefined && c._2.get._2 == Classfile) > maxCacheSize) { + val removeId = idCounter.get - targetSize + val toRemove = classes.iterator.collect({ + case (name, Some((_, Classfile, id))) if id < removeId => name + }).toList + toRemove foreach classes.remove + } + } + + def add(classNode: ClassNode, source: Source) = { + classes(classNode.name) = Some((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): Option[(ClassNode, Source)] = { - classes.getOrElseUpdate(internalName, parseClass(internalName).map((_, Classfile))) + val r = classes.getOrElseUpdate(internalName, { + limitCacheSize() + parseClass(internalName).map((_, Classfile, idCounter.incrementAndGet())) + }) + r.map(v => (v._1, v._2)) } /** diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala index ef0f6bcd77..91404acba7 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala @@ -31,7 +31,7 @@ class InlinerIllegalAccessTest extends ClearAfterClass { val compiler = InlinerIllegalAccessTest.compiler import compiler.genBCode.bTypes._ - def addToRepo(cls: List[ClassNode]): Unit = for (c <- cls) byteCodeRepository.classes(c.name) = Some((c, ByteCodeRepository.Classfile)) + 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 -- cgit v1.2.3 From f8731c5b17274d68de3469e34727e24a937ffc84 Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Wed, 11 Mar 2015 11:38:17 -0700 Subject: Issue inliner warnings for callsites that cannot be inlined Issue precise warnings when the inliner fails to inline or analyze a callsite. Inline failures may have various causes, for example because some class cannot be found on the classpath when building the call graph. So we need to store problems that happen early in the optimizer (when building the necessary data structures, call graph, ClassBTypes) to be able to report them later in case the inliner accesses the related data. We use Either to store these warning messages. The commit introduces an implicit class `RightBiasedEither` to make Either easier to use for error propagation. This would be subsumed by a biased either in the standard library (or could use a Validation). The `info` of each ClassBType is now an Either. There are two cases where the info is not available: - The type info should be parsed from a classfile, but the class cannot be found on the classpath - SI-9111, the type of a Java source originating class symbol cannot be completed This means that the operations on ClassBType that query the info now return an Either, too. Each Callsite in the call graph now stores the source position of the call instruction. Since the call graph is built after code generation, we build a map from invocation nodes to positions during code gen and query it when building the call graph. The new inliner can report a large number of precise warnings when a callsite cannot be inlined, or if the inlining metadata cannot be computed precisely, for example due to a missing classfile. The new -Yopt-warnings multi-choice option allows configuring inliner warnings. By default (no option provided), a one-line summary is issued in case there were callsites annotated @inline that could not be inlined. --- src/compiler/scala/tools/nsc/Reporting.scala | 12 +- .../tools/nsc/backend/jvm/BCodeAsmCommon.scala | 7 +- .../tools/nsc/backend/jvm/BCodeBodyBuilder.scala | 55 +++-- .../scala/tools/nsc/backend/jvm/BCodeHelpers.scala | 19 +- .../tools/nsc/backend/jvm/BCodeIdiomatic.scala | 38 +-- .../tools/nsc/backend/jvm/BCodeSkelBuilder.scala | 15 +- .../scala/tools/nsc/backend/jvm/BTypes.scala | 271 ++++++++++++--------- .../tools/nsc/backend/jvm/BTypesFromSymbols.scala | 53 ++-- .../tools/nsc/backend/jvm/BackendReporting.scala | 265 ++++++++++++++++++++ .../scala/tools/nsc/backend/jvm/CoreBTypes.scala | 5 +- .../scala/tools/nsc/backend/jvm/GenBCode.scala | 4 + .../nsc/backend/jvm/opt/ByteCodeRepository.scala | 83 ++++--- .../tools/nsc/backend/jvm/opt/BytecodeUtils.scala | 2 +- .../tools/nsc/backend/jvm/opt/CallGraph.scala | 111 +++++---- .../nsc/backend/jvm/opt/InlineInfoAttribute.scala | 3 +- .../scala/tools/nsc/backend/jvm/opt/Inliner.scala | 262 +++++++++++++------- .../nsc/backend/jvm/opt/OptimizerReporting.scala | 28 --- .../scala/tools/nsc/settings/ScalaSettings.scala | 19 ++ test/files/run/colltest1.scala | 2 +- test/files/run/compiler-asSeenFrom.scala | 2 +- test/files/run/existentials-in-compiler.scala | 2 +- test/files/run/is-valid-num.scala | 2 +- test/files/run/iterator-from.scala | 2 +- test/files/run/mapConserve.scala | 2 +- test/files/run/pc-conversions.scala | 2 +- test/files/run/stringinterpolation_macro-run.scala | 2 +- test/files/run/synchronized.check | 4 + test/files/run/t7096.scala | 2 +- test/files/run/t7582.check | 4 + test/files/run/t7582b.check | 4 + .../scala/tools/nsc/backend/jvm/CodeGenTools.scala | 44 ++-- .../tools/nsc/backend/jvm/DirectCompileTest.scala | 4 + .../backend/jvm/opt/BTypesFromClassfileTest.scala | 8 +- .../tools/nsc/backend/jvm/opt/CallGraphTest.scala | 23 +- .../tools/nsc/backend/jvm/opt/InlineInfoTest.scala | 4 +- .../nsc/backend/jvm/opt/InlineWarningTest.scala | 146 +++++++++++ .../backend/jvm/opt/InlinerIllegalAccessTest.scala | 4 +- .../jvm/opt/InlinerSeparateCompilationTest.scala | 3 +- .../tools/nsc/backend/jvm/opt/InlinerTest.scala | 84 +++++-- .../nsc/backend/jvm/opt/MethodLevelOpts.scala | 3 +- .../nsc/backend/jvm/opt/UnreachableCodeTest.scala | 3 +- 41 files changed, 1157 insertions(+), 451 deletions(-) create mode 100644 src/compiler/scala/tools/nsc/backend/jvm/BackendReporting.scala delete mode 100644 src/compiler/scala/tools/nsc/backend/jvm/opt/OptimizerReporting.scala create mode 100644 test/junit/scala/tools/nsc/backend/jvm/opt/InlineWarningTest.scala (limited to 'src/compiler') 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/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 1b3f124dd8..15b014bdd3 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala @@ -13,6 +13,7 @@ import scala.annotation.switch import scala.tools.asm import GenBCode._ +import BackendReporting._ /* * @@ -93,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. @@ -230,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 @@ -584,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 @@ -626,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") @@ -636,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) => @@ -644,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 @@ -695,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() @@ -810,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) @@ -976,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 @@ -1036,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 } @@ -1187,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) @@ -1208,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 @@ -1232,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 { @@ -1248,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) @@ -1269,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 246d565987..3b7dbc18da 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala @@ -11,6 +11,7 @@ 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. @@ -67,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. @@ -205,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) } } @@ -341,7 +342,7 @@ abstract class BCodeHelpers extends BCodeIdiomatic with BytecodeWriters { */ final def getClassBTypeAndRegisterInnerClass(sym: Symbol): ClassBType = { val r = classBTypeFromSymbol(sym) - if (r.isNestedClass) innerClassBufferASM += r + if (r.isNestedClass.get) innerClassBufferASM += r r } @@ -351,7 +352,7 @@ abstract class BCodeHelpers extends BCodeIdiomatic with BytecodeWriters { * TODO: clean up the way we track referenced inner classes. */ final def toTypeKind(t: Type): BType = typeToBType(t) match { - case c: ClassBType if c.isNestedClass => + case c: ClassBType if c.isNestedClass.get => innerClassBufferASM += c c case r => r @@ -364,7 +365,7 @@ abstract class BCodeHelpers extends BCodeIdiomatic with BytecodeWriters { final def asmMethodType(msym: Symbol): MethodBType = { val r = methodBTypeFromSymbol(msym) (r.returnType :: r.argumentTypes) foreach { - case c: ClassBType if c.isNestedClass => innerClassBufferASM += c + case c: ClassBType if c.isNestedClass.get => innerClassBufferASM += c case _ => } r @@ -714,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, @@ -730,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() @@ -846,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 c743ebd16f..9993357eee 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeIdiomatic.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeIdiomatic.scala @@ -11,6 +11,7 @@ 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. @@ -105,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 } @@ -205,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 @@ -226,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) } /* @@ -389,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 61606419bd..2a06c62e37 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala @@ -4,20 +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._ /* * @@ -122,11 +119,11 @@ abstract class BCodeSkelBuilder extends BCodeHelpers { addClassFields() - innerClassBufferASM ++= classBType.info.nestedClasses + innerClassBufferASM ++= classBType.info.get.nestedClasses gen(cd.impl) addInnerClassesASM(cnode, innerClassBufferASM.toList) - cnode.visitAttribute(classBType.inlineInfoAttribute) + cnode.visitAttribute(classBType.inlineInfoAttribute.get) if (AsmUtils.traceClassEnabled && cnode.name.contains(AsmUtils.traceClassPattern)) AsmUtils.traceClass(cnode) @@ -146,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 872d1cc522..d2ee944916 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala @@ -7,12 +7,15 @@ 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 scala.tools.asm.tree.{MethodInsnNode, InnerClassNode, ClassNode} import scala.tools.nsc.backend.jvm.BTypes.{InlineInfo, MethodInlineInfo} +import scala.tools.nsc.backend.jvm.BackendReporting._ +import BackendReporting.RightBiasedEither import scala.tools.nsc.backend.jvm.opt._ -import opt.OptimizerReporting._ import scala.collection.convert.decorateAsScala._ /** @@ -41,6 +44,8 @@ abstract class BTypes { 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 @@ -50,6 +55,9 @@ abstract class BTypes { // 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. @@ -61,7 +69,19 @@ 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 @@ -73,29 +93,33 @@ abstract class BTypes { * * This method supports both descriptors and internal names. */ - def bTypeForDescriptorOrInternalNameFromClassfile(desc: String): Option[BType] = (desc(0): @switch) match { - case 'V' => Some(UNIT) - case 'Z' => Some(BOOL) - case 'C' => Some(CHAR) - case 'B' => Some(BYTE) - case 'S' => Some(SHORT) - case 'I' => Some(INT) - case 'F' => Some(FLOAT) - case 'J' => Some(LONG) - case 'D' => Some(DOUBLE) - case '[' => bTypeForDescriptorOrInternalNameFromClassfile(desc.substring(1)) map ArrayBType + 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]]. Returns `None` if the - * classfile cannot be found in the `byteCodeRepository`. + * 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): Option[ClassBType] = { - classBTypeFromInternalName.get(internalName) orElse { - byteCodeRepository.classNode(internalName) map classBTypeFromClassNode - } + def classBTypeFromParsedClassfile(internalName: InternalName): ClassBType = { + 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) + } + }) } /** @@ -108,26 +132,15 @@ abstract class BTypes { } private def setClassInfoFromParsedClassfile(classNode: ClassNode, classBType: ClassBType): ClassBType = { - def ensureClassBTypeFromParsedClassfile(internalName: InternalName): ClassBType = { - classBTypeFromParsedClassfile(internalName) getOrElse { - // When building a ClassBType from a parsed classfile, we need the ClassBTypes for all - // referenced types. - // TODO: make this more robust with respect to incomplete classpaths. - // Maybe not those parts of the ClassBType that require the missing class are not actually - // queried during the backend, so every part of a ClassBType that requires parsing a - // (potentially missing) classfile should be computed lazily. - assertionError(s"Could not find bytecode for class $internalName") - } - } val superClass = classNode.superName match { case null => assert(classNode.name == ObjectReference.internalName, s"class with missing super type: ${classNode.name}") None case superName => - Some(ensureClassBTypeFromParsedClassfile(superName)) + Some(classBTypeFromParsedClassfile(superName)) } - val interfaces: List[ClassBType] = classNode.interfaces.asScala.map(ensureClassBTypeFromParsedClassfile)(collection.breakOut) + val interfaces: List[ClassBType] = classNode.interfaces.asScala.map(classBTypeFromParsedClassfile)(collection.breakOut) val flags = classNode.access @@ -145,15 +158,13 @@ abstract class BTypes { def nestedInCurrentClass(innerClassNode: InnerClassNode): Boolean = { (innerClassNode.outerName != null && innerClassNode.outerName == classNode.name) || (innerClassNode.outerName == null && { - val classNodeForInnerClass = byteCodeRepository.classNode(innerClassNode.name) getOrElse { - assertionError(s"Could not find bytecode for class ${innerClassNode.name}") - } + 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({ - case i if nestedInCurrentClass(i) => ensureClassBTypeFromParsedClassfile(i.name) + case i if nestedInCurrentClass(i) => classBTypeFromParsedClassfile(i.name) })(collection.breakOut) // if classNode is a nested class, it has an innerClass attribute for itself. in this @@ -163,11 +174,11 @@ abstract class BTypes { val enclosingClass = if (innerEntry.outerName != null) { // if classNode is a member class, the outerName is non-null - ensureClassBTypeFromParsedClassfile(innerEntry.outerName) + classBTypeFromParsedClassfile(innerEntry.outerName) } else { // for anonymous or local classes, the outerName is null, but the enclosing class is // stored in the EnclosingMethod attribute (which ASM encodes in classNode.outerClass). - ensureClassBTypeFromParsedClassfile(classNode.outerClass) + classBTypeFromParsedClassfile(classNode.outerClass) } val staticFlag = (innerEntry.access & Opcodes.ACC_STATIC) != 0 NestedInfo(enclosingClass, Option(innerEntry.outerName), Option(innerEntry.innerName), staticFlag) @@ -175,7 +186,7 @@ abstract class BTypes { val inlineInfo = inlineInfoFromClassfile(classNode) - classBType.info = ClassInfo(superClass, interfaces, flags, nestedClasses, nestedInfo, inlineInfo) + classBType.info = Right(ClassInfo(superClass, interfaces, flags, nestedClasses, nestedInfo, inlineInfo)) classBType } @@ -186,12 +197,16 @@ abstract class BTypes { */ def inlineInfoFromClassfile(classNode: ClassNode): InlineInfo = { def fromClassfileAttribute: Option[InlineInfo] = { - // TODO: if this is a scala class and there's no attribute, emit an inliner warning if the InlineInfo is used if (classNode.attrs == null) None else classNode.attrs.asScala.collect({ case a: InlineInfoAttribute => a}).headOption.map(_.inlineInfo) } def fromClassfileWithoutAttribute = { + 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. @@ -209,7 +224,7 @@ abstract class BTypes { traitImplClassSelfType = None, isEffectivelyFinal = BytecodeUtils.isFinalClass(classNode), methodInfos = methodInfos, - warning = None) + warning) } // The InlineInfo is built from the classfile (not from the symbol) for all classes that are NOT @@ -283,7 +298,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") @@ -291,7 +306,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 } @@ -300,7 +315,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) { @@ -310,7 +325,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 @@ -325,7 +340,7 @@ abstract class BTypes { assert(isPrimitive && other.isPrimitive, s"Expected primitive types $this - $other") this == other } - } + })) /** * Compute the upper bound of two types. @@ -765,9 +780,22 @@ 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. * - * Currently non-final due to SI-9111 + * 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 { + final case class ClassBType(internalName: InternalName) extends RefBType { /** * Write-once variable allows initializing a cyclic graph of infos. This is required for * nested classes. Example: for the definition `class A { class B }` we have @@ -775,14 +803,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() @@ -791,27 +819,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) } /** @@ -819,12 +849,12 @@ 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: List[ClassBType] = info.superClass match { - case None => Nil - case Some(sc) => sc :: sc.superClassesTransitive - } + def superClassesTransitive: Either[NoClassBTypeInfo, List[ClassBType]] = info.flatMap(i => i.superClass match { + case None => Right(Nil) + case Some(sc) => sc.superClassesTransitive.map(sc :: _) + }) /** * The prefix of the internal name until the last '/', or the empty string. @@ -837,15 +867,19 @@ abstract class BTypes { } } - def isPublic = (info.flags & asm.Opcodes.ACC_PUBLIC) != 0 + def isPublic: Either[NoClassBTypeInfo, Boolean] = info.map(i => (i.flags & asm.Opcodes.ACC_PUBLIC) != 0) - def isNestedClass = info.nestedInfo.isDefined + 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, @@ -853,30 +887,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 inlineInfoAttribute: InlineInfoAttribute = InlineInfoAttribute(info.inlineInfo) + }) - 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) } /** @@ -886,34 +929,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 = { @@ -1080,11 +1125,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) @@ -1102,4 +1151,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 b90030dd8c..eeb6ed24a2 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala @@ -9,6 +9,7 @@ package backend.jvm import scala.tools.asm 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 @@ -34,12 +35,14 @@ 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)) + 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)) } @@ -50,6 +53,16 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { 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 @@ -104,27 +117,19 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { 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)) { - new ErroneousClassBType(internalName) + res.info = Left(NoClassBTypeInfoClassSymbolInfoFailedSI9111(classSym.fullName)) + res } else { - // 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)) + setClassInfo(classSym, res) } }) } } - /** - * Part of the workaround for SI-9111. Makes sure that the compiler only fails if the ClassInfo - * of the symbol that could not be completed is actually required. - */ - private class ErroneousClassBType(internalName: InternalName) extends ClassBType(internalName) { - def msg = s"The class info for $internalName could not be completed due to SI-9111." - override def info: ClassInfo = opt.OptimizerReporting.assertionError(msg) - override def info_=(i: ClassInfo): Unit = opt.OptimizerReporting.assertionError(msg) - } - /** * Builds a [[MethodBType]] for a method symbol. */ @@ -202,7 +207,7 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { 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)) + case RefinedType(parents, _) => parents.map(typeToBType(_).asClassBType).reduceLeft((a, b) => a.jvmWiseLUB(b).get) } } } @@ -340,7 +345,7 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { val inlineInfo = buildInlineInfo(classSym, classBType.internalName) - classBType.info = ClassInfo(superClass, interfaces, flags, nestedClasses, nestedInfo, inlineInfo) + classBType.info = Right(ClassInfo(superClass, interfaces, flags, nestedClasses, nestedInfo, inlineInfo)) classBType } @@ -421,13 +426,10 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { // symbols being compiled. For non-compiled classes, we could not build MethodInlineInfos // for those mixin members, which prevents inlining. byteCodeRepository.classNode(internalName) match { - case Some(classNode) => + case Right(classNode) => inlineInfoFromClassfile(classNode) - case None => - // TODO: inliner warning if the InlineInfo for that class is being used - // We can still use the inline information built from the symbol, even though mixin - // members will be missing. - buildFromSymbol + case Left(missingClass) => + InlineInfo(None, false, Map.empty, Some(ClassNotFoundWhenBuildingInlineInfoFromSymbol(missingClass))) } } } @@ -444,14 +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, - InlineInfo(None, true, Map.empty, None) // no InlineInfo needed, scala never invokes methods on the mirror class - ) + InlineInfo(None, true, Map.empty, None))) // no InlineInfo needed, scala never invokes methods on the mirror class c }) } 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 173aa0ca30..be1595dc29 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala @@ -307,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. 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 0958601d73..607b7145d6 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala @@ -11,9 +11,9 @@ 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 @@ -29,10 +29,10 @@ import java.util.concurrent.atomic.AtomicLong * 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 `None`: there is no - * ClassNode generated by the backend and also no classfile that could be parsed. + * 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, Option[(ClassNode, Source, Long)]]) { +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 @@ -46,24 +46,24 @@ class ByteCodeRepository(val classPath: ClassFileLookup[AbstractFile], val class * 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.isDefined && c._2.get._2 == Classfile) > maxCacheSize) { + 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, Some((_, Classfile, id))) if id < removeId => name + case (name, Right((_, Classfile, id))) if id < removeId => name }).toList toRemove foreach classes.remove } } def add(classNode: ClassNode, source: Source) = { - classes(classNode.name) = Some((classNode, source, idCounter.incrementAndGet())) + 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): Option[(ClassNode, Source)] = { + def classNodeAndSource(internalName: InternalName): Either[ClassNotFound, (ClassNode, Source)] = { val r = classes.getOrElseUpdate(internalName, { limitCacheSize() parseClass(internalName).map((_, Classfile, idCounter.incrementAndGet())) @@ -75,42 +75,66 @@ class ByteCodeRepository(val classPath: ClassFileLookup[AbstractFile], val class * 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): Option[ClassNode] = classNodeAndSource(internalName).map(_._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)] = { - classNode(classInternalName).flatMap(c => - 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(ownerInternalNameOrArrayDescriptor: String, name: String, descriptor: String): Option[(MethodNode, InternalName)] = { - // In a MethodInsnNode, the `owner` field may be an array descriptor, for exmple when invoking `clone`. - // We don't inline array methods (they are native anyway), so just return None. - if (ownerInternalNameOrArrayDescriptor.charAt(0) == '[') None - else { - classNode(ownerInternalNameOrArrayDescriptor).flatMap(c => - c.methods.asScala.find(m => m.name == name && m.desc == descriptor).map((_, ownerInternalNameOrArrayDescriptor)) 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): Option[ClassNode] = { + private def parseClass(internalName: InternalName): Either[ClassNotFound, ClassNode] = { val fullName = internalName.replace('/', '.') classPath.findClassFile(fullName) map { classFile => val classNode = new asm.tree.ClassNode() @@ -131,6 +155,9 @@ class ByteCodeRepository(val classPath: ClassFileLookup[AbstractFile], val class // https://jcp.org/aboutJava/communityprocess/final/jsr045/index.html removeLineNumberNodes(classNode) classNode + } 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 d2658bcd2a..14e8cccc60 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala @@ -85,7 +85,7 @@ object BytecodeUtils { def isFinalClass(classNode: ClassNode): Boolean = (classNode.access & Opcodes.ACC_FINAL) != 0 - def isFinalMethod(methodNode: MethodNode): Boolean = (methodNode.access & (Opcodes.ACC_FINAL | Opcodes.ACC_PRIVATE)) != 0 + def 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 diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala index 18b95184e5..cd204e8043 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala @@ -7,9 +7,11 @@ 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.InternalName +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} @@ -25,39 +27,40 @@ class CallGraph[BT <: BTypes](val btypes: BT) { def analyzeCallsites(methodNode: MethodNode, definingClass: ClassBType): List[Callsite] = { + case class CallsiteInfo(safeToInline: Boolean, annotatedInline: Boolean, annotatedNoInline: Boolean, warning: Option[CalleeInfoWarning]) + /** * Analyze a callsite and gather meta-data that can be used for inlining decisions. - * - * @return Three booleans indicating whether - * 1. the callsite can be safely inlined - * 2. the callee is annotated `@inline` - * 3. the callee is annotated `@noinline` */ - def analyzeCallsite(calleeMethodNode: MethodNode, calleeDeclarationClassBType: ClassBType, receiverTypeInternalName: InternalName, calleeSource: Source): (Boolean, Boolean, Boolean) = { + def analyzeCallsite(calleeMethodNode: MethodNode, calleeDeclarationClassBType: ClassBType, receiverTypeInternalName: InternalName, calleeSource: Source): CallsiteInfo = { val methodSignature = calleeMethodNode.name + calleeMethodNode.desc - // The inlineInfo.methodInfos of a ClassBType holds an InlineInfo for each method *declared* - // within a class (not for inherited methods). Since we already have the classBType of the - // callee, we only check there for the methodInlineInfo, we should find it there. - calleeDeclarationClassBType.info.inlineInfo.methodInfos.find(_._1 == methodSignature) match { - case Some((_, methodInlineInfo)) => - val canInlineFromSource = inlineGlobalEnabled || calleeSource == CompilationUnit - // A non-final method can be inline if the receiver type is a final subclass. Example: - // class A { @inline def f = 1 }; object B extends A; B.f // can be inlined - def isStaticallyResolved: Boolean = { - // TODO: type analysis can render more calls statically resolved - // Example: `new A.f` can be inlined, the receiver type is known to be exactly A. - methodInlineInfo.effectivelyFinal || { - // TODO: inline warning when the receiver class cannot be found on the classpath - classBTypeFromParsedClassfile(receiverTypeInternalName).exists(_.info.inlineInfo.isEffectivelyFinal) + 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 + // A non-final method can be inline if the receiver type is a final subclass. Example: + // class A { @inline def f = 1 }; object B extends A; B.f // can be inlined + def isStaticallyResolved: Boolean = { + // TODO: type analysis can render more calls statically resolved + // Example: `new A.f` can be inlined, the receiver type is known to be exactly A. + methodInlineInfo.effectivelyFinal || classBTypeFromParsedClassfile(receiverTypeInternalName).info.orThrow.inlineInfo.isEffectivelyFinal } - } - - (canInlineFromSource && isStaticallyResolved, methodInlineInfo.annotatedInline, methodInlineInfo.annotatedNoInline) + val warning = calleeDeclarationClassBType.info.orThrow.inlineInfo.warning.map( + MethodInlineInfoIncomplete(calleeDeclarationClassBType.internalName, calleeMethodNode.name, calleeMethodNode.desc, _)) + CallsiteInfo(canInlineFromSource && isStaticallyResolved, methodInlineInfo.annotatedInline, methodInlineInfo.annotatedNoInline, warning) - case None => - // TODO: issue inliner warning - (false, false, false) + case None => + val warning = MethodInlineInfoMissing(calleeDeclarationClassBType.internalName, calleeMethodNode.name, calleeMethodNode.desc, calleeDeclarationClassBType.info.orThrow.inlineInfo.warning) + CallsiteInfo(false, false, false, Some(warning)) + } + } catch { + case Invalid(noInfo: NoClassBTypeInfo) => + val warning = MethodInlineInfoError(calleeDeclarationClassBType.internalName, calleeMethodNode.name, calleeMethodNode.desc, noInfo) + CallsiteInfo(false, false, false, Some(warning)) } } @@ -72,25 +75,22 @@ class CallGraph[BT <: BTypes](val btypes: BT) { methodNode.instructions.iterator.asScala.collect({ case call: MethodInsnNode => - // TODO: log an inliner warning if the callee method cannot be found in the code repo? eg it's not on the classpath. - val callee = byteCodeRepository.methodNode(call.owner, call.name, call.desc) flatMap { - case (method, declarationClass) => - // TODO: log inliner warning if callee decl class cannot be found? - byteCodeRepository.classNodeAndSource(declarationClass) map { - case (declarationClassNode, source) => - val declarationClassBType = classBTypeFromClassNode(declarationClassNode) - val (safeToInline, annotatedInline, annotatedNoInline) = analyzeCallsite(method, declarationClassBType, call.owner, source) - Callee( - callee = method, - calleeDeclarationClass = declarationClassBType, - safeToInline = safeToInline, - annotatedInline = annotatedInline, - annotatedNoInline = annotatedNoInline - ) - } + 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, annotatedInline, annotatedNoInline, warning) = analyzeCallsite(method, declarationClassBType, call.owner, source) + Callee( + callee = method, + calleeDeclarationClass = declarationClassBType, + safeToInline = safeToInline, + annotatedInline = annotatedInline, + annotatedNoInline = annotatedNoInline, + calleeInfoWarning = warning) } - val argInfos = if (callee.isEmpty) Nil else { + 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 @@ -104,7 +104,8 @@ class CallGraph[BT <: BTypes](val btypes: BT) { callsiteClass = definingClass, callee = callee, argInfos = argInfos, - callsiteStackHeight = analyzer.frameAt(call).getStackSize + callsiteStackHeight = analyzer.frameAt(call).getStackSize, + callsitePosition = callsitePositions.getOrElse(call, NoPosition) ) }).toList } @@ -117,15 +118,20 @@ class CallGraph[BT <: BTypes](val btypes: BT) { * @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. `None` if the callee MethodNode cannot be found in - * the bytecode repository. + * 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: Option[Callee], argInfos: List[ArgInfo], - callsiteStackHeight: Int) { - override def toString = s"Invocation of ${callee.map(_.calleeDeclarationClass.internalName).getOrElse("?")}.${callsiteInstruction.name + callsiteInstruction.desc}@${callsiteMethod.instructions.indexOf(callsiteInstruction)} in ${callsiteClass.internalName}.${callsiteMethod.name}" + 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}" } /** @@ -147,8 +153,11 @@ class CallGraph[BT <: BTypes](val btypes: BT) { * and the inliner settings (project / global) allow inlining it. * @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, - annotatedInline: Boolean, annotatedNoInline: Boolean) + annotatedInline: Boolean, annotatedNoInline: Boolean, + calleeInfoWarning: Option[CalleeInfoWarning]) } 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 index b2459862ea..7ce98ecff1 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala @@ -15,9 +15,10 @@ import scala.collection.convert.decorateAsScala._ import scala.collection.convert.decorateAsJava._ import AsmUtils._ import BytecodeUtils._ -import OptimizerReporting._ 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._ @@ -27,10 +28,19 @@ class Inliner[BT <: BTypes](val btypes: BT) { rewriteFinalTraitMethodInvocations() for (request <- collectAndOrderInlineRequests) { - val Some(callee) = request.callee - inline(request.callsiteInstruction, request.callsiteStackHeight, request.callsiteMethod, request.callsiteClass, + 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) + } + } } } @@ -61,18 +71,51 @@ class Inliner[BT <: BTypes](val btypes: BT) { */ def selectCallsitesForInlining: List[Callsite] = { callsites.valuesIterator.filter({ - case Callsite(_, _, _, Some(Callee(callee, _, safeToInline, annotatedInline, _)), _, _) => - // For trait methods the callee is abstract: "trait T { @inline final def f = 1}". - // A callsite (t: T).f is `safeToInline` (effectivelyFinal is true), but the callee is the - // abstract method in the interface. - // Even though we such invocations are re-written using `rewriteFinalTraitMethodInvocation`, - // the guard is kept here for the cases where the rewrite fails. - !isAbstractMethod(callee) && safeToInline && annotatedInline - - case _ => false + 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 (!safeToInline) + backendReporting.inlinerWarning(pos, s"$initMsg: the method is not final and may be overridden." + warnMsg) + else if (doRewriteTraitCallsite(callsite) && isAbstractMethod(callee)) + backendReporting.inlinerWarning(pos, s"$initMsg: the trait method call could not be rewritten to the static implementation method." + 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) => + // Usually, safeToInline implies that the callee is not abstract. + // But for final trait methods, the callee is abstract: "trait T { @inline final def f = 1}". + // A callsite (t: T).f is `safeToInline`, but the callee is the abstract method in the interface. + // We try to rewrite these calls to the static impl method, but that may not always succeed, + // in which case we cannot inline the call. + annotatedInline && safeToInline && !isAbstractMethod(callee) + + 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 @@ -80,6 +123,20 @@ class Inliner[BT <: BTypes](val btypes: BT) { 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, true, annotatedInline, annotatedNoInline, infoWarning)) if isAbstractMethod(callee) => + // The pattern matches abstract methods that are `safeToInline`. This can only match the interface method of a final, concrete + // trait method. An abstract method (in a trait or abstract class) is never `safeToInline` (abstract methods cannot be final). + // See also comment in `doInlineCallsite` + for (i <- calleeDeclarationClass.isInterface) assert(i, s"expected interface call (final trait method) when inlining abstract method: $callsite") + 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. @@ -87,27 +144,31 @@ class Inliner[BT <: BTypes](val btypes: BT) { * In a final trait method callsite, the callee is safeToInline and the callee method is abstract * (the receiver type is the interface, so the method is abstract). */ - def rewriteFinalTraitMethodInvocation(callsite: Callsite): Unit = callsite.callee match { - case Some(Callee(callee, calleeDeclarationClass, true, true, annotatedNoInline)) if isAbstractMethod(callee) => - assert(calleeDeclarationClass.isInterface, s"expected interface call (final trait method) when inlining abstract method: $callsite") + def rewriteFinalTraitMethodInvocation(callsite: Callsite): Unit = { + if (doRewriteTraitCallsite(callsite)) { + val Right(Callee(callee, calleeDeclarationClass, safeToInline, annotatedInline, annotatedNoInline, infoWarning)) = callsite.callee val traitMethodArgumentTypes = asm.Type.getArgumentTypes(callee.desc) - val selfParamType = calleeDeclarationClass.info.inlineInfo.traitImplClassSelfType match { + val implClassInternalName = calleeDeclarationClass.internalName + "$class" + + val selfParamTypeV: Either[OptimizerWarning, ClassBType] = calleeDeclarationClass.info.map(_.inlineInfo.traitImplClassSelfType match { case Some(internalName) => classBTypeFromParsedClassfile(internalName) - case None => Some(calleeDeclarationClass) - } + case None => calleeDeclarationClass + }) - val implClassInternalName = calleeDeclarationClass.internalName + "$class" + 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. - for { - // TODO: inline warnings if selfClassType, impl class or impl method cannot be found - selfType <- selfParamType - implClassMethodDescriptor = asm.Type.getMethodDescriptor(asm.Type.getReturnType(callee.desc), selfType.toASMType +: traitMethodArgumentTypes: _*) - (implClassMethod, _) <- byteCodeRepository.methodNode(implClassInternalName, callee.name, implClassMethodDescriptor) - implClassBType <- classBTypeFromParsedClassfile(implClassInternalName) + 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. @@ -116,16 +177,16 @@ class Inliner[BT <: BTypes](val btypes: BT) { // 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 (!calleeDeclarationClass.isSubtypeOf(selfType)) { + 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, selfType.internalName) + val cast = new TypeInsnNode(CHECKCAST, selfParamType.internalName) callsite.callsiteMethod.instructions.insert(i, cast) } } - val newCallsiteInstruction = new MethodInsnNode(INVOKESTATIC, implClassInternalName, callee.name, implClassMethodDescriptor, false) + val newCallsiteInstruction = new MethodInsnNode(INVOKESTATIC, implClassInternalName, callee.name, implMethodDescriptor, false) callsite.callsiteMethod.instructions.insert(callsite.callsiteInstruction, newCallsiteInstruction) callsite.callsiteMethod.instructions.remove(callsite.callsiteInstruction) @@ -134,19 +195,26 @@ class Inliner[BT <: BTypes](val btypes: BT) { callsiteInstruction = newCallsiteInstruction, callsiteMethod = callsite.callsiteMethod, callsiteClass = callsite.callsiteClass, - callee = Some(Callee( + callee = Right(Callee( callee = implClassMethod, calleeDeclarationClass = implClassBType, - safeToInline = true, - annotatedInline = true, - annotatedNoInline = annotatedNoInline)), + safeToInline = safeToInline, + annotatedInline = annotatedInline, + annotatedNoInline = annotatedNoInline, + calleeInfoWarning = infoWarning)), argInfos = Nil, - callsiteStackHeight = callsite.callsiteStackHeight + callsiteStackHeight = callsite.callsiteStackHeight, + callsitePosition = callsite.callsitePosition ) callGraph.callsites(newCallsiteInstruction) = staticCallsite } - case _ => + 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)) + } + } } /** @@ -240,7 +308,7 @@ class Inliner[BT <: BTypes](val btypes: BT) { */ def inline(callsiteInstruction: MethodInsnNode, callsiteStackHeight: Int, callsiteMethod: MethodNode, callsiteClass: ClassBType, callee: MethodNode, calleeDeclarationClass: ClassBType, - receiverKnownNotNull: Boolean, keepLineNumbers: Boolean): Option[String] = { + receiverKnownNotNull: Boolean, keepLineNumbers: Boolean): Option[CannotInlineWarning] = { canInline(callsiteInstruction, callsiteStackHeight, callsiteMethod, callsiteClass, callee, calleeDeclarationClass) orElse { // New labels for the cloned instructions val labelsMap = cloneLabels(callee) @@ -363,7 +431,8 @@ class Inliner[BT <: BTypes](val btypes: BT) { callsiteClass = callsiteClass, callee = originalCallsite.callee, argInfos = Nil, // TODO: re-compute argInfos for new destination (once we actually compute them) - callsiteStackHeight = callsiteStackHeight + originalCallsite.callsiteStackHeight + callsiteStackHeight = callsiteStackHeight + originalCallsite.callsiteStackHeight, + callsitePosition = originalCallsite.callsitePosition ) case None => @@ -386,7 +455,7 @@ class Inliner[BT <: BTypes](val btypes: BT) { * @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[String] = { + 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" @@ -416,37 +485,43 @@ class Inliner[BT <: BTypes](val btypes: BT) { 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(s"Method ${methodSignature(calleeDeclarationClass.internalName, callee)} is not inlined because it is synchronized") + Some(SynchronizedMethod(calleeDeclarationClass.internalName, callee.name, callee.desc)) } else if (!callee.tryCatchBlocks.isEmpty && stackHasNonParameters) { - Some( - s"""The operand stack at the callsite in ${methodSignature(callsiteClass.internalName, callsiteMethod)} contains more values than the - |arguments expected by the callee ${methodSignature(calleeDeclarationClass.internalName, callee)}. These values would be discarded - |when entering an exception handler declared in the inlined method.""".stripMargin - ) + Some(MethodWithHandlerCalledOnNonEmptyStack( + calleeDeclarationClass.internalName, callee.name, callee.desc, + callsiteClass.internalName, callsiteMethod.name, callsiteMethod.desc)) } else findIllegalAccess(callee.instructions, callsiteClass) map { - case illegalAccessIns => - s"""The callee ${methodSignature(calleeDeclarationClass.internalName, callee)} contains the instruction ${AsmUtils.textify(illegalAccessIns)} - |that would cause an IllegalAccessError when inlined into class ${callsiteClass.internalName}""".stripMargin + 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`. Returns `None` if - * all instructions can be legally transplanted. + * [[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, destinationClass: ClassBType): Option[AbstractInsnNode] = { + def findIllegalAccess(instructions: InsnList, 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): Boolean = (accessed: @unchecked) match { + 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 || c.packageInternalName == from.packageInternalName + case c: ClassBType => c.isPublic.map(_ || c.packageInternalName == from.packageInternalName) case a: ArrayBType => classIsAccessible(a.elementType, from) - case _: PrimitiveBType => true + case _: PrimitiveBType => Right(true) } /** @@ -471,27 +546,29 @@ class Inliner[BT <: BTypes](val btypes: BT) { * run-time package as D. * (B4) R is private and is declared in D. */ - def memberIsAccessible(memberFlags: Int, memberDeclClass: ClassBType, memberRefClass: ClassBType): Boolean = { + 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 - true + Right(true) case ACC_PROTECTED => // B2 - val condB2 = destinationClass.isSubtypeOf(memberDeclClass) && { - val isStatic = (ACC_STATIC & memberFlags) != 0 - isStatic || memberRefClass.isSubtypeOf(destinationClass) || destinationClass.isSubtypeOf(memberRefClass) + 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) } - condB2 || samePackageAsDestination // B3 (protected) - case 0 => // B3 (default access) - samePackageAsDestination + case 0 => // B3 (default access) + Right(samePackageAsDestination) case ACC_PRIVATE => // B4 - memberDeclClass == destinationClass + Right(memberDeclClass == destinationClass) } } @@ -502,49 +579,68 @@ class Inliner[BT <: BTypes](val btypes: BT) { * 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): Boolean = instruction match { + 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. - bTypeForDescriptorOrInternalNameFromClassfile(ti.desc).exists(classIsAccessible(_)) + classIsAccessible(bTypeForDescriptorOrInternalNameFromClassfile(ti.desc)) case ma: MultiANewArrayInsnNode => // "a symbolic reference to a class, array, or interface type" - bTypeForDescriptorOrInternalNameFromClassfile(ma.desc).exists(classIsAccessible(_)) + classIsAccessible(bTypeForDescriptorOrInternalNameFromClassfile(ma.desc)) case fi: FieldInsnNode => - (for { - fieldRefClass <- classBTypeFromParsedClassfile(fi.owner) - (fieldNode, fieldDeclClassNode) <- byteCodeRepository.fieldNode(fieldRefClass.internalName, fi.name, fi.desc) - fieldDeclClass <- classBTypeFromParsedClassfile(fieldDeclClassNode) + 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 { - memberIsAccessible(fieldNode.access, fieldDeclClass, fieldRefClass) - }) getOrElse false + res + } case mi: MethodInsnNode => - if (mi.owner.charAt(0) == '[') true // array methods are accessible - else (for { - methodRefClass <- classBTypeFromParsedClassfile(mi.owner) - (methodNode, methodDeclClassNode) <- byteCodeRepository.methodNode(methodRefClass.internalName, mi.name, mi.desc) - methodDeclClass <- classBTypeFromParsedClassfile(methodDeclClassNode) - } yield { - memberIsAccessible(methodNode.access, methodDeclClass, methodRefClass) - }) getOrElse false + if (mi.owner.charAt(0) == '[') Right(true) // array methods are accessible + else { + val methodRefClass = classBTypeFromParsedClassfile(mi.owner) + for { + (methodNode, methodDeclClassNode) <- byteCodeRepository.methodNode(methodRefClass.internalName, mi.name, mi.desc): Either[OptimizerWarning, (MethodNode, InternalName)] + methodDeclClass = classBTypeFromParsedClassfile(methodDeclClassNode) + res <- memberIsAccessible(methodNode.access, methodDeclClass, methodRefClass) + } yield { + res + } + } case ivd: InvokeDynamicInsnNode => // TODO @lry check necessary conditions to inline an indy, instead of giving up - false + Right(false) case ci: LdcInsnNode => ci.cst match { - case t: asm.Type => bTypeForDescriptorOrInternalNameFromClassfile(t.getInternalName).exists(classIsAccessible(_)) - case _ => true + case t: asm.Type => classIsAccessible(bTypeForDescriptorOrInternalNameFromClassfile(t.getInternalName)) + case _ => Right(true) } - case _ => true + case _ => Right(true) } - instructions.iterator.asScala.find(!isLegal(_)) + 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/OptimizerReporting.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/OptimizerReporting.scala deleted file mode 100644 index 5b47bc88c2..0000000000 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/OptimizerReporting.scala +++ /dev/null @@ -1,28 +0,0 @@ -/* NSC -- new Scala compiler - * Copyright 2005-2014 LAMP/EPFL - * @author Martin Odersky - */ - -package scala.tools.nsc -package backend.jvm -package opt - -import scala.tools.asm -import asm.tree._ -import scala.tools.nsc.backend.jvm.BTypes.InternalName - -/** - * Reporting utilities used in the optimizer. - * - * TODO: move out of opt package, rename: it's already used outside the optimizer. - * Centralize backend reporting here. - */ -object OptimizerReporting { - def methodSignature(classInternalName: InternalName, method: MethodNode): String = { - classInternalName + "::" + method.name + method.desc - } - - // TODO: clean up reporting of the inliner, test inline failure warnings, etc - 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 43b634eee1..d273995e6e 100644 --- a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala @@ -256,6 +256,25 @@ trait ScalaSettings extends AbsScalaSettings 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/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/tools/nsc/backend/jvm/CodeGenTools.scala b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala index c64f6e7f10..5d5215d887 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala @@ -6,11 +6,12 @@ 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.io.AbstractFile -import scala.tools.nsc.settings.{MutableSettings, ScalaSettings} +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._ @@ -52,7 +53,7 @@ object CodeGenTools { val settings = new Settings() val args = (CommandLineParser tokenize defaultArgs) ++ (CommandLineParser tokenize extraArgs) settings.processArguments(args, processAll = true) - new Global(settings) + new Global(settings, new StoreReporter) } def newRun(compiler: Global): compiler.Run = { @@ -61,6 +62,8 @@ object CodeGenTools { 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])] = { @@ -75,9 +78,18 @@ object CodeGenTools { files(outDir) } - def compile(compiler: Global)(scalaCode: String, javaCode: List[(String, String)] = Nil): List[(String, Array[Byte])] = { + 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) } @@ -90,7 +102,7 @@ object CodeGenTools { * The output directory is a physical directory, I have not figured out if / how it's possible to * add a VirtualDirectory to the classpath of a compiler. */ - def compileSeparately(codes: List[String], extraArgs: String = ""): List[(String, Array[Byte])] = { + 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" @@ -98,6 +110,8 @@ object CodeGenTools { 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) @@ -105,29 +119,29 @@ object CodeGenTools { classfiles } - def compileClassesSeparately(codes: List[String], extraArgs: String = "") = { - readAsmClasses(compileSeparately(codes, extraArgs)) + 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, javaCode: List[(String, String)] = Nil): List[ClassNode] = { - readAsmClasses(compile(compiler)(code, javaCode)) + 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 == "") + 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 == "") } - 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 94877fb037..4086f7dd7b 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala @@ -89,6 +89,10 @@ class DirectCompileTest extends ClearAfterClass { 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 761f214f82..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,6 +15,8 @@ import CodeGenTools._ import scala.tools.partest.ASMConverters import ASMConverters._ +import BackendReporting._ + import scala.collection.convert.decorateAsScala._ @RunWith(classOf[JUnit4]) @@ -39,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) } } @@ -73,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). @@ -85,7 +87,7 @@ class BTypesFromClassfileTest { clearCache() val fromSymbol = classBTypeFromSymbol(classSym) clearCache() - val fromClassfile = bTypes.classBTypeFromParsedClassfile(fromSymbol.internalName).get + val fromClassfile = bTypes.classBTypeFromParsedClassfile(fromSymbol.internalName) sameBType(fromSymbol, fromClassfile) } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala index d7344ae61f..9fda034a04 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala @@ -11,27 +11,29 @@ 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") + 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): List[ClassNode] = { + def compile(code: String, allowMessage: StoreReporter#Info => Boolean): List[ClassNode] = { notPerRun.foreach(_.clear()) - compileClasses(compiler)(code) + compileClasses(compiler)(code, allowMessage = allowMessage) } def callsInMethod(methodNode: MethodNode): List[MethodInsnNode] = methodNode.instructions.iterator.asScala.collect({ @@ -69,7 +71,20 @@ class CallGraphTest { // 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 List(cCls, cMod, dCls, testCls) = compile(code).map(c => byteCodeRepository.classNode(c.name).get) + + 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) diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala index 4e12ed757e..57088bdd2f 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala @@ -14,6 +14,8 @@ import ASMConverters._ import AsmUtils._ import scala.tools.testing.ClearAfterClass +import BackendReporting._ + import scala.collection.convert.decorateAsScala._ object InlineInfoTest extends ClearAfterClass.Clearable { @@ -53,7 +55,7 @@ class InlineInfoTest { |class C extends T with U """.stripMargin val classes = compile(code) - val fromSyms = classes.map(c => compiler.genBCode.bTypes.classBTypeFromInternalName(c.name).info.inlineInfo) + val 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) 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 index 91404acba7..03d2f2f108 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala @@ -59,7 +59,7 @@ class InlinerIllegalAccessTest extends ClearAfterClass { def check(classNode: ClassNode, test: Option[AbstractInsnNode] => Unit) = { for (m <- methods) - test(inliner.findIllegalAccess(m.instructions, classBTypeFromParsedClassfile(classNode.name).get)) + test(inliner.findIllegalAccess(m.instructions, classBTypeFromParsedClassfile(classNode.name)).map(_._1)) } check(cClass, assertEmpty) @@ -153,7 +153,7 @@ class InlinerIllegalAccessTest extends ClearAfterClass { val List(rbD, rcD, rfD, rgD) = dCl.methods.asScala.toList.filter(_.name(0) == 'r').sortBy(_.name) def check(method: MethodNode, dest: ClassNode, test: Option[AbstractInsnNode] => Unit): Unit = { - test(inliner.findIllegalAccess(method.instructions, classBTypeFromParsedClassfile(dest.name).get)) + test(inliner.findIllegalAccess(method.instructions, classBTypeFromParsedClassfile(dest.name)).map(_._1)) } val cOrDOwner = (_: Option[AbstractInsnNode] @unchecked) match { diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala index 58a262c401..5c9bd1c188 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala @@ -43,7 +43,8 @@ class InlinerSeparateCompilationTest { |} """.stripMargin - val List(c, o, oMod, t, tCls) = compileClassesSeparately(List(codeA, codeB), args) + 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")) diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala index 7f58f77b15..d32c1b2958 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -15,6 +15,7 @@ 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._ @@ -22,11 +23,13 @@ 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 { - var compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:classpath") + var compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:classpath -Yopt-warnings") // 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) @@ -61,9 +64,9 @@ class InlinerTest extends ClearAfterClass { val compiler = InlinerTest.compiler import compiler.genBCode.bTypes._ - def compile(scalaCode: String, javaCode: List[(String, String)] = Nil): List[ClassNode] = { + def compile(scalaCode: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[ClassNode] = { InlinerTest.notPerRun.foreach(_.clear()) - compileClasses(compiler)(scalaCode, javaCode) + compileClasses(compiler)(scalaCode, javaCode, allowMessage) } def checkCallsite(callsite: callGraph.Callsite, callee: MethodNode) = { @@ -76,10 +79,10 @@ class InlinerTest extends ClearAfterClass { } // inline first invocation of f into g in class C - def inlineTest(code: String, mod: ClassNode => Unit = _ => ()): (MethodNode, Option[String]) = { + def inlineTest(code: String, mod: ClassNode => Unit = _ => ()): (MethodNode, Option[CannotInlineWarning]) = { val List(cls) = compile(code) mod(cls) - val clsBType = classBTypeFromParsedClassfile(cls.name).get + 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() @@ -166,7 +169,7 @@ class InlinerTest extends ClearAfterClass { val f = cls.methods.asScala.find(_.name == "f").get f.access |= ACC_SYNCHRONIZED }) - assert(can.get contains "synchronized", can) + assert(can.get.isInstanceOf[SynchronizedMethod], can) } @Test @@ -192,7 +195,7 @@ class InlinerTest extends ClearAfterClass { |} """.stripMargin val (_, r) = inlineTest(code) - assert(r.get contains "operand stack at the callsite", r) + assert(r.get.isInstanceOf[MethodWithHandlerCalledOnNonEmptyStack], r) } @Test @@ -213,8 +216,8 @@ class InlinerTest extends ClearAfterClass { val List(c, d) = compile(code) - val cTp = classBTypeFromParsedClassfile(c.name).get - val dTp = classBTypeFromParsedClassfile(d.name).get + 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 @@ -234,7 +237,7 @@ class InlinerTest extends ClearAfterClass { receiverKnownNotNull = true, keepLineNumbers = true) - assert(r.get contains "would cause an IllegalAccessError", r) + assert(r.get.isInstanceOf[IllegalAccessInstruction], r) } @Test @@ -373,7 +376,7 @@ class InlinerTest extends ClearAfterClass { 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).get + val clsBType = classBTypeFromParsedClassfile(c.name) val analyzer = new AsmAnalyzer(f, clsBType.internalName) val integerClassBType = classBTypeFromInternalName("java/lang/Integer") @@ -457,8 +460,15 @@ class InlinerTest extends ClearAfterClass { |} """.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 - val List(b) = compile(scalaCode, List((javaCode, "A.java"))) + 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) @@ -526,7 +536,12 @@ class InlinerTest extends ClearAfterClass { | def t2 = this.f |} """.stripMargin - val List(c, t, tClass) = compile(code) + 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") } @@ -561,7 +576,10 @@ class InlinerTest extends ClearAfterClass { | def t3(t: T) = t.f // no inlining here |} """.stripMargin - val List(c, oMirror, oModule, t, tClass) = compile(code) + 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")) @@ -663,7 +681,11 @@ class InlinerTest extends ClearAfterClass { | def m5b(t: T2b) = t.f // re-written to T2b$class.f, inlined, ICONST_1 |} """.stripMargin - val List(ca, cb, t1, t1C, t2a, t2aC, t2b, t2bC) = compile(code) + + val 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 @@ -737,4 +759,36 @@ class InlinerTest extends ClearAfterClass { 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) + } } 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 2c71e9d533..1ce1b88ff2 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala @@ -31,7 +31,8 @@ class MethodLevelOpts extends ClearAfterClass { @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 c2e2a1b883..da9853148b 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala @@ -154,7 +154,8 @@ class UnreachableCodeTest extends ClearAfterClass { 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))) } -- cgit v1.2.3 From 2d88143f37144f3db5a1d1d27806518bea13ba47 Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Wed, 11 Mar 2015 10:39:40 -0700 Subject: Ensure to re-write only trait method calls of actual trait methods The inliner would incorrectly treat trait field accessors like ordinary trait member methods and try to re-write invocations to the corresponding static method in the implementation class. This rewrite usually failed because no method was found in the impl class. However, for lazy val fields, there exists a member in the impl class with the same name, and the rewrite was performed. The result was that every field access would execute the lazy initializer instead of reading the field. This commit checks the traitMethodWithStaticImplementation field of the ScalaInlineInfo classfile attribute and puts an explicit `safeToRewrite` flag to each call site in the call graph. This cleans up the code in the inliner that deals with rewriting trait callsites. --- .../scala/tools/nsc/backend/jvm/BTypes.scala | 1 - .../tools/nsc/backend/jvm/opt/CallGraph.scala | 52 ++++++++++++---- .../scala/tools/nsc/backend/jvm/opt/Inliner.scala | 30 ++++------ .../tools/nsc/backend/jvm/opt/InlinerTest.scala | 70 ++++++++++++++++++++++ 4 files changed, 119 insertions(+), 34 deletions(-) (limited to 'src/compiler') diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala index d2ee944916..d8a17e975e 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala @@ -14,7 +14,6 @@ import asm.Opcodes 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 BackendReporting.RightBiasedEither import scala.tools.nsc.backend.jvm.opt._ import scala.collection.convert.decorateAsScala._ diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala index cd204e8043..47d32c94cb 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala @@ -27,7 +27,9 @@ class CallGraph[BT <: BTypes](val btypes: BT) { def analyzeCallsites(methodNode: MethodNode, definingClass: ClassBType): List[Callsite] = { - case class CallsiteInfo(safeToInline: Boolean, annotatedInline: Boolean, annotatedNoInline: Boolean, warning: Option[CalleeInfoWarning]) + 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. @@ -42,25 +44,44 @@ class CallGraph[BT <: BTypes](val btypes: BT) { calleeDeclarationClassBType.info.orThrow.inlineInfo.methodInfos.get(methodSignature) match { case Some(methodInlineInfo) => val canInlineFromSource = inlineGlobalEnabled || calleeSource == CompilationUnit - // A non-final method can be inline if the receiver type is a final subclass. Example: - // class A { @inline def f = 1 }; object B extends A; B.f // can be inlined - def isStaticallyResolved: Boolean = { - // TODO: type analysis can render more calls statically resolved - // Example: `new A.f` can be inlined, the receiver type is known to be exactly A. - methodInlineInfo.effectivelyFinal || classBTypeFromParsedClassfile(receiverTypeInternalName).info.orThrow.inlineInfo.isEffectivelyFinal + + 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, _)) - CallsiteInfo(canInlineFromSource && isStaticallyResolved, methodInlineInfo.annotatedInline, methodInlineInfo.annotatedNoInline, warning) + + // (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, Some(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, Some(warning)) + CallsiteInfo(false, false, false, false, Some(warning)) } } @@ -80,11 +101,12 @@ class CallGraph[BT <: BTypes](val btypes: BT) { (declarationClassNode, source) <- byteCodeRepository.classNodeAndSource(declarationClass): Either[OptimizerWarning, (ClassNode, Source)] declarationClassBType = classBTypeFromClassNode(declarationClassNode) } yield { - val CallsiteInfo(safeToInline, annotatedInline, annotatedNoInline, warning) = analyzeCallsite(method, declarationClassBType, call.owner, source) + 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) @@ -151,13 +173,17 @@ class CallGraph[BT <: BTypes](val btypes: BT) { * @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, + safeToInline: Boolean, safeToRewrite: Boolean, annotatedInline: Boolean, annotatedNoInline: Boolean, - calleeInfoWarning: Option[CalleeInfoWarning]) + 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/Inliner.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala index 7ce98ecff1..0f4c7d5287 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala @@ -71,7 +71,7 @@ class Inliner[BT <: BTypes](val btypes: BT) { */ def selectCallsitesForInlining: List[Callsite] = { callsites.valuesIterator.filter({ - case callsite @ Callsite(_, _, _, Right(Callee(callee, calleeDeclClass, safeToInline, annotatedInline, _, warning)), _, _, pos) => + case callsite @ Callsite(_, _, _, Right(Callee(callee, calleeDeclClass, safeToInline, _, annotatedInline, _, warning)), _, _, pos) => val res = doInlineCallsite(callsite) if (!res) { @@ -80,10 +80,10 @@ class Inliner[BT <: BTypes](val btypes: BT) { // 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 (!safeToInline) - backendReporting.inlinerWarning(pos, s"$initMsg: the method is not final and may be overridden." + warnMsg) - else if (doRewriteTraitCallsite(callsite) && isAbstractMethod(callee)) + 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)) { @@ -105,13 +105,8 @@ class Inliner[BT <: BTypes](val btypes: BT) { * 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) => - // Usually, safeToInline implies that the callee is not abstract. - // But for final trait methods, the callee is abstract: "trait T { @inline final def f = 1}". - // A callsite (t: T).f is `safeToInline`, but the callee is the abstract method in the interface. - // We try to rewrite these calls to the static impl method, but that may not always succeed, - // in which case we cannot inline the call. - annotatedInline && safeToInline && !isAbstractMethod(callee) + case Callsite(_, _, _, Right(Callee(callee, calleeDeclClass, safeToInline, _, annotatedInline, _, warning)), _, _, pos) => + annotatedInline && safeToInline case _ => false } @@ -127,13 +122,7 @@ class Inliner[BT <: BTypes](val btypes: BT) { * 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, true, annotatedInline, annotatedNoInline, infoWarning)) if isAbstractMethod(callee) => - // The pattern matches abstract methods that are `safeToInline`. This can only match the interface method of a final, concrete - // trait method. An abstract method (in a trait or abstract class) is never `safeToInline` (abstract methods cannot be final). - // See also comment in `doInlineCallsite` - for (i <- calleeDeclarationClass.isInterface) assert(i, s"expected interface call (final trait method) when inlining abstract method: $callsite") - true - + case Right(Callee(callee, calleeDeclarationClass, safeToInline, true, annotatedInline, annotatedNoInline, infoWarning)) => true case _ => false } @@ -146,7 +135,7 @@ class Inliner[BT <: BTypes](val btypes: BT) { */ def rewriteFinalTraitMethodInvocation(callsite: Callsite): Unit = { if (doRewriteTraitCallsite(callsite)) { - val Right(Callee(callee, calleeDeclarationClass, safeToInline, annotatedInline, annotatedNoInline, infoWarning)) = callsite.callee + val Right(Callee(callee, calleeDeclarationClass, _, _, annotatedInline, annotatedNoInline, infoWarning)) = callsite.callee val traitMethodArgumentTypes = asm.Type.getArgumentTypes(callee.desc) @@ -198,7 +187,8 @@ class Inliner[BT <: BTypes](val btypes: BT) { callee = Right(Callee( callee = implClassMethod, calleeDeclarationClass = implClassBType, - safeToInline = safeToInline, + safeToInline = true, + safeToRewrite = false, annotatedInline = annotatedInline, annotatedNoInline = annotatedNoInline, calleeInfoWarning = infoWarning)), diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala index d32c1b2958..caaa65bf7e 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -791,4 +791,74 @@ class InlinerTest extends ClearAfterClass { 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") + } } -- cgit v1.2.3 From c2ab768287cc02b5e01342ac993d6c2b6e7ee2aa Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Thu, 12 Mar 2015 15:32:46 -0700 Subject: Don't inline methods containing super calls into other classes Method bodies that contain a super call cannot be inlined into other classes. The same goes for methods containing calls to private methods, but that was already ensured before by accessibility checks. The last case of `invokespecial` instructions is constructor calls. Those can be safely moved into different classes (as long as the constructor is accessible at the new location). Note that scalac never emits methods / constructors as private in bytecode. --- .../scala/tools/nsc/backend/jvm/opt/Inliner.scala | 18 ++++++-- .../backend/jvm/opt/InlinerIllegalAccessTest.scala | 26 ++++++------ .../tools/nsc/backend/jvm/opt/InlinerTest.scala | 48 ++++++++++++++++++++++ 3 files changed, 76 insertions(+), 16 deletions(-) (limited to 'src/compiler') diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala index 0f4c7d5287..e14e57d3ab 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala @@ -480,7 +480,7 @@ class Inliner[BT <: BTypes](val btypes: BT) { Some(MethodWithHandlerCalledOnNonEmptyStack( calleeDeclarationClass.internalName, callee.name, callee.desc, callsiteClass.internalName, callsiteMethod.name, callsiteMethod.desc)) - } else findIllegalAccess(callee.instructions, callsiteClass) map { + } else findIllegalAccess(callee.instructions, calleeDeclarationClass, callsiteClass) map { case (illegalAccessIns, None) => IllegalAccessInstruction( calleeDeclarationClass.internalName, callee.name, callee.desc, @@ -500,7 +500,7 @@ class Inliner[BT <: BTypes](val btypes: BT) { * 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, destinationClass: ClassBType): Option[(AbstractInsnNode, Option[OptimizerWarning])] = { + 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. @@ -597,11 +597,23 @@ class Inliner[BT <: BTypes](val btypes: BT) { 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 <- memberIsAccessible(methodNode.access, methodDeclClass, methodRefClass) + res <- canInlineCall(mi.getOpcode, methodNode.access, methodDeclClass, methodRefClass) } yield { res } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala index 03d2f2f108..b4839dcec8 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala @@ -59,7 +59,7 @@ class InlinerIllegalAccessTest extends ClearAfterClass { def check(classNode: ClassNode, test: Option[AbstractInsnNode] => Unit) = { for (m <- methods) - test(inliner.findIllegalAccess(m.instructions, classBTypeFromParsedClassfile(classNode.name)).map(_._1)) + test(inliner.findIllegalAccess(m.instructions, classBTypeFromParsedClassfile(cClass.name), classBTypeFromParsedClassfile(classNode.name)).map(_._1)) } check(cClass, assertEmpty) @@ -152,8 +152,8 @@ class InlinerIllegalAccessTest extends ClearAfterClass { val List(rbD, rcD, rfD, rgD) = dCl.methods.asScala.toList.filter(_.name(0) == 'r').sortBy(_.name) - def check(method: MethodNode, dest: ClassNode, test: Option[AbstractInsnNode] => Unit): Unit = { - test(inliner.findIllegalAccess(method.instructions, classBTypeFromParsedClassfile(dest.name)).map(_._1)) + 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 { @@ -164,35 +164,35 @@ class InlinerIllegalAccessTest extends ClearAfterClass { // PUBLIC // public methods allowed everywhere - for (m <- Set(raC, reC); c <- allClasses) check(m, c, assertEmpty) + for (m <- Set(raC, reC); c <- allClasses) check(m, cCl, c, assertEmpty) // DEFAULT ACCESS // default access OK in same package - for (m <- Set(rbC, rfC, rbD, rfD); c <- allClasses) { - if (c.name startsWith "a/") check(m, c, assertEmpty) - else check(m, c, cOrDOwner) + 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 <- Set(rcC, rgC, rgD); c <- Set(cCl, dCl, eCl, fCl, gCl, hCl)) check(m, c, assertEmpty) + 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, iCl, cOrDOwner) + 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, c, assertEmpty) + 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, c, cOrDOwner) + 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, assertEmpty) - for (m <- Set(rdC, rhC); c <- allClasses.tail) check(m, c, cOrDOwner) + 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/InlinerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala index 0581bd24fe..39fb28570e 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -902,4 +902,52 @@ class InlinerTest extends ClearAfterClass { 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.() + | @inline final def f4 = new B(1) // invokespecial B.(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", "") + assertInvoke(getSingleMethod(b, "t4"), "B", "") + + assertInvoke(getSingleMethod(t, "t1"), "B", "f1") + assertInvoke(getSingleMethod(t, "t2"), "B", "B$$f2m") + assertInvoke(getSingleMethod(t, "t3"), "B", "") + assertInvoke(getSingleMethod(t, "t4"), "B", "") + } } -- cgit v1.2.3