Skip to content

Commit fbd2e35

Browse files
committed
Merge pull request scala#5140 from lrytz/inlineDefaultMethods
SD-140 inline the correct default method
2 parents 1b1bc81 + ca382b7 commit fbd2e35

File tree

8 files changed

+200
-65
lines changed

8 files changed

+200
-65
lines changed

src/compiler/scala/tools/nsc/backend/jvm/BackendReporting.scala

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,15 @@ object BackendReporting {
4242
def assertionError(message: String): Nothing = throw new AssertionError(message)
4343

4444
implicit class RightBiasedEither[A, B](val v: Either[A, B]) extends AnyVal {
45-
def map[U](f: B => U) = v.right.map(f)
46-
def flatMap[BB](f: B => Either[A, BB]) = v.right.flatMap(f)
45+
def map[C](f: B => C): Either[A, C] = v.right.map(f)
46+
def flatMap[C](f: B => Either[A, C]): Either[A, C] = v.right.flatMap(f)
4747
def withFilter(f: B => Boolean)(implicit empty: A): Either[A, B] = v match {
4848
case Left(_) => v
4949
case Right(e) => if (f(e)) v else Left(empty) // scalaz.\/ requires an implicit Monoid m to get m.empty
5050
}
51-
def foreach[U](f: B => U) = v.right.foreach(f)
51+
def foreach[U](f: B => U): Unit = v.right.foreach(f)
5252

53-
def getOrElse[BB >: B](alt: => BB): BB = v.right.getOrElse(alt)
53+
def getOrElse[C >: B](alt: => C): C = v.right.getOrElse(alt)
5454

5555
/**
5656
* Get the value, fail with an assertion if this is an error.
@@ -101,11 +101,14 @@ object BackendReporting {
101101
else ""
102102
}
103103

104-
case MethodNotFound(name, descriptor, ownerInternalName, missingClasses) =>
105-
val (javaDef, others) = missingClasses.partition(_.definedInJavaSource)
106-
s"The method $name$descriptor could not be found in the class $ownerInternalName or any of its parents." +
107-
(if (others.isEmpty) "" else others.map(_.internalName).mkString("\nNote that the following parent classes could not be found on the classpath: ", ", ", "")) +
108-
(if (javaDef.isEmpty) "" else javaDef.map(_.internalName).mkString("\nNote that the following parent classes are defined in Java sources (mixed compilation), no bytecode is available: ", ",", ""))
104+
case MethodNotFound(name, descriptor, ownerInternalName, missingClass) =>
105+
val missingClassWarning = missingClass match {
106+
case None => ""
107+
case Some(c) =>
108+
if (c.definedInJavaSource) s"\nNote that the parent class ${c.internalName} is defined in a Java source (mixed compilation), no bytecode is available."
109+
else s"\nNote that the parent class ${c.internalName} could not be found on the classpath."
110+
}
111+
s"The method $name$descriptor could not be found in the class $ownerInternalName or any of its parents." + missingClassWarning
109112

110113
case FieldNotFound(name, descriptor, ownerInternalName, missingClass) =>
111114
s"The field node $name$descriptor could not be found because the classfile $ownerInternalName cannot be found on the classpath." +
@@ -127,7 +130,7 @@ object BackendReporting {
127130
}
128131

129132
case class ClassNotFound(internalName: InternalName, definedInJavaSource: Boolean) extends MissingBytecodeWarning
130-
case class MethodNotFound(name: String, descriptor: String, ownerInternalNameOrArrayDescriptor: InternalName, missingClasses: List[ClassNotFound]) extends MissingBytecodeWarning {
133+
case class MethodNotFound(name: String, descriptor: String, ownerInternalNameOrArrayDescriptor: InternalName, missingClass: Option[ClassNotFound]) extends MissingBytecodeWarning {
131134
def isArrayMethod = ownerInternalNameOrArrayDescriptor.charAt(0) == '['
132135
}
133136
case class FieldNotFound(name: String, descriptor: String, ownerInternalName: InternalName, missingClass: Option[ClassNotFound]) extends MissingBytecodeWarning

src/compiler/scala/tools/nsc/backend/jvm/CoreBTypes.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ class CoreBTypes[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: BTFS) {
107107
lazy val juHashMapRef : ClassBType = classBTypeFromSymbol(JavaUtilHashMap) // java/util/HashMap
108108
lazy val sbScalaBeanInfoRef : ClassBType = classBTypeFromSymbol(requiredClass[scala.beans.ScalaBeanInfo])
109109
lazy val jliSerializedLambdaRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.invoke.SerializedLambda])
110+
lazy val jliMethodHandleRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.invoke.MethodHandle])
110111
lazy val jliMethodHandlesRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.invoke.MethodHandles])
111112
lazy val jliMethodHandlesLookupRef : ClassBType = classBTypeFromSymbol(exitingPickler(getRequiredClass("java.lang.invoke.MethodHandles.Lookup"))) // didn't find a reliable non-stringly-typed way that works for inner classes in the backend
112113
lazy val jliMethodTypeRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.invoke.MethodType])
@@ -320,6 +321,7 @@ trait CoreBTypesProxyGlobalIndependent[BTS <: BTypes] {
320321
def jliCallSiteRef : ClassBType
321322
def jliMethodTypeRef : ClassBType
322323
def jliSerializedLambdaRef : ClassBType
324+
def jliMethodHandleRef : ClassBType
323325
def jliMethodHandlesLookupRef : ClassBType
324326
def srBoxesRunTimeRef : ClassBType
325327
def srBoxedUnitRef : ClassBType
@@ -383,6 +385,7 @@ final class CoreBTypesProxy[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes:
383385
def juHashMapRef : ClassBType = _coreBTypes.juHashMapRef
384386
def sbScalaBeanInfoRef : ClassBType = _coreBTypes.sbScalaBeanInfoRef
385387
def jliSerializedLambdaRef : ClassBType = _coreBTypes.jliSerializedLambdaRef
388+
def jliMethodHandleRef : ClassBType = _coreBTypes.jliMethodHandleRef
386389
def jliMethodHandlesRef : ClassBType = _coreBTypes.jliMethodHandlesRef
387390
def jliMethodHandlesLookupRef : ClassBType = _coreBTypes.jliMethodHandlesLookupRef
388391
def jliMethodTypeRef : ClassBType = _coreBTypes.jliMethodTypeRef

src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala

Lines changed: 120 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ package opt
1010
import scala.tools.asm
1111
import asm.tree._
1212
import scala.collection.JavaConverters._
13-
import scala.collection.concurrent
13+
import scala.collection.{concurrent, mutable}
1414
import scala.tools.asm.Attribute
1515
import scala.tools.nsc.backend.jvm.BackendReporting._
1616
import scala.tools.nsc.io.AbstractFile
@@ -132,38 +132,135 @@ class ByteCodeRepository[BT <: BTypes](val classPath: ClassFileLookup[AbstractFi
132132
* The method node for a method matching `name` and `descriptor`, accessed in class `ownerInternalNameOrArrayDescriptor`.
133133
* The declaration of the method may be in one of the parents.
134134
*
135-
* TODO: make sure we always return the right method, the one being invoked. write tests.
136-
* - if there's an abstract and a concrete one. could possibly somehow the abstract be returned?
137-
* - with traits and default methods, if there is more than one default method inherited and
138-
* no override: what should be returned? We should not just inline one of the two.
135+
* Note that the JVM spec performs method lookup in two steps: resolution and selection.
136+
*
137+
* Method resolution, defined in jvms-5.4.3.3 and jvms-5.4.3.4, is the first step and is identical
138+
* for all invocation styles (virtual, interface, special, static). If C is the receiver class
139+
* in the invocation instruction:
140+
* 1 find a matching method (name and descriptor) in C
141+
* 2 then in C's superclasses
142+
* 3 then find the maximally-specific matching superinterface methods, succeed if there's a
143+
* single non-abstract one. static and private methods in superinterfaces are not considered.
144+
* 4 then pick a random non-static, non-private superinterface method.
145+
* 5 then fail.
146+
*
147+
* Note that for an `invokestatic` instruction, a method reference `B.m` may resolve to `A.m`, if
148+
* class `B` doesn't specify a matching method `m`, but the parent `A` does.
149+
*
150+
* Selection depends on the invocation style and is defined in jvms-6.5.
151+
* - invokestatic: invokes the resolved method
152+
* - invokevirtual / invokeinterface: searches for an override of the resolved method starting
153+
* at the dynamic receiver type. the search procedure is basically the same as in resolution,
154+
* but it fails at 4 instead of picking a superinterface method at random.
155+
* - invokespecial: if C is the receiver in the invocation instruction, searches for an override
156+
* of the resolved method starting at
157+
* - the superclass of the current class, if C is a superclass of the current class
158+
* - C otherwise
159+
* again, the search procedure is the same.
160+
*
161+
* In the method here we implement method *resolution*. Whether or not the returned method is
162+
* actually invoked at runtime depends on the invocation instruction and the class hierarchy, so
163+
* the users (e.g. the inliner) have to be aware of method selection.
164+
*
165+
* Note that the returned method may be abstract (ACC_ABSTRACT), native (ACC_NATIVE) or signature
166+
* polymorphic (methods `invoke` and `invokeExact` in class `MehtodHandles`).
139167
*
140168
* @return The [[MethodNode]] of the requested method and the [[InternalName]] of its declaring
141-
* class, or an error message if the method could not be found.
169+
* class, or an error message if the method could not be found. An error message is also
170+
* returned if method resolution results in multiple default methods.
142171
*/
143172
def methodNode(ownerInternalNameOrArrayDescriptor: String, name: String, descriptor: String): Either[MethodNotFound, (MethodNode, InternalName)] = {
144-
// on failure, returns a list of class names that could not be found on the classpath
145-
def methodNodeImpl(ownerInternalName: InternalName): Either[List[ClassNotFound], (MethodNode, InternalName)] = {
146-
classNode(ownerInternalName) match {
147-
case Left(e) => Left(List(e))
148-
case Right(c) =>
149-
c.methods.asScala.find(m => m.name == name && m.desc == descriptor) match {
150-
case Some(m) => Right((m, ownerInternalName))
151-
case None => findInParents(Option(c.superName) ++: c.interfaces.asScala.toList, Nil)
152-
}
173+
def findMethod(c: ClassNode): Option[MethodNode] = c.methods.asScala.find(m => m.name == name && m.desc == descriptor)
174+
175+
// https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.9: "In Java SE 8, the only
176+
// signature polymorphic methods are the invoke and invokeExact methods of the class MethodHandle.
177+
def isSignaturePolymorphic(owner: InternalName) = owner == coreBTypes.jliMethodHandleRef.internalName && (name == "invoke" || name == "invokeExact")
178+
179+
// Note: if `owner` is an interface, in the first iteration we search for a matching member in the interface itself.
180+
// If that fails, the recursive invocation checks in the superclass (which is Object) with `publicInstanceOnly == true`.
181+
// This is specified in jvms-5.4.3.4: interface method resolution only returns public, non-static methods of Object.
182+
def findInSuperClasses(owner: ClassNode, publicInstanceOnly: Boolean = false): Either[ClassNotFound, Option[(MethodNode, InternalName)]] = {
183+
findMethod(owner) match {
184+
case Some(m) if !publicInstanceOnly || (isPublicMethod(m) && !isStaticMethod(m)) => Right(Some((m, owner.name)))
185+
case None =>
186+
if (isSignaturePolymorphic(owner.name)) Right(Some((owner.methods.asScala.find(_.name == name).get, owner.name)))
187+
else if (owner.superName == null) Right(None)
188+
else classNode(owner.superName).flatMap(findInSuperClasses(_, isInterface(owner)))
153189
}
154190
}
155191

156-
// find the MethodNode in one of the parent classes
157-
def findInParents(parents: List[InternalName], failedClasses: List[ClassNotFound]): Either[List[ClassNotFound], (MethodNode, InternalName)] = parents match {
158-
case x :: xs => methodNodeImpl(x).left.flatMap(failed => findInParents(xs, failed ::: failedClasses))
159-
case Nil => Left(failedClasses)
192+
def findInInterfaces(initialOwner: ClassNode): Either[ClassNotFound, Option[(MethodNode, InternalName)]] = {
193+
val visited = mutable.Set.empty[InternalName]
194+
val found = mutable.ListBuffer.empty[(MethodNode, ClassNode)]
195+
196+
def findIn(owner: ClassNode): Option[ClassNotFound] = {
197+
for (i <- owner.interfaces.asScala if !visited(i)) classNode(i) match {
198+
case Left(e) => return Some(e)
199+
case Right(c) =>
200+
visited += i
201+
// abstract and static methods are excluded, see jvms-5.4.3.3
202+
for (m <- findMethod(c) if !isPrivateMethod(m) && !isStaticMethod(m)) found += ((m, c))
203+
val recusionResult = findIn(c)
204+
if (recusionResult.isDefined) return recusionResult
205+
}
206+
None
207+
}
208+
209+
findIn(initialOwner)
210+
211+
val result =
212+
if (found.size <= 1) found.headOption
213+
else {
214+
val maxSpecific = found.filterNot({
215+
case (method, owner) =>
216+
isAbstractMethod(method) || {
217+
val ownerTp = classBTypeFromClassNode(owner)
218+
found exists {
219+
case (other, otherOwner) =>
220+
(other ne method) && {
221+
val otherTp = classBTypeFromClassNode(otherOwner)
222+
otherTp.isSubtypeOf(ownerTp).get
223+
}
224+
}
225+
}
226+
})
227+
// (*) note that if there's no single, non-abstract, maximally-specific method, the jvm
228+
// method resolution (jvms-5.4.3.3) returns any of the non-private, non-static parent
229+
// methods at random (abstract or concrete).
230+
// we chose not to do this here, to prevent the inliner from potentially inlining the
231+
// wrong method. in other words, we guarantee that a concrete method is only returned if
232+
// it resolves deterministically.
233+
// however, there may be multiple abstract methods inherited. in this case we *do* want
234+
// to return a result to allow performing accessibility checks in the inliner. note that
235+
// for accessibility it does not matter which of these methods is return, as they are all
236+
// non-private (i.e., public, protected is not possible, jvms-4.1).
237+
// the remaining case (when there's no max-specific method, but some non-abstract one)
238+
// does not occur in bytecode generated by scalac or javac. we return no result in this
239+
// case. this may at worst prevent some optimizations from happening.
240+
if (maxSpecific.size == 1) maxSpecific.headOption
241+
else if (found.forall(p => isAbstractMethod(p._1))) found.headOption // (*)
242+
else None
243+
}
244+
Right(result.map(p => (p._1, p._2.name)))
160245
}
161246

162247
// In a MethodInsnNode, the `owner` field may be an array descriptor, for example when invoking `clone`. We don't have a method node to return in this case.
163-
if (ownerInternalNameOrArrayDescriptor.charAt(0) == '[')
164-
Left(MethodNotFound(name, descriptor, ownerInternalNameOrArrayDescriptor, Nil))
165-
else
166-
methodNodeImpl(ownerInternalNameOrArrayDescriptor).left.map(MethodNotFound(name, descriptor, ownerInternalNameOrArrayDescriptor, _))
248+
if (ownerInternalNameOrArrayDescriptor.charAt(0) == '[') {
249+
Left(MethodNotFound(name, descriptor, ownerInternalNameOrArrayDescriptor, None))
250+
} else {
251+
def notFound(cnf: Option[ClassNotFound]) = Left(MethodNotFound(name, descriptor, ownerInternalNameOrArrayDescriptor, cnf))
252+
val res: Either[ClassNotFound, Option[(MethodNode, InternalName)]] = classNode(ownerInternalNameOrArrayDescriptor).flatMap(c =>
253+
findInSuperClasses(c) flatMap {
254+
case None => findInInterfaces(c)
255+
case res => Right(res)
256+
}
257+
)
258+
res match {
259+
case Left(e) => notFound(Some(e))
260+
case Right(None) => notFound(None)
261+
case Right(Some(res)) => Right(res)
262+
}
263+
}
167264
}
168265

169266
private def parseClass(internalName: InternalName): Either[ClassNotFound, ClassNode] = {

src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ object BytecodeUtils {
9999
methodNode.name == INSTANCE_CONSTRUCTOR_NAME || methodNode.name == CLASS_CONSTRUCTOR_NAME
100100
}
101101

102+
def isPublicMethod(methodNode: MethodNode): Boolean = (methodNode.access & ACC_PUBLIC) != 0
103+
104+
def isPrivateMethod(methodNode: MethodNode): Boolean = (methodNode.access & ACC_PRIVATE) != 0
105+
102106
def isStaticMethod(methodNode: MethodNode): Boolean = (methodNode.access & ACC_STATIC) != 0
103107

104108
def isAbstractMethod(methodNode: MethodNode): Boolean = (methodNode.access & ACC_ABSTRACT) != 0
@@ -107,10 +111,12 @@ object BytecodeUtils {
107111

108112
def isNativeMethod(methodNode: MethodNode): Boolean = (methodNode.access & ACC_NATIVE) != 0
109113

110-
def hasCallerSensitiveAnnotation(methodNode: MethodNode) = methodNode.visibleAnnotations != null && methodNode.visibleAnnotations.asScala.exists(_.desc == "Lsun/reflect/CallerSensitive;")
114+
def hasCallerSensitiveAnnotation(methodNode: MethodNode): Boolean = methodNode.visibleAnnotations != null && methodNode.visibleAnnotations.asScala.exists(_.desc == "Lsun/reflect/CallerSensitive;")
111115

112116
def isFinalClass(classNode: ClassNode): Boolean = (classNode.access & ACC_FINAL) != 0
113117

118+
def isInterface(classNode: ClassNode): Boolean = (classNode.access & ACC_INTERFACE) != 0
119+
114120
def isFinalMethod(methodNode: MethodNode): Boolean = (methodNode.access & (ACC_FINAL | ACC_PRIVATE | ACC_STATIC)) != 0
115121

116122
def isStrictfpMethod(methodNode: MethodNode): Boolean = (methodNode.access & ACC_STRICT) != 0

src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -131,19 +131,19 @@ class CallGraph[BT <: BTypes](val btypes: BT) {
131131
(method, declarationClass) <- byteCodeRepository.methodNode(call.owner, call.name, call.desc): Either[OptimizerWarning, (MethodNode, InternalName)]
132132
(declarationClassNode, source) <- byteCodeRepository.classNodeAndSource(declarationClass): Either[OptimizerWarning, (ClassNode, Source)]
133133
} yield {
134-
val declarationClassBType = classBTypeFromClassNode(declarationClassNode)
135-
val info = analyzeCallsite(method, declarationClassBType, call, source)
136-
import info._
137-
Callee(
138-
callee = method,
139-
calleeDeclarationClass = declarationClassBType,
140-
safeToInline = safeToInline,
141-
canInlineFromSource = canInlineFromSource,
142-
annotatedInline = annotatedInline,
143-
annotatedNoInline = annotatedNoInline,
144-
samParamTypes = info.samParamTypes,
145-
calleeInfoWarning = warning)
146-
}
134+
val declarationClassBType = classBTypeFromClassNode(declarationClassNode)
135+
val info = analyzeCallsite(method, declarationClassBType, call, source)
136+
import info._
137+
Callee(
138+
callee = method,
139+
calleeDeclarationClass = declarationClassBType,
140+
safeToInline = safeToInline,
141+
canInlineFromSource = canInlineFromSource,
142+
annotatedInline = annotatedInline,
143+
annotatedNoInline = annotatedNoInline,
144+
samParamTypes = info.samParamTypes,
145+
calleeInfoWarning = warning)
146+
}
147147

148148
val argInfos = computeArgInfos(callee, call, prodCons)
149149

@@ -388,12 +388,11 @@ class CallGraph[BT <: BTypes](val btypes: BT) {
388388
* @param calleeInfoWarning An inliner warning if some information was not available while
389389
* gathering the information about this callee.
390390
*/
391-
final case class Callee(
392-
callee: MethodNode, calleeDeclarationClass: btypes.ClassBType,
393-
safeToInline: Boolean, canInlineFromSource: Boolean,
394-
annotatedInline: Boolean, annotatedNoInline: Boolean,
395-
samParamTypes: IntMap[btypes.ClassBType],
396-
calleeInfoWarning: Option[CalleeInfoWarning]) {
391+
final case class Callee(callee: MethodNode, calleeDeclarationClass: btypes.ClassBType,
392+
safeToInline: Boolean, canInlineFromSource: Boolean,
393+
annotatedInline: Boolean, annotatedNoInline: Boolean,
394+
samParamTypes: IntMap[btypes.ClassBType],
395+
calleeInfoWarning: Option[CalleeInfoWarning]) {
397396
override def toString = s"Callee($calleeDeclarationClass.${callee.name})"
398397
}
399398

0 commit comments

Comments
 (0)