diff options
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.scala | 936 |
1 files changed, 536 insertions, 400 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 index 6b2786c1a3..1c29859f46 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala @@ -9,59 +9,125 @@ package opt import scala.annotation.tailrec import scala.tools.asm -import asm.Handle import asm.Opcodes._ import asm.tree._ -import scala.collection.convert.decorateAsScala._ -import scala.collection.convert.decorateAsJava._ +import scala.collection.JavaConverters._ import AsmUtils._ import BytecodeUtils._ import collection.mutable -import scala.tools.asm.tree.analysis.SourceInterpreter import BackendReporting._ import scala.tools.nsc.backend.jvm.BTypes.InternalName class Inliner[BT <: BTypes](val btypes: BT) { import btypes._ import callGraph._ + import inlinerHeuristics._ + import backendUtils._ - def eliminateUnreachableCodeAndUpdateCallGraph(methodNode: MethodNode, definingClass: InternalName): Unit = { - localOpt.minimalRemoveUnreachableCode(methodNode, definingClass) foreach { - case invocation: MethodInsnNode => callGraph.callsites.remove(invocation) - case indy: InvokeDynamicInsnNode => callGraph.closureInstantiations.remove(indy) - case _ => + sealed trait InlineLog { + def request: InlineRequest + } + final case class InlineLogSuccess(request: InlineRequest, sizeBefore: Int, sizeInlined: Int) extends InlineLog { + var downstreamLog: mutable.Buffer[InlineLog] = mutable.ListBuffer.empty + } + final case class InlineLogFail(request: InlineRequest, warning: CannotInlineWarning) extends InlineLog + final case class InlineLogRollback(request: InlineRequest, warnings: List[CannotInlineWarning]) extends InlineLog + + object InlineLog { + private def shouldLog(request: InlineRequest): Boolean = { + def logEnabled = compilerSettings.YoptLogInline.isSetByUser + def matchesName = { + val prefix = compilerSettings.YoptLogInline.value match { + case "_" => "" + case p => p + } + val name: String = request.callsite.callsiteClass.internalName + "." + request.callsite.callsiteMethod.name + name startsWith prefix + } + logEnabled && (upstream != null || (isTopLevel && matchesName)) + } + + // indexed by callsite method + private val logs = mutable.Map.empty[MethodNode, mutable.LinkedHashSet[InlineLog]] + + private var upstream: InlineLogSuccess = _ + private var isTopLevel = true + + def withInlineLogging[T](request: InlineRequest)(inlineRequest: => Unit)(inlinePost: => T): T = { + def doInlinePost(): T = { + val savedIsTopLevel = isTopLevel + isTopLevel = false + try inlinePost + finally isTopLevel = savedIsTopLevel + } + if (shouldLog(request)) { + val sizeBefore = request.callsite.callsiteMethod.instructions.size + inlineRequest + val log = InlineLogSuccess(request, sizeBefore, request.callsite.callee.get.callee.instructions.size) + apply(log) + + val savedUpstream = upstream + upstream = log + try doInlinePost() + finally upstream = savedUpstream + } else { + inlineRequest + doInlinePost() + } + } + + def apply(log: => InlineLog): Unit = if (shouldLog(log.request)) { + if (upstream != null) upstream.downstreamLog += log + else { + val methodLogs = logs.getOrElseUpdate(log.request.callsite.callsiteMethod, mutable.LinkedHashSet.empty) + methodLogs += log + } + } + + def entryString(log: InlineLog, indent: Int = 0): String = { + val callee = log.request.callsite.callee.get + val calleeString = callee.calleeDeclarationClass.internalName + "." + callee.callee.name + val indentString = " " * indent + log match { + case s @ InlineLogSuccess(_, sizeBefore, sizeInlined) => + val self = s"${indentString}inlined $calleeString. Before: $sizeBefore ins, inlined: $sizeInlined ins." + if (s.downstreamLog.isEmpty) self + else s.downstreamLog.iterator.map(entryString(_, indent + 2)).mkString(self + "\n", "\n", "") + + case InlineLogFail(_, w) => + s"${indentString}failed $calleeString. ${w.toString.replace('\n', ' ')}" + + case InlineLogRollback(_, _) => + s"${indentString}rolling back, nested inline failed." + } + } + + def print(): Unit = if (compilerSettings.YoptLogInline.isSetByUser) { + val byClassAndMethod: List[(InternalName, mutable.Map[MethodNode, mutable.LinkedHashSet[InlineLog]])] = { + logs. + groupBy(_._2.head.request.callsite.callsiteClass.internalName). + toList.sortBy(_._1) + } + for { + (c, methodLogs) <- byClassAndMethod + (m, mLogs) <- methodLogs.toList.sortBy(_._1.name) + mLog <- mLogs // insertion order + } { + println(s"Inline into $c.${m.name}: ${entryString(mLog)}") + } } } def runInliner(): Unit = { - rewriteFinalTraitMethodInvocations() - for (request <- collectAndOrderInlineRequests) { - val Right(callee) = request.callee // collectAndOrderInlineRequests returns callsites with a known callee - - // Inlining a method can create unreachable code. Example: - // def f = throw e - // def g = f; println() // println is unreachable after inlining f - // If we have an inline request for a call to g, and f has been already inlined into g, we - // need to run DCE before inlining g. - eliminateUnreachableCodeAndUpdateCallGraph(callee.callee, callee.calleeDeclarationClass.internalName) - - // DCE above removes unreachable callsites from the call graph. If the inlining request denotes - // such an eliminated callsite, do nothing. - if (callGraph.callsites contains request.callsiteInstruction) { - val r = inline(request.callsiteInstruction, request.callsiteStackHeight, request.callsiteMethod, request.callsiteClass, - callee.callee, callee.calleeDeclarationClass, - request.receiverKnownNotNull, keepLineNumbers = false) - - for (warning <- r) { - if ((callee.annotatedInline && btypes.compilerSettings.YoptWarningEmitAtInlineFailed) || warning.emitWarning(compilerSettings)) { - 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) - } - } + val Right(callee) = request.callsite.callee // collectAndOrderInlineRequests returns callsites with a known callee + val warnings = inline(request) + for (warning <- warnings) { + if (warning.emitWarning(compilerSettings)) + backendReporting.inlinerWarning(request.callsite.callsitePosition, warning.toString) } } + InlineLog.print() } /** @@ -69,165 +135,21 @@ class Inliner[BT <: BTypes](val btypes: BT) { * - 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 + object callsiteOrdering extends Ordering[InlineRequest] { + override def compare(x: InlineRequest, y: InlineRequest): Int = { + val xCs = x.callsite + val yCs = y.callsite + val cls = xCs.callsiteClass.internalName compareTo yCs.callsiteClass.internalName if (cls != 0) return cls - val name = x.callsiteMethod.name compareTo y.callsiteMethod.name + val name = xCs.callsiteMethod.name compareTo yCs.callsiteMethod.name if (name != 0) return name - val desc = x.callsiteMethod.desc compareTo y.callsiteMethod.desc + val desc = xCs.callsiteMethod.desc compareTo yCs.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.compilerSettings.YoptWarningEmitAtInlineFailed) { - // 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(compilerSettings)) { - // 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(compilerSettings)) - 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) => - if (compilerSettings.YoptInlineHeuristics.value == "everything") safeToInline - else 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) { - // there's no need to run eliminateUnreachableCode here. building the call graph does that - // already, no code can become unreachable in the meantime. - val analyzer = new AsmAnalyzer(callsite.callsiteMethod, callsite.callsiteClass.internalName, new SourceInterpreter) - val receiverValue = analyzer.frameAt(callsite.callsiteInstruction).peekStack(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, - receiverKnownNotNull = callsite.receiverKnownNotNull, - 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)) - } + pos(xCs) - pos(yCs) } } @@ -238,15 +160,13 @@ class Inliner[BT <: BTypes](val btypes: BT) { * 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 + private def collectAndOrderInlineRequests: List[InlineRequest] = { + val requestsByMethod = selectCallsitesForInlining withDefaultValue Set.empty + + val elided = mutable.Set.empty[InlineRequest] + def nonElidedRequests(methodNode: MethodNode): Set[InlineRequest] = requestsByMethod(methodNode) diff elided - // 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 + def allCallees(r: InlineRequest): Set[MethodNode] = r.post.flatMap(allCallees).toSet + r.callsite.callee.get.callee /** * Break cycles in the inline request graph by removing callsites. @@ -254,236 +174,454 @@ class Inliner[BT <: BTypes](val btypes: BT) { * 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] = { + def breakInlineCycles: List[InlineRequest] = { // 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 => + def isReachable(start: Set[MethodNode], goal: MethodNode): Boolean = { + @tailrec def reachableImpl(check: Set[MethodNode], visited: Set[MethodNode]): Boolean = { + if (check.isEmpty) false + else { + val x = check.head if (x == goal) true - else if (visited(x)) reachableImpl(xs, visited) + else if (visited(x)) reachableImpl(check - x, visited) else { - val callees = inlineRequestsForMethod(x).map(_.callee.get.callee) - reachableImpl(xs ::: callees.toList, visited + x) + val callees = nonElidedRequests(x).flatMap(allCallees) + reachableImpl(check - x ++ callees, visited + x) } - - case Nil => - false + } } - reachableImpl(List(start), Set.empty) + reachableImpl(start, Set.empty) } - val result = new mutable.ListBuffer[Callsite]() + val result = new mutable.ListBuffer[InlineRequest]() + val requests = requestsByMethod.valuesIterator.flatten.toArray // sort the inline requests to ensure that removing requests is deterministic - for (r <- requests.sorted(callsiteOrdering)) { + java.util.Arrays.sort(requests, callsiteOrdering) + for (r <- requests) { // 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 + if (isReachable(allCallees(r), r.callsite.callsiteMethod)) + elided += 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] = { + def leavesFirst(requests: List[InlineRequest], visited: Set[InlineRequest] = Set.empty): List[InlineRequest] = { if (requests.isEmpty) Nil else { val (leaves, others) = requests.partition(r => { - val inlineRequestsForCallee = inlineRequestsForMethod(r.callee.get.callee) - inlineRequestsForCallee.forall(visited) + val inlineRequestsForCallees = allCallees(r).flatMap(nonElidedRequests) + inlineRequestsForCallees.forall(visited) }) assert(leaves.nonEmpty, requests) leaves ::: leavesFirst(others, visited ++ leaves) } } - leavesFirst(breakInlineCycles(requests)) + leavesFirst(breakInlineCycles) } - /** - * Copy and adapt the instructions of a method to a callsite. + * Given an InlineRequest(mainCallsite, post = List(postCallsite)), the postCallsite is a callsite + * in the method `mainCallsite.callee`. Once the mainCallsite is inlined into the target method + * (mainCallsite.callsiteMethod), we need to find the cloned callsite that corresponds to the + * postCallsite so we can inline that into the target method as well. * - * 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 + * However, it is possible that there is no cloned callsite at all that corresponds to the + * postCallsite, for example if the corresponding callsite already inlined. Example: + * + * def a() = 1 + * def b() = a() + 2 + * def c() = b() + 3 + * def d() = c() + 4 + * + * We have the following callsite objects in the call graph: + * + * c1 = a() in b + * c2 = b() in c + * c3 = c() in d * - * @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 + * Assume we have the following inline request + * r = InlineRequest(c3, + * post = List(InlineRequest(c2, + * post = List(InlineRequest(c1, post = Nil))))) + * + * But before inlining r, assume a separate InlineRequest(c2, post = Nil) is inlined first. We get + * + * c1' = a() in c // added to the call graph + * c1.inlinedClones += (c1' at c2) // remember that c1' was created when inlining c2 + * ~c2~ // c2 is removed from the call graph + * + * If we now inline r, we first inline c3. We get + * + * c1'' = a() in d // added to call graph + * c1'.inlinedClones += (c1'' at c3) // remember that c1'' was created when inlining c3 + * ~c3~ + * + * Now we continue with the post-requests for r, i.e. c2. + * - we try to find the clone of c2 that was created when inlining c3 - but there is none. c2 + * was already inlined before + * - we continue with the post-request of c2: c1 + * - we search for the callsite of c1 that was cloned when inlining c2, we find c1' + * - recursively we search for the callsite of c1' that was cloned when inlining c3, we find c1'' + * - so we create an inline request for c1'' */ - 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) + def adaptPostRequestForMainCallsite(post: InlineRequest, mainCallsite: Callsite): List[InlineRequest] = { + def impl(post: InlineRequest, at: Callsite): List[InlineRequest] = { + post.callsite.inlinedClones.find(_.clonedWhenInlining == at) match { + case Some(clonedCallsite) => + List(InlineRequest(clonedCallsite.callsite, post.post, post.reason)) + case None => + post.post.flatMap(impl(_, post.callsite)).flatMap(impl(_, at)) } + } + impl(post, mainCallsite) + } + + class UndoLog(active: Boolean = true) { + import java.util.{ ArrayList => JArrayList } + + private var actions = List.empty[() => Unit] + private var methodStateSaved = false + + def apply(a: => Unit): Unit = if (active) actions = (() => a) :: actions + def rollback(): Unit = if (active) actions.foreach(_.apply()) - // 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 iinc: IincInsnNode => iinc.`var` += localVarShift - case _ => () + def saveMethodState(methodNode: MethodNode): Unit = if (active && !methodStateSaved) { + methodStateSaved = true + val currentInstructions = methodNode.instructions.toArray + val currentLocalVariables = new JArrayList(methodNode.localVariables) + val currentTryCatchBlocks = new JArrayList(methodNode.tryCatchBlocks) + val currentMaxLocals = methodNode.maxLocals + val currentMaxStack = methodNode.maxStack + + apply { + // `methodNode.instructions.clear()` doesn't work: it keeps the `prev` / `next` / `index` of + // instruction nodes. `instructions.removeAll(true)` would work, but is not public. + methodNode.instructions.iterator.asScala.toList.foreach(methodNode.instructions.remove) + for (i <- currentInstructions) methodNode.instructions.add(i) + + methodNode.localVariables.clear() + methodNode.localVariables.addAll(currentLocalVariables) + + methodNode.tryCatchBlocks.clear() + methodNode.tryCatchBlocks.addAll(currentTryCatchBlocks) + + methodNode.maxLocals = currentMaxLocals + methodNode.maxStack = currentMaxStack } + } + } - // 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) + val NoUndoLogging = new UndoLog(active = false) + + /** + * Inline the callsite of an inlining request and its post-inlining requests. + * + * @return An inliner warning for each callsite that could not be inlined. + */ + def inline(request: InlineRequest, undo: UndoLog = NoUndoLogging): List[CannotInlineWarning] = { + def doInline(undo: UndoLog, callRollback: Boolean = false): List[CannotInlineWarning] = { + InlineLog.withInlineLogging(request) { + inlineCallsite(request.callsite, undo) + } { + val postRequests = request.post.flatMap(adaptPostRequestForMainCallsite(_, request.callsite)) + val warnings = postRequests.flatMap(inline(_, undo)) + if (callRollback && warnings.nonEmpty) { + undo.rollback() + InlineLog(InlineLogRollback(request, warnings)) } - argStores.add(new VarInsnNode(ASTORE, nextLocalIndex)) - nextLocalIndex += 1 + warnings } + } - // We just use an asm.Type here, no need to create the MethodBType. - val calleAsmType = asm.Type.getMethodType(callee.desc) + def inlinedByPost(insns: List[AbstractInsnNode]): Boolean = + insns.nonEmpty && insns.forall(ins => request.post.exists(_.callsite.callsiteInstruction == ins)) - 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 + canInlineCallsite(request.callsite) match { + case None => + doInline(undo) + + case Some((_, illegalAccessInsns)) if inlinedByPost(illegalAccessInsns) => + // speculatively inline, roll back if an illegalAccessInsn cannot be eliminated + if (undo == NoUndoLogging) doInline(new UndoLog(), callRollback = true) + else doInline(undo) + + case Some((w, _)) => + InlineLog(InlineLogFail(request, w)) + List(w) + } + } + + /** + * Copy and adapt the instructions of a method to a callsite. + * + * Preconditions: + * - The callsite can safely be inlined (canInlineBody is true) + * - The maxLocals and maxStack values of the callsite method are correctly computed + * + * @return A map associating instruction nodes of the callee with the corresponding cloned + * instruction in the callsite method. + */ + def inlineCallsite(callsite: Callsite, undo: UndoLog = NoUndoLogging): Unit = { + import callsite.{callsiteClass, callsiteMethod, callsiteInstruction, receiverKnownNotNull, callsiteStackHeight} + val Right(callsiteCallee) = callsite.callee + import callsiteCallee.{callee, calleeDeclarationClass, sourceFilePath} + + // Inlining requires the callee not to have unreachable code, the analyzer used below should not + // return any `null` frames. Note that inlining a method can create unreachable code. Example: + // def f = throw e + // def g = f; println() // println is unreachable after inlining f + // If we have an inline request for a call to g, and f has been already inlined into g, we + // need to run DCE on g's body before inlining g. + localOpt.minimalRemoveUnreachableCode(callee, calleeDeclarationClass.internalName) + + // If the callsite was eliminated by DCE, do nothing. + if (!callGraph.containsCallsite(callsite)) return + + // New labels for the cloned instructions + val labelsMap = cloneLabels(callee) + val sameSourceFile = sourceFilePath match { + case Some(calleeSource) => byteCodeRepository.compilingClasses.get(callsiteClass.internalName) match { + case Some((_, `calleeSource`)) => true + case _ => false } + case _ => false + } + val (clonedInstructions, instructionMap, targetHandles) = cloneInstructions(callee, labelsMap, keepLineNumbers = sameSourceFile) + + // 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 iinc: IincInsnNode => iinc.`var` += localVarShift + case _ => () + } - clonedInstructions.insert(argStores) - - // label for the exit of the inlined functions. xRETURNs are replaced 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) + // 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 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) + // We just use an asm.Type here, no need to create the MethodBType. + val calleAsmType = asm.Type.getMethodType(callee.desc) + val calleeParamTypes = calleAsmType.getArgumentTypes - for (originalReturn <- callee.instructions.iterator().asScala if isReturn(originalReturn)) { - val frame = analyzer.frameAt(originalReturn) - var stackHeight = frame.getStackSize + for(argTp <- calleeParamTypes) { + 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 + } - val inlinedReturn = instructionMap(originalReturn) - val returnReplacement = new InsnList + clonedInstructions.insert(argStores) + + // label for the exit of the inlined functions. xRETURNs are replaced 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) + } - def drop(slot: Int) = returnReplacement add getPop(frame.peekStack(slot).getSize) + // We run an interpreter to know the stack height at each xRETURN instruction and the sizes + // of the values on the stack. + // We don't need to worry about the method being too large for running an analysis. Callsites of + // large methods are not added to the call graph. + val analyzer = new AsmAnalyzer(callee, calleeDeclarationClass.internalName) - // for non-void methods, store the stack top into the return local variable - if (hasReturnValue) { - returnReplacement add returnValueStore(originalReturn) - stackHeight -= 1 - } + for (originalReturn <- callee.instructions.iterator().asScala if isReturn(originalReturn)) { + val frame = analyzer.frameAt(originalReturn) + var stackHeight = frame.getStackSize - // drop the rest of the stack - for (i <- 0 until stackHeight) drop(i) + val inlinedReturn = instructionMap(originalReturn) + val returnReplacement = new InsnList - returnReplacement add new JumpInsnNode(GOTO, postCallLabel) - clonedInstructions.insert(inlinedReturn, returnReplacement) - clonedInstructions.remove(inlinedReturn) - } + def drop(slot: Int) = returnReplacement add getPop(frame.peekStack(slot).getSize) - // Load instruction for the return value + // for non-void methods, store the stack top into the return local variable if (hasReturnValue) { - val retVarLoad = { - val opc = returnType.getOpcode(ILOAD) - new VarInsnNode(opc, returnValueIndex) - } - clonedInstructions.insert(postCallLabel, retVarLoad) + returnReplacement add returnValueStore(originalReturn) + stackHeight -= 1 } - 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 and closure instantiations 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, - receiverKnownNotNull = originalCallsite.receiverKnownNotNull, - callsitePosition = originalCallsite.callsitePosition - ) - - case None => - } + // drop the rest of the stack + for (i <- 0 until stackHeight) drop(i) - case indy: InvokeDynamicInsnNode => - callGraph.closureInstantiations.get(indy) match { - case Some(closureInit) => - val newIndy = instructionMap(indy).asInstanceOf[InvokeDynamicInsnNode] - callGraph.closureInstantiations(newIndy) = ClosureInstantiation(closureInit.lambdaMetaFactoryCall.copy(indy = newIndy), callsiteMethod, callsiteClass) - - case None => - } + returnReplacement add new JumpInsnNode(GOTO, postCallLabel) + clonedInstructions.insert(inlinedReturn, returnReplacement) + clonedInstructions.remove(inlinedReturn) + } - case _ => + // Load instruction for the return value + if (hasReturnValue) { + val retVarLoad = { + val opc = returnType.getOpcode(ILOAD) + new VarInsnNode(opc, returnValueIndex) } - // Remove the elided invocation from the call graph - callGraph.callsites.remove(callsiteInstruction) + clonedInstructions.insert(postCallLabel, retVarLoad) + } - // Inlining a method body can render some code unreachable, see example above (in runInliner). - unreachableCodeEliminated -= callsiteMethod + undo.saveMethodState(callsiteMethod) - callsiteMethod.maxLocals += returnType.getSize + callee.maxLocals - callsiteMethod.maxStack = math.max(callsiteMethod.maxStack, callee.maxStack + callsiteStackHeight) + callsiteMethod.instructions.insert(callsiteInstruction, clonedInstructions) + callsiteMethod.instructions.remove(callsiteInstruction) - None + callsiteMethod.localVariables.addAll(cloneLocalVariableNodes(callee, labelsMap, callee.name, localVarShift).asJava) + // prepend the handlers of the callee. the order of handlers matters: when an exception is thrown + // at some instruction, the first handler guarding that instruction and having a matching exception + // type is executed. prepending the callee's handlers makes sure to test those handlers first if + // an exception is thrown in the inlined code. + callsiteMethod.tryCatchBlocks.addAll(0, cloneTryCatchBlockNodes(callee, labelsMap).asJava) + + callsiteMethod.maxLocals += returnType.getSize + callee.maxLocals + val maxStackOfInlinedCode = { + // One slot per value is correct for long / double, see comment in the `analysis` package object. + val numStoredArgs = calleeParamTypes.length + (if (isStaticMethod(callee)) 0 else 1) + callee.maxStack + callsiteStackHeight - numStoredArgs + } + val stackHeightAtNullCheck = { + // When adding a null check for the receiver, a DUP is inserted, which might cause a new maxStack. + // If the callsite has other argument values than the receiver on the stack, these are pop'ed + // and stored into locals before the null check, so in that case the maxStack doesn't grow. + val stackSlotForNullCheck = if (!isStaticMethod(callee) && !receiverKnownNotNull && calleeParamTypes.isEmpty) 1 else 0 + callsiteStackHeight + stackSlotForNullCheck } + + callsiteMethod.maxStack = math.max(callsiteMethod.maxStack, math.max(stackHeightAtNullCheck, maxStackOfInlinedCode)) + + val added = addIndyLambdaImplMethod(callsiteClass.internalName, targetHandles) + undo { removeIndyLambdaImplMethod(callsiteClass.internalName, added) } + + callGraph.addIfMissing(callee, calleeDeclarationClass) + + def mapArgInfo(argInfo: (Int, ArgInfo)): Option[(Int, ArgInfo)] = argInfo match { + case lit @ (_, FunctionLiteral) => Some(lit) + case (argIndex, ForwardedParam(paramIndex)) => callsite.argInfos.get(paramIndex).map((argIndex, _)) + } + + // Add all invocation instructions and closure instantiations that were inlined to the call graph + callGraph.callsites(callee).valuesIterator foreach { originalCallsite => + val newCallsiteIns = instructionMap(originalCallsite.callsiteInstruction).asInstanceOf[MethodInsnNode] + val argInfos = originalCallsite.argInfos flatMap mapArgInfo + val newCallsite = originalCallsite.copy( + callsiteInstruction = newCallsiteIns, + callsiteMethod = callsiteMethod, + callsiteClass = callsiteClass, + argInfos = argInfos, + callsiteStackHeight = callsiteStackHeight + originalCallsite.callsiteStackHeight + ) + val clonedCallsite = ClonedCallsite(newCallsite, callsite) + originalCallsite.inlinedClones += clonedCallsite + callGraph.addCallsite(newCallsite) + undo { + originalCallsite.inlinedClones -= clonedCallsite + callGraph.removeCallsite(newCallsite.callsiteInstruction, newCallsite.callsiteMethod) + } + } + + callGraph.closureInstantiations(callee).valuesIterator foreach { originalClosureInit => + val newIndy = instructionMap(originalClosureInit.lambdaMetaFactoryCall.indy).asInstanceOf[InvokeDynamicInsnNode] + val capturedArgInfos = originalClosureInit.capturedArgInfos flatMap mapArgInfo + val newClosureInit = ClosureInstantiation( + originalClosureInit.lambdaMetaFactoryCall.copy(indy = newIndy), + callsiteMethod, + callsiteClass, + capturedArgInfos) + originalClosureInit.inlinedClones += newClosureInit + callGraph.addClosureInstantiation(newClosureInit) + undo { + callGraph.removeClosureInstantiation(newClosureInit.lambdaMetaFactoryCall.indy, newClosureInit.ownerMethod) + } + } + + // Remove the elided invocation from the call graph + callGraph.removeCallsite(callsiteInstruction, callsiteMethod) + undo { callGraph.addCallsite(callsite) } + + // Inlining a method body can render some code unreachable, see example above in this method. + unreachableCodeEliminated -= callsiteMethod } /** - * Check whether an inling can be performed. Parmeters are described in method [[inline]]. + * Check whether an inlining can be performed. This method performs tests that don't change even + * if the body of the callee is changed by the inliner / optimizer, so it can be used early + * (when looking at the call graph and collecting inline requests for the program). + * + * The tests that inspect the callee's instructions are implemented in method `canInlineBody`, + * which is queried when performing an 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 earlyCanInlineCheck(callsite: Callsite): Option[CannotInlineWarning] = { + import callsite.{callsiteMethod, callsiteClass} + val Right(callsiteCallee) = callsite.callee + import callsiteCallee.{callee, calleeDeclarationClass} + + 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, callsite.isInlineAnnotated)) + } else if (isStrictfpMethod(callsiteMethod) != isStrictfpMethod(callee)) { + Some(StrictfpMismatch( + calleeDeclarationClass.internalName, callee.name, callee.desc, callsite.isInlineAnnotated, + callsiteClass.internalName, callsiteMethod.name, callsiteMethod.desc)) + } else + None + } + + /** + * Check whether the body of the callee contains any instructions that prevent the callsite from + * being inlined. See also method `earlyCanInlineCheck`. + * + * The result of this check depends on changes to the callee method's body. For example, if the + * callee initially invokes a private method, it cannot be inlined into a different class. If the + * private method is inlined into the callee, inlining the callee becomes possible. Therefore + * we don't query it while traversing the call graph and selecting callsites to inline - it might + * rule out callsites that can be inlined just fine. + * + * Returns + * - `None` if the callsite can be inlined + * - `Some((message, Nil))` if there was an issue performing the access checks, for example + * because of a missing classfile + * - `Some((message, instructions))` if inlining `instructions` into the callsite method would + * cause an IllegalAccessError + */ + def canInlineCallsite(callsite: Callsite): Option[(CannotInlineWarning, List[AbstractInsnNode])] = { + import callsite.{callsiteInstruction, callsiteMethod, callsiteClass, callsiteStackHeight} + val Right(callsiteCallee) = callsite.callee + import callsiteCallee.{callee, calleeDeclarationClass} def calleeDesc = s"${callee.name} of type ${callee.desc} in ${calleeDeclarationClass.internalName}" def methodMismatch = s"Wrong method node for inlining ${textify(callsiteInstruction)}: $calleeDesc" @@ -511,31 +649,30 @@ class Inliner[BT <: BTypes](val btypes: BT) { } if (codeSizeOKForInlining(callsiteMethod, callee)) { - Some(ResultingMethodTooLarge( - calleeDeclarationClass.internalName, callee.name, callee.desc, - callsiteClass.internalName, callsiteMethod.name, callsiteMethod.desc)) - } else 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 (isStrictfpMethod(callsiteMethod) != isStrictfpMethod(callee)) { - Some(StrictfpMismatch( - calleeDeclarationClass.internalName, callee.name, callee.desc, - callsiteClass.internalName, callsiteMethod.name, callsiteMethod.desc)) + val warning = ResultingMethodTooLarge( + calleeDeclarationClass.internalName, callee.name, callee.desc, callsite.isInlineAnnotated, + callsiteClass.internalName, callsiteMethod.name, callsiteMethod.desc) + Some((warning, Nil)) } 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) + val warning = MethodWithHandlerCalledOnNonEmptyStack( + calleeDeclarationClass.internalName, callee.name, callee.desc, callsite.isInlineAnnotated, + callsiteClass.internalName, callsiteMethod.name, callsiteMethod.desc) + Some((warning, Nil)) + } else findIllegalAccess(callee.instructions, calleeDeclarationClass, callsiteClass) match { + case Right(Nil) => + None + + case Right(illegalAccessInsns) => + val warning = IllegalAccessInstruction( + calleeDeclarationClass.internalName, callee.name, callee.desc, callsite.isInlineAnnotated, + callsiteClass.internalName, illegalAccessInsns.head) + Some((warning, illegalAccessInsns)) + + case Left((illegalAccessIns, cause)) => + val warning = IllegalAccessCheckFailed( + calleeDeclarationClass.internalName, callee.name, callee.desc, callsite.isInlineAnnotated, + callsiteClass.internalName, illegalAccessIns, cause) + Some((warning, Nil)) } } @@ -545,7 +682,7 @@ class Inliner[BT <: BTypes](val btypes: BT) { * (A2) C and D are members of the same run-time package */ def classIsAccessible(accessed: BType, from: ClassBType): 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? + // TODO: A2 requires "same run-time package", which seems to be package + classloader (JVMS 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) @@ -587,7 +724,7 @@ class Inliner[BT <: BTypes](val btypes: BT) { * type from there (https://github.com/scala-opt/scala/issues/13). */ def memberIsAccessible(memberFlags: Int, memberDeclClass: ClassBType, memberRefClass: ClassBType, from: ClassBType): Either[OptimizerWarning, Boolean] = { - // TODO: B3 requires "same run-time package", which seems to be package + classloader (JMVS 5.3.). is the below ok? + // TODO: B3 requires "same run-time package", which seems to be package + classloader (JVMS 5.3.). is the below ok? def samePackageAsDestination = memberDeclClass.packageInternalName == from.packageInternalName def targetObjectConformsToDestinationClass = false // needs type propagation analysis, see above @@ -624,13 +761,14 @@ 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`. - * - * 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. + * Returns + * - `Right(Nil)` if all instructions can be safely inlined + * - `Right(insns)` if inlining any of `insns` would cause a [[java.lang.IllegalAccessError]] + * when inlined into the `destinationClass` + * - `Left((insn, warning))` if validity of some instruction could not be checked because an + * error occurred */ - def findIllegalAccess(instructions: InsnList, calleeDeclarationClass: ClassBType, destinationClass: ClassBType): Option[(AbstractInsnNode, Option[OptimizerWarning])] = { + def findIllegalAccess(instructions: InsnList, calleeDeclarationClass: ClassBType, destinationClass: ClassBType): Either[(AbstractInsnNode, OptimizerWarning), List[AbstractInsnNode]] = { /** * Check if `instruction` can be transplanted to `destinationClass`. * @@ -759,17 +897,15 @@ class Inliner[BT <: BTypes](val btypes: BT) { } 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 - } + val illegalAccess = mutable.ListBuffer.empty[AbstractInsnNode] + while (it.hasNext) { + val i = it.next() + isLegal(i) match { + case Left(warning) => return Left((i, warning)) // checking isLegal for i failed + case Right(false) => illegalAccess += i // an illegal instruction was found + case _ => } } - find + Right(illegalAccess.toList) } } |