summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukas Rytz <lukas.rytz@gmail.com>2015-06-02 15:16:47 +0200
committerLukas Rytz <lukas.rytz@gmail.com>2015-06-22 13:02:41 +0200
commitd159f1420e51fb17e38c0de3690c5d27c870e9ac (patch)
tree8507d03e688033ca2d45b477dd8f18cf6b3b5bf3
parent7f1336a2ffaf573dd71192932e7b599213e5a1d0 (diff)
downloadscala-d159f1420e51fb17e38c0de3690c5d27c870e9ac.tar.gz
scala-d159f1420e51fb17e38c0de3690c5d27c870e9ac.tar.bz2
scala-d159f1420e51fb17e38c0de3690c5d27c870e9ac.zip
Producers / Consumers Analysis
ASM has a built-in `SourceValue` analysis which computes for each value a set of instructions that may possibly have constructed it. The ProdConsAnalyzer class provides additional queries over the result of the SourceValue analysis: - consumers of values - tracking producers / consumers through copying operations (load, store, etc) A fix to (and therefore a new version of) ASM was required. See here: https://github.com/scala/scala-asm/commit/94106a5472
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/analysis/InstructionStackEffect.scala4
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzer.scala450
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala12
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala14
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzerTest.scala11
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzerTest.scala249
-rw-r--r--versions.properties2
7 files changed, 724 insertions, 18 deletions
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/InstructionStackEffect.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/InstructionStackEffect.scala
index 98e93c125b..8d8ea839e6 100644
--- a/src/compiler/scala/tools/nsc/backend/jvm/analysis/InstructionStackEffect.scala
+++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/InstructionStackEffect.scala
@@ -94,7 +94,7 @@ object InstructionStackEffect {
val isSize2 = peekStack(0).getSize == 2
if (isSize2) t(1, 0) else t(2, 0)
- case DUP => t(0, 1)
+ case DUP => t(1, 2)
case DUP_X1 => t(2, 3)
@@ -104,7 +104,7 @@ object InstructionStackEffect {
case DUP2 =>
val isSize2 = peekStack(0).getSize == 2
- if (isSize2) t(0, 1) else t(0, 2)
+ if (isSize2) t(1, 2) else t(2, 4)
case DUP2_X1 =>
val isSize2 = peekStack(0).getSize == 2
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzer.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzer.scala
new file mode 100644
index 0000000000..40f91cbed4
--- /dev/null
+++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzer.scala
@@ -0,0 +1,450 @@
+/* NSC -- new Scala compiler
+ * Copyright 2005-2015 LAMP/EPFL
+ * @author Martin Odersky
+ */
+
+package scala.tools.nsc
+package backend.jvm
+package analysis
+
+import java.util
+
+import scala.annotation.switch
+import scala.collection.mutable
+import scala.tools.asm.{Type, MethodVisitor}
+import scala.tools.asm.Opcodes._
+import scala.tools.asm.tree._
+import scala.tools.asm.tree.analysis._
+import scala.tools.nsc.backend.jvm.BTypes.InternalName
+
+import opt.BytecodeUtils._
+
+import scala.collection.convert.decorateAsScala._
+
+/**
+ * This class provides additional queries over ASM's built-in `SourceValue` analysis.
+ *
+ * The analysis computes for each value in a frame a set of source instructions, which are the
+ * potential producers. Most instructions produce either nothing or a stack value. For example,
+ * a `LOAD` instruction is the producer of the value pushed onto the stack. The exception are
+ * `STORE` instructions, which produce a new value for a local variable slot, so they are used
+ * as producers for the value they stored.
+ *
+ * Note that pseudo-instructions are used as initial producers for parameters and local variables.
+ * See the documentation on class InitialProducer.
+ *
+ * This class implements the following queries over the data computed by the SourceValue analysis:
+ *
+ * - producersForValueAt(insn, slot)
+ * - consumersOfValueAt(insn, slot)
+ *
+ * - producersForInputsOf(insn)
+ * - consumersOfOutputsFrom(insn)
+ *
+ * - initialProducersForValueAt(insn, slot)
+ * - ultimateConsumersOfValueAt(insn, slot)
+ *
+ * - initialProducersForInputsOf(insn)
+ * - ultimateConsumersOfOutputsFrom(insn)
+ *
+ * The following operations are considered as copying operations:
+ * - xLOAD, xSTORE
+ * - DUP, DUP2, DUP_X1, DUP_X2, DUP2_X1, DUP2_X2
+ * - SWAP
+ * - CHECKCAST
+ *
+ * If ever needed, we could introduce a mode where primitive conversions (l2i) are considered as
+ * copying operations.
+ */
+class ProdConsAnalyzer(methodNode: MethodNode, classInternalName: InternalName) {
+ val analyzer = new Analyzer(new InitialProducerSourceInterpreter)
+ analyzer.analyze(classInternalName, methodNode)
+
+ def frameAt(insn: AbstractInsnNode) = analyzer.frameAt(insn, methodNode)
+
+ /**
+ * Returns the potential producer instructions of a (local or stack) value in the frame of `insn`.
+ * This method simply returns the producer information computed by the SourceValue analysis.
+ */
+ def producersForValueAt(insn: AbstractInsnNode, slot: Int): Set[AbstractInsnNode] = {
+ frameAt(insn).getValue(slot).insns.asScala.toSet
+ }
+
+ /**
+ * Returns the potential consumer instructions of a (local or stack) value in the frame of `insn`.
+ * This is the counterpart of `producersForValueAt`.
+ */
+ def consumersOfValueAt(insn: AbstractInsnNode, slot: Int): Set[AbstractInsnNode] = {
+ producersForValueAt(insn, slot).flatMap(prod => {
+ val outputNumber = outputValueSlots(prod).indexOf(slot)
+ _consumersOfOutputsFrom.get(prod).map(v => {
+ v(outputNumber)
+ }).getOrElse(Set.empty)
+ })
+ }
+
+ /**
+ * Returns the potential producer instructions of any of the values consumed by `insn`.
+ */
+ def producersForInputsOf(insn: AbstractInsnNode): Set[AbstractInsnNode] = {
+ inputValues(insn).iterator.flatMap(v => v.insns.asScala).toSet
+ }
+
+ def consumersOfOutputsFrom(insn: AbstractInsnNode): Set[AbstractInsnNode] =
+ _consumersOfOutputsFrom.get(insn).map(v => v.indices.flatMap(v.apply)(collection.breakOut): Set[AbstractInsnNode]).getOrElse(Set.empty)
+
+ /**
+ * Returns the potential initial producer instructions of a value in the frame of `insn`.
+ *
+ * Unlike `producersForValueAt`, producers are tracked through copying instructions such as STORE
+ * and LOAD. If the producer of the value is a LOAD, then the producers of the stored value(s) are
+ * returned instead.
+ */
+ def initialProducersForValueAt(insn: AbstractInsnNode, slot: Int): Set[AbstractInsnNode] = {
+ def initialProducers(insn: AbstractInsnNode, producedSlot: Int): Set[AbstractInsnNode] = {
+ if (isCopyOperation(insn)) {
+ _initialProducersCache.getOrElseUpdate((insn, producedSlot), {
+ val (sourceValue, sourceValueSlot) = copyOperationSourceValue(insn, producedSlot)
+ sourceValue.insns.iterator.asScala.flatMap(initialProducers(_, sourceValueSlot)).toSet
+ })
+ } else {
+ Set(insn)
+ }
+ }
+ producersForValueAt(insn, slot).flatMap(initialProducers(_, slot))
+ }
+
+ /**
+ * Returns the potential ultimate consumers of a value in the frame of `insn`. Consumers are
+ * tracked through copying operations such as SOTRE and LOAD.
+ */
+ def ultimateConsumersOfValueAt(insn: AbstractInsnNode, slot: Int): Set[AbstractInsnNode] = {
+ def ultimateConsumers(insn: AbstractInsnNode, consumedSlot: Int): Set[AbstractInsnNode] = {
+ if (isCopyOperation(insn)) {
+ _ultimateConsumersCache.getOrElseUpdate((insn, consumedSlot), {
+ for {
+ producedSlot <- copyOperationProducedValueSlots(insn, consumedSlot)
+ consumer <- consumersOfValueAt(insn.getNext, producedSlot)
+ ultimateConsumer <- ultimateConsumers(consumer, producedSlot)
+ } yield ultimateConsumer
+ })
+ } else {
+ Set(insn)
+ }
+ }
+ consumersOfValueAt(insn, slot).flatMap(ultimateConsumers(_, slot))
+ }
+
+ def initialProducersForInputsOf(insn: AbstractInsnNode): Set[AbstractInsnNode] = {
+ inputValueSlots(insn).flatMap(slot => initialProducersForValueAt(insn, slot)).toSet
+ }
+
+ def ultimateConsumersOfOutputsFrom(insn: AbstractInsnNode): Set[AbstractInsnNode] = {
+ lazy val next = insn.getNext
+ outputValueSlots(insn).flatMap(slot => ultimateConsumersOfValueAt(next, slot)).toSet
+ }
+
+ private def isCopyOperation(insn: AbstractInsnNode): Boolean = {
+ isVarInstruction(insn) || {
+ (insn.getOpcode: @switch) match {
+ case DUP | DUP_X1 | DUP_X2 | DUP2 | DUP2_X1 | DUP2_X2 | SWAP | CHECKCAST => true
+ case _ => false
+ }
+ }
+ }
+
+ /**
+ * Returns the value and its frame slot that `copyOp` copies into `producedSlot`.
+ *
+ * Example:
+ * - copyOp = DUP_X1, assume it produces slots 2,3,4
+ * - producedSlot = 3
+ * - the result is the value at slot 2 in the frame of `copyOp`
+ */
+ private def copyOperationSourceValue(copyOp: AbstractInsnNode, producedSlot: Int): (SourceValue, Int) = {
+ val frame = frameAt(copyOp)
+
+ // Index of the produced value. Example: DUP_X1 produces 3 values, so producedIndex is 0, 1 or 2,
+ // where 0 corresponds to the lowest value on the stack.
+ def producedIndex(numConsumed: Int) = {
+ val numUsedSlotsBeforeCopy = frame.stackTop + 1
+ producedSlot - (numUsedSlotsBeforeCopy - numConsumed)
+ }
+
+ def stackValue(n: Int) = (frame.peekStack(n), frame.stackTop - n)
+
+ def dupX1Case = (producedIndex(2): @switch) match {
+ case 0 | 2 => stackValue(0)
+ case 1 => stackValue(1)
+ }
+
+ // Form 1 of dup_x2
+ def dupX2Case = (producedIndex(3): @switch) match {
+ case 0 | 3 => stackValue(0)
+ case 1 => stackValue(2)
+ case 2 => stackValue(1)
+ }
+
+ // Form 1 of dup2_x1
+ def dup2X1Case = (producedIndex(3): @switch) match {
+ case 0 | 3 => stackValue(1)
+ case 1 | 4 => stackValue(0)
+ case 2 => stackValue(2)
+ }
+
+ if (isLoad(copyOp)) {
+ val slot = copyOp.asInstanceOf[VarInsnNode].`var`
+ (frame.getLocal(slot), slot)
+ } else if (isStore(copyOp)) {
+ stackValue(0)
+ } else (copyOp.getOpcode: @switch) match {
+ case DUP =>
+ stackValue(0) // the current stack top is the source of both produced values
+
+ case DUP_X1 =>
+ dupX1Case
+
+ case DUP_X2 =>
+ if (frame.peekStack(1).getSize == 2) dupX1Case
+ else dupX2Case
+
+ case DUP2 =>
+ if (frame.peekStack(0).getSize == 2) stackValue(0)
+ else {
+ (producedIndex(2): @switch) match {
+ case 0 | 2 => stackValue(1)
+ case 1 | 3 => stackValue(0)
+ }
+ }
+
+ case DUP2_X1 =>
+ if (frame.peekStack(0).getSize == 2) dupX1Case
+ else dup2X1Case
+
+ case DUP2_X2 =>
+ val v1isSize2 = frame.peekStack(0).getSize == 2
+ if (v1isSize2) {
+ val v2isSize2 = frame.peekStack(1).getSize == 2
+ if (v2isSize2) dupX1Case // Form 4
+ else dupX2Case // Form 2
+ } else {
+ val v3isSize2 = frame.peekStack(2).getSize == 2
+ if (v3isSize2) dup2X1Case // Form 3
+ else {
+ // Form 1
+ (producedIndex(4): @switch) match {
+ case 0 | 4 => stackValue(1)
+ case 1 | 5 => stackValue(0)
+ case 2 => stackValue(3)
+ case 3 => stackValue(2)
+ }
+ }
+ }
+
+ case SWAP =>
+ if (producedIndex(2) == 0) stackValue(0)
+ else stackValue(1)
+
+ case CHECKCAST =>
+ stackValue(0)
+ }
+ }
+
+ /**
+ * Returns the value slots into which `copyOp` copies the value at `consumedSlot`.
+ *
+ * Example:
+ * - copyOp = DUP_X1, assume it consumes slots 2,3 and produces 2,3,4
+ * - if consumedSlot == 2, the result is Set(3)
+ * - if consumedSlot == 3, the result is Set(2, 4)
+ */
+ private def copyOperationProducedValueSlots(copyOp: AbstractInsnNode, consumedSlot: Int): Set[Int] = {
+ if (isStore(copyOp)) Set(copyOp.asInstanceOf[VarInsnNode].`var`)
+ else {
+ val nextFrame = frameAt(copyOp.getNext)
+ val top = nextFrame.stackTop
+
+ // Index of the consumed value. Example: DUP_X1 consumes two values, so consumedIndex is
+ // 0 or 1, where 0 corresponds to the lower value on the stack.
+ def consumedIndex(numProduced: Int) = {
+ val numUsedSlotsAfterCopy = top + 1
+ consumedSlot - (numUsedSlotsAfterCopy - numProduced)
+ }
+
+ def dupX1Case = (consumedIndex(3): @switch) match {
+ case 0 => Set(top - 1)
+ case 1 => Set(top - 2, top)
+ }
+
+ def dupX2Case = (consumedIndex(4): @switch) match {
+ case 0 => Set(top - 2)
+ case 1 => Set(top - 1)
+ case 2 => Set(top - 3, top)
+ }
+
+ def dup2X1Case = (consumedIndex(5): @switch) match {
+ case 0 => Set(top - 2)
+ case 1 => Set(top - 4, top - 1)
+ case 2 => Set(top - 3, top)
+ }
+
+ if (isLoad(copyOp)) Set(top)
+ else (copyOp.getOpcode: @switch) match {
+ case DUP =>
+ Set(top - 1, top)
+
+ case DUP_X1 =>
+ dupX1Case
+
+ case DUP_X2 =>
+ if (nextFrame.peekStack(1).getSize == 2) dupX1Case
+ else dupX2Case
+
+ case DUP2 =>
+ if (nextFrame.peekStack(0).getSize == 2) Set(top - 1, top)
+ else (consumedIndex(4): @switch) match {
+ case 0 => Set(top - 3, top - 1)
+ case 1 => Set(top - 2, top)
+ }
+
+ case DUP2_X1 =>
+ if (nextFrame.peekStack(0).getSize == 2) dupX1Case
+ else dup2X1Case
+
+ case DUP2_X2 =>
+ val v1isSize2 = nextFrame.peekStack(0).getSize == 2
+ if (v1isSize2) {
+ val v2isSize2 = nextFrame.peekStack(1).getSize == 2
+ if (v2isSize2) dupX1Case // Form 4
+ else dupX2Case // Form 2
+ } else {
+ val v3isSize2 = nextFrame.peekStack(2).getSize == 2
+ if (v3isSize2) dup2X1Case // Form 3
+ else {
+ // Form 1
+ (consumedIndex(6): @switch) match {
+ case 0 => Set(top - 3)
+ case 1 => Set(top - 2)
+ case 2 => Set(top - 5, top - 1)
+ case 3 => Set(top - 4, top)
+ }
+ }
+ }
+
+ case SWAP =>
+ if (consumedIndex(2) == 0) Set(top)
+ else Set(top - 1)
+
+ case CHECKCAST =>
+ Set(top)
+ }
+ }
+ }
+
+ /** Returns the frame values consumed by executing `insn`. */
+ private def inputValues(insn: AbstractInsnNode): Seq[SourceValue] = {
+ lazy val frame = frameAt(insn)
+ inputValueSlots(insn) map frame.getValue
+ }
+
+ /** Returns the frame slots holding the values consumed by executing `insn`. */
+ private def inputValueSlots(insn: AbstractInsnNode): Seq[Int] = {
+ if (insn.getOpcode == -1) return Seq.empty
+ if (isLoad(insn)) {
+ Seq(insn.asInstanceOf[VarInsnNode].`var`)
+ } else if (insn.getOpcode == IINC) {
+ Seq(insn.asInstanceOf[IincInsnNode].`var`)
+ } else {
+ val frame = frameAt(insn)
+ val stackEffect = InstructionStackEffect(insn, frame)
+ val stackSize = frame.getLocals + frame.getStackSize
+ (stackSize - stackEffect._1) until stackSize
+ }
+ }
+
+ /** Returns the frame slots holding the values produced by executing `insn`. */
+ private def outputValueSlots(insn: AbstractInsnNode): Seq[Int] = insn match {
+ case ParameterProducer(local) => Seq(local)
+ case UninitializedLocalProducer(local) => Seq(local)
+ case ExceptionProducer(frame) => Seq(frame.stackTop)
+ case _ =>
+ if (insn.getOpcode == -1) return Seq.empty
+ if (isStore(insn)) {
+ Seq(insn.asInstanceOf[VarInsnNode].`var`)
+ } else if (insn.getOpcode == IINC) {
+ Seq(insn.asInstanceOf[IincInsnNode].`var`)
+ } else {
+ val frame = frameAt(insn)
+ val stackEffect = InstructionStackEffect(insn, frame)
+ val nextFrame = frameAt(insn.getNext)
+ val stackSize = nextFrame.getLocals + nextFrame.getStackSize
+ (stackSize - stackEffect._2) until stackSize
+ }
+ }
+
+ /** For each instruction, a set of potential consumers of the produced values. */
+ private lazy val _consumersOfOutputsFrom: Map[AbstractInsnNode, Vector[Set[AbstractInsnNode]]] = {
+ var res = Map.empty[AbstractInsnNode, Vector[Set[AbstractInsnNode]]]
+ for {
+ insn <- methodNode.instructions.iterator.asScala
+ frame = frameAt(insn)
+ i <- inputValueSlots(insn)
+ producer <- frame.getValue(i).insns.asScala
+ } {
+ val producedSlots = outputValueSlots(producer)
+ val currentConsumers = res.getOrElse(producer, Vector.fill(producedSlots.size)(Set.empty[AbstractInsnNode]))
+ val outputIndex = producedSlots.indexOf(i)
+ res = res.updated(producer, currentConsumers.updated(outputIndex, currentConsumers(outputIndex) + insn))
+ }
+ res
+ }
+
+ private val _initialProducersCache: mutable.AnyRefMap[(AbstractInsnNode, Int), Set[AbstractInsnNode]] = mutable.AnyRefMap.empty
+ private val _ultimateConsumersCache: mutable.AnyRefMap[(AbstractInsnNode, Int), Set[AbstractInsnNode]] = mutable.AnyRefMap.empty
+}
+
+/**
+ * A class for pseudo-instructions representing the initial producers of local values that have
+ * no producer instruction in the method:
+ * - parameters, including `this`
+ * - uninitialized local variables
+ * - exception values in handlers
+ *
+ * The ASM built-in SourceValue analysis yields an empty producers set for such values. This leads
+ * to ambiguities. Example (in Java one can re-assign parameter):
+ *
+ * void foo(int a) {
+ * if (a == 0) a = 1;
+ * return a;
+ * }
+ *
+ * In the first frame of the method, the SoruceValue for parameter `a` gives an empty set of
+ * producer instructions.
+ *
+ * In the frame of the `IRETURN` instruction, the SoruceValue for parameter `a` lists a single
+ * producer instruction: the `ISTORE 1`. This makes it look as if there was a single producer for
+ * `a`, where in fact it might still hold the parameter's initial value.
+ */
+abstract class InitialProducer extends AbstractInsnNode(-1) {
+ override def getType: Int = throw new UnsupportedOperationException
+ override def clone(labels: util.Map[LabelNode, LabelNode]): AbstractInsnNode = throw new UnsupportedOperationException
+ override def accept(cv: MethodVisitor): Unit = throw new UnsupportedOperationException
+}
+
+case class ParameterProducer(local: Int) extends InitialProducer
+case class UninitializedLocalProducer(local: Int) extends InitialProducer
+case class ExceptionProducer(handlerFrame: Frame[_ <: Value]) extends InitialProducer
+
+class InitialProducerSourceInterpreter extends SourceInterpreter {
+ override def newParameterValue(isInstanceMethod: Boolean, local: Int, tp: Type): SourceValue = {
+ new SourceValue(tp.getSize, ParameterProducer(local))
+ }
+
+ override def newEmptyNonParameterLocalValue(local: Int): SourceValue = {
+ new SourceValue(1, UninitializedLocalProducer(local))
+ }
+
+ override def newExceptionValue(tryCatchBlockNode: TryCatchBlockNode, handlerFrame: Frame[_ <: Value], exceptionType: Type): SourceValue = {
+ new SourceValue(1, ExceptionProducer(handlerFrame))
+ }
+} \ No newline at end of file
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala
index 9bd016f964..0ec550981a 100644
--- a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala
+++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala
@@ -14,7 +14,6 @@ import scala.tools.asm.commons.CodeSizeEvaluator
import scala.tools.asm.tree.analysis._
import scala.tools.asm.{MethodWriter, ClassWriter, Label, Opcodes}
import scala.tools.asm.tree._
-import scala.collection.convert.decorateAsScala._
import GenBCode._
import scala.collection.convert.decorateAsScala._
import scala.collection.convert.decorateAsJava._
@@ -73,11 +72,18 @@ object BytecodeUtils {
op >= Opcodes.IRETURN && op <= Opcodes.RETURN
}
- def isVarInstruction(instruction: AbstractInsnNode): Boolean = {
+ def isLoad(instruction: AbstractInsnNode): Boolean = {
val op = instruction.getOpcode
- (op >= Opcodes.ILOAD && op <= Opcodes.ALOAD) || (op >= Opcodes.ISTORE && op <= Opcodes.ASTORE)
+ op >= Opcodes.ILOAD && op <= Opcodes.ALOAD
}
+ def isStore(instruction: AbstractInsnNode): Boolean = {
+ val op = instruction.getOpcode
+ op >= Opcodes.ISTORE && op <= Opcodes.ASTORE
+ }
+
+ def isVarInstruction(instruction: AbstractInsnNode): Boolean = isLoad(instruction) || isStore(instruction)
+
def isExecutable(instruction: AbstractInsnNode): Boolean = instruction.getOpcode >= 0
def isConstructor(methodNode: MethodNode): Boolean = {
diff --git a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala
index d0ffd06b01..ee9580c1c3 100644
--- a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala
+++ b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala
@@ -6,7 +6,7 @@ import scala.collection.mutable.ListBuffer
import scala.reflect.internal.util.BatchSourceFile
import scala.reflect.io.VirtualDirectory
import scala.tools.asm.Opcodes
-import scala.tools.asm.tree.{ClassNode, MethodNode}
+import scala.tools.asm.tree.{AbstractInsnNode, ClassNode, MethodNode}
import scala.tools.cmd.CommandLineParser
import scala.tools.nsc.io.AbstractFile
import scala.tools.nsc.reporters.StoreReporter
@@ -15,6 +15,7 @@ import scala.tools.nsc.{Settings, Global}
import scala.tools.partest.ASMConverters
import scala.collection.JavaConverters._
import scala.tools.testing.TempDir
+import AsmUtils._
object CodeGenTools {
import ASMConverters._
@@ -151,6 +152,17 @@ object CodeGenTools {
def getSingleMethod(classNode: ClassNode, name: String): Method =
convertMethod(classNode.methods.asScala.toList.find(_.name == name).get)
+ /**
+ * Instructions that match `query` when textified.
+ * If `query` starts with a `+`, the next instruction is returned.
+ */
+ def findInstr(method: MethodNode, query: String): List[AbstractInsnNode] = {
+ val useNext = query(0) == '+'
+ val instrPart = if (useNext) query.drop(1) else query
+ val insns = method.instructions.iterator.asScala.find(i => textify(i) contains instrPart).toList
+ if (useNext) insns.map(_.getNext) else insns
+ }
+
def assertHandlerLabelPostions(h: ExceptionHandler, instructions: List[Instruction], startIndex: Int, endIndex: Int, handlerIndex: Int): Unit = {
val insVec = instructions.toVector
assertTrue(h.start == insVec(startIndex) && h.end == insVec(endIndex) && h.handler == insVec(handlerIndex))
diff --git a/test/junit/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzerTest.scala
index 3a85f03da2..94e776aadb 100644
--- a/test/junit/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzerTest.scala
+++ b/test/junit/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzerTest.scala
@@ -38,17 +38,6 @@ class NullnessAnalyzerTest extends ClearAfterClass {
nullnessAnalyzer
}
- /**
- * Instructions that match `query` when textified.
- * If `query` starts with a `+`, the next instruction is returned.
- */
- def findInstr(method: MethodNode, query: String): List[AbstractInsnNode] = {
- val useNext = query(0) == '+'
- val instrPart = if (useNext) query.drop(1) else query
- val insns = method.instructions.iterator.asScala.find(i => textify(i) contains instrPart).toList
- if (useNext) insns.map(_.getNext) else insns
- }
-
def testNullness(analyzer: NullnessAnalyzer, method: MethodNode, query: String, index: Int, nullness: Nullness): Unit = {
for (i <- findInstr(method, query)) {
val r = analyzer.frameAt(i, method).getValue(index).nullness
diff --git a/test/junit/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzerTest.scala
new file mode 100644
index 0000000000..9af9ef54fc
--- /dev/null
+++ b/test/junit/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzerTest.scala
@@ -0,0 +1,249 @@
+package scala.tools.nsc
+package backend.jvm
+package analysis
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.Assert._
+
+import scala.tools.asm.Opcodes
+import scala.tools.asm.tree.AbstractInsnNode
+import scala.tools.partest.ASMConverters._
+import scala.tools.testing.ClearAfterClass
+import CodeGenTools._
+import AsmUtils._
+
+object ProdConsAnalyzerTest extends ClearAfterClass.Clearable {
+ var noOptCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:none")
+
+ def clear(): Unit = {
+ noOptCompiler = null
+ }
+}
+
+@RunWith(classOf[JUnit4])
+class ProdConsAnalyzerTest extends ClearAfterClass {
+ ClearAfterClass.stateToClear = ProdConsAnalyzerTest
+ val noOptCompiler = ProdConsAnalyzerTest.noOptCompiler
+
+ def prodToString(producer: AbstractInsnNode) = producer match {
+ case p: InitialProducer => p.toString
+ case p => textify(p)
+ }
+
+ def testSingleInsn(singletonInsns: Traversable[AbstractInsnNode], expected: String): Unit = {
+ testInsn(single(singletonInsns), expected)
+ }
+
+ def testMultiInsns(insns: Traversable[AbstractInsnNode], expected: Traversable[String]): Unit = {
+ assertTrue(s"Sizes don't match: ${insns.size} vs ${expected.size}", insns.size == expected.size)
+ for (insn <- insns) {
+ val txt = prodToString(insn)
+ assertTrue(s"Instruction $txt not found in ${expected mkString ", "}", expected.exists(txt.contains))
+ }
+ }
+
+ def testInsn(insn: AbstractInsnNode, expected: String): Unit = {
+ val txt = prodToString(insn)
+ assertTrue(s"Expected $expected, found $txt", txt contains expected)
+ }
+
+ def single[T](c: Traversable[T]): T = {
+ assertTrue(s"Expected singleton collection, got $c", c.size == 1)
+ c.head
+ }
+
+ @Test
+ def parameters(): Unit = {
+ val List(m) = compileMethods(noOptCompiler)("def f = this.toString")
+ val a = new ProdConsAnalyzer(m, "C")
+ val call = findInstr(m, "INVOKEVIRTUAL").head
+
+ testSingleInsn(a.producersForValueAt(call, 1), "ALOAD 0") // producer of stack value
+ testSingleInsn(a.producersForInputsOf(call), "ALOAD 0")
+
+ testSingleInsn(a.consumersOfValueAt(call.getNext, 1), "ARETURN") // consumer of `toString` result
+ testSingleInsn(a.consumersOfOutputsFrom(call), "ARETURN")
+
+ testSingleInsn(a.ultimateConsumersOfValueAt(call.getNext, 1), "ARETURN")
+
+ testSingleInsn(a.initialProducersForValueAt(call, 1), "ParameterProducer")
+ testSingleInsn(a.producersForValueAt(call, 0), "ParameterProducer")
+ }
+
+ @Test
+ def parametersInitialProducer(): Unit = {
+ // mutates a parameter local (not possible in scala, but in bytecode)
+ import Opcodes._
+ val m = genMethod(descriptor = "(I)I")(
+ Label(0),
+ VarOp(ILOAD, 1),
+ Jump(IFNE, Label(1)),
+ Op(ICONST_1),
+ VarOp(ISTORE, 1),
+ Label(1),
+ VarOp(ILOAD, 1),
+ Op(IRETURN),
+ Label(2)
+ )
+ m.maxLocals = 2
+ m.maxStack = 1
+ val a = new ProdConsAnalyzer(m, "C")
+
+ val ifne = findInstr(m, "IFNE").head
+ testSingleInsn(a.producersForValueAt(ifne, 1), "ParameterProducer")
+
+ val ret = findInstr(m, "IRETURN").head
+ testMultiInsns(a.producersForValueAt(ret, 1), List("ParameterProducer", "ISTORE 1"))
+ }
+
+ @Test
+ def branching(): Unit = {
+ val List(m) = compileMethods(noOptCompiler)("def f(x: Int) = { var a = x; if (a == 0) a = 12; a }")
+ val a = new ProdConsAnalyzer(m, "C")
+
+ val List(ret) = findInstr(m, "IRETURN")
+ testMultiInsns(a.producersForValueAt(ret, 2), List("ISTORE 2", "ISTORE 2"))
+ testMultiInsns(a.initialProducersForValueAt(ret, 2), List("BIPUSH 12", "ParameterProducer"))
+
+ val List(bipush) = findInstr(m, "BIPUSH 12")
+ testSingleInsn(a.consumersOfOutputsFrom(bipush), "ISTORE 2")
+ testSingleInsn(a.ultimateConsumersOfValueAt(bipush.getNext, 3), "IRETURN")
+ }
+
+ @Test
+ def checkCast(): Unit = {
+ val List(m) = compileMethods(noOptCompiler)("def f(o: Object) = o.asInstanceOf[String]")
+ val a = new ProdConsAnalyzer(m, "C")
+ assert(findInstr(m, "CHECKCAST java/lang/String").length == 1)
+
+ val List(ret) = findInstr(m, "ARETURN")
+ testSingleInsn(a.initialProducersForInputsOf(ret), "ParameterProducer(1)")
+ }
+
+ @Test
+ def instanceOf(): Unit = {
+ val List(m) = compileMethods(noOptCompiler)("def f(o: Object) = o.isInstanceOf[String]")
+ val a = new ProdConsAnalyzer(m, "C")
+ assert(findInstr(m, "INSTANCEOF java/lang/String").length == 1)
+
+ val List(ret) = findInstr(m, "IRETURN")
+ testSingleInsn(a.initialProducersForInputsOf(ret), "INSTANCEOF")
+ }
+
+ @Test
+ def unInitLocal(): Unit = {
+ val List(m) = compileMethods(noOptCompiler)("def f(b: Boolean) = { if (b) { var a = 0; println(a) }; 1 }")
+ val a = new ProdConsAnalyzer(m, "C")
+
+ val List(store) = findInstr(m, "ISTORE")
+ val List(call) = findInstr(m, "INVOKEVIRTUAL")
+ val List(ret) = findInstr(m, "IRETURN")
+
+ testSingleInsn(a.producersForValueAt(store, 2), "UninitializedLocalProducer(2)")
+ testSingleInsn(a.producersForValueAt(call, 2), "ISTORE")
+ testMultiInsns(a.producersForValueAt(ret, 2), List("UninitializedLocalProducer", "ISTORE"))
+ }
+
+ @Test
+ def dupCopying(): Unit = {
+ val List(m) = compileMethods(noOptCompiler)("def f = new Object")
+ val a = new ProdConsAnalyzer(m, "C")
+
+ val List(newO) = findInstr(m, "NEW")
+ val List(constr) = findInstr(m, "INVOKESPECIAL")
+
+ testSingleInsn(a.producersForInputsOf(constr), "DUP")
+ testSingleInsn(a.initialProducersForInputsOf(constr), "NEW")
+
+ testSingleInsn(a.consumersOfOutputsFrom(newO), "DUP")
+ testMultiInsns(a.ultimateConsumersOfOutputsFrom(newO), List("INVOKESPECIAL", "ARETURN"))
+ }
+
+ @Test
+ def multiProducer(): Unit = {
+ import Opcodes._
+ val m = genMethod(descriptor = "(I)I")(
+ VarOp(ILOAD, 1),
+ VarOp(ILOAD, 1),
+ Op(DUP2),
+ Op(IADD),
+ Op(SWAP),
+ VarOp(ISTORE, 1),
+ Op(IRETURN)
+ )
+ m.maxLocals = 2
+ m.maxStack = 4
+ val a = new ProdConsAnalyzer(m, "C")
+
+ val List(dup2) = findInstr(m, "DUP2")
+ val List(add) = findInstr(m, "IADD")
+ val List(swap) = findInstr(m, "SWAP")
+ val List(store) = findInstr(m, "ISTORE")
+ val List(ret) = findInstr(m, "IRETURN")
+
+ testMultiInsns(a.producersForInputsOf(dup2), List("ILOAD", "ILOAD"))
+ testSingleInsn(a.consumersOfValueAt(dup2.getNext, 4), "IADD")
+ testSingleInsn(a.consumersOfValueAt(dup2.getNext, 5), "IADD")
+ testMultiInsns(a.consumersOfOutputsFrom(dup2), List("IADD", "SWAP"))
+
+ testSingleInsn(a.ultimateConsumersOfOutputsFrom(dup2), "IADD") // the 'store' is not here: it's a copying instr, so not an ultimate consumer.
+ testMultiInsns(a.consumersOfOutputsFrom(swap), List("IRETURN", "ISTORE"))
+ testSingleInsn(a.ultimateConsumersOfOutputsFrom(swap), "IRETURN") // again, no store
+ testSingleInsn(a.initialProducersForInputsOf(add), "ParameterProducer(1)")
+
+ testMultiInsns(a.producersForInputsOf(swap), List("IADD", "DUP2"))
+ testSingleInsn(a.consumersOfValueAt(swap.getNext, 4), "ISTORE")
+ testSingleInsn(a.consumersOfValueAt(swap.getNext, 3), "IRETURN")
+ testSingleInsn(a.initialProducersForInputsOf(store), "ParameterProducer(1)")
+ testSingleInsn(a.initialProducersForInputsOf(ret), "IADD")
+ }
+
+ @Test
+ def iincProdCons(): Unit = {
+ import Opcodes._
+ val m = genMethod(descriptor = "(I)I")(
+ Incr(IINC, 1, 1), // producer and cosumer of local variable 1
+ VarOp(ILOAD, 1),
+ Op(IRETURN)
+ )
+ m.maxLocals = 2
+ m.maxStack = 1
+ val a = new ProdConsAnalyzer(m, "C")
+
+ val List(inc) = findInstr(m, "IINC")
+ val List(load) = findInstr(m, "ILOAD")
+ val List(ret) = findInstr(m, "IRETURN")
+
+ testSingleInsn(a.producersForInputsOf(inc), "ParameterProducer(1)")
+ testSingleInsn(a.consumersOfOutputsFrom(inc), "ILOAD")
+ testSingleInsn(a.ultimateConsumersOfOutputsFrom(inc), "IRETURN")
+ testSingleInsn(a.consumersOfValueAt(inc, 1), "IINC") // parameter value has a single consumer, the IINC
+ testSingleInsn(a.ultimateConsumersOfValueAt(inc, 1), "IINC")
+
+ testSingleInsn(a.producersForInputsOf(load), "IINC")
+ testSingleInsn(a.producersForValueAt(load, 1), "IINC")
+
+ testSingleInsn(a.initialProducersForInputsOf(ret), "IINC")
+ }
+
+ @Test
+ def copyingInsns(): Unit = {
+ val List(m) = compileMethods(noOptCompiler)("def f = 0l.asInstanceOf[Int]")
+ val a = new ProdConsAnalyzer(m, "C")
+
+ val List(cnst) = findInstr(m, "LCONST_0")
+ val List(l2i) = findInstr(m, "L2I") // l2i is not a copying instruction
+ val List(ret) = findInstr(m, "IRETURN")
+
+ testSingleInsn(a.consumersOfOutputsFrom(cnst), "L2I")
+ testSingleInsn(a.ultimateConsumersOfOutputsFrom(cnst), "L2I")
+
+ testSingleInsn(a.producersForInputsOf(l2i), "LCONST_0")
+ testSingleInsn(a.initialProducersForInputsOf(l2i), "LCONST_0")
+
+ testSingleInsn(a.consumersOfOutputsFrom(l2i), "IRETURN")
+ testSingleInsn(a.producersForInputsOf(ret), "L2I")
+ }
+}
diff --git a/versions.properties b/versions.properties
index a7ec8caedc..9f297e9b93 100644
--- a/versions.properties
+++ b/versions.properties
@@ -33,7 +33,7 @@ scala-swing.version.number=1.0.2
akka-actor.version.number=2.3.10
actors-migration.version.number=1.1.0
jline.version=2.12.1
-scala-asm.version=5.0.4-scala-1
+scala-asm.version=5.0.4-scala-2
# external modules, used internally (not shipped)
partest.version.number=1.0.7