/* NSC -- new Scala compiler
* Copyright 2005-2014 LAMP/EPFL
* @author Martin Odersky
*/
package scala.tools.nsc
package backend.jvm
package opt
import scala.annotation.tailrec
import scala.tools.asm.Type
import scala.tools.asm.Opcodes._
import scala.tools.asm.tree._
import scala.collection.mutable
import scala.collection.convert.decorateAsScala._
import scala.tools.nsc.backend.jvm.BTypes.InternalName
import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._
class BoxUnbox[BT <: BTypes](val btypes: BT) {
import btypes._
import backendUtils._
/**
* Eliminate box-unbox paris within `method`. Such appear commonly after closure elimination:
*
* def t2 = {
* val f = (b: Byte, i: Int) => i + b // no specialized variant for this function type
* f(1, 2) // invokes the generic `apply`
* }
*
* The closure optimizer re-writes the `apply` call to `anonfun$adapted` method, which takes
* boxed arguments. After inlining this method, we get
*
* def t2 = {
* val a = boxByte(1)
* val b = boxInteger(2)
* val r = boxInteger(anonfun$(unboxByte(a), unboxInt(b)))
* unboxInt(r)
* }
*
* All these box/unbox operations are eliminated here.
*
* Implementation: for every box operation, find all consumers of the boxed value, then all
* producers of these consumers, repeat until reaching a fixpoint. If this results in a set of
* boxing and unboxing operations, the box can be eliminated.
*
* There are two methods for eliminating boxes:
* M1: If there is a single boxing operation, the boxed value(s) are stored into new local
* variable(s) at the allocation site. Accesses to the boxed value are re-written to reads /
* writes of these locals. Advantages:
* - supports mutable boxes (IntRef and friends)
* - supports eliminating unbox operations even if the box object needs to be created
* because it escapes (see E4)
* - works by keeping the unboxed value(s) in locals AND the box in its original form
* - only for immutable boxes: modifications to the escaped box cannot be applied to
* the local variable(s) holding the boxed value(s).
* Restriction:
* - does not work if there are multiple boxing operations (see E1)
*
* M2: If there are multiple boxing operations, the boxing operations are simply eliminated,
* leaving the unboxed value(s) on the stack. Store / load operations that previously
* acted on the box are adapted to handle the boxed type(s). If the box contains multiple
* values (or a size-2 value, which doesn't fit into locals that were used for the box),
* new local slots are used for store / load operations. Restrictions:
* - does not support re-writing writes to (mutable) boxes (see E2)
* - does not support re-writing reads of boxes that also escape (see E3)
*
*
* E1: M1 only works if there's a single boxing operation.
* def e1(b: Boolean) = {
* val i: Integer = box(10) // 10 is stored into a new local, box operation and i removed
* val j: Integer = box(20) // 20 is stored into a new local, box operation adn j removed
* val r = if (b) i else j // loads and stores of the box are eliminated, r no longer exists
* unbox(r) // cannot rewrite: we don't know which local to load
* }
* Note: the example has no write and the box does not escape, so M2 works here.
*
* E2: mutable boxes with multiple boxing operations cannot be eliminated.
* M1: see E1
* M2: cannot replace an `IntRef` on the stack by an `Int` value on the stack, an Int on the
* stack cannot be modified.
*
* def e2(b: Boolean) = {
* val r1 = new IntRef(0)
* val r2 = new IntRef(1)
* val modRef = if (b) r1 else r2
* modRef.elem += 10 // M1: cannot rewrite: which local to write? same as E1.
* (if (b) r1 else r2).elem += 10 // M2: cannot change an Int on the stack
* (r1.elem, r2.elem)
* }
*
*
* E3: escaping boxes with multiple boxing operations cannot be rewritten.
* M1: see E1.
* M2: at *, instead of an Integer, an Int is on the stack, but the escape method expects an
* Integer. We cannot just create a box at this point: if there are multiple escapes (or
* an escape is executed more than once), the difference could be observed (reference
* equality).
*
* def e3(b: Boolean) = {
* val i: Integer = box(1)
* val j: Integer = box(2)
* escape(if (b) i else j) // *
* unbox(if (b) i else j)
* }
*
*
* E4: M1 supports rewriting unbox operations of immutable boxes that escape
* def e4 = {
* val i: Integer = box(10) // 10 is stored into a new local, loaded as argument for the box call
* escape(i) // not changed, still loads the local i holding the box
* unbox(i) // rewritten to a pop (of the box) and a load of the local variable
* }
*
*
* E4 seems to be a bit of a corner case, but it's necessary to unblock box eliminations with
* mutual dependencies. Example:
*
* val ((a, b), c) = ((1, 2), 3)
* a + b + c
*
* generates (after a few cleanups) the following (pseudo-bytecode, ignoring primitive boxing, specialization):
*
* load 1, load 2, new Tuple2 // stack: Tuple2
* load 3 // stack: Tuple2; Int
* val local1 = new Tuple2
* val local2 = local1._1.asInstanceOf[Tuple2]
* val c = local1._2.asInstanceOf[Int]
* if (local2 == null) throw new MatchError(local1)
* val a = local2._1
* val b = local2._2
* a + b + c
*
* In order to eliminate the tuples, we first need to eliminate the outer tuple (stored in local1)
* - single box operation, so we use M1
* - there are three consumers of the outer tuple: `local1._1`, `local1._2` and
* `new MatchError(local1)`. in the last one, the tuple escapes.
* - note that the MatchError creation is dead code: local2 is never null. However, our nullness
* analysis cannot identify this: it does not track nullness through tuple stores and loads.
* - if we re-write the non-escaping consumers of the outer tuple, but keep the tuple allocation
* and the escaping consumer, we get the follwoing:
*
* load 1, load 2
* val newLocal1 = new Tuple2; load newLocal1 // stack: Tuple2
* val newLocal2 = 3; load newLocal2 // stack: Tuple2; Int
* val local1 = new Tuple2
* val local2 = newLocal1
* val c = newLocal2
* if (local2 == null) throw new MatchError(local1)
* val a = local2._1
* val b = local2._2
* a + b + c
*
* At this point, the nullness analysis sees that `local2 == null` is false, dead code elimination
* removes the `throw new MatchError(local1)`. After eliminating the allocation of the outer tuple,
* the inner tuple (stored in newLocal1) can also be eliminated.
*
*
* Special case for tuples wrt specialization: a tuple getter may box or unbox the value stored
* in the tuple: calling `_1` on a `Tuple2$mcII$sp` boxes the primitive Int stored in the tuple.
* Similarly, calling `_1$mcI$sp` on a non-specialized `Tuple2` unboxes the Integer in the tuple.
* When eliminating such getters, we have to introduce appropriate box / unbox calls.
*
*
* TODO: add new calls (box / unbox) to the call graph (not urgent)
* TODO: update the call graph because stack heights change (not urgent).
* this may also affect other optimizations, we ignored the issue so far. check how stack
* heights stored in the call graph are used.
* Note: these tasks are not urgent because the call graph is not currently used during / after
* method-local optimizations, only before to perform inlining and closure rewriting.
*/
def boxUnboxElimination(method: MethodNode, owner: InternalName): Boolean = {
AsmAnalyzer.sizeOKForSourceValue(method) && {
val toInsertBefore = mutable.Map.empty[AbstractInsnNode, List[AbstractInsnNode]]
val toReplace = mutable.Map.empty[AbstractInsnNode, List[AbstractInsnNode]]
val toDelete = mutable.Set.empty[AbstractInsnNode]
val knownHandled = mutable.Set.empty[AbstractInsnNode]
lazy val prodCons = new ProdConsAnalyzer(method, owner)
var nextLocal = method.maxLocals
def getLocal(size: Int) = {
val r = nextLocal
nextLocal += size
r
}
var maxStackGrowth = 0
/** Mehtod M1 for eliminating box-unbox pairs (see doc comment in the beginning of this file) */
def replaceBoxOperationsSingleCreation(creation: BoxCreation, finalCons: Set[BoxConsumer], boxKind: BoxKind, keepBox: Boolean): Unit = {
/**
* If the box is eliminated, all copy operations (loads, stores, others) of the box need to
* be removed. This method returns all copy operations that should be removed.
*
* Returns `None` in case some exotic copy operation is found that cannot be removed
* (DUP2_X1 and friends - these are never emitted by scalac). In this case, the box cannot
* be eliminated.
*/
def copyOpsToEliminate: Option[Set[AbstractInsnNode]] = {
var elidableCopyOps = Set.empty[AbstractInsnNode]
var replaceOK = true
val copyOps = new CopyOpsIterator(Set(creation), finalCons, prodCons)
while (replaceOK && copyOps.hasNext) copyOps.next() match {
case vi: VarInsnNode =>
elidableCopyOps += vi
case copyOp if copyOp.getOpcode == DUP =>
elidableCopyOps += copyOp
case _ =>
replaceOK = false
}
if (replaceOK) Some(elidableCopyOps) else None
}
val canRewrite = keepBox || (copyOpsToEliminate match {
case Some(copyOps) =>
toDelete ++= copyOps
true
case _ => false
})
if (canRewrite) {
val localSlots: Vector[(Int, Type)] = boxKind.boxedTypes.map(tp => (getLocal(tp.getSize), tp))(collection.breakOut)
// store boxed value(s) into localSlots
val storeOps = localSlots.toList reverseMap { case (slot, tp) =>
new VarInsnNode(tp.getOpcode(ISTORE), slot)
}
val storeInitialValues = creation.loadInitialValues match {
case Some(ops) => ops ::: storeOps
case None => storeOps
}
if (keepBox) {
val loadOps: List[VarInsnNode] = localSlots.map({ case (slot, tp) =>
new VarInsnNode(tp.getOpcode(ILOAD), slot)
})(collection.breakOut)
toInsertBefore(creation.valuesConsumer) = storeInitialValues ::: loadOps
} else {
toReplace(creation.valuesConsumer) = storeInitialValues
toDelete ++= creation.allInsns - creation.valuesConsumer
}
// rewrite consumers
finalCons foreach {
case write: StaticSetterOrInstanceWrite =>
assert(!keepBox, s"cannot eliminate box write if the box remains (and escapes): $write")
val (slot, tp) = localSlots(boxKind.extractedValueIndex(write))
val storeOp = new VarInsnNode(tp.getOpcode(ISTORE), slot)
toReplace(write.consumer) = List(storeOp)
case c: EscapingConsumer =>
assert(keepBox, s"found escaping consumer, but box is eliminated: $c")
case extraction =>
val (slot, tp) = localSlots(boxKind.extractedValueIndex(extraction))
val loadOps = new VarInsnNode(tp.getOpcode(ILOAD), slot) :: extraction.postExtractionAdaptationOps(tp)
if (keepBox) toReplace(extraction.consumer) = getPop(1) :: loadOps
else toReplace(extraction.consumer) = loadOps
toDelete ++= extraction.allInsns - extraction.consumer
}
}
}
/** Method M2 for eliminating box-unbox pairs (see doc comment in the beginning of this file) */
def replaceBoxOperationsMultipleCreations(allCreations: Set[BoxCreation], allConsumers: Set[BoxConsumer], boxKind: BoxKind): Unit = {
/**
* If a single-value size-1 box is eliminated, local variables slots holding the box are
* reused to hold the unboxed value. In case there's an entry for that local variable in the
* method's local variables table (debug info), adapt the type.
*
* If there are multiple entries for a local variable that's changing types, then all
* entries for that variable are deleted - it's not obvious how to find the correct entry.
* Note that scalac never re-uses local variable slots for non-overlapping locals. Also note
* that all locals that are newly created during the optimizer don't have an entry either.
*
* Finally, note that variables that become unused are removed later from the table by
* removeUnusedLocalVariableNodes in LocalOpt.
*
* Unlike modifications that affect the method's instructions (which uses toReplace etc),
* we can directly modify the local variable table - it does not affect the frames of the
* ProdCons analysis.
*/
def updateLocalVariableTypes(reTypedLocals: Map[Int, Type]): Unit = {
lazy val localsByIndex = method.localVariables.asScala.groupBy(_.index)
for ((index, tp) <- reTypedLocals) localsByIndex.get(index).map(_.toList) match {
case Some(List(local)) =>
local.desc = tp.getDescriptor
case Some(locals) =>
locals foreach method.localVariables.remove
case _ =>
}
}
/** Remove box creations - leave the boxed value(s) on the stack instead. */
def replaceCreationOps(): Unit = {
for (creation <- allCreations) creation.loadInitialValues match {
case None =>
toDelete ++= creation.allInsns
case Some(ops) =>
toReplace(creation.valuesConsumer) = ops
toDelete ++= (creation.allInsns - creation.valuesConsumer)
}
}
/**
* Replace a value extraction operation. For a single-value box, the extraction operation can
* just be removed. An extraction from a multi-value box is replaced by POP operations for the
* non-used values, and an xSTORE / xLOAD for the extracted value. Example: tuple3._2 becomes
* POP; xSTORE n; POP; xLOAD n.
*/
def replaceExtractionOps(): Unit = {
if (boxKind.boxedTypes.lengthCompare(1) == 0) {
// fast path for single-value boxes
allConsumers.foreach(extraction => extraction.postExtractionAdaptationOps(boxKind.boxedTypes.head) match {
case Nil =>
toDelete ++= extraction.allInsns
case ops =>
toReplace(extraction.consumer) = ops
toDelete ++= extraction.allInsns - extraction.consumer
})
} else {
for (extraction <- allConsumers) {
val valueIndex = boxKind.extractedValueIndex(extraction)
val replacementOps = if (valueIndex == 0) {
val pops = boxKind.boxedTypes.tail.map(t => getPop(t.getSize))
pops ::: extraction.postExtractionAdaptationOps(boxKind.boxedTypes.head)
} else {
var loadOps: List[AbstractInsnNode] = null
val consumeStack = boxKind.boxedTypes.zipWithIndex reverseMap {
case (tp, i) =>
if (i == valueIndex) {
val resultSlot = getLocal(tp.getSize)
loadOps = new VarInsnNode(tp.getOpcode(ILOAD), resultSlot) :: extraction.postExtractionAdaptationOps(tp)
new VarInsnNode(tp.getOpcode(ISTORE), resultSlot)
} else {
getPop(tp.getSize)
}
}
consumeStack ::: loadOps
}
toReplace(extraction.consumer) = replacementOps
toDelete ++= extraction.allInsns - extraction.consumer
}
}
}
checkCopyOpReplacements(allCreations, allConsumers, boxKind.boxedTypes, nextLocal, prodCons) match {
case Some((replacements, nextCopyOpLocal, reTypedLocals)) =>
toReplace ++= replacements
updateLocalVariableTypes(reTypedLocals)
nextLocal = nextCopyOpLocal
replaceCreationOps()
replaceExtractionOps()
// Conservative (safe) value for stack growth. In every frame that initially has a multi-value
// box on the stack, the stack now contains all of the individual values. So for every eliminated
// box, the maxStack may be up to N-1 slots larger.
maxStackGrowth += boxKind.boxedTypes.length - 1
case None =>
}
}
val it = method.instructions.iterator
while (it.hasNext) {
val insn = it.next()
if (!knownHandled(insn)) BoxKind.valueCreationKind(insn, prodCons) match {
case Some((boxCreation, boxKind)) =>
allCreationsConsumers(boxCreation, boxKind, prodCons) match {
case Some((allCreations, allConsumers)) =>
val (escapingConsumers, boxConsumers) = allConsumers.partition(_.isEscaping)
if (boxConsumers.nonEmpty) {
for (c <- allCreations) knownHandled ++= c.allInsns
for (e <- allConsumers) knownHandled ++= e.allInsns
val hasEscaping = escapingConsumers.nonEmpty
val hasWrite = allConsumers.exists(_.isWrite)
if (!hasEscaping && !hasWrite) {
// M2 -- see doc comment in the beginning of this file
// If both M1 and M2 can be applied, we prefer M2 because it doesn't introduce new locals.
replaceBoxOperationsMultipleCreations(allCreations, allConsumers, boxKind)
} else if (allCreations.size == 1 && (!hasEscaping || !boxKind.isMutable)) {
// M1 -- see doc comment in the beginning of this file
replaceBoxOperationsSingleCreation(allCreations.head, allConsumers, boxKind, keepBox = hasEscaping)
}
}
case None =>
}
case None =>
}
}
def removeFromCallGraph(insn: AbstractInsnNode): Unit = insn match {
case mi: MethodInsnNode => callGraph.removeCallsite(mi, method)
case _ =>
}
for ((location, ops) <- toInsertBefore; op <- ops)
method.instructions.insertBefore(location, op)
for ((oldOp, newOps) <- toReplace) {
for (newOp <- newOps) method.instructions.insertBefore(oldOp, newOp)
method.instructions.remove(oldOp)
removeFromCallGraph(oldOp)
}
for (op <- toDelete) {
method.instructions.remove(op)
removeFromCallGraph(op)
}
method.maxLocals = nextLocal
method.maxStack += maxStackGrowth
toInsertBefore.nonEmpty || toReplace.nonEmpty || toDelete.nonEmpty
}
}
/**
* Given a box creations operation
* - find all ultimate consumers for the produced value. then:
* - for all consumed values, find all producer operations. check that all are box creations
* - recurse until reaching a fixpoint
*
* Returns a set of box creations and a set of box consumers. Note that the box consumers may
* contain [[EscapingConsumer]]s, even if there are multiple box creation operations. The callee
* will handle this case (and not attempt to eliminate the box).
*/
def allCreationsConsumers(initialCreation: BoxCreation, boxKind: BoxKind, prodCons: ProdConsAnalyzer): Option[(Set[BoxCreation], Set[BoxConsumer])] = {
var creations = Set(initialCreation)
var consumers = Set.empty[BoxConsumer]
def addCreations(boxConsumer: BoxConsumer): Boolean = {
val newProds = boxConsumer.boxProducers(prodCons).filterNot(prod => creations.exists(_.producer == prod))
newProds.forall(prod => boxKind.checkBoxCreation(prod, prodCons) match {
case Some(boxCreation) =>
creations += boxCreation
addBoxConsumers(boxCreation)
case _ => false
})
}
def addBoxConsumers(creation: BoxCreation): Boolean = {
val newCons = creation.boxConsumers(prodCons, ultimate = true).filterNot(cons => consumers.exists(_.consumer == cons))
newCons.forall(cons => boxKind.checkBoxConsumer(cons, prodCons) match {
case Some(boxConsumer) =>
consumers += boxConsumer
addCreations(boxConsumer)
case _ =>
creations.size <= 1 && {
// If there's a single box creation, the box operations can still be rewritten
consumers += EscapingConsumer(cons)
true
}
})
}
if (addBoxConsumers(initialCreation)) Some((creations, consumers))
else None
}
/**
* Takes two sets `initialProds` and `finalCons` such that all boxes produced by the first set
* are only consumed by an operation in the second set.
*
* Returns a map that replaces copy operations (ALOAD / ASTORE) between the producers and
* consumers with corresponding copy operations for the values stored in the box. The returned
* `Int` value returns the next free local variable slot.
*
* Examples:
* - for an Integer box, an ASTORE x is simply replaced by ISTORE x
* - for a pair of two references, an ASTORE x is replaced by `ASTORE x1; ASTORE x2` where x1
* and x2 are fresh locals
*
* Not all copy operations can be supported: DUP only works for single-value boxes, the more
* exotic copy operations (DUP2_X2) are not supported (note that Scalac never emits them). If a
* copy operation cannot be replaced, this method returns `None`.
*/
def checkCopyOpReplacements(initialProds: Set[BoxCreation], finalCons: Set[BoxConsumer], valueTypes: List[Type], nextLocal: Int, prodCons: ProdConsAnalyzer): Option[(Map[AbstractInsnNode, List[AbstractInsnNode]], Int, Map[Int, Type])] = {
var replacements = Map.empty[AbstractInsnNode, List[AbstractInsnNode]]
var reTypedLocals = Map.empty[Int, Type]
var nextCopyOpLocal = nextLocal
val newLocalsMap: mutable.LongMap[List[(Type, Int)]] = mutable.LongMap.empty
def newLocals(index: Int) = newLocalsMap.getOrElseUpdate(index, valueTypes match {
case List(t) if t.getSize == 1 =>
reTypedLocals += index -> t
List((t, index))
case _ => valueTypes.map(t => {
val newIndex = nextCopyOpLocal
nextCopyOpLocal += t.getSize
(t, newIndex)
})
})
var replaceOK = true
val copyOps = new CopyOpsIterator(initialProds, finalCons, prodCons)
while (replaceOK && copyOps.hasNext) copyOps.next() match {
case vi: VarInsnNode =>
val isLoad = vi.getOpcode == ALOAD
val typedVarOp = (tp: (Type, Int)) => {
val opc = tp._1.getOpcode(if (isLoad) ILOAD else ISTORE)
new VarInsnNode(opc, tp._2)
}
val locs = newLocals(vi.`var`)
replacements += vi -> (if (isLoad) locs.map(typedVarOp) else locs.reverseMap(typedVarOp))
case copyOp =>
if (copyOp.getOpcode == DUP && valueTypes.lengthCompare(1) == 0) {
if (valueTypes.head.getSize == 2)
replacements += copyOp -> List(new InsnNode(DUP2))
} else {
replaceOK = false
}
}
if (replaceOK) Some((replacements, nextCopyOpLocal, reTypedLocals)) else None
}
/**
* For a set of box creation operations and a corresponding set of box consumer operations,
* this iterator returns all copy operations (load, store, dup) that are in between.
*/
class CopyOpsIterator(initialCreations: Set[BoxCreation], finalCons: Set[BoxConsumer], prodCons: ProdConsAnalyzer) extends Iterator[AbstractInsnNode] {
private var queue = mutable.Queue.empty[AbstractInsnNode] ++ initialCreations.iterator.flatMap(_.boxConsumers(prodCons, ultimate = false))
// a single copy operation can consume multiple producers: val a = if (b) box(1) else box(2).
// the `ASTORE a` has two producers (the two box operations). we need to handle it only once.
private val visited = mutable.Set.empty[AbstractInsnNode]
private val boxConsumingOps = finalCons.map(_.consumer)
@tailrec private def advanceToNextCopyOp(): Unit = {
if (queue.nonEmpty) {
val h = queue.front
if (visited(h) || boxConsumingOps(h)) {
queue.dequeue()
advanceToNextCopyOp()
}
}
}
def hasNext: Boolean = {
advanceToNextCopyOp()
queue.nonEmpty
}
def next(): AbstractInsnNode = {
advanceToNextCopyOp()
val r = queue.dequeue()
visited += r
queue ++= prodCons.consumersOfOutputsFrom(r)
r
}
}
trait BoxKind {
def checkBoxCreation(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxCreation]
def checkBoxConsumer(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxConsumer]
def boxedTypes: List[Type]
def extractedValueIndex(extraction: BoxConsumer): Int
def isMutable: Boolean
}
object BoxKind {
def valueCreationKind(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[(BoxCreation, BoxKind)] = {
PrimitiveBox.checkPrimitiveBox(insn, None, prodCons) orElse
Ref.checkRefCreation(insn, None, prodCons) orElse
Tuple.checkTupleCreation(insn, None, prodCons)
}
/**
* Check if `newOp` is part of a standard object construction pattern in which:
*
* NEW T
* DUP
* [load constructor args]
* INVOKESPECIAL T.init
*
* The method ensures that the entire construction pattern is closed in itself, without any
* branches going in or out. This is checked by looking at producers / consumers:
* - `DUP` is the only consumer of `NEW`, and vice versa
* - `DUP` the only producer for the receiver of the constructor call
* - The set of consumers of `DUP` without the constructor call is the same as
* the set of consumers of the value on the stack top after the constructor call
*/
def checkInstanceCreation(newOp: TypeInsnNode, prodCons: ProdConsAnalyzer): Option[(InsnNode, MethodInsnNode)] = {
val newCons = prodCons.consumersOfOutputsFrom(newOp)
if (newCons.size == 1 && newCons.head.getOpcode == DUP) {
val dupOp = newCons.head.asInstanceOf[InsnNode]
if (prodCons.producersForInputsOf(dupOp) == Set(newOp)) {
val dupCons = prodCons.consumersOfOutputsFrom(dupOp)
val initCalls = dupCons collect {
case mi: MethodInsnNode if mi.name == GenBCode.INSTANCE_CONSTRUCTOR_NAME && mi.owner == newOp.desc => mi
}
if (initCalls.size == 1) {
val initCall = initCalls.head
val numArgs = Type.getArgumentTypes(initCall.desc).length
val receiverProds = prodCons.producersForValueAt(initCall, prodCons.frameAt(initCall).stackTop - numArgs)
if (receiverProds == Set(dupOp)) {
val dupConsWithoutInit = dupCons - initCall
val afterInit = initCall.getNext
val stackTopAfterInit = prodCons.frameAt(afterInit).stackTop
val initializedInstanceCons = prodCons.consumersOfValueAt(afterInit, stackTopAfterInit)
if (initializedInstanceCons == dupConsWithoutInit && prodCons.producersForValueAt(afterInit, stackTopAfterInit) == Set(dupOp)) {
return Some((dupOp, initCall))
}
}
}
}
}
None
}
/**
* If `mi` is an invocation of a method on Predef, check if the receiver is a GETSTATIC of
* Predef.MODULE$ and return it.
*/
def checkReceiverPredefLoad(mi: MethodInsnNode, prodCons: ProdConsAnalyzer): Option[AbstractInsnNode] = {
val numArgs = Type.getArgumentTypes(mi.desc).length
val receiverProds = prodCons.producersForValueAt(mi, prodCons.frameAt(mi).stackTop - numArgs)
if (receiverProds.size == 1) {
val prod = receiverProds.head
if (isPredefLoad(prod) && prodCons.consumersOfOutputsFrom(prod) == Set(mi)) return Some(prod)
}
None
}
}
case class PrimitiveBox(boxedType: Type, boxClass: InternalName) extends BoxKind {
import PrimitiveBox._
def checkBoxCreation(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxCreation] = checkPrimitiveBox(insn, Some(this), prodCons).map(_._1)
def checkBoxConsumer(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxConsumer] = checkPrimitiveUnbox(insn, this, prodCons)
def boxedTypes: List[Type] = List(boxedType)
def extractedValueIndex(extraction: BoxConsumer): Int = 0
def isMutable = false
}
object PrimitiveBox {
private def boxedType(mi: MethodInsnNode) = Type.getArgumentTypes(mi.desc)(0)
private def boxClass(mi: MethodInsnNode) = {
if (mi.name == GenBCode.INSTANCE_CONSTRUCTOR_NAME) mi.owner
else Type.getReturnType(mi.desc).getInternalName
}
def checkPrimitiveBox(insn: AbstractInsnNode, expectedKind: Option[PrimitiveBox], prodCons: ProdConsAnalyzer): Option[(BoxCreation, PrimitiveBox)] = {
// mi is either a box factory or a box constructor invocation
def checkKind(mi: MethodInsnNode) = expectedKind match {
case Some(kind) => if (kind.boxClass == boxClass(mi)) expectedKind else None
case None => Some(PrimitiveBox(boxedType(mi), boxClass(mi)))
}
insn match {
case mi: MethodInsnNode =>
if (isScalaBox(mi) || isJavaBox(mi)) checkKind(mi).map((StaticFactory(mi, loadInitialValues = None), _))
else if (isPredefAutoBox(mi))
for (predefLoad <- BoxKind.checkReceiverPredefLoad(mi, prodCons); kind <- checkKind(mi))
yield (ModuleFactory(predefLoad, mi), kind)
else None
case ti: TypeInsnNode if ti.getOpcode == NEW =>
for ((dupOp, initCall) <- BoxKind.checkInstanceCreation(ti, prodCons) if isPrimitiveBoxConstructor(initCall); kind <- checkKind(initCall))
yield (InstanceCreation(ti, dupOp, initCall), kind)
case _ => None
}
}
def checkPrimitiveUnbox(insn: AbstractInsnNode, kind: PrimitiveBox, prodCons: ProdConsAnalyzer): Option[BoxConsumer] = {
def typeOK(mi: MethodInsnNode) = kind.boxedType == Type.getReturnType(mi.desc)
insn match {
case mi: MethodInsnNode =>
if ((isScalaUnbox(mi) || isJavaUnbox(mi)) && typeOK(mi)) Some(StaticGetterOrInstanceRead(mi))
else if (isPredefAutoUnbox(mi) && typeOK(mi)) BoxKind.checkReceiverPredefLoad(mi, prodCons).map(ModuleGetter(_, mi))
else None
case _ => None
}
}
}
case class Ref(boxedType: Type, refClass: InternalName) extends BoxKind {
import Ref._
def checkBoxCreation(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxCreation] = checkRefCreation(insn, Some(this), prodCons).map(_._1)
def checkBoxConsumer(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxConsumer] = checkRefConsumer(insn, this, prodCons)
def boxedTypes: List[Type] = List(boxedType)
def extractedValueIndex(extraction: BoxConsumer): Int = 0
def isMutable = true
}
object Ref {
private def boxedType(mi: MethodInsnNode): Type = runtimeRefClassBoxedType(mi.owner)
private def refClass(mi: MethodInsnNode): InternalName = mi.owner
private def loadZeroValue(refZeroCall: MethodInsnNode): List[AbstractInsnNode] = List(loadZeroForTypeSort(runtimeRefClassBoxedType(refZeroCall.owner).getSort))
def checkRefCreation(insn: AbstractInsnNode, expectedKind: Option[Ref], prodCons: ProdConsAnalyzer): Option[(BoxCreation, Ref)] = {
def checkKind(mi: MethodInsnNode): Option[Ref] = expectedKind match {
case Some(kind) => if (kind.refClass == refClass(mi)) expectedKind else None
case None => Some(Ref(boxedType(mi), refClass(mi)))
}
insn match {
case mi: MethodInsnNode =>
if (isRefCreate(mi)) checkKind(mi).map((StaticFactory(mi, loadInitialValues = None), _))
else if (isRefZero(mi)) checkKind(mi).map((StaticFactory(mi, loadInitialValues = Some(loadZeroValue(mi))), _))
else None
case ti: TypeInsnNode if ti.getOpcode == NEW =>
for ((dupOp, initCall) <- BoxKind.checkInstanceCreation(ti, prodCons) if isRuntimeRefConstructor(initCall); kind <- checkKind(initCall))
yield (InstanceCreation(ti, dupOp, initCall), kind)
case _ => None
}
}
def checkRefConsumer(insn: AbstractInsnNode, kind: Ref, prodCons: ProdConsAnalyzer): Option[BoxConsumer] = insn match {
case fi: FieldInsnNode if fi.owner == kind.refClass && fi.name == "elem" =>
if (fi.getOpcode == GETFIELD) Some(StaticGetterOrInstanceRead(fi))
else if (fi.getOpcode == PUTFIELD) Some(StaticSetterOrInstanceWrite(fi))
else None
case _ => None
}
}
case class Tuple(boxedTypes: List[Type], tupleClass: InternalName) extends BoxKind {
import Tuple._
def checkBoxCreation(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxCreation] = checkTupleCreation(insn, Some(this), prodCons).map(_._1)
def checkBoxConsumer(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxConsumer] = checkTupleExtraction(insn, this, prodCons)
def extractedValueIndex(extraction: BoxConsumer): Int = extraction match {
case StaticGetterOrInstanceRead(mi: MethodInsnNode) => tupleGetterIndex(mi.name)
case PrimitiveBoxingGetter(mi) => tupleGetterIndex(mi.name)
case PrimitiveUnboxingGetter(mi, _) => tupleGetterIndex(mi.name)
case _ => throw new AssertionError(s"Expected tuple getter, found $extraction")
}
def isMutable = false
}
object Tuple {
private def boxedTypes(mi: MethodInsnNode): List[Type] = Type.getArgumentTypes(mi.desc).toList
private def tupleClass(mi: MethodInsnNode): InternalName = mi.owner
def checkTupleCreation(insn: AbstractInsnNode, expectedKind: Option[Tuple], prodCons: ProdConsAnalyzer): Option[(BoxCreation, Tuple)] = {
def checkKind(mi: MethodInsnNode): Option[Tuple] = expectedKind match {
case Some(kind) => if (kind.tupleClass == tupleClass(mi)) expectedKind else None
case None => Some(Tuple(boxedTypes(mi), tupleClass(mi)))
}
insn match {
// no need to check for TupleN.apply: the compiler transforms case companion apply calls to constructor invocations
case ti: TypeInsnNode if ti.getOpcode == NEW =>
for ((dupOp, initCall) <- BoxKind.checkInstanceCreation(ti, prodCons) if isTupleConstructor(initCall); kind <- checkKind(initCall))
yield (InstanceCreation(ti, dupOp, initCall), kind)
case _ => None
}
}
private val specializedTupleClassR = "scala/Tuple[12]\\$mc[IJDCZ]{1,2}\\$sp".r
private def isSpecializedTupleClass(tupleClass: InternalName) = specializedTupleClassR.pattern.matcher(tupleClass).matches
private val specializedTupleGetterR = "_[12]\\$mc[IJDCZ]\\$sp".r
private def isSpecializedTupleGetter(mi: MethodInsnNode) = specializedTupleGetterR.pattern.matcher(mi.name)matches
private val tupleGetterR = "_\\d\\d?".r
private def isTupleGetter(mi: MethodInsnNode) = tupleGetterR.pattern.matcher(mi.name).matches
def checkTupleExtraction(insn: AbstractInsnNode, kind: Tuple, prodCons: ProdConsAnalyzer): Option[BoxConsumer] = {
val expectedTupleClass = kind.tupleClass
insn match {
case mi: MethodInsnNode =>
val tupleClass = mi.owner
if (isSpecializedTupleClass(expectedTupleClass)) {
val typeOK = tupleClass == expectedTupleClass || tupleClass == expectedTupleClass.substring(0, expectedTupleClass.indexOf('$'))
if (typeOK) {
if (isSpecializedTupleGetter(mi)) return Some(StaticGetterOrInstanceRead(mi))
else if (isTupleGetter(mi)) return Some(PrimitiveBoxingGetter(mi))
}
} else if (expectedTupleClass == tupleClass) {
if (isSpecializedTupleGetter(mi)) return Some(PrimitiveUnboxingGetter(mi, Type.getReturnType(mi.desc)))
else if (isTupleGetter(mi)) return Some(StaticGetterOrInstanceRead(mi))
}
case _ =>
}
None
}
private val getterIndexPattern = "_(\\d{1,2}).*".r
def tupleGetterIndex(getterName: String) = getterName match { case getterIndexPattern(i) => i.toInt - 1 }
}
// TODO: add more
// case class ValueClass(valueClass: Type, valueType: Type) extends BoxKind
sealed trait BoxCreation {
// to support box creation operations that don't consume an initial value from the stack, e.g., IntRef.zero
val loadInitialValues: Option[List[AbstractInsnNode]]
/**
* The instruction that produces the box value; for instance creations, the `NEW` operation.
*/
def producer: AbstractInsnNode
/**
* The instruction that consumes the boxed values; for instance creations, the `init` call.
*/
def valuesConsumer: MethodInsnNode = this match {
case StaticFactory(call, _) => call
case ModuleFactory(_, call) => call
case InstanceCreation(_, _, initCall) => initCall
}
def allInsns: Set[AbstractInsnNode] = this match {
case StaticFactory(c, _) => Set(c)
case ModuleFactory(m, c) => Set(m, c)
case InstanceCreation(n, d, i) => Set(n, d, i)
}
/**
* The consumers of the box produced by this box creation. If `ultimate` is true, then the
* final consumers are returned (e.g., an unbox operation), otherwise direct consumers (e.g.,
* a store operation).
*/
def boxConsumers(prodCons: ProdConsAnalyzer, ultimate: Boolean): Set[AbstractInsnNode] = {
val startInsn = this match {
// for the non-transitive case (ultimate == false), it's important to start at the `dupOp`,
// not the `newOp` - look at the BoxCreation as a black box, get its consumers.
case InstanceCreation(_, dupOp, _) => dupOp
case _ => producer
}
val cons = if (ultimate) prodCons.ultimateConsumersOfOutputsFrom(startInsn) else prodCons.consumersOfOutputsFrom(startInsn)
this match {
case InstanceCreation(_, _, initCall) => cons - initCall
case _ => cons
}
}
}
case class StaticFactory(producer: MethodInsnNode, loadInitialValues: Option[List[AbstractInsnNode]]) extends BoxCreation
case class ModuleFactory(moduleLoad: AbstractInsnNode, producer: MethodInsnNode) extends BoxCreation {
val loadInitialValues: Option[List[AbstractInsnNode]] = None
}
case class InstanceCreation(newOp: TypeInsnNode, dupOp: InsnNode, initCall: MethodInsnNode) extends BoxCreation {
def producer = newOp
val loadInitialValues: Option[List[AbstractInsnNode]] = None
}
sealed trait BoxConsumer {
val consumer: AbstractInsnNode
def allInsns: Set[AbstractInsnNode] = this match {
case ModuleGetter(m, c) => Set(m, c)
case _ => Set(consumer)
}
/**
* The initial producers of the box value consumed by this box consumer
*/
def boxProducers(prodCons: ProdConsAnalyzer): Set[AbstractInsnNode] = {
val stackTop = prodCons.frameAt(consumer).stackTop
val slot = if (isWrite) stackTop - 1 else stackTop
prodCons.initialProducersForValueAt(consumer, slot)
}
def isEscaping = this match {
case _: EscapingConsumer => true
case _ => false
}
def isWrite = this match {
case _: StaticSetterOrInstanceWrite => true
case _ => false
}
/**
* If this box consumer extracts a boxed value and applies a conversion, this method returns
* equivalent conversion operations. For example, invoking `_1$mcI$sp` on a non-specialized
* `Tuple2` extracts the Integer value and unboxes it.
*/
def postExtractionAdaptationOps(typeOfExtractedValue: Type): List[AbstractInsnNode] = this match {
case PrimitiveBoxingGetter(_) => List(getScalaBox(typeOfExtractedValue))
case PrimitiveUnboxingGetter(_, unboxedPrimitive) => List(getScalaUnbox(unboxedPrimitive))
case _ => Nil
}
}
/** Static extractor (BoxesRunTime.unboxToInt) or GETFIELD or getter invocation */
case class StaticGetterOrInstanceRead(consumer: AbstractInsnNode) extends BoxConsumer
/** A getter that boxes the returned value, e.g., `Tuple2$mcII$sp._1` */
case class PrimitiveBoxingGetter(consumer: MethodInsnNode) extends BoxConsumer
/** A getter that unboxes the returned value, e.g., `Tuple2._1$mcI$sp` */
case class PrimitiveUnboxingGetter(consumer: MethodInsnNode, unboxedPrimitive: Type) extends BoxConsumer
/** An extractor method in a Scala module, e.g., `Predef.Integer2int` */
case class ModuleGetter(moduleLoad: AbstractInsnNode, consumer: MethodInsnNode) extends BoxConsumer
/** PUTFIELD or setter invocation */
case class StaticSetterOrInstanceWrite(consumer: AbstractInsnNode) extends BoxConsumer
/** An unknown box consumer */
case class EscapingConsumer(consumer: AbstractInsnNode) extends BoxConsumer
}