summaryrefslogtreecommitdiff
path: root/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala
diff options
context:
space:
mode:
Diffstat (limited to 'src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala')
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala648
1 files changed, 648 insertions, 0 deletions
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala
new file mode 100644
index 0000000000..e14e57d3ab
--- /dev/null
+++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala
@@ -0,0 +1,648 @@
+/* NSC -- new Scala compiler
+ * Copyright 2005-2014 LAMP/EPFL
+ * @author Martin Odersky
+ */
+
+package scala.tools.nsc
+package backend.jvm
+package opt
+
+import scala.annotation.tailrec
+import scala.tools.asm
+import asm.Opcodes._
+import asm.tree._
+import scala.collection.convert.decorateAsScala._
+import scala.collection.convert.decorateAsJava._
+import AsmUtils._
+import BytecodeUtils._
+import collection.mutable
+import scala.tools.asm.tree.analysis.{SourceInterpreter, Analyzer}
+import BackendReporting._
+import scala.tools.nsc.backend.jvm.BTypes.InternalName
+
+class Inliner[BT <: BTypes](val btypes: BT) {
+ import btypes._
+ import callGraph._
+
+ def runInliner(): Unit = {
+ rewriteFinalTraitMethodInvocations()
+
+ for (request <- collectAndOrderInlineRequests) {
+ val Right(callee) = request.callee // collectAndOrderInlineRequests returns callsites with a known callee
+
+ val r = inline(request.callsiteInstruction, request.callsiteStackHeight, request.callsiteMethod, request.callsiteClass,
+ callee.callee, callee.calleeDeclarationClass,
+ receiverKnownNotNull = false, keepLineNumbers = false)
+
+ for (warning <- r) {
+ if ((callee.annotatedInline && btypes.warnSettings.atInlineFailed) || warning.emitWarning(warnSettings)) {
+ val annotWarn = if (callee.annotatedInline) " is annotated @inline but" else ""
+ val msg = s"${BackendReporting.methodSignature(callee.calleeDeclarationClass.internalName, callee.callee)}$annotWarn could not be inlined:\n$warning"
+ backendReporting.inlinerWarning(request.callsitePosition, msg)
+ }
+ }
+ }
+ }
+
+ /**
+ * Ordering for inline requests. Required to make the inliner deterministic:
+ * - Always remove the same request when breaking inlining cycles
+ * - Perform inlinings in a consistent order
+ */
+ object callsiteOrdering extends Ordering[Callsite] {
+ override def compare(x: Callsite, y: Callsite): Int = {
+ val cls = x.callsiteClass.internalName compareTo y.callsiteClass.internalName
+ if (cls != 0) return cls
+
+ val name = x.callsiteMethod.name compareTo y.callsiteMethod.name
+ if (name != 0) return name
+
+ val desc = x.callsiteMethod.desc compareTo y.callsiteMethod.desc
+ if (desc != 0) return desc
+
+ def pos(c: Callsite) = c.callsiteMethod.instructions.indexOf(c.callsiteInstruction)
+ pos(x) - pos(y)
+ }
+ }
+
+ /**
+ * Select callsites from the call graph that should be inlined. The resulting list of inlining
+ * requests is allowed to have cycles, and the callsites can appear in any order.
+ */
+ def selectCallsitesForInlining: List[Callsite] = {
+ callsites.valuesIterator.filter({
+ case callsite @ Callsite(_, _, _, Right(Callee(callee, calleeDeclClass, safeToInline, _, annotatedInline, _, warning)), _, _, pos) =>
+ val res = doInlineCallsite(callsite)
+
+ if (!res) {
+ if (annotatedInline && btypes.warnSettings.atInlineFailed) {
+ // if the callsite is annotated @inline, we report an inline warning even if the underlying
+ // reason is, for example, mixed compilation (which has a separate -Yopt-warning flag).
+ def initMsg = s"${BackendReporting.methodSignature(calleeDeclClass.internalName, callee)} is annotated @inline but cannot be inlined"
+ def warnMsg = warning.map(" Possible reason:\n" + _).getOrElse("")
+ if (doRewriteTraitCallsite(callsite))
+ backendReporting.inlinerWarning(pos, s"$initMsg: the trait method call could not be rewritten to the static implementation method." + warnMsg)
+ else if (!safeToInline)
+ backendReporting.inlinerWarning(pos, s"$initMsg: the method is not final and may be overridden." + warnMsg)
+ else
+ backendReporting.inlinerWarning(pos, s"$initMsg." + warnMsg)
+ } else if (warning.isDefined && warning.get.emitWarning(warnSettings)) {
+ // when annotatedInline is false, and there is some warning, the callsite metadata is possibly incomplete.
+ backendReporting.inlinerWarning(pos, s"there was a problem determining if method ${callee.name} can be inlined: \n"+ warning.get)
+ }
+ }
+
+ res
+
+ case Callsite(ins, _, _, Left(warning), _, _, pos) =>
+ if (warning.emitWarning(warnSettings))
+ backendReporting.inlinerWarning(pos, s"failed to determine if ${ins.name} should be inlined:\n$warning")
+ false
+ }).toList
+ }
+
+ /**
+ * The current inlining heuristics are simple: inline calls to methods annotated @inline.
+ */
+ def doInlineCallsite(callsite: Callsite): Boolean = callsite match {
+ case Callsite(_, _, _, Right(Callee(callee, calleeDeclClass, safeToInline, _, annotatedInline, _, warning)), _, _, pos) =>
+ annotatedInline && safeToInline
+
+ case _ => false
+ }
+
+ def rewriteFinalTraitMethodInvocations(): Unit = {
+ // Rewriting final trait method callsites to the implementation class enables inlining.
+ // We cannot just iterate over the values of the `callsites` map because the rewrite changes the
+ // map. Therefore we first copy the values to a list.
+ callsites.values.toList.foreach(rewriteFinalTraitMethodInvocation)
+ }
+
+ /**
+ * True for statically resolved trait callsites that should be rewritten to the static implementation method.
+ */
+ def doRewriteTraitCallsite(callsite: Callsite) = callsite.callee match {
+ case Right(Callee(callee, calleeDeclarationClass, safeToInline, true, annotatedInline, annotatedNoInline, infoWarning)) => true
+ case _ => false
+ }
+
+ /**
+ * Rewrite the INVOKEINTERFACE callsite of a final trait method invocation to INVOKESTATIC of the
+ * corresponding method in the implementation class. This enables inlining final trait methods.
+ *
+ * In a final trait method callsite, the callee is safeToInline and the callee method is abstract
+ * (the receiver type is the interface, so the method is abstract).
+ */
+ def rewriteFinalTraitMethodInvocation(callsite: Callsite): Unit = {
+ if (doRewriteTraitCallsite(callsite)) {
+ val Right(Callee(callee, calleeDeclarationClass, _, _, annotatedInline, annotatedNoInline, infoWarning)) = callsite.callee
+
+ val traitMethodArgumentTypes = asm.Type.getArgumentTypes(callee.desc)
+
+ val implClassInternalName = calleeDeclarationClass.internalName + "$class"
+
+ val selfParamTypeV: Either[OptimizerWarning, ClassBType] = calleeDeclarationClass.info.map(_.inlineInfo.traitImplClassSelfType match {
+ case Some(internalName) => classBTypeFromParsedClassfile(internalName)
+ case None => calleeDeclarationClass
+ })
+
+ def implClassMethodV(implMethodDescriptor: String): Either[OptimizerWarning, MethodNode] = {
+ byteCodeRepository.methodNode(implClassInternalName, callee.name, implMethodDescriptor).map(_._1)
+ }
+
+ // The rewrite reading the implementation class and the implementation method from the bytecode
+ // repository. If either of the two fails, the rewrite is not performed.
+ val res = for {
+ selfParamType <- selfParamTypeV
+ implMethodDescriptor = asm.Type.getMethodDescriptor(asm.Type.getReturnType(callee.desc), selfParamType.toASMType +: traitMethodArgumentTypes: _*)
+ implClassMethod <- implClassMethodV(implMethodDescriptor)
+ implClassBType = classBTypeFromParsedClassfile(implClassInternalName)
+ selfTypeOk <- calleeDeclarationClass.isSubtypeOf(selfParamType)
+ } yield {
+
+ // The self parameter type may be incompatible with the trait type.
+ // trait T { self: S => def foo = 1 }
+ // The $self parameter type of T$class.foo is S, which may be unrelated to T. If we re-write
+ // a call to T.foo to T$class.foo, we need to cast the receiver to S, otherwise we get a
+ // VerifyError. We run a `SourceInterpreter` to find all producer instructions of the
+ // receiver value and add a cast to the self type after each.
+ if (!selfTypeOk) {
+ val analyzer = new AsmAnalyzer(callsite.callsiteMethod, callsite.callsiteClass.internalName, new SourceInterpreter)
+ val receiverValue = analyzer.frameAt(callsite.callsiteInstruction).peekDown(traitMethodArgumentTypes.length)
+ for (i <- receiverValue.insns.asScala) {
+ val cast = new TypeInsnNode(CHECKCAST, selfParamType.internalName)
+ callsite.callsiteMethod.instructions.insert(i, cast)
+ }
+ }
+
+ val newCallsiteInstruction = new MethodInsnNode(INVOKESTATIC, implClassInternalName, callee.name, implMethodDescriptor, false)
+ callsite.callsiteMethod.instructions.insert(callsite.callsiteInstruction, newCallsiteInstruction)
+ callsite.callsiteMethod.instructions.remove(callsite.callsiteInstruction)
+
+ callGraph.callsites.remove(callsite.callsiteInstruction)
+ val staticCallsite = Callsite(
+ callsiteInstruction = newCallsiteInstruction,
+ callsiteMethod = callsite.callsiteMethod,
+ callsiteClass = callsite.callsiteClass,
+ callee = Right(Callee(
+ callee = implClassMethod,
+ calleeDeclarationClass = implClassBType,
+ safeToInline = true,
+ safeToRewrite = false,
+ annotatedInline = annotatedInline,
+ annotatedNoInline = annotatedNoInline,
+ calleeInfoWarning = infoWarning)),
+ argInfos = Nil,
+ callsiteStackHeight = callsite.callsiteStackHeight,
+ callsitePosition = callsite.callsitePosition
+ )
+ callGraph.callsites(newCallsiteInstruction) = staticCallsite
+ }
+
+ for (warning <- res.left) {
+ val Right(callee) = callsite.callee
+ val newCallee = callee.copy(calleeInfoWarning = Some(RewriteTraitCallToStaticImplMethodFailed(calleeDeclarationClass.internalName, callee.callee.name, callee.callee.desc, warning)))
+ callGraph.callsites(callsite.callsiteInstruction) = callsite.copy(callee = Right(newCallee))
+ }
+ }
+ }
+
+ /**
+ * Returns the callsites that can be inlined. Ensures that the returned inline request graph does
+ * not contain cycles.
+ *
+ * The resulting list is sorted such that the leaves of the inline request graph are on the left.
+ * Once these leaves are inlined, the successive elements will be leaves, etc.
+ */
+ private def collectAndOrderInlineRequests: List[Callsite] = {
+ val requests = selectCallsitesForInlining
+
+ // This map is an index to look up the inlining requests for a method. The value sets are mutable
+ // to allow removing elided requests (to break inlining cycles). The map itself is mutable to
+ // allow efficient building: requests.groupBy would build values as List[Callsite] that need to
+ // be transformed to mutable sets.
+ val inlineRequestsForMethod: mutable.Map[MethodNode, mutable.Set[Callsite]] = mutable.HashMap.empty.withDefaultValue(mutable.HashSet.empty)
+ for (r <- requests) inlineRequestsForMethod.getOrElseUpdate(r.callsiteMethod, mutable.HashSet.empty) += r
+
+ /**
+ * Break cycles in the inline request graph by removing callsites.
+ *
+ * The list `requests` is traversed left-to-right, removing those callsites that are part of a
+ * cycle. Elided callsites are also removed from the `inlineRequestsForMethod` map.
+ */
+ def breakInlineCycles(requests: List[Callsite]): List[Callsite] = {
+ // is there a path of inline requests from start to goal?
+ def isReachable(start: MethodNode, goal: MethodNode): Boolean = {
+ @tailrec def reachableImpl(check: List[MethodNode], visited: Set[MethodNode]): Boolean = check match {
+ case x :: xs =>
+ if (x == goal) true
+ else if (visited(x)) reachableImpl(xs, visited)
+ else {
+ val callees = inlineRequestsForMethod(x).map(_.callee.get.callee)
+ reachableImpl(xs ::: callees.toList, visited + x)
+ }
+
+ case Nil =>
+ false
+ }
+ reachableImpl(List(start), Set.empty)
+ }
+
+ val result = new mutable.ListBuffer[Callsite]()
+ // sort the inline requests to ensure that removing requests is deterministic
+ for (r <- requests.sorted(callsiteOrdering)) {
+ // is there a chain of inlining requests that would inline the callsite method into the callee?
+ if (isReachable(r.callee.get.callee, r.callsiteMethod))
+ inlineRequestsForMethod(r.callsiteMethod) -= r
+ else
+ result += r
+ }
+ result.toList
+ }
+
+ // sort the remaining inline requests such that the leaves appear first, then those requests
+ // that become leaves, etc.
+ def leavesFirst(requests: List[Callsite], visited: Set[Callsite] = Set.empty): List[Callsite] = {
+ if (requests.isEmpty) Nil
+ else {
+ val (leaves, others) = requests.partition(r => {
+ val inlineRequestsForCallee = inlineRequestsForMethod(r.callee.get.callee)
+ inlineRequestsForCallee.forall(visited)
+ })
+ assert(leaves.nonEmpty, requests)
+ leaves ::: leavesFirst(others, visited ++ leaves)
+ }
+ }
+
+ leavesFirst(breakInlineCycles(requests))
+ }
+
+
+ /**
+ * Copy and adapt the instructions of a method to a callsite.
+ *
+ * Preconditions:
+ * - The maxLocals and maxStack values of the callsite method are correctly computed
+ * - The callsite method contains no unreachable basic blocks, i.e., running an [[Analyzer]]
+ * does not produce any `null` frames
+ *
+ * @param callsiteInstruction The invocation instruction
+ * @param callsiteStackHeight The stack height at the callsite
+ * @param callsiteMethod The method in which the invocation occurs
+ * @param callsiteClass The class in which the callsite method is defined
+ * @param callee The invoked method
+ * @param calleeDeclarationClass The class in which the invoked method is defined
+ * @param receiverKnownNotNull `true` if the receiver is known to be non-null
+ * @param keepLineNumbers `true` if LineNumberNodes should be copied to the call site
+ * @return `Some(message)` if inlining cannot be performed, `None` otherwise
+ */
+ def inline(callsiteInstruction: MethodInsnNode, callsiteStackHeight: Int, callsiteMethod: MethodNode, callsiteClass: ClassBType,
+ callee: MethodNode, calleeDeclarationClass: ClassBType,
+ receiverKnownNotNull: Boolean, keepLineNumbers: Boolean): Option[CannotInlineWarning] = {
+ canInline(callsiteInstruction, callsiteStackHeight, callsiteMethod, callsiteClass, callee, calleeDeclarationClass) orElse {
+ // New labels for the cloned instructions
+ val labelsMap = cloneLabels(callee)
+ val (clonedInstructions, instructionMap) = cloneInstructions(callee, labelsMap)
+ if (!keepLineNumbers) {
+ removeLineNumberNodes(clonedInstructions)
+ }
+
+ // local vars in the callee are shifted by the number of locals at the callsite
+ val localVarShift = callsiteMethod.maxLocals
+ clonedInstructions.iterator.asScala foreach {
+ case varInstruction: VarInsnNode => varInstruction.`var` += localVarShift
+ case _ => ()
+ }
+
+ // add a STORE instruction for each expected argument, including for THIS instance if any
+ val argStores = new InsnList
+ var nextLocalIndex = callsiteMethod.maxLocals
+ if (!isStaticMethod(callee)) {
+ if (!receiverKnownNotNull) {
+ argStores.add(new InsnNode(DUP))
+ val nonNullLabel = newLabelNode
+ argStores.add(new JumpInsnNode(IFNONNULL, nonNullLabel))
+ argStores.add(new InsnNode(ACONST_NULL))
+ argStores.add(new InsnNode(ATHROW))
+ argStores.add(nonNullLabel)
+ }
+ argStores.add(new VarInsnNode(ASTORE, nextLocalIndex))
+ nextLocalIndex += 1
+ }
+
+ // We just use an asm.Type here, no need to create the MethodBType.
+ val calleAsmType = asm.Type.getMethodType(callee.desc)
+
+ for(argTp <- calleAsmType.getArgumentTypes) {
+ val opc = argTp.getOpcode(ISTORE) // returns the correct xSTORE instruction for argTp
+ argStores.insert(new VarInsnNode(opc, nextLocalIndex)) // "insert" is "prepend" - the last argument is on the top of the stack
+ nextLocalIndex += argTp.getSize
+ }
+
+ clonedInstructions.insert(argStores)
+
+ // label for the exit of the inlined functions. xRETURNs are rplaced by GOTOs to this label.
+ val postCallLabel = newLabelNode
+ clonedInstructions.add(postCallLabel)
+
+ // replace xRETURNs:
+ // - store the return value (if any)
+ // - clear the stack of the inlined method (insert DROPs)
+ // - load the return value
+ // - GOTO postCallLabel
+
+ val returnType = calleAsmType.getReturnType
+ val hasReturnValue = returnType.getSort != asm.Type.VOID
+ val returnValueIndex = callsiteMethod.maxLocals + callee.maxLocals
+ nextLocalIndex += returnType.getSize
+
+ def returnValueStore(returnInstruction: AbstractInsnNode) = {
+ val opc = returnInstruction.getOpcode match {
+ case IRETURN => ISTORE
+ case LRETURN => LSTORE
+ case FRETURN => FSTORE
+ case DRETURN => DSTORE
+ case ARETURN => ASTORE
+ }
+ new VarInsnNode(opc, returnValueIndex)
+ }
+
+ // We run an interpreter to know the stack height at each xRETURN instruction and the sizes
+ // of the values on the stack.
+ val analyzer = new AsmAnalyzer(callee, calleeDeclarationClass.internalName)
+
+ for (originalReturn <- callee.instructions.iterator().asScala if isReturn(originalReturn)) {
+ val frame = analyzer.frameAt(originalReturn)
+ var stackHeight = frame.getStackSize
+
+ val inlinedReturn = instructionMap(originalReturn)
+ val returnReplacement = new InsnList
+
+ def drop(slot: Int) = returnReplacement add getPop(frame.peekDown(slot).getSize)
+
+ // for non-void methods, store the stack top into the return local variable
+ if (hasReturnValue) {
+ returnReplacement add returnValueStore(originalReturn)
+ stackHeight -= 1
+ }
+
+ // drop the rest of the stack
+ for (i <- 0 until stackHeight) drop(i)
+
+ returnReplacement add new JumpInsnNode(GOTO, postCallLabel)
+ clonedInstructions.insert(inlinedReturn, returnReplacement)
+ clonedInstructions.remove(inlinedReturn)
+ }
+
+ // Load instruction for the return value
+ if (hasReturnValue) {
+ val retVarLoad = {
+ val opc = returnType.getOpcode(ILOAD)
+ new VarInsnNode(opc, returnValueIndex)
+ }
+ clonedInstructions.insert(postCallLabel, retVarLoad)
+ }
+
+ callsiteMethod.instructions.insert(callsiteInstruction, clonedInstructions)
+ callsiteMethod.instructions.remove(callsiteInstruction)
+
+ callsiteMethod.localVariables.addAll(cloneLocalVariableNodes(callee, labelsMap, callee.name + "_").asJava)
+ callsiteMethod.tryCatchBlocks.addAll(cloneTryCatchBlockNodes(callee, labelsMap).asJava)
+
+ // Add all invocation instructions that were inlined to the call graph
+ callee.instructions.iterator().asScala foreach {
+ case originalCallsiteIns: MethodInsnNode =>
+ callGraph.callsites.get(originalCallsiteIns) match {
+ case Some(originalCallsite) =>
+ val newCallsiteIns = instructionMap(originalCallsiteIns).asInstanceOf[MethodInsnNode]
+ callGraph.callsites(newCallsiteIns) = Callsite(
+ callsiteInstruction = newCallsiteIns,
+ callsiteMethod = callsiteMethod,
+ callsiteClass = callsiteClass,
+ callee = originalCallsite.callee,
+ argInfos = Nil, // TODO: re-compute argInfos for new destination (once we actually compute them)
+ callsiteStackHeight = callsiteStackHeight + originalCallsite.callsiteStackHeight,
+ callsitePosition = originalCallsite.callsitePosition
+ )
+
+ case None =>
+ }
+
+ case _ =>
+ }
+ // Remove the elided invocation from the call graph
+ callGraph.callsites.remove(callsiteInstruction)
+
+ callsiteMethod.maxLocals += returnType.getSize + callee.maxLocals
+ callsiteMethod.maxStack = math.max(callsiteMethod.maxStack, callee.maxStack + callsiteStackHeight)
+
+ None
+ }
+ }
+
+ /**
+ * Check whether an inling can be performed. Parmeters are described in method [[inline]].
+ * @return `Some(message)` if inlining cannot be performed, `None` otherwise
+ */
+ def canInline(callsiteInstruction: MethodInsnNode, callsiteStackHeight: Int, callsiteMethod: MethodNode, callsiteClass: ClassBType,
+ callee: MethodNode, calleeDeclarationClass: ClassBType): Option[CannotInlineWarning] = {
+
+ def calleeDesc = s"${callee.name} of type ${callee.desc} in ${calleeDeclarationClass.internalName}"
+ def methodMismatch = s"Wrong method node for inlining ${textify(callsiteInstruction)}: $calleeDesc"
+ assert(callsiteInstruction.name == callee.name, methodMismatch)
+ assert(callsiteInstruction.desc == callee.desc, methodMismatch)
+ assert(!isConstructor(callee), s"Constructors cannot be inlined: $calleeDesc")
+ assert(!BytecodeUtils.isAbstractMethod(callee), s"Callee is abstract: $calleeDesc")
+ assert(callsiteMethod.instructions.contains(callsiteInstruction), s"Callsite ${textify(callsiteInstruction)} is not an instruction of $calleeDesc")
+
+ // When an exception is thrown, the stack is cleared before jumping to the handler. When
+ // inlining a method that catches an exception, all values that were on the stack before the
+ // call (in addition to the arguments) would be cleared (SI-6157). So we don't inline methods
+ // with handlers in case there are values on the stack.
+ // Alternatively, we could save all stack values below the method arguments into locals, but
+ // that would be inefficient: we'd need to pop all parameters, save the values, and push the
+ // parameters back for the (inlined) invocation. Similarly for the result after the call.
+ def stackHasNonParameters: Boolean = {
+ val expectedArgs = asm.Type.getArgumentTypes(callsiteInstruction.desc).length + (callsiteInstruction.getOpcode match {
+ case INVOKEVIRTUAL | INVOKESPECIAL | INVOKEINTERFACE => 1
+ case INVOKESTATIC => 0
+ case INVOKEDYNAMIC =>
+ assertionError(s"Unexpected opcode, cannot inline ${textify(callsiteInstruction)}")
+ })
+ callsiteStackHeight > expectedArgs
+ }
+
+ if (isSynchronizedMethod(callee)) {
+ // Could be done by locking on the receiver, wrapping the inlined code in a try and unlocking
+ // in finally. But it's probably not worth the effort, scala never emits synchronized methods.
+ Some(SynchronizedMethod(calleeDeclarationClass.internalName, callee.name, callee.desc))
+ } else if (!callee.tryCatchBlocks.isEmpty && stackHasNonParameters) {
+ Some(MethodWithHandlerCalledOnNonEmptyStack(
+ calleeDeclarationClass.internalName, callee.name, callee.desc,
+ callsiteClass.internalName, callsiteMethod.name, callsiteMethod.desc))
+ } else findIllegalAccess(callee.instructions, calleeDeclarationClass, callsiteClass) map {
+ case (illegalAccessIns, None) =>
+ IllegalAccessInstruction(
+ calleeDeclarationClass.internalName, callee.name, callee.desc,
+ callsiteClass.internalName, illegalAccessIns)
+
+ case (illegalAccessIns, Some(warning)) =>
+ IllegalAccessCheckFailed(
+ calleeDeclarationClass.internalName, callee.name, callee.desc,
+ callsiteClass.internalName, illegalAccessIns, warning)
+ }
+ }
+
+ /**
+ * Returns the first instruction in the `instructions` list that would cause a
+ * [[java.lang.IllegalAccessError]] when inlined into the `destinationClass`.
+ *
+ * If validity of some instruction could not be checked because an error occurred, the instruction
+ * is returned together with a warning message that describes the problem.
+ */
+ def findIllegalAccess(instructions: InsnList, calleeDeclarationClass: ClassBType, destinationClass: ClassBType): Option[(AbstractInsnNode, Option[OptimizerWarning])] = {
+
+ /**
+ * Check if a type is accessible to some class, as defined in JVMS 5.4.4.
+ * (A1) C is public
+ * (A2) C and D are members of the same run-time package
+ */
+ def classIsAccessible(accessed: BType, from: ClassBType = destinationClass): Either[OptimizerWarning, Boolean] = (accessed: @unchecked) match {
+ // TODO: A2 requires "same run-time package", which seems to be package + classloader (JMVS 5.3.). is the below ok?
+ case c: ClassBType => c.isPublic.map(_ || c.packageInternalName == from.packageInternalName)
+ case a: ArrayBType => classIsAccessible(a.elementType, from)
+ case _: PrimitiveBType => Right(true)
+ }
+
+ /**
+ * Check if a member reference is accessible from the [[destinationClass]], as defined in the
+ * JVMS 5.4.4. Note that the class name in a field / method reference is not necessarily the
+ * class in which the member is declared:
+ *
+ * class A { def f = 0 }; class B extends A { f }
+ *
+ * The INVOKEVIRTUAL instruction uses a method reference "B.f ()I". Therefore this method has
+ * two parameters:
+ *
+ * @param memberDeclClass The class in which the member is declared (A)
+ * @param memberRefClass The class used in the member reference (B)
+ *
+ * JVMS 5.4.4 summary: A field or method R is accessible to a class D (destinationClass) iff
+ * (B1) R is public
+ * (B2) R is protected, declared in C (memberDeclClass) and D is a subclass of C.
+ * If R is not static, R must contain a symbolic reference to a class T (memberRefClass),
+ * such that T is either a subclass of D, a superclass of D, or D itself.
+ * (B3) R is either protected or has default access and declared by a class in the same
+ * run-time package as D.
+ * (B4) R is private and is declared in D.
+ */
+ def memberIsAccessible(memberFlags: Int, memberDeclClass: ClassBType, memberRefClass: ClassBType): Either[OptimizerWarning, Boolean] = {
+ // TODO: B3 requires "same run-time package", which seems to be package + classloader (JMVS 5.3.). is the below ok?
+ def samePackageAsDestination = memberDeclClass.packageInternalName == destinationClass.packageInternalName
+
+ val key = (ACC_PUBLIC | ACC_PROTECTED | ACC_PRIVATE) & memberFlags
+ key match {
+ case ACC_PUBLIC => // B1
+ Right(true)
+
+ case ACC_PROTECTED => // B2
+ tryEither {
+ val condB2 = destinationClass.isSubtypeOf(memberDeclClass).orThrow && {
+ val isStatic = (ACC_STATIC & memberFlags) != 0
+ isStatic || memberRefClass.isSubtypeOf(destinationClass).orThrow || destinationClass.isSubtypeOf(memberRefClass).orThrow
+ }
+ Right(condB2 || samePackageAsDestination) // B3 (protected)
+ }
+
+ case 0 => // B3 (default access)
+ Right(samePackageAsDestination)
+
+ case ACC_PRIVATE => // B4
+ Right(memberDeclClass == destinationClass)
+ }
+ }
+
+ /**
+ * Check if `instruction` can be transplanted to `destinationClass`.
+ *
+ * If the instruction references a class, method or field that cannot be found in the
+ * byteCodeRepository, it is considered as not legal. This is known to happen in mixed
+ * compilation: for Java classes there is no classfile that could be parsed, nor does the
+ * compiler generate any bytecode.
+ *
+ * Returns a warning message describing the problem if checking the legality for the instruction
+ * failed.
+ */
+ def isLegal(instruction: AbstractInsnNode): Either[OptimizerWarning, Boolean] = instruction match {
+ case ti: TypeInsnNode =>
+ // NEW, ANEWARRAY, CHECKCAST or INSTANCEOF. For these instructions, the reference
+ // "must be a symbolic reference to a class, array, or interface type" (JVMS 6), so
+ // it can be an internal name, or a full array descriptor.
+ classIsAccessible(bTypeForDescriptorOrInternalNameFromClassfile(ti.desc))
+
+ case ma: MultiANewArrayInsnNode =>
+ // "a symbolic reference to a class, array, or interface type"
+ classIsAccessible(bTypeForDescriptorOrInternalNameFromClassfile(ma.desc))
+
+ case fi: FieldInsnNode =>
+ val fieldRefClass = classBTypeFromParsedClassfile(fi.owner)
+ for {
+ (fieldNode, fieldDeclClassNode) <- byteCodeRepository.fieldNode(fieldRefClass.internalName, fi.name, fi.desc): Either[OptimizerWarning, (FieldNode, InternalName)]
+ fieldDeclClass = classBTypeFromParsedClassfile(fieldDeclClassNode)
+ res <- memberIsAccessible(fieldNode.access, fieldDeclClass, fieldRefClass)
+ } yield {
+ res
+ }
+
+ case mi: MethodInsnNode =>
+ if (mi.owner.charAt(0) == '[') Right(true) // array methods are accessible
+ else {
+ def canInlineCall(opcode: Int, methodFlags: Int, methodDeclClass: ClassBType, methodRefClass: ClassBType): Either[OptimizerWarning, Boolean] = {
+ opcode match {
+ case INVOKESPECIAL if mi.name != GenBCode.INSTANCE_CONSTRUCTOR_NAME =>
+ // invokespecial is used for private method calls, super calls and instance constructor calls.
+ // private method and super calls can only be inlined into the same class.
+ Right(destinationClass == calleeDeclarationClass)
+
+ case _ => // INVOKEVIRTUAL, INVOKESTATIC, INVOKEINTERFACE and INVOKESPECIAL of constructors
+ memberIsAccessible(methodFlags, methodDeclClass, methodRefClass)
+ }
+ }
+
+ val methodRefClass = classBTypeFromParsedClassfile(mi.owner)
+ for {
+ (methodNode, methodDeclClassNode) <- byteCodeRepository.methodNode(methodRefClass.internalName, mi.name, mi.desc): Either[OptimizerWarning, (MethodNode, InternalName)]
+ methodDeclClass = classBTypeFromParsedClassfile(methodDeclClassNode)
+ res <- canInlineCall(mi.getOpcode, methodNode.access, methodDeclClass, methodRefClass)
+ } yield {
+ res
+ }
+ }
+
+ case ivd: InvokeDynamicInsnNode =>
+ // TODO @lry check necessary conditions to inline an indy, instead of giving up
+ Right(false)
+
+ case ci: LdcInsnNode => ci.cst match {
+ case t: asm.Type => classIsAccessible(bTypeForDescriptorOrInternalNameFromClassfile(t.getInternalName))
+ case _ => Right(true)
+ }
+
+ case _ => Right(true)
+ }
+
+ val it = instructions.iterator.asScala
+ @tailrec def find: Option[(AbstractInsnNode, Option[OptimizerWarning])] = {
+ if (!it.hasNext) None // all instructions are legal
+ else {
+ val i = it.next()
+ isLegal(i) match {
+ case Left(warning) => Some((i, Some(warning))) // checking isLegal for i failed
+ case Right(false) => Some((i, None)) // an illegal instruction was found
+ case _ => find
+ }
+ }
+ }
+ find
+ }
+}