Skip to content
This repository was archived by the owner on Sep 1, 2020. It is now read-only.

Commit 3e7776e

Browse files
committed
Cast arguments where necessary before rewriting closure invocations
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`.
1 parent 5be0722 commit 3e7776e

File tree

1 file changed

+70
-15
lines changed

1 file changed

+70
-15
lines changed

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

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,53 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) {
5252
case Array(samMethodType: Type, implMethod: Handle, instantiatedMethodType: Type, xs @ _*) =>
5353
// LambdaMetaFactory performs a number of automatic adaptations when invoking the lambda
5454
// implementation method (casting, boxing, unboxing, and primitive widening, see Javadoc).
55-
// The closure optimizer (rewriteClosureApplyInvocations) does not currently support these
56-
// adaptations, so we don't consider indy calls that need adaptations for rewriting.
57-
// Indy calls emitted by scalac never rely on adaptation, they are implemented explicitly
58-
// in the implMethod.
5955
//
60-
// Note that we don't check all the invariants requried for a metafactory indy call, only
61-
// those required not to crash the compiler.
56+
// The closure optimizer supports only one of those adaptations: it will cast arguments
57+
// to the correct type when re-writing a closure call to the body method. Example:
58+
//
59+
// val fun: String => String = l => l
60+
// val l = List("")
61+
// fun(l.head)
62+
//
63+
// The samMethodType of Function1 is `(Object)Object`, while the instantiatedMethodType
64+
// is `(String)String`. The return type of `List.head` is `Object`.
65+
//
66+
// The implMethod has the signature `C$anonfun(String)String`.
67+
//
68+
// At the closure callsite, we have an `INVOKEINTERFACE Function1.apply (Object)Object`,
69+
// so the object returned by `List.head` can be directly passed into the call (no cast).
70+
//
71+
// The closure object will cast the object to String before passing it to the implMethod.
72+
//
73+
// When re-writing the closure callsite to the implMethod, we have to insert a cast.
74+
//
75+
// The check below ensures that
76+
// (1) the implMethod type has the expected singature (captured types plus argument types
77+
// from instantiatedMethodType)
78+
// (2) the receiver of the implMethod matches the first captured type
79+
// (3) all parameters that are not the same in samMethodType and instantiatedMethodType
80+
// are reference types, so that we can insert casts to perform the same adaptation
81+
// that the closure object would.
82+
83+
val isStatic = implMethod.getTag == H_INVOKESTATIC
84+
val indyParamTypes = Type.getArgumentTypes(indy.desc)
85+
val instantiatedMethodArgTypes = instantiatedMethodType.getArgumentTypes
86+
val expectedImplMethodType = {
87+
val paramTypes = (if (isStatic) indyParamTypes else indyParamTypes.tail) ++ instantiatedMethodArgTypes
88+
Type.getMethodType(instantiatedMethodType.getReturnType, paramTypes: _*)
89+
}
6290

63-
val implMethodType = Type.getType(implMethod.getDesc)
64-
val numCaptures = implMethodType.getArgumentTypes.length - instantiatedMethodType.getArgumentTypes.length
65-
val implMethodTypeWithoutCaputres = Type.getMethodType(implMethodType.getReturnType, implMethodType.getArgumentTypes.drop(numCaptures): _*)
66-
implMethodTypeWithoutCaputres == instantiatedMethodType
91+
{
92+
Type.getType(implMethod.getDesc) == expectedImplMethodType // (1)
93+
} && {
94+
isStatic || implMethod.getOwner == indyParamTypes(0).getInternalName // (2)
95+
} && {
96+
def isReference(t: Type) = t.getSort == Type.OBJECT || t.getSort == Type.ARRAY
97+
(samMethodType.getArgumentTypes, instantiatedMethodArgTypes).zipped forall {
98+
case (samArgType, instArgType) =>
99+
samArgType == instArgType || isReference(samArgType) && isReference(instArgType) // (3)
100+
}
101+
}
67102

68103
case _ =>
69104
false
@@ -104,7 +139,11 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) {
104139
// If the variable is not modified within the method, we could avoid introducing yet another
105140
// local. On the other hand, further optimizations (copy propagation, remove unused locals) will
106141
// clean it up.
107-
val localsForCaptures = LocalsList.fromTypes(firstCaptureLocal, capturedTypes)
142+
143+
// Captured variables don't need to be cast when loaded at the callsite (castLoadTypes are None).
144+
// This is checked in `isClosureInstantiation`: the types of the captured variables in the indy
145+
// instruction match exactly the corresponding parameter types in the body method.
146+
val localsForCaptures = LocalsList.fromTypes(firstCaptureLocal, capturedTypes, castLoadTypes = _ => None)
108147
methodNode.maxLocals = firstCaptureLocal + localsForCaptures.size
109148

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

@@ -187,7 +228,21 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) {
187228
// if there are multiple callsites, the same locals are re-used.
188229
val argTypes = indy.bsmArgs(0).asInstanceOf[Type].getArgumentTypes // safe, checked in isClosureInstantiation
189230
val firstArgLocal = methodNode.maxLocals
190-
val argLocals = LocalsList.fromTypes(firstArgLocal, argTypes)
231+
232+
// The comment in `isClosureInstantiation` explains why we have to introduce casts for
233+
// arguments that have different types in samMethodType and instantiatedMethodType.
234+
val castLoadTypes = {
235+
val instantiatedMethodType = indy.bsmArgs(2).asInstanceOf[Type]
236+
(argTypes, instantiatedMethodType.getArgumentTypes).zipped map {
237+
case (samArgType, instantiatedArgType) if samArgType != instantiatedArgType =>
238+
// isClosureInstantiation ensures that the two types are reference types, so we don't
239+
// end up casting primitive values.
240+
Some(instantiatedArgType)
241+
case _ =>
242+
None
243+
}
244+
}
245+
val argLocals = LocalsList.fromTypes(firstArgLocal, argTypes, castLoadTypes)
191246
methodNode.maxLocals = firstArgLocal + argLocals.size
192247

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

311366
def loadOpcode = ILOAD + opcodeOffset

0 commit comments

Comments
 (0)