package scala.tools.nsc package backend.jvm package analysis import scala.annotation.switch import scala.collection.{mutable, immutable} import scala.tools.asm.Opcodes import scala.tools.asm.tree._ import scala.tools.asm.tree.analysis.{Analyzer, Value, Frame, Interpreter} import opt.BytecodeUtils._ object AliasingFrame { private var _idCounter: Long = 0l private def nextId = { _idCounter += 1; _idCounter } } class AliasingFrame[V <: Value](nLocals: Int, nStack: Int) extends Frame[V](nLocals, nStack) { import Opcodes._ // Auxiliary constructor required for implementing `AliasingAnalyzer.newFrame` def this(src: Frame[_ <: V]) { this(src.getLocals, src.getMaxStackSize) init(src) } /** * For each slot (entry in the `values` array of the frame), an id that uniquely represents * the object stored in it. If two values have the same id, they are aliases of the same * object. */ private val aliasIds: Array[Long] = Array.fill(nLocals + nStack)(AliasingFrame.nextId) /** * The object alias id of for a value index. */ def aliasId(entry: Int) = aliasIds(entry) /** * Returns the indices of the values array which are aliases of the object `id`. */ def valuesWithAliasId(id: Long): Set[Int] = immutable.BitSet.empty ++ aliasIds.indices.iterator.filter(i => aliasId(i) == id) /** * The set of aliased values for a given entry in the `values` array. */ def aliasesOf(entry: Int): Set[Int] = valuesWithAliasId(aliasIds(entry)) /** * Define a new alias. For example, given * var a = this // this, a have the same aliasId * then an assignment * b = a * will set the same the aliasId for `b`. */ private def newAlias(assignee: Int, source: Int): Unit = { aliasIds(assignee) = aliasIds(source) } /** * An assignment * a = someUnknownValue() * sets a fresh alias id for `a`. * A stack value is also removed from its alias set when being consumed. */ private def removeAlias(assignee: Int): Unit = { aliasIds(assignee) = AliasingFrame.nextId } override def execute(insn: AbstractInsnNode, interpreter: Interpreter[V]): Unit = { // Make the extendsion methods easier to use (otherwise we have to repeat `this`.stackTop) def stackTop: Int = this.stackTop def peekStack(n: Int): V = this.peekStack(n) // the val pattern `val (p, c) = f` still allocates a tuple (https://github.com/scala-opt/scala/issues/28) val prodCons = InstructionStackEffect(insn, this) // needs to be called before super.execute, see its doc val consumed = prodCons._1 val produced = prodCons._2 super.execute(insn, interpreter) (insn.getOpcode: @switch) match { case ALOAD => newAlias(assignee = stackTop, source = insn.asInstanceOf[VarInsnNode].`var`) case DUP => val top = stackTop newAlias(assignee = top, source = top - 1) case DUP_X1 => val top = stackTop newAlias(assignee = top, source = top - 1) newAlias(assignee = top - 1, source = top - 2) newAlias(assignee = top - 2, source = top) case DUP_X2 => // Check if the second element on the stack is size 2 // https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.dup_x2 val isSize2 = peekStack(1).getSize == 2 val top = stackTop newAlias(assignee = top, source = top - 1) newAlias(assignee = top - 1, source = top - 2) if (isSize2) { // Size 2 values on the stack only take one slot in the `values` array newAlias(assignee = top - 2, source = top) } else { newAlias(assignee = top - 2, source = top - 3) newAlias(assignee = top - 3, source = top) } case DUP2 => val isSize2 = peekStack(0).getSize == 2 val top = stackTop if (isSize2) { newAlias(assignee = top, source = top - 1) } else { newAlias(assignee = top - 1, source = top - 3) newAlias(assignee = top, source = top - 2) } case DUP2_X1 => val isSize2 = peekStack(0).getSize == 2 val top = stackTop if (isSize2) { newAlias(assignee = top, source = top - 1) newAlias(assignee = top - 1, source = top - 2) newAlias(assignee = top - 2, source = top) } else { newAlias(assignee = top, source = top - 2) newAlias(assignee = top - 1, source = top - 3) newAlias(assignee = top - 2, source = top - 4) newAlias(assignee = top - 4, source = top) newAlias(assignee = top - 5, source = top - 1) } case DUP2_X2 => val top = stackTop // https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.dup2_x2 val v1isSize2 = peekStack(0).getSize == 2 if (v1isSize2) { newAlias(assignee = top, source = top - 1) newAlias(assignee = top - 1, source = top - 2) val v2isSize2 = peekStack(1).getSize == 2 if (v2isSize2) { // Form 4 newAlias(assignee = top - 2, source = top) } else { // Form 2 newAlias(assignee = top - 2, source = top - 3) newAlias(assignee = top - 3, source = top) } } else { newAlias(assignee = top, source = top - 2) newAlias(assignee = top - 1, source = top - 3) newAlias(assignee = top - 2, source = top - 4) val v3isSize2 = peekStack(2).getSize == 2 if (v3isSize2) { // Form 3 newAlias(assignee = top - 3, source = top) newAlias(assignee = top - 4, source = top - 1) } else { // Form 1 newAlias(assignee = top - 3, source = top - 5) newAlias(assignee = top - 4, source = top) newAlias(assignee = top - 5, source = top - 1) } } case SWAP => val top = stackTop val idTop = aliasIds(top) aliasIds(top) = aliasIds(top - 1) aliasIds(top - 1) = idTop case opcode => if (opcode == ASTORE) { // Not a separate case because we need to remove the consumed stack value from alias sets after. val stackTopBefore = stackTop - produced + consumed val local = insn.asInstanceOf[VarInsnNode].`var` newAlias(assignee = local, source = stackTopBefore) // if the value written is size 2, it overwrites the subsequent slot, which is then no // longer an alias of anything. see the corresponding case in `Frame.execute`. if (getLocal(local).getSize == 2) removeAlias(local + 1) // if the value at the preceding index is size 2, it is no longer valid, so we remove its // aliasing. see corresponding case in `Frame.execute` if (local > 0) { val precedingValue = getLocal(local - 1) if (precedingValue != null && precedingValue.getSize == 2) removeAlias(local - 1) } } // Remove consumed stack values from aliasing sets. // Example: iadd // - before: local1, local2, stack1, consumed1, consumed2 // - after: local1, local2, stack1, produced1 // stackTop = 3 val firstConsumed = stackTop - produced + 1 // firstConsumed = 3 for (i <- 0 until consumed) removeAlias(firstConsumed + i) // remove aliases for 3 and 4 // We don't need to set the aliases ids for the produced values: the aliasIds array already // contains fresh ids for non-used stack values (ensured by removeAlias). } } /** * Merge the AliasingFrame `other` into this AliasingFrame. * * Aliases that are common in both frames are kept. Example: * * var x, y = null * if (...) { * x = a * y = a // (x, y, a) are aliases * } else { * x = a * y = b // (x, a) and (y, b) * } * [...] // (x, a) */ override def merge(other: Frame[_ <: V], interpreter: Interpreter[V]): Boolean = { val valuesChanged = super.merge(other, interpreter) var aliasesChanged = false val aliasingOther = other.asInstanceOf[AliasingFrame[_]] for (i <- aliasIds.indices) { val thisAliases = aliasesOf(i) val thisNotOther = thisAliases diff (thisAliases intersect aliasingOther.aliasesOf(i)) if (thisNotOther.nonEmpty) { aliasesChanged = true thisNotOther foreach removeAlias } } valuesChanged || aliasesChanged } override def init(src: Frame[_ <: V]): Frame[V] = { super.init(src) compat.Platform.arraycopy(src.asInstanceOf[AliasingFrame[_]].aliasIds, 0, aliasIds, 0, aliasIds.length) this } } /** * An analyzer that uses AliasingFrames instead of bare Frames. This can be used when an analysis * needs to track aliases, but doesn't require a more specific Frame subclass. */ class AliasingAnalyzer[V <: Value](interpreter: Interpreter[V]) extends Analyzer[V](interpreter) { override def newFrame(nLocals: Int, nStack: Int): AliasingFrame[V] = new AliasingFrame(nLocals, nStack) override def newFrame(src: Frame[_ <: V]): AliasingFrame[V] = new AliasingFrame(src) }