Skip to content

Commit

Permalink
Cast arguments where necessary before rewriting closure invocations
Browse files Browse the repository at this point in the history
The parameter types of a closure invocation can be supertypes of the
parameter types in the implementation method. The closure automatically
casts arguments to the right type. We need to do the same when
rewriting closure invocations. Example:

    val fun: String => String = l => l
    val l = List("")
    fun(l.head)

The closure object calls `apply(Object)Object`. The body method takes
an argument of type `String`.
  • Loading branch information
lrytz committed Jun 23, 2015
1 parent 5be0722 commit 3e7776e
Showing 1 changed file with 70 additions and 15 deletions.
85 changes: 70 additions & 15 deletions src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,53 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) {
case Array(samMethodType: Type, implMethod: Handle, instantiatedMethodType: Type, xs @ _*) =>
// LambdaMetaFactory performs a number of automatic adaptations when invoking the lambda
// implementation method (casting, boxing, unboxing, and primitive widening, see Javadoc).
// The closure optimizer (rewriteClosureApplyInvocations) does not currently support these
// adaptations, so we don't consider indy calls that need adaptations for rewriting.
// Indy calls emitted by scalac never rely on adaptation, they are implemented explicitly
// in the implMethod.
//
// Note that we don't check all the invariants requried for a metafactory indy call, only
// those required not to crash the compiler.
// 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 == 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 implMethodType = Type.getType(implMethod.getDesc)
val numCaptures = implMethodType.getArgumentTypes.length - instantiatedMethodType.getArgumentTypes.length
val implMethodTypeWithoutCaputres = Type.getMethodType(implMethodType.getReturnType, implMethodType.getArgumentTypes.drop(numCaptures): _*)
implMethodTypeWithoutCaputres == instantiatedMethodType
{
Type.getType(implMethod.getDesc) == expectedImplMethodType // (1)
} && {
isStatic || implMethod.getOwner == indyParamTypes(0).getInternalName // (2)
} && {
def isReference(t: Type) = t.getSort == Type.OBJECT || t.getSort == Type.ARRAY
(samMethodType.getArgumentTypes, instantiatedMethodArgTypes).zipped forall {
case (samArgType, instArgType) =>
samArgType == instArgType || isReference(samArgType) && isReference(instArgType) // (3)
}
}

case _ =>
false
Expand Down Expand Up @@ -104,7 +139,11 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) {
// If the variable is not modified within the method, we could avoid introducing yet another
// local. On the other hand, further optimizations (copy propagation, remove unused locals) will
// clean it up.
val localsForCaptures = LocalsList.fromTypes(firstCaptureLocal, capturedTypes)

// Captured variables don't need to be cast when loaded at the callsite (castLoadTypes are None).
// This is checked in `isClosureInstantiation`: the types of the captured variables in the indy
// instruction match exactly the corresponding parameter types in the body method.
val localsForCaptures = LocalsList.fromTypes(firstCaptureLocal, capturedTypes, castLoadTypes = _ => None)
methodNode.maxLocals = firstCaptureLocal + localsForCaptures.size

insertStoreOps(indy, methodNode, localsForCaptures)
Expand Down Expand Up @@ -140,6 +179,8 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) {
val varOp = new VarInsnNode(if (store) l.storeOpcode else l.loadOpcode, l.local)
if (store) methodNode.instructions.insert(previous, varOp)
else methodNode.instructions.insertBefore(before, varOp)
if (!store) for (castType <- l.castLoadedValue)
methodNode.instructions.insert(varOp, new TypeInsnNode(CHECKCAST, castType.getInternalName))
}
}

Expand Down Expand Up @@ -187,7 +228,21 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) {
// if there are multiple callsites, the same locals are re-used.
val argTypes = indy.bsmArgs(0).asInstanceOf[Type].getArgumentTypes // safe, checked in isClosureInstantiation
val firstArgLocal = methodNode.maxLocals
val argLocals = LocalsList.fromTypes(firstArgLocal, argTypes)

// The comment in `isClosureInstantiation` explains why we have to introduce casts for
// arguments that have different types in samMethodType and instantiatedMethodType.
val castLoadTypes = {
val instantiatedMethodType = indy.bsmArgs(2).asInstanceOf[Type]
(argTypes, instantiatedMethodType.getArgumentTypes).zipped map {
case (samArgType, instantiatedArgType) if samArgType != instantiatedArgType =>
// isClosureInstantiation ensures that the two types are reference types, so we don't
// end up casting primitive values.
Some(instantiatedArgType)
case _ =>
None
}
}
val argLocals = LocalsList.fromTypes(firstArgLocal, argTypes, castLoadTypes)
methodNode.maxLocals = firstArgLocal + argLocals.size

(captureLocals, argLocals)
Expand Down Expand Up @@ -286,12 +341,12 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) {
* Local(6, refOpOffset) ::
* Nil
*/
def fromTypes(firstLocal: Int, types: Array[Type]): LocalsList = {
def fromTypes(firstLocal: Int, types: Array[Type], castLoadTypes: Int => Option[Type]): LocalsList = {
var sizeTwoOffset = 0
val locals: List[Local] = types.indices.map(i => {
// The ASM method `type.getOpcode` returns the opcode for operating on a value of `type`.
val offset = types(i).getOpcode(ILOAD) - ILOAD
val local = Local(firstLocal + i + sizeTwoOffset, offset)
val local = Local(firstLocal + i + sizeTwoOffset, offset, castLoadTypes(i))
if (local.size == 2) sizeTwoOffset += 1
local
})(collection.breakOut)
Expand All @@ -305,7 +360,7 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) {
* The xLOAD / xSTORE opcodes are in the following sequence: I, L, F, D, A, so the offset for
* a local variable holding a reference (`A`) is 4. See also method `getOpcode` in [[scala.tools.asm.Type]].
*/
case class Local(local: Int, opcodeOffset: Int) {
case class Local(local: Int, opcodeOffset: Int, castLoadedValue: Option[Type]) {
def size = if (loadOpcode == LLOAD || loadOpcode == DLOAD) 2 else 1

def loadOpcode = ILOAD + opcodeOffset
Expand Down

0 comments on commit 3e7776e

Please sign in to comment.