Skip to content

Commit 3ee785a

Browse files
committed
Merge pull request #1256 from felixmulder/topic/test-bcode
Add bytecode checking infrastructure
2 parents 4b35f6f + 772f3d2 commit 3ee785a

File tree

8 files changed

+728
-4
lines changed

8 files changed

+728
-4
lines changed

AUTHORS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,10 @@ The majority of the dotty codebase is new code, with the exception of the compon
4545
`dotty.tools.io`
4646

4747
> The I/O support library was adapted from current Scala compiler. Original authors were Paul Phillips and others.
48+
49+
`dotty.test.DottyBytecodeTest`
50+
51+
> Is an adaptation of the bytecode testing from
52+
> [scala/scala](https://github.com/scala/scala). It has been reworked to fit
53+
> the needs of dotty. Original authors include: Adrian Moors, Lukas Rytz,
54+
> Grzegorz Kossakowski, Paul Phillips

src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,8 @@ class Definitions {
486486
def TASTYLongSignatureAnnot(implicit ctx: Context) = TASTYLongSignatureAnnotType.symbol.asClass
487487
lazy val TailrecAnnotType = ctx.requiredClassRef("scala.annotation.tailrec")
488488
def TailrecAnnot(implicit ctx: Context) = TailrecAnnotType.symbol.asClass
489+
lazy val SwitchAnnotType = ctx.requiredClassRef("scala.annotation.switch")
490+
def SwitchAnnot(implicit ctx: Context) = SwitchAnnotType.symbol.asClass
489491
lazy val ThrowsAnnotType = ctx.requiredClassRef("scala.throws")
490492
def ThrowsAnnot(implicit ctx: Context) = ThrowsAnnotType.symbol.asClass
491493
lazy val TransientAnnotType = ctx.requiredClassRef("scala.transient")

src/dotty/tools/dotc/core/Types.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ object Types {
107107
* It makes no sense for it to be an alias type because isRef would always
108108
* return false in that case.
109109
*/
110-
def isRef(sym: Symbol)(implicit ctx: Context): Boolean = stripTypeVar match {
110+
def isRef(sym: Symbol)(implicit ctx: Context): Boolean = stripAnnots.stripTypeVar match {
111111
case this1: TypeRef =>
112112
this1.info match { // see comment in Namer#typeDefSig
113113
case TypeAlias(tp) => tp.isRef(sym)

src/dotty/tools/dotc/transform/PatternMatcher.scala

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -306,12 +306,11 @@ class PatternMatcher extends MiniPhaseTransform with DenotTransformer {thisTrans
306306
def emitSwitch(scrut: Tree, scrutSym: Symbol, cases: List[List[TreeMaker]], pt: Type, matchFailGenOverride: Option[Symbol => Tree], unchecked: Boolean): Option[Tree] = {
307307
// TODO Deal with guards?
308308

309-
def isSwitchableType(tpe: Type): Boolean = {
309+
def isSwitchableType(tpe: Type): Boolean =
310310
(tpe isRef defn.IntClass) ||
311311
(tpe isRef defn.ByteClass) ||
312312
(tpe isRef defn.ShortClass) ||
313313
(tpe isRef defn.CharClass)
314-
}
315314

316315
object IntEqualityTestTreeMaker {
317316
def unapply(treeMaker: EqualityTestTreeMaker): Option[Int] = treeMaker match {
@@ -423,7 +422,8 @@ class PatternMatcher extends MiniPhaseTransform with DenotTransformer {thisTrans
423422
Match(intScrut, newCases :+ defaultCase)
424423
}
425424

426-
if (isSwitchableType(scrut.tpe.widenDealias) && cases.forall(isSwitchCase)) {
425+
val dealiased = scrut.tpe.widenDealias
426+
if (isSwitchableType(dealiased) && cases.forall(isSwitchCase)) {
427427
val valuesToCases = cases.map(extractSwitchCase)
428428
val values = valuesToCases.map(_._1)
429429
if (values.tails.exists { tail => tail.nonEmpty && tail.tail.exists(doOverlap(_, tail.head)) }) {
@@ -433,6 +433,8 @@ class PatternMatcher extends MiniPhaseTransform with DenotTransformer {thisTrans
433433
Some(makeSwitch(valuesToCases))
434434
}
435435
} else {
436+
if (dealiased hasAnnotation defn.SwitchAnnot)
437+
ctx.warning("failed to emit switch for `@switch` annotated match")
436438
None
437439
}
438440
}

test/test/AsmConverters.scala

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
package test
2+
3+
import scala.tools.asm
4+
import asm._
5+
import asm.tree._
6+
import scala.collection.JavaConverters._
7+
8+
/** Makes using ASM from tests more convenient.
9+
*
10+
* Wraps ASM instructions in case classes so that equals and toString work
11+
* for the purpose of bytecode diffing and pretty printing.
12+
*/
13+
object ASMConverters {
14+
import asm.{tree => t}
15+
16+
/**
17+
* Transform the instructions of an ASM Method into a list of [[Instruction]]s.
18+
*/
19+
def instructionsFromMethod(meth: t.MethodNode): List[Instruction] = new AsmToScala(meth).instructions
20+
21+
def convertMethod(meth: t.MethodNode): Method = new AsmToScala(meth).method
22+
23+
implicit class RichInstructionLists(val self: List[Instruction]) extends AnyVal {
24+
def === (other: List[Instruction]) = equivalentBytecode(self, other)
25+
26+
def dropLinesFrames = self.filterNot(i => i.isInstanceOf[LineNumber] || i.isInstanceOf[FrameEntry])
27+
28+
private def referencedLabels(instruction: Instruction): Set[Instruction] = instruction match {
29+
case Jump(op, label) => Set(label)
30+
case LookupSwitch(op, dflt, keys, labels) => (dflt :: labels).toSet
31+
case TableSwitch(op, min, max, dflt, labels) => (dflt :: labels).toSet
32+
case LineNumber(line, start) => Set(start)
33+
case _ => Set.empty
34+
}
35+
36+
def dropStaleLabels = {
37+
val definedLabels: Set[Instruction] = self.filter(_.isInstanceOf[Label]).toSet
38+
val usedLabels: Set[Instruction] = self.flatMap(referencedLabels)(collection.breakOut)
39+
self.filterNot(definedLabels diff usedLabels)
40+
}
41+
42+
def dropNonOp = dropLinesFrames.dropStaleLabels
43+
44+
def summary: List[Any] = dropNonOp map {
45+
case i: Invoke => i.name
46+
case i => i.opcode
47+
}
48+
49+
def summaryText: String = {
50+
def comment(i: Instruction) = i match {
51+
case j: Jump => s" /*${j.label.offset}*/"
52+
case l: Label => s" /*${l.offset}*/"
53+
case _ => ""
54+
}
55+
dropNonOp.map({
56+
case i: Invoke => s""""${i.name}""""
57+
case ins => opcodeToString(ins.opcode, ins.opcode) + comment(ins)
58+
}).mkString("List(", ", ", ")")
59+
}
60+
}
61+
62+
def opcodeToString(op: Int, default: Any = "?"): String = {
63+
import scala.tools.asm.util.Printer.OPCODES
64+
if (OPCODES.isDefinedAt(op)) OPCODES(op) else default.toString
65+
}
66+
67+
sealed abstract class Instruction extends Product {
68+
def opcode: Int
69+
70+
// toString such that the first field, "opcode: Int", is printed textually.
71+
final override def toString() = {
72+
val printOpcode = opcode != -1
73+
productPrefix + (
74+
if (printOpcode) Iterator(opcodeToString(opcode)) ++ productIterator.drop(1)
75+
else productIterator
76+
).mkString("(", ", ", ")")
77+
}
78+
}
79+
80+
case class Method(instructions: List[Instruction], handlers: List[ExceptionHandler], localVars: List[LocalVariable])
81+
82+
case class Field (opcode: Int, owner: String, name: String, desc: String) extends Instruction
83+
case class Incr (opcode: Int, `var`: Int, incr: Int) extends Instruction
84+
case class Op (opcode: Int) extends Instruction
85+
case class IntOp (opcode: Int, operand: Int) extends Instruction
86+
case class Jump (opcode: Int, label: Label) extends Instruction
87+
case class Ldc (opcode: Int, cst: Any) extends Instruction
88+
case class LookupSwitch (opcode: Int, dflt: Label, keys: List[Int], labels: List[Label]) extends Instruction
89+
case class TableSwitch (opcode: Int, min: Int, max: Int, dflt: Label, labels: List[Label]) extends Instruction
90+
case class Invoke (opcode: Int, owner: String, name: String, desc: String, itf: Boolean) extends Instruction
91+
case class InvokeDynamic(opcode: Int, name: String, desc: String, bsm: MethodHandle, bsmArgs: List[AnyRef]) extends Instruction
92+
case class NewArray (opcode: Int, desc: String, dims: Int) extends Instruction
93+
case class TypeOp (opcode: Int, desc: String) extends Instruction
94+
case class VarOp (opcode: Int, `var`: Int) extends Instruction
95+
case class Label (offset: Int) extends Instruction { def opcode: Int = -1 }
96+
case class FrameEntry (`type`: Int, local: List[Any], stack: List[Any]) extends Instruction { def opcode: Int = -1 }
97+
case class LineNumber (line: Int, start: Label) extends Instruction { def opcode: Int = -1 }
98+
99+
case class MethodHandle(tag: Int, owner: String, name: String, desc: String)
100+
101+
case class ExceptionHandler(start: Label, end: Label, handler: Label, desc: Option[String])
102+
case class LocalVariable(name: String, desc: String, signature: Option[String], start: Label, end: Label, index: Int)
103+
104+
class AsmToScala(asmMethod: t.MethodNode) {
105+
106+
def instructions: List[Instruction] = asmMethod.instructions.iterator.asScala.toList map apply
107+
108+
def method: Method = Method(instructions, convertHandlers(asmMethod), convertLocalVars(asmMethod))
109+
110+
private def labelIndex(l: t.LabelNode): Int = asmMethod.instructions.indexOf(l)
111+
112+
private def op(i: t.AbstractInsnNode): Int = i.getOpcode
113+
114+
private def lst[T](xs: java.util.List[T]): List[T] = if (xs == null) Nil else xs.asScala.toList
115+
116+
// Heterogeneous List[Any] is used in FrameNode: type information about locals / stack values
117+
// are stored in a List[Any] (Integer, String or LabelNode), see Javadoc of MethodNode#visitFrame.
118+
// Opcodes (eg Opcodes.INTEGER) and Reference types (eg "java/lang/Object") are returned unchanged,
119+
// LabelNodes are mapped to their LabelEntry.
120+
private def mapOverFrameTypes(is: List[Any]): List[Any] = is map {
121+
case i: t.LabelNode => applyLabel(i)
122+
case x => x
123+
}
124+
125+
// avoids some casts
126+
private def applyLabel(l: t.LabelNode) = this(l: t.AbstractInsnNode).asInstanceOf[Label]
127+
128+
private def apply(x: t.AbstractInsnNode): Instruction = x match {
129+
case i: t.FieldInsnNode => Field (op(i), i.owner, i.name, i.desc)
130+
case i: t.IincInsnNode => Incr (op(i), i.`var`, i.incr)
131+
case i: t.InsnNode => Op (op(i))
132+
case i: t.IntInsnNode => IntOp (op(i), i.operand)
133+
case i: t.JumpInsnNode => Jump (op(i), applyLabel(i.label))
134+
case i: t.LdcInsnNode => Ldc (op(i), i.cst: Any)
135+
case i: t.LookupSwitchInsnNode => LookupSwitch (op(i), applyLabel(i.dflt), lst(i.keys) map (x => x: Int), lst(i.labels) map applyLabel)
136+
case i: t.TableSwitchInsnNode => TableSwitch (op(i), i.min, i.max, applyLabel(i.dflt), lst(i.labels) map applyLabel)
137+
case i: t.MethodInsnNode => Invoke (op(i), i.owner, i.name, i.desc, i.itf)
138+
case i: t.InvokeDynamicInsnNode => InvokeDynamic(op(i), i.name, i.desc, convertMethodHandle(i.bsm), convertBsmArgs(i.bsmArgs))
139+
case i: t.MultiANewArrayInsnNode => NewArray (op(i), i.desc, i.dims)
140+
case i: t.TypeInsnNode => TypeOp (op(i), i.desc)
141+
case i: t.VarInsnNode => VarOp (op(i), i.`var`)
142+
case i: t.LabelNode => Label (labelIndex(i))
143+
case i: t.FrameNode => FrameEntry (i.`type`, mapOverFrameTypes(lst(i.local)), mapOverFrameTypes(lst(i.stack)))
144+
case i: t.LineNumberNode => LineNumber (i.line, applyLabel(i.start))
145+
}
146+
147+
private def convertBsmArgs(a: Array[Object]): List[Object] = a.map({
148+
case h: asm.Handle => convertMethodHandle(h)
149+
case _ => a // can be: Class, method Type, primitive constant
150+
})(collection.breakOut)
151+
152+
private def convertMethodHandle(h: asm.Handle): MethodHandle = MethodHandle(h.getTag, h.getOwner, h.getName, h.getDesc)
153+
154+
private def convertHandlers(method: t.MethodNode): List[ExceptionHandler] = {
155+
method.tryCatchBlocks.asScala.map(h => ExceptionHandler(applyLabel(h.start), applyLabel(h.end), applyLabel(h.handler), Option(h.`type`)))(collection.breakOut)
156+
}
157+
158+
private def convertLocalVars(method: t.MethodNode): List[LocalVariable] = {
159+
method.localVariables.asScala.map(v => LocalVariable(v.name, v.desc, Option(v.signature), applyLabel(v.start), applyLabel(v.end), v.index))(collection.breakOut)
160+
}
161+
}
162+
163+
import collection.mutable.{Map => MMap}
164+
165+
/**
166+
* Bytecode is equal modulo local variable numbering and label numbering.
167+
*/
168+
def equivalentBytecode(as: List[Instruction], bs: List[Instruction], varMap: MMap[Int, Int] = MMap(), labelMap: MMap[Int, Int] = MMap()): Boolean = {
169+
def same(v1: Int, v2: Int, m: MMap[Int, Int]) = {
170+
if (m contains v1) m(v1) == v2
171+
else if (m.valuesIterator contains v2) false // v2 is already associated with some different value v1
172+
else { m(v1) = v2; true }
173+
}
174+
def sameVar(v1: Int, v2: Int) = same(v1, v2, varMap)
175+
def sameLabel(l1: Label, l2: Label) = same(l1.offset, l2.offset, labelMap)
176+
def sameLabels(ls1: List[Label], ls2: List[Label]) = (ls1 corresponds ls2)(sameLabel)
177+
178+
def sameFrameTypes(ts1: List[Any], ts2: List[Any]) = (ts1 corresponds ts2) {
179+
case (t1: Label, t2: Label) => sameLabel(t1, t2)
180+
case (x, y) => x == y
181+
}
182+
183+
if (as.isEmpty) bs.isEmpty
184+
else if (bs.isEmpty) false
185+
else ((as.head, bs.head) match {
186+
case (VarOp(op1, v1), VarOp(op2, v2)) => op1 == op2 && sameVar(v1, v2)
187+
case (Incr(op1, v1, inc1), Incr(op2, v2, inc2)) => op1 == op2 && sameVar(v1, v2) && inc1 == inc2
188+
189+
case (l1 @ Label(_), l2 @ Label(_)) => sameLabel(l1, l2)
190+
case (Jump(op1, l1), Jump(op2, l2)) => op1 == op2 && sameLabel(l1, l2)
191+
case (LookupSwitch(op1, l1, keys1, ls1), LookupSwitch(op2, l2, keys2, ls2)) => op1 == op2 && sameLabel(l1, l2) && keys1 == keys2 && sameLabels(ls1, ls2)
192+
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)
193+
case (LineNumber(line1, l1), LineNumber(line2, l2)) => line1 == line2 && sameLabel(l1, l2)
194+
case (FrameEntry(tp1, loc1, stk1), FrameEntry(tp2, loc2, stk2)) => tp1 == tp2 && sameFrameTypes(loc1, loc2) && sameFrameTypes(stk1, stk2)
195+
196+
// this needs to go after the above. For example, Label(1) may not equal Label(1), if before
197+
// the left 1 was associated with another right index.
198+
case (a, b) if a == b => true
199+
200+
case _ => false
201+
}) && equivalentBytecode(as.tail, bs.tail, varMap, labelMap)
202+
}
203+
204+
def applyToMethod(method: t.MethodNode, instructions: List[Instruction]): Unit = {
205+
val asmLabel = createLabelNodes(instructions)
206+
instructions.foreach(visitMethod(method, _, asmLabel))
207+
}
208+
209+
/**
210+
* Convert back a [[Method]] to ASM land. The code is emitted into the parameter `asmMethod`.
211+
*/
212+
def applyToMethod(asmMethod: t.MethodNode, method: Method): Unit = {
213+
val asmLabel = createLabelNodes(method.instructions)
214+
method.instructions.foreach(visitMethod(asmMethod, _, asmLabel))
215+
method.handlers.foreach(h => asmMethod.visitTryCatchBlock(asmLabel(h.start), asmLabel(h.end), asmLabel(h.handler), h.desc.orNull))
216+
method.localVars.foreach(v => asmMethod.visitLocalVariable(v.name, v.desc, v.signature.orNull, asmLabel(v.start), asmLabel(v.end), v.index))
217+
}
218+
219+
private def createLabelNodes(instructions: List[Instruction]): Map[Label, asm.Label] = {
220+
val labels = instructions collect {
221+
case l: Label => l
222+
}
223+
assert(labels.distinct == labels, s"Duplicate labels in: $labels")
224+
labels.map(l => (l, new asm.Label())).toMap
225+
}
226+
227+
private def frameTypesToAsm(l: List[Any], asmLabel: Map[Label, asm.Label]): List[Object] = l map {
228+
case l: Label => asmLabel(l)
229+
case x => x.asInstanceOf[Object]
230+
}
231+
232+
def unconvertMethodHandle(h: MethodHandle): asm.Handle = new asm.Handle(h.tag, h.owner, h.name, h.desc)
233+
def unconvertBsmArgs(a: List[Object]): Array[Object] = a.map({
234+
case h: MethodHandle => unconvertMethodHandle(h)
235+
case o => o
236+
})(collection.breakOut)
237+
238+
private def visitMethod(method: t.MethodNode, instruction: Instruction, asmLabel: Map[Label, asm.Label]): Unit = instruction match {
239+
case Field(op, owner, name, desc) => method.visitFieldInsn(op, owner, name, desc)
240+
case Incr(op, vr, incr) => method.visitIincInsn(vr, incr)
241+
case Op(op) => method.visitInsn(op)
242+
case IntOp(op, operand) => method.visitIntInsn(op, operand)
243+
case Jump(op, label) => method.visitJumpInsn(op, asmLabel(label))
244+
case Ldc(op, cst) => method.visitLdcInsn(cst)
245+
case LookupSwitch(op, dflt, keys, labels) => method.visitLookupSwitchInsn(asmLabel(dflt), keys.toArray, (labels map asmLabel).toArray)
246+
case TableSwitch(op, min, max, dflt, labels) => method.visitTableSwitchInsn(min, max, asmLabel(dflt), (labels map asmLabel).toArray: _*)
247+
case Invoke(op, owner, name, desc, itf) => method.visitMethodInsn(op, owner, name, desc, itf)
248+
case InvokeDynamic(op, name, desc, bsm, bsmArgs) => method.visitInvokeDynamicInsn(name, desc, unconvertMethodHandle(bsm), unconvertBsmArgs(bsmArgs))
249+
case NewArray(op, desc, dims) => method.visitMultiANewArrayInsn(desc, dims)
250+
case TypeOp(op, desc) => method.visitTypeInsn(op, desc)
251+
case VarOp(op, vr) => method.visitVarInsn(op, vr)
252+
case l: Label => method.visitLabel(asmLabel(l))
253+
case FrameEntry(tp, local, stack) => method.visitFrame(tp, local.length, frameTypesToAsm(local, asmLabel).toArray, stack.length, frameTypesToAsm(stack, asmLabel).toArray)
254+
case LineNumber(line, start) => method.visitLineNumber(line, asmLabel(start))
255+
}
256+
}

0 commit comments

Comments
 (0)