diff options
author | Dmitry Petrashko <dark@d-d.me> | 2016-05-27 10:57:34 +0200 |
---|---|---|
committer | Dmitry Petrashko <dark@d-d.me> | 2016-05-27 10:57:34 +0200 |
commit | 3ee785afadbc57f53ccb9cb0d8a98d0eb395dde2 (patch) | |
tree | 5c5765e616a346849abb31456f66678a77b53b43 | |
parent | 4b35f6f96caea6586de0c4ab001b5429b44d754b (diff) | |
parent | 772f3d20fd3605d077abf28e80195f1dda343ee1 (diff) | |
download | dotty-3ee785afadbc57f53ccb9cb0d8a98d0eb395dde2.tar.gz dotty-3ee785afadbc57f53ccb9cb0d8a98d0eb395dde2.tar.bz2 dotty-3ee785afadbc57f53ccb9cb0d8a98d0eb395dde2.zip |
Merge pull request #1256 from felixmulder/topic/test-bcode
Add bytecode checking infrastructure
-rw-r--r-- | AUTHORS.md | 7 | ||||
-rw-r--r-- | src/dotty/tools/dotc/core/Definitions.scala | 2 | ||||
-rw-r--r-- | src/dotty/tools/dotc/core/Types.scala | 2 | ||||
-rw-r--r-- | src/dotty/tools/dotc/transform/PatternMatcher.scala | 8 | ||||
-rw-r--r-- | test/test/AsmConverters.scala | 256 | ||||
-rw-r--r-- | test/test/AsmNode.scala | 61 | ||||
-rw-r--r-- | test/test/DottyBytecodeTest.scala | 208 | ||||
-rw-r--r-- | test/test/DottyBytecodeTests.scala | 188 |
8 files changed, 728 insertions, 4 deletions
diff --git a/AUTHORS.md b/AUTHORS.md index 4e8bb3d30..4d54d2ee3 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -45,3 +45,10 @@ The majority of the dotty codebase is new code, with the exception of the compon `dotty.tools.io` > The I/O support library was adapted from current Scala compiler. Original authors were Paul Phillips and others. + +`dotty.test.DottyBytecodeTest` + +> Is an adaptation of the bytecode testing from +> [scala/scala](https://github.com/scala/scala). It has been reworked to fit +> the needs of dotty. Original authors include: Adrian Moors, Lukas Rytz, +> Grzegorz Kossakowski, Paul Phillips diff --git a/src/dotty/tools/dotc/core/Definitions.scala b/src/dotty/tools/dotc/core/Definitions.scala index bcc986755..5cb373cfd 100644 --- a/src/dotty/tools/dotc/core/Definitions.scala +++ b/src/dotty/tools/dotc/core/Definitions.scala @@ -486,6 +486,8 @@ class Definitions { def TASTYLongSignatureAnnot(implicit ctx: Context) = TASTYLongSignatureAnnotType.symbol.asClass lazy val TailrecAnnotType = ctx.requiredClassRef("scala.annotation.tailrec") def TailrecAnnot(implicit ctx: Context) = TailrecAnnotType.symbol.asClass + lazy val SwitchAnnotType = ctx.requiredClassRef("scala.annotation.switch") + def SwitchAnnot(implicit ctx: Context) = SwitchAnnotType.symbol.asClass lazy val ThrowsAnnotType = ctx.requiredClassRef("scala.throws") def ThrowsAnnot(implicit ctx: Context) = ThrowsAnnotType.symbol.asClass lazy val TransientAnnotType = ctx.requiredClassRef("scala.transient") diff --git a/src/dotty/tools/dotc/core/Types.scala b/src/dotty/tools/dotc/core/Types.scala index 71ea6d2b3..cec6fd3b1 100644 --- a/src/dotty/tools/dotc/core/Types.scala +++ b/src/dotty/tools/dotc/core/Types.scala @@ -107,7 +107,7 @@ object Types { * It makes no sense for it to be an alias type because isRef would always * return false in that case. */ - def isRef(sym: Symbol)(implicit ctx: Context): Boolean = stripTypeVar match { + def isRef(sym: Symbol)(implicit ctx: Context): Boolean = stripAnnots.stripTypeVar match { case this1: TypeRef => this1.info match { // see comment in Namer#typeDefSig case TypeAlias(tp) => tp.isRef(sym) diff --git a/src/dotty/tools/dotc/transform/PatternMatcher.scala b/src/dotty/tools/dotc/transform/PatternMatcher.scala index 64b85a51f..24d0e9ae0 100644 --- a/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -306,12 +306,11 @@ class PatternMatcher extends MiniPhaseTransform with DenotTransformer {thisTrans def emitSwitch(scrut: Tree, scrutSym: Symbol, cases: List[List[TreeMaker]], pt: Type, matchFailGenOverride: Option[Symbol => Tree], unchecked: Boolean): Option[Tree] = { // TODO Deal with guards? - def isSwitchableType(tpe: Type): Boolean = { + def isSwitchableType(tpe: Type): Boolean = (tpe isRef defn.IntClass) || (tpe isRef defn.ByteClass) || (tpe isRef defn.ShortClass) || (tpe isRef defn.CharClass) - } object IntEqualityTestTreeMaker { def unapply(treeMaker: EqualityTestTreeMaker): Option[Int] = treeMaker match { @@ -423,7 +422,8 @@ class PatternMatcher extends MiniPhaseTransform with DenotTransformer {thisTrans Match(intScrut, newCases :+ defaultCase) } - if (isSwitchableType(scrut.tpe.widenDealias) && cases.forall(isSwitchCase)) { + val dealiased = scrut.tpe.widenDealias + if (isSwitchableType(dealiased) && cases.forall(isSwitchCase)) { val valuesToCases = cases.map(extractSwitchCase) val values = valuesToCases.map(_._1) if (values.tails.exists { tail => tail.nonEmpty && tail.tail.exists(doOverlap(_, tail.head)) }) { @@ -433,6 +433,8 @@ class PatternMatcher extends MiniPhaseTransform with DenotTransformer {thisTrans Some(makeSwitch(valuesToCases)) } } else { + if (dealiased hasAnnotation defn.SwitchAnnot) + ctx.warning("failed to emit switch for `@switch` annotated match") None } } diff --git a/test/test/AsmConverters.scala b/test/test/AsmConverters.scala new file mode 100644 index 000000000..5734e8660 --- /dev/null +++ b/test/test/AsmConverters.scala @@ -0,0 +1,256 @@ +package test + +import scala.tools.asm +import asm._ +import asm.tree._ +import scala.collection.JavaConverters._ + +/** Makes using ASM from tests more convenient. + * + * Wraps ASM instructions in case classes so that equals and toString work + * for the purpose of bytecode diffing and pretty printing. + */ +object ASMConverters { + import asm.{tree => t} + + /** + * Transform the instructions of an ASM Method into a list of [[Instruction]]s. + */ + def instructionsFromMethod(meth: t.MethodNode): List[Instruction] = new AsmToScala(meth).instructions + + def convertMethod(meth: t.MethodNode): Method = new AsmToScala(meth).method + + implicit class RichInstructionLists(val self: List[Instruction]) extends AnyVal { + def === (other: List[Instruction]) = equivalentBytecode(self, other) + + def dropLinesFrames = self.filterNot(i => i.isInstanceOf[LineNumber] || i.isInstanceOf[FrameEntry]) + + private def referencedLabels(instruction: Instruction): Set[Instruction] = instruction match { + case Jump(op, label) => Set(label) + case LookupSwitch(op, dflt, keys, labels) => (dflt :: labels).toSet + case TableSwitch(op, min, max, dflt, labels) => (dflt :: labels).toSet + case LineNumber(line, start) => Set(start) + case _ => Set.empty + } + + def dropStaleLabels = { + val definedLabels: Set[Instruction] = self.filter(_.isInstanceOf[Label]).toSet + val usedLabels: Set[Instruction] = self.flatMap(referencedLabels)(collection.breakOut) + self.filterNot(definedLabels diff usedLabels) + } + + def dropNonOp = dropLinesFrames.dropStaleLabels + + def summary: List[Any] = dropNonOp map { + case i: Invoke => i.name + case i => i.opcode + } + + def summaryText: String = { + def comment(i: Instruction) = i match { + case j: Jump => s" /*${j.label.offset}*/" + case l: Label => s" /*${l.offset}*/" + case _ => "" + } + dropNonOp.map({ + case i: Invoke => s""""${i.name}"""" + case ins => opcodeToString(ins.opcode, ins.opcode) + comment(ins) + }).mkString("List(", ", ", ")") + } + } + + def opcodeToString(op: Int, default: Any = "?"): String = { + import scala.tools.asm.util.Printer.OPCODES + if (OPCODES.isDefinedAt(op)) OPCODES(op) else default.toString + } + + sealed abstract class Instruction extends Product { + def opcode: Int + + // toString such that the first field, "opcode: Int", is printed textually. + final override def toString() = { + val printOpcode = opcode != -1 + productPrefix + ( + if (printOpcode) Iterator(opcodeToString(opcode)) ++ productIterator.drop(1) + else productIterator + ).mkString("(", ", ", ")") + } + } + + case class Method(instructions: List[Instruction], handlers: List[ExceptionHandler], localVars: List[LocalVariable]) + + case class Field (opcode: Int, owner: String, name: String, desc: String) extends Instruction + case class Incr (opcode: Int, `var`: Int, incr: Int) extends Instruction + case class Op (opcode: Int) extends Instruction + case class IntOp (opcode: Int, operand: Int) extends Instruction + case class Jump (opcode: Int, label: Label) extends Instruction + case class Ldc (opcode: Int, cst: Any) extends Instruction + case class LookupSwitch (opcode: Int, dflt: Label, keys: List[Int], labels: List[Label]) extends Instruction + case class TableSwitch (opcode: Int, min: Int, max: Int, dflt: Label, labels: List[Label]) extends Instruction + case class Invoke (opcode: Int, owner: String, name: String, desc: String, itf: Boolean) extends Instruction + case class InvokeDynamic(opcode: Int, name: String, desc: String, bsm: MethodHandle, bsmArgs: List[AnyRef]) extends Instruction + case class NewArray (opcode: Int, desc: String, dims: Int) extends Instruction + case class TypeOp (opcode: Int, desc: String) extends Instruction + case class VarOp (opcode: Int, `var`: Int) extends Instruction + case class Label (offset: Int) extends Instruction { def opcode: Int = -1 } + case class FrameEntry (`type`: Int, local: List[Any], stack: List[Any]) extends Instruction { def opcode: Int = -1 } + case class LineNumber (line: Int, start: Label) extends Instruction { def opcode: Int = -1 } + + case class MethodHandle(tag: Int, owner: String, name: String, desc: String) + + case class ExceptionHandler(start: Label, end: Label, handler: Label, desc: Option[String]) + case class LocalVariable(name: String, desc: String, signature: Option[String], start: Label, end: Label, index: Int) + + class AsmToScala(asmMethod: t.MethodNode) { + + def instructions: List[Instruction] = asmMethod.instructions.iterator.asScala.toList map apply + + def method: Method = Method(instructions, convertHandlers(asmMethod), convertLocalVars(asmMethod)) + + private def labelIndex(l: t.LabelNode): Int = asmMethod.instructions.indexOf(l) + + private def op(i: t.AbstractInsnNode): Int = i.getOpcode + + private def lst[T](xs: java.util.List[T]): List[T] = if (xs == null) Nil else xs.asScala.toList + + // Heterogeneous List[Any] is used in FrameNode: type information about locals / stack values + // are stored in a List[Any] (Integer, String or LabelNode), see Javadoc of MethodNode#visitFrame. + // Opcodes (eg Opcodes.INTEGER) and Reference types (eg "java/lang/Object") are returned unchanged, + // LabelNodes are mapped to their LabelEntry. + private def mapOverFrameTypes(is: List[Any]): List[Any] = is map { + case i: t.LabelNode => applyLabel(i) + case x => x + } + + // avoids some casts + private def applyLabel(l: t.LabelNode) = this(l: t.AbstractInsnNode).asInstanceOf[Label] + + private def apply(x: t.AbstractInsnNode): Instruction = x match { + case i: t.FieldInsnNode => Field (op(i), i.owner, i.name, i.desc) + case i: t.IincInsnNode => Incr (op(i), i.`var`, i.incr) + case i: t.InsnNode => Op (op(i)) + case i: t.IntInsnNode => IntOp (op(i), i.operand) + case i: t.JumpInsnNode => Jump (op(i), applyLabel(i.label)) + case i: t.LdcInsnNode => Ldc (op(i), i.cst: Any) + case i: t.LookupSwitchInsnNode => LookupSwitch (op(i), applyLabel(i.dflt), lst(i.keys) map (x => x: Int), lst(i.labels) map applyLabel) + case i: t.TableSwitchInsnNode => TableSwitch (op(i), i.min, i.max, applyLabel(i.dflt), lst(i.labels) map applyLabel) + case i: t.MethodInsnNode => Invoke (op(i), i.owner, i.name, i.desc, i.itf) + case i: t.InvokeDynamicInsnNode => InvokeDynamic(op(i), i.name, i.desc, convertMethodHandle(i.bsm), convertBsmArgs(i.bsmArgs)) + case i: t.MultiANewArrayInsnNode => NewArray (op(i), i.desc, i.dims) + case i: t.TypeInsnNode => TypeOp (op(i), i.desc) + case i: t.VarInsnNode => VarOp (op(i), i.`var`) + case i: t.LabelNode => Label (labelIndex(i)) + case i: t.FrameNode => FrameEntry (i.`type`, mapOverFrameTypes(lst(i.local)), mapOverFrameTypes(lst(i.stack))) + case i: t.LineNumberNode => LineNumber (i.line, applyLabel(i.start)) + } + + private def convertBsmArgs(a: Array[Object]): List[Object] = a.map({ + case h: asm.Handle => convertMethodHandle(h) + case _ => a // can be: Class, method Type, primitive constant + })(collection.breakOut) + + private def convertMethodHandle(h: asm.Handle): MethodHandle = MethodHandle(h.getTag, h.getOwner, h.getName, h.getDesc) + + private def convertHandlers(method: t.MethodNode): List[ExceptionHandler] = { + method.tryCatchBlocks.asScala.map(h => ExceptionHandler(applyLabel(h.start), applyLabel(h.end), applyLabel(h.handler), Option(h.`type`)))(collection.breakOut) + } + + private def convertLocalVars(method: t.MethodNode): List[LocalVariable] = { + method.localVariables.asScala.map(v => LocalVariable(v.name, v.desc, Option(v.signature), applyLabel(v.start), applyLabel(v.end), v.index))(collection.breakOut) + } + } + + import collection.mutable.{Map => MMap} + + /** + * Bytecode is equal modulo local variable numbering and label numbering. + */ + def equivalentBytecode(as: List[Instruction], bs: List[Instruction], varMap: MMap[Int, Int] = MMap(), labelMap: MMap[Int, Int] = MMap()): Boolean = { + def same(v1: Int, v2: Int, m: MMap[Int, Int]) = { + if (m contains v1) m(v1) == v2 + else if (m.valuesIterator contains v2) false // v2 is already associated with some different value v1 + else { m(v1) = v2; true } + } + def sameVar(v1: Int, v2: Int) = same(v1, v2, varMap) + def sameLabel(l1: Label, l2: Label) = same(l1.offset, l2.offset, labelMap) + def sameLabels(ls1: List[Label], ls2: List[Label]) = (ls1 corresponds ls2)(sameLabel) + + def sameFrameTypes(ts1: List[Any], ts2: List[Any]) = (ts1 corresponds ts2) { + case (t1: Label, t2: Label) => sameLabel(t1, t2) + case (x, y) => x == y + } + + if (as.isEmpty) bs.isEmpty + else if (bs.isEmpty) false + else ((as.head, bs.head) match { + case (VarOp(op1, v1), VarOp(op2, v2)) => op1 == op2 && sameVar(v1, v2) + case (Incr(op1, v1, inc1), Incr(op2, v2, inc2)) => op1 == op2 && sameVar(v1, v2) && inc1 == inc2 + + case (l1 @ Label(_), l2 @ Label(_)) => sameLabel(l1, l2) + case (Jump(op1, l1), Jump(op2, l2)) => op1 == op2 && sameLabel(l1, l2) + case (LookupSwitch(op1, l1, keys1, ls1), LookupSwitch(op2, l2, keys2, ls2)) => op1 == op2 && sameLabel(l1, l2) && keys1 == keys2 && sameLabels(ls1, ls2) + case (TableSwitch(op1, min1, max1, l1, ls1), TableSwitch(op2, min2, max2, l2, ls2)) => op1 == op2 && min1 == min2 && max1 == max2 && sameLabel(l1, l2) && sameLabels(ls1, ls2) + case (LineNumber(line1, l1), LineNumber(line2, l2)) => line1 == line2 && sameLabel(l1, l2) + case (FrameEntry(tp1, loc1, stk1), FrameEntry(tp2, loc2, stk2)) => tp1 == tp2 && sameFrameTypes(loc1, loc2) && sameFrameTypes(stk1, stk2) + + // this needs to go after the above. For example, Label(1) may not equal Label(1), if before + // the left 1 was associated with another right index. + case (a, b) if a == b => true + + case _ => false + }) && equivalentBytecode(as.tail, bs.tail, varMap, labelMap) + } + + def applyToMethod(method: t.MethodNode, instructions: List[Instruction]): Unit = { + val asmLabel = createLabelNodes(instructions) + instructions.foreach(visitMethod(method, _, asmLabel)) + } + + /** + * Convert back a [[Method]] to ASM land. The code is emitted into the parameter `asmMethod`. + */ + def applyToMethod(asmMethod: t.MethodNode, method: Method): Unit = { + val asmLabel = createLabelNodes(method.instructions) + method.instructions.foreach(visitMethod(asmMethod, _, asmLabel)) + method.handlers.foreach(h => asmMethod.visitTryCatchBlock(asmLabel(h.start), asmLabel(h.end), asmLabel(h.handler), h.desc.orNull)) + method.localVars.foreach(v => asmMethod.visitLocalVariable(v.name, v.desc, v.signature.orNull, asmLabel(v.start), asmLabel(v.end), v.index)) + } + + private def createLabelNodes(instructions: List[Instruction]): Map[Label, asm.Label] = { + val labels = instructions collect { + case l: Label => l + } + assert(labels.distinct == labels, s"Duplicate labels in: $labels") + labels.map(l => (l, new asm.Label())).toMap + } + + private def frameTypesToAsm(l: List[Any], asmLabel: Map[Label, asm.Label]): List[Object] = l map { + case l: Label => asmLabel(l) + case x => x.asInstanceOf[Object] + } + + def unconvertMethodHandle(h: MethodHandle): asm.Handle = new asm.Handle(h.tag, h.owner, h.name, h.desc) + def unconvertBsmArgs(a: List[Object]): Array[Object] = a.map({ + case h: MethodHandle => unconvertMethodHandle(h) + case o => o + })(collection.breakOut) + + private def visitMethod(method: t.MethodNode, instruction: Instruction, asmLabel: Map[Label, asm.Label]): Unit = instruction match { + case Field(op, owner, name, desc) => method.visitFieldInsn(op, owner, name, desc) + case Incr(op, vr, incr) => method.visitIincInsn(vr, incr) + case Op(op) => method.visitInsn(op) + case IntOp(op, operand) => method.visitIntInsn(op, operand) + case Jump(op, label) => method.visitJumpInsn(op, asmLabel(label)) + case Ldc(op, cst) => method.visitLdcInsn(cst) + case LookupSwitch(op, dflt, keys, labels) => method.visitLookupSwitchInsn(asmLabel(dflt), keys.toArray, (labels map asmLabel).toArray) + case TableSwitch(op, min, max, dflt, labels) => method.visitTableSwitchInsn(min, max, asmLabel(dflt), (labels map asmLabel).toArray: _*) + case Invoke(op, owner, name, desc, itf) => method.visitMethodInsn(op, owner, name, desc, itf) + case InvokeDynamic(op, name, desc, bsm, bsmArgs) => method.visitInvokeDynamicInsn(name, desc, unconvertMethodHandle(bsm), unconvertBsmArgs(bsmArgs)) + case NewArray(op, desc, dims) => method.visitMultiANewArrayInsn(desc, dims) + case TypeOp(op, desc) => method.visitTypeInsn(op, desc) + case VarOp(op, vr) => method.visitVarInsn(op, vr) + case l: Label => method.visitLabel(asmLabel(l)) + case FrameEntry(tp, local, stack) => method.visitFrame(tp, local.length, frameTypesToAsm(local, asmLabel).toArray, stack.length, frameTypesToAsm(stack, asmLabel).toArray) + case LineNumber(line, start) => method.visitLineNumber(line, asmLabel(start)) + } +} diff --git a/test/test/AsmNode.scala b/test/test/AsmNode.scala new file mode 100644 index 000000000..80211e2af --- /dev/null +++ b/test/test/AsmNode.scala @@ -0,0 +1,61 @@ +package test + +import java.lang.reflect.Modifier +import scala.tools.asm +import asm._ +import asm.tree._ +import scala.collection.JavaConverters._ + +sealed trait AsmNode[+T] { + def node: T + def access: Int + def desc: String + def name: String + def signature: String + def attrs: List[Attribute] + def visibleAnnotations: List[AnnotationNode] + def invisibleAnnotations: List[AnnotationNode] + def characteristics = f"$name%15s $desc%-30s$accessString$sigString" + def erasedCharacteristics = f"$name%15s $desc%-30s$accessString" + + private def accessString = if (access == 0) "" else " " + Modifier.toString(access) + private def sigString = if (signature == null) "" else " " + signature + override def toString = characteristics +} + +object AsmNode { + type AsmMethod = AsmNode[MethodNode] + type AsmField = AsmNode[FieldNode] + type AsmMember = AsmNode[_] + + implicit class ClassNodeOps(val node: ClassNode) { + def fieldsAndMethods: List[AsmMember] = { + val xs: List[AsmMember] = ( + node.methods.asScala.toList.map(x => (x: AsmMethod)) + ++ node.fields.asScala.toList.map(x => (x: AsmField)) + ) + xs sortBy (_.characteristics) + } + } + implicit class AsmMethodNode(val node: MethodNode) extends AsmNode[MethodNode] { + def access: Int = node.access + def desc: String = node.desc + def name: String = node.name + def signature: String = node.signature + def attrs: List[Attribute] = node.attrs.asScala.toList + def visibleAnnotations: List[AnnotationNode] = node.visibleAnnotations.asScala.toList + def invisibleAnnotations: List[AnnotationNode] = node.invisibleAnnotations.asScala.toList + } + implicit class AsmFieldNode(val node: FieldNode) extends AsmNode[FieldNode] { + def access: Int = node.access + def desc: String = node.desc + def name: String = node.name + def signature: String = node.signature + def attrs: List[Attribute] = node.attrs.asScala.toList + def visibleAnnotations: List[AnnotationNode] = node.visibleAnnotations.asScala.toList + def invisibleAnnotations: List[AnnotationNode] = node.invisibleAnnotations.asScala.toList + } + + def apply(node: MethodNode): AsmMethodNode = new AsmMethodNode(node) + def apply(node: FieldNode): AsmFieldNode = new AsmFieldNode(node) +} diff --git a/test/test/DottyBytecodeTest.scala b/test/test/DottyBytecodeTest.scala new file mode 100644 index 000000000..f2218d4b6 --- /dev/null +++ b/test/test/DottyBytecodeTest.scala @@ -0,0 +1,208 @@ +package test + +import dotty.tools.dotc.core.Contexts.{Context, ContextBase} +import dotty.tools.dotc.core.Phases.Phase +import dotty.tools.backend.jvm.GenBCode +import dotty.tools.dotc.Compiler + +import scala.reflect.io.{VirtualDirectory => Directory} +import scala.tools.asm +import asm._ +import asm.tree._ +import scala.collection.JavaConverters._ + +import scala.tools.nsc.util.JavaClassPath +import scala.collection.JavaConverters._ +import scala.tools.asm.{ClassWriter, ClassReader} +import scala.tools.asm.tree._ +import java.io.{File => JFile, InputStream} + +private[test] class TestGenBCode(val outDir: String) extends GenBCode { + override def phaseName: String = "testGenBCode" + val virtualDir = new Directory(outDir, None) + override def outputDir(implicit ctx: Context) = virtualDir +} + +trait DottyBytecodeTest extends DottyTest { + import AsmNode._ + import ASMConverters._ + + protected object Opcode { + val newarray = 188 + val anewarray = 189 + val multianewarray = 197 + + val boolean = 4 + val char = 5 + val float = 6 + val double = 7 + val byte = 8 + val short = 9 + val int = 10 + val long = 11 + + val boxedUnit = "scala/runtime/BoxedUnit" + val javaString = "java/lang/String" + } + + private def bCodeCheckingComp(phase: TestGenBCode)(check: Directory => Unit) = + new Compiler { + override def phases = { + val updatedPhases = { + def replacePhase: Phase => Phase = + { p => if (p.phaseName == "genBCode") phase else p } + + for (phaseList <- super.phases) yield phaseList.map(replacePhase) + } + + val checkerPhase = List(List(new Phase { + def phaseName = "assertionChecker" + override def run(implicit ctx: Context): Unit = + check(phase.virtualDir) + })) + + updatedPhases ::: checkerPhase + } + } + + private def outPath(obj: Any) = + "/genBCodeTest" + math.abs(obj.hashCode) + System.currentTimeMillis + + /** Checks source code from raw string */ + def checkBCode(source: String)(assertion: Directory => Unit) = { + val comp = bCodeCheckingComp(new TestGenBCode(outPath(source)))(assertion) + comp.rootContext(ctx) + comp.newRun.compile(source) + } + + /** Checks actual _files_ referenced in `sources` list */ + def checkBCode(sources: List[String])(assertion: Directory => Unit) = { + val comp = bCodeCheckingComp(new TestGenBCode(outPath(sources)))(assertion) + comp.rootContext(ctx) + comp.newRun.compile(sources) + } + + protected def loadClassNode(input: InputStream, skipDebugInfo: Boolean = true): ClassNode = { + val cr = new ClassReader(input) + val cn = new ClassNode() + cr.accept(cn, if (skipDebugInfo) ClassReader.SKIP_DEBUG else 0) + cn + } + + protected def getMethod(classNode: ClassNode, name: String): MethodNode = + classNode.methods.asScala.find(_.name == name) getOrElse + sys.error(s"Didn't find method '$name' in class '${classNode.name}'") + + def diffInstructions(isa: List[Instruction], isb: List[Instruction]): String = { + val len = Math.max(isa.length, isb.length) + val sb = new StringBuilder + if (len > 0 ) { + val width = isa.map(_.toString.length).max + val lineWidth = len.toString.length + (1 to len) foreach { line => + val isaPadded = isa.map(_.toString) orElse Stream.continually("") + val isbPadded = isb.map(_.toString) orElse Stream.continually("") + val a = isaPadded(line-1) + val b = isbPadded(line-1) + + sb append (s"""$line${" " * (lineWidth-line.toString.length)} ${if (a==b) "==" else "<>"} $a${" " * (width-a.length)} | $b""") + } + } + sb.toString + } + + /**************************** Comparison Methods ****************************/ + def verifySwitch(method: MethodNode, shouldFail: Boolean = false, debug: Boolean = false): Boolean = { + val instructions = instructionsFromMethod(method) + + val succ = instructions + .collect { + case x: TableSwitch => x + case x: LookupSwitch => x + } + .length > 0 + + if (debug || !succ && !shouldFail || succ && shouldFail) + instructions.foreach(Console.err.println) + + succ && !shouldFail || shouldFail && !succ + } + + def sameBytecode(methA: MethodNode, methB: MethodNode) = { + val isa = instructionsFromMethod(methA) + val isb = instructionsFromMethod(methB) + assert(isa == isb, s"Bytecode wasn't same:\n${diffInstructions(isa, isb)}") + } + + def similarBytecode( + methA: MethodNode, + methB: MethodNode, + similar: (List[Instruction], List[Instruction]) => Boolean + ) = { + val isa = instructionsFromMethod(methA) + val isb = instructionsFromMethod(methB) + assert( + similar(isa, isb), + s"""|Bytecode wasn't similar according to the provided predicate: + |${diffInstructions(isa, isb)}""".stripMargin) + } + + def sameMethodAndFieldSignatures(clazzA: ClassNode, clazzB: ClassNode) = + sameCharacteristics(clazzA, clazzB)(_.characteristics) + + /** + * Same as sameMethodAndFieldSignatures, but ignoring generic signatures. + * This allows for methods which receive the same descriptor but differing + * generic signatures. In particular, this happens with value classes, which + * get a generic signature where a method written in terms of the underlying + * values does not. + */ + def sameMethodAndFieldDescriptors(clazzA: ClassNode, clazzB: ClassNode): Unit = { + val (succ, msg) = sameCharacteristics(clazzA, clazzB)(_.erasedCharacteristics) + assert(succ, msg) + } + + private def sameCharacteristics(clazzA: ClassNode, clazzB: ClassNode)(f: AsmNode[_] => String): (Boolean, String) = { + val ms1 = clazzA.fieldsAndMethods.toIndexedSeq + val ms2 = clazzB.fieldsAndMethods.toIndexedSeq + val name1 = clazzA.name + val name2 = clazzB.name + + if (ms1.length != ms2.length) { + (false, s"Different member counts in $name1 and $name2") + } else { + val msg = new StringBuilder + val success = (ms1, ms2).zipped forall { (m1, m2) => + val c1 = f(m1) + val c2 = f(m2).replaceAllLiterally(name2, name1) + if (c1 == c2) + msg append (s"[ok] $m1") + else + msg append (s"[fail]\n in $name1: $c1\n in $name2: $c2") + + c1 == c2 + } + + (success, msg.toString) + } + } + + def correctNumberOfNullChecks(expectedChecks: Int, insnList: InsnList) = { + /** Is given instruction a null check? + * + * This will detect direct null comparison as in + * if (x == null) ... + * and not indirect as in + * val foo = null + * if (x == foo) ... + */ + def isNullCheck(node: asm.tree.AbstractInsnNode): Boolean = { + val opcode = node.getOpcode + (opcode == asm.Opcodes.IFNULL) || (opcode == asm.Opcodes.IFNONNULL) + } + val actualChecks = insnList.iterator.asScala.count(isNullCheck) + assert(expectedChecks == actualChecks, + s"Wrong number of null checks ($actualChecks), expected: $expectedChecks" + ) + } +} diff --git a/test/test/DottyBytecodeTests.scala b/test/test/DottyBytecodeTests.scala new file mode 100644 index 000000000..5b5989474 --- /dev/null +++ b/test/test/DottyBytecodeTests.scala @@ -0,0 +1,188 @@ +package test + +import org.junit.Assert._ +import org.junit.Test + +class TestBCode extends DottyBytecodeTest { + import ASMConverters._ + @Test def nullChecks = { + val source = """ + |class Foo { + | def foo(x: AnyRef): Int = { + | val bool = x == null + | if (x != null) 1 + | else 0 + | } + |} + """.stripMargin + + checkBCode(source) { dir => + val clsIn = dir.lookupName("Foo.class", directory = false).input + val clsNode = loadClassNode(clsIn) + val methodNode = getMethod(clsNode, "foo") + correctNumberOfNullChecks(2, methodNode.instructions) + } + } + + /** This test verifies that simple matches are transformed if possible + * despite no annotation + */ + @Test def basicTransformNonAnnotated = { + val source = """ + |object Foo { + | def foo(i: Int) = i match { + | case 2 => println(2) + | case 1 => println(1) + | } + |}""".stripMargin + + checkBCode(source) { dir => + val moduleIn = dir.lookupName("Foo$.class", directory = false) + val moduleNode = loadClassNode(moduleIn.input) + val methodNode = getMethod(moduleNode, "foo") + assert(verifySwitch(methodNode)) + } + } + + /** This test verifies that simple matches with `@switch` annotations are + * indeed transformed to a switch + */ + @Test def basicTransfromAnnotated = { + val source = """ + |object Foo { + | import scala.annotation.switch + | def foo(i: Int) = (i: @switch) match { + | case 2 => println(2) + | case 1 => println(1) + | } + |}""".stripMargin + + checkBCode(source) { dir => + val moduleIn = dir.lookupName("Foo$.class", directory = false) + val moduleNode = loadClassNode(moduleIn.input) + val methodNode = getMethod(moduleNode, "foo") + assert(verifySwitch(methodNode)) + } + } + + @Test def failTransform = { + val source = """ + |object Foo { + | import scala.annotation.switch + | def foo(i: Any) = (i: @switch) match { + | case x: String => println("string!") + | case x :: xs => println("list!") + | } + |}""".stripMargin + checkBCode(source) { dir => + val moduleIn = dir.lookupName("Foo$.class", directory = false) + val moduleNode = loadClassNode(moduleIn.input) + val methodNode = getMethod(moduleNode, "foo") + + assert(verifySwitch(methodNode, shouldFail = true)) + } + } + + /** Make sure that creating multidim arrays reduces to "multinewarray" + * instruction + */ + @Test def multidimArraysFromOfDim = { + val source = """ + |object Arr { + | def arr = Array.ofDim[Int](2, 1) + |}""".stripMargin + checkBCode(source) { dir => + val moduleIn = dir.lookupName("Arr$.class", directory = false) + val moduleNode = loadClassNode(moduleIn.input) + val method = getMethod(moduleNode, "arr") + + val hadCorrectInstr = + instructionsFromMethod(method) + .collect { + case x @ NewArray(op, _, dims) + if op == Opcode.multianewarray && dims == 2 => x + } + .length > 0 + + assert(hadCorrectInstr, + "Did not contain \"multianewarray\" instruction in:\n" + + instructionsFromMethod(method).mkString("\n")) + } + } + + @Test def arraysFromOfDim = { + val source = """ + |object Arr { + | def arr1 = Array.ofDim[Int](2) + | def arr2 = Array.ofDim[Unit](2) + | def arr3 = Array.ofDim[String](2) + | def arr4 = Array.ofDim[Map[String, String]](2) + |}""".stripMargin + checkBCode(source) { dir => + val moduleIn = dir.lookupName("Arr$.class", directory = false) + val moduleNode = loadClassNode(moduleIn.input) + val arr1 = getMethod(moduleNode, "arr1") + val arr2 = getMethod(moduleNode, "arr2") + val arr3 = getMethod(moduleNode, "arr3") + + val arr1CorrectInstr = + instructionsFromMethod(arr1) + .collect { + case x @ IntOp(op, oprnd) + if op == Opcode.newarray && oprnd == Opcode.int => x + } + .length > 0 + + assert(arr1CorrectInstr, + "Did not contain \"multianewarray\" instruction in:\n" + + instructionsFromMethod(arr1).mkString("\n")) + + val arr2CorrectInstr = + instructionsFromMethod(arr2) + .collect { + case x @ TypeOp(op, oprnd) + if op == Opcode.anewarray && oprnd == Opcode.boxedUnit => x + } + .length > 0 + + assert(arr2CorrectInstr, + "arr2 bytecode did not contain correct `anewarray` instruction:\n" + + instructionsFromMethod(arr2)mkString("\n")) + + val arr3CorrectInstr = + instructionsFromMethod(arr3) + .collect { + case x @ TypeOp(op, oprnd) + if op == Opcode.anewarray && oprnd == Opcode.javaString => x + } + .length > 0 + + assert(arr3CorrectInstr, + "arr3 bytecode did not contain correct `anewarray` instruction:\n" + + instructionsFromMethod(arr3).mkString("\n")) + } + } + + @Test def arraysFromDimAndFromNewEqual = { + val source = """ + |object Arr { + | def arr1 = Array.ofDim[Int](2) + | def arr2 = new Array[Int](2) + |}""".stripMargin + + checkBCode(source) { dir => + val moduleIn = dir.lookupName("Arr$.class", directory = false) + val moduleNode = loadClassNode(moduleIn.input) + val arr1 = getMethod(moduleNode, "arr1") + val arr2 = getMethod(moduleNode, "arr2") + + // First two instructions of `arr1` fetch the static reference to `Array` + val instructions1 = instructionsFromMethod(arr1).drop(2) + val instructions2 = instructionsFromMethod(arr2) + + assert(instructions1 == instructions2, + "Creating arrays using `Array.ofDim[Int](2)` did not equal bytecode for `new Array[Int](2)`\n" + + diffInstructions(instructions1, instructions2)) + } + } +} |