/* NSC -- new Scala compiler * Copyright 2005-2014 LAMP/EPFL * @author Martin Odersky */ package scala.tools.nsc package backend.jvm package opt import scala.reflect.internal.util.{NoPosition, Position} import scala.tools.asm.tree.analysis.{Value, Analyzer, BasicInterpreter} import scala.tools.asm.{Opcodes, Type, Handle} import scala.tools.asm.tree._ import scala.collection.concurrent import scala.collection.convert.decorateAsScala._ import scala.tools.nsc.backend.jvm.BTypes.InternalName import scala.tools.nsc.backend.jvm.BackendReporting._ import scala.tools.nsc.backend.jvm.analysis.{NotNull, NullnessAnalyzer} import ByteCodeRepository.{Source, CompilationUnit} import BytecodeUtils._ class CallGraph[BT <: BTypes](val btypes: BT) { import btypes._ val callsites: concurrent.Map[MethodInsnNode, Callsite] = recordPerRunCache(concurrent.TrieMap.empty) val closureInstantiations: concurrent.Map[InvokeDynamicInsnNode, ClosureInstantiation] = recordPerRunCache(concurrent.TrieMap.empty) def addClass(classNode: ClassNode): Unit = { val classType = classBTypeFromClassNode(classNode) for { m <- classNode.methods.asScala (calls, closureInits) = analyzeCallsites(m, classType) } { calls foreach (callsite => callsites(callsite.callsiteInstruction) = callsite) closureInits foreach (lmf => closureInstantiations(lmf.indy) = ClosureInstantiation(lmf, m, classType)) } } /** * Returns a list of callsites in the method, plus a list of closure instantiation indy instructions. */ def analyzeCallsites(methodNode: MethodNode, definingClass: ClassBType): (List[Callsite], List[LambdaMetaFactoryCall]) = { case class CallsiteInfo(safeToInline: Boolean, safeToRewrite: Boolean, annotatedInline: Boolean, annotatedNoInline: Boolean, warning: Option[CalleeInfoWarning]) /** * Analyze a callsite and gather meta-data that can be used for inlining decisions. */ def analyzeCallsite(calleeMethodNode: MethodNode, calleeDeclarationClassBType: ClassBType, receiverTypeInternalName: InternalName, calleeSource: Source): CallsiteInfo = { val methodSignature = calleeMethodNode.name + calleeMethodNode.desc try { // The inlineInfo.methodInfos of a ClassBType holds an InlineInfo for each method *declared* // within a class (not for inherited methods). Since we already have the classBType of the // callee, we only check there for the methodInlineInfo, we should find it there. calleeDeclarationClassBType.info.orThrow.inlineInfo.methodInfos.get(methodSignature) match { case Some(methodInlineInfo) => val canInlineFromSource = compilerSettings.YoptInlineGlobal || calleeSource == CompilationUnit val isAbstract = BytecodeUtils.isAbstractMethod(calleeMethodNode) // (1) A non-final method can be safe to inline if the receiver type is a final subclass. Example: // class A { @inline def f = 1 }; object B extends A; B.f // can be inlined // // TODO: type analysis can render more calls statically resolved. Example: // new A.f // can be inlined, the receiver type is known to be exactly A. val isStaticallyResolved: Boolean = { methodInlineInfo.effectivelyFinal || classBTypeFromParsedClassfile(receiverTypeInternalName).info.orThrow.inlineInfo.isEffectivelyFinal // (1) } val isRewritableTraitCall = isStaticallyResolved && methodInlineInfo.traitMethodWithStaticImplementation val warning = calleeDeclarationClassBType.info.orThrow.inlineInfo.warning.map( MethodInlineInfoIncomplete(calleeDeclarationClassBType.internalName, calleeMethodNode.name, calleeMethodNode.desc, _)) // (1) For invocations of final trait methods, the callee isStaticallyResolved but also // abstract. Such a callee is not safe to inline - it needs to be re-written to the // static impl method first (safeToRewrite). // (2) Final trait methods can be rewritten from the interface to the static implementation // method to enable inlining. CallsiteInfo( safeToInline = canInlineFromSource && isStaticallyResolved && // (1) !isAbstract && !BytecodeUtils.isConstructor(calleeMethodNode) && !BytecodeUtils.isNativeMethod(calleeMethodNode), safeToRewrite = canInlineFromSource && isRewritableTraitCall, // (2) annotatedInline = methodInlineInfo.annotatedInline, annotatedNoInline = methodInlineInfo.annotatedNoInline, warning = warning) case None => val warning = MethodInlineInfoMissing(calleeDeclarationClassBType.internalName, calleeMethodNode.name, calleeMethodNode.desc, calleeDeclarationClassBType.info.orThrow.inlineInfo.warning) CallsiteInfo(false, false, false, false, Some(warning)) } } catch { case Invalid(noInfo: NoClassBTypeInfo) => val warning = MethodInlineInfoError(calleeDeclarationClassBType.internalName, calleeMethodNode.name, calleeMethodNode.desc, noInfo) CallsiteInfo(false, false, false, false, Some(warning)) } } // TODO: run dataflow analyses to make the call graph more precise // - producers to get forwarded parameters (ForwardedParam) // - typeAnalysis for more precise argument types, more precise callee // For now we run a NullnessAnalyzer. It is used to determine if the receiver of an instance // call is known to be not-null, in which case we don't have to emit a null check when inlining. // It is also used to get the stack height at the call site. localOpt.minimalRemoveUnreachableCode(methodNode, definingClass.internalName) val analyzer: Analyzer[_ <: Value] = { if (compilerSettings.YoptNullnessTracking) new NullnessAnalyzer else new Analyzer(new BasicInterpreter) } analyzer.analyze(definingClass.internalName, methodNode) def receiverNotNullByAnalysis(call: MethodInsnNode, numArgs: Int) = analyzer match { case nullnessAnalyzer: NullnessAnalyzer => val frame = nullnessAnalyzer.frameAt(call, methodNode) frame.getStack(frame.getStackSize - 1 - numArgs).nullness == NotNull case _ => false } val callsites = new collection.mutable.ListBuffer[Callsite] val closureInstantiations = new collection.mutable.ListBuffer[LambdaMetaFactoryCall] methodNode.instructions.iterator.asScala foreach { case call: MethodInsnNode => val callee: Either[OptimizerWarning, Callee] = for { (method, declarationClass) <- byteCodeRepository.methodNode(call.owner, call.name, call.desc): Either[OptimizerWarning, (MethodNode, InternalName)] (declarationClassNode, source) <- byteCodeRepository.classNodeAndSource(declarationClass): Either[OptimizerWarning, (ClassNode, Source)] declarationClassBType = classBTypeFromClassNode(declarationClassNode) } yield { val CallsiteInfo(safeToInline, safeToRewrite, annotatedInline, annotatedNoInline, warning) = analyzeCallsite(method, declarationClassBType, call.owner, source) Callee( callee = method, calleeDeclarationClass = declarationClassBType, safeToInline = safeToInline, safeToRewrite = safeToRewrite, annotatedInline = annotatedInline, annotatedNoInline = annotatedNoInline, calleeInfoWarning = warning) } val argInfos = if (callee.isLeft) Nil else { // TODO: for now it's Nil, because we don't run any data flow analysis // there's no point in using the parameter types, that doesn't add any information. // NOTE: need to run the same analyses after inlining, to re-compute the argInfos for the // new duplicated callsites, see Inliner.inline Nil } val receiverNotNull = call.getOpcode == Opcodes.INVOKESTATIC || { val numArgs = Type.getArgumentTypes(call.desc).length receiverNotNullByAnalysis(call, numArgs) } callsites += Callsite( callsiteInstruction = call, callsiteMethod = methodNode, callsiteClass = definingClass, callee = callee, argInfos = argInfos, callsiteStackHeight = analyzer.frameAt(call, methodNode).getStackSize, receiverKnownNotNull = receiverNotNull, callsitePosition = callsitePositions.getOrElse(call, NoPosition) ) case LambdaMetaFactoryCall(indy, samMethodType, implMethod, instantiatedMethodType) => closureInstantiations += LambdaMetaFactoryCall(indy, samMethodType, implMethod, instantiatedMethodType) case _ => } (callsites.toList, closureInstantiations.toList) } /** * A callsite in the call graph. * * @param callsiteInstruction The invocation instruction * @param callsiteMethod The method containing the callsite * @param callsiteClass The class containing the callsite * @param callee The callee, as it appears in the invocation instruction. For virtual * calls, an override of the callee might be invoked. Also, the callee * can be abstract. Contains a warning message if the callee MethodNode * cannot be found in the bytecode repository. * @param argInfos Information about the invocation receiver and arguments * @param callsiteStackHeight The stack height at the callsite, required by the inliner * @param callsitePosition The source position of the callsite, used for inliner warnings. */ final case class Callsite(callsiteInstruction: MethodInsnNode, callsiteMethod: MethodNode, callsiteClass: ClassBType, callee: Either[OptimizerWarning, Callee], argInfos: List[ArgInfo], callsiteStackHeight: Int, receiverKnownNotNull: Boolean, callsitePosition: Position) { override def toString = "Invocation of" + s" ${callee.map(_.calleeDeclarationClass.internalName).getOrElse("?")}.${callsiteInstruction.name + callsiteInstruction.desc}" + s"@${callsiteMethod.instructions.indexOf(callsiteInstruction)}" + s" in ${callsiteClass.internalName}.${callsiteMethod.name}" } /** * Information about invocation arguments, obtained through data flow analysis of the callsite method. */ sealed trait ArgInfo final case class ArgTypeInfo(argType: BType, isPrecise: Boolean, knownNotNull: Boolean) extends ArgInfo final case class ForwardedParam(index: Int) extends ArgInfo // can be extended, e.g., with constant types /** * A callee in the call graph. * * @param callee The callee, as it appears in the invocation instruction. For * virtual calls, an override of the callee might be invoked. Also, * the callee can be abstract. * @param calleeDeclarationClass The class in which the callee is declared * @param safeToInline True if the callee can be safely inlined: it cannot be overridden, * and the inliner settings (project / global) allow inlining it. * @param safeToRewrite True if the callee is the interface method of a concrete trait method * that can be safely re-written to the static implementation method. * @param annotatedInline True if the callee is annotated @inline * @param annotatedNoInline True if the callee is annotated @noinline * @param calleeInfoWarning An inliner warning if some information was not available while * gathering the information about this callee. */ final case class Callee(callee: MethodNode, calleeDeclarationClass: ClassBType, safeToInline: Boolean, safeToRewrite: Boolean, annotatedInline: Boolean, annotatedNoInline: Boolean, calleeInfoWarning: Option[CalleeInfoWarning]) { assert(!(safeToInline && safeToRewrite), s"A callee of ${callee.name} can be either safeToInline or safeToRewrite, but not both.") } final case class ClosureInstantiation(lambdaMetaFactoryCall: LambdaMetaFactoryCall, ownerMethod: MethodNode, ownerClass: ClassBType) { override def toString = s"ClosureInstantiation($lambdaMetaFactoryCall, ${ownerMethod.name + ownerMethod.desc}, $ownerClass)" } final case class LambdaMetaFactoryCall(indy: InvokeDynamicInsnNode, samMethodType: Type, implMethod: Handle, instantiatedMethodType: Type) object LambdaMetaFactoryCall { private val lambdaMetaFactoryInternalName: InternalName = "java/lang/invoke/LambdaMetafactory" private val metafactoryHandle = { val metafactoryMethodName: String = "metafactory" val metafactoryDesc: String = "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;" new Handle(Opcodes.H_INVOKESTATIC, lambdaMetaFactoryInternalName, metafactoryMethodName, metafactoryDesc) } private val altMetafactoryHandle = { val altMetafactoryMethodName: String = "altMetafactory" val altMetafactoryDesc: String = "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;" new Handle(Opcodes.H_INVOKESTATIC, lambdaMetaFactoryInternalName, altMetafactoryMethodName, altMetafactoryDesc) } def unapply(insn: AbstractInsnNode): Option[(InvokeDynamicInsnNode, Type, Handle, Type)] = insn match { case indy: InvokeDynamicInsnNode if indy.bsm == metafactoryHandle || indy.bsm == altMetafactoryHandle => indy.bsmArgs match { case Array(samMethodType: Type, implMethod: Handle, instantiatedMethodType: Type, xs@_*) => // xs binding because IntelliJ gets confused about _@_* // LambdaMetaFactory performs a number of automatic adaptations when invoking the lambda // implementation method (casting, boxing, unboxing, and primitive widening, see Javadoc). // // The closure optimizer supports only one of those adaptations: it will cast arguments // to the correct type when re-writing a closure call to the body method. Example: // // val fun: String => String = l => l // val l = List("") // fun(l.head) // // The samMethodType of Function1 is `(Object)Object`, while the instantiatedMethodType // is `(String)String`. The return type of `List.head` is `Object`. // // The implMethod has the signature `C$anonfun(String)String`. // // At the closure callsite, we have an `INVOKEINTERFACE Function1.apply (Object)Object`, // so the object returned by `List.head` can be directly passed into the call (no cast). // // The closure object will cast the object to String before passing it to the implMethod. // // When re-writing the closure callsite to the implMethod, we have to insert a cast. // // The check below ensures that // (1) the implMethod type has the expected singature (captured types plus argument types // from instantiatedMethodType) // (2) the receiver of the implMethod matches the first captured type // (3) all parameters that are not the same in samMethodType and instantiatedMethodType // are reference types, so that we can insert casts to perform the same adaptation // that the closure object would. val isStatic = implMethod.getTag == Opcodes.H_INVOKESTATIC val indyParamTypes = Type.getArgumentTypes(indy.desc) val instantiatedMethodArgTypes = instantiatedMethodType.getArgumentTypes val expectedImplMethodType = { val paramTypes = (if (isStatic) indyParamTypes else indyParamTypes.tail) ++ instantiatedMethodArgTypes Type.getMethodType(instantiatedMethodType.getReturnType, paramTypes: _*) } val isIndyLambda = ( Type.getType(implMethod.getDesc) == expectedImplMethodType // (1) && (isStatic || implMethod.getOwner == indyParamTypes(0).getInternalName) // (2) && samMethodType.getArgumentTypes.corresponds(instantiatedMethodArgTypes)((samArgType, instArgType) => samArgType == instArgType || isReference(samArgType) && isReference(instArgType)) // (3) ) if (isIndyLambda) Some((indy, samMethodType, implMethod, instantiatedMethodType)) else None case _ => None } case _ => None } } }