diff options
author | Lukas Rytz <lukas.rytz@gmail.com> | 2015-04-01 16:47:22 +0200 |
---|---|---|
committer | Lukas Rytz <lukas.rytz@gmail.com> | 2015-04-01 16:47:22 +0200 |
commit | dcd9a83916f9e0128ef6869def82d4f23bdea0e0 (patch) | |
tree | e58331aebe8ba06fd1c15faf36fdbb3f77b489b9 /test/junit | |
parent | ebf0976c363c67e6a46c66d70b39704f1ce5e74a (diff) | |
parent | fcc20fe4d3ac5caceb50965bc202b880e61f984c (diff) | |
download | scala-dcd9a83916f9e0128ef6869def82d4f23bdea0e0.tar.gz scala-dcd9a83916f9e0128ef6869def82d4f23bdea0e0.tar.bz2 scala-dcd9a83916f9e0128ef6869def82d4f23bdea0e0.zip |
Merge commit 'fcc20fe' into merge/2.11-to-2.12-apr-1
Diffstat (limited to 'test/junit')
33 files changed, 2307 insertions, 93 deletions
diff --git a/test/junit/scala/StringContextTest.scala b/test/junit/scala/StringContextTest.scala index 608b82bd96..7e9e775d58 100644 --- a/test/junit/scala/StringContextTest.scala +++ b/test/junit/scala/StringContextTest.scala @@ -65,14 +65,23 @@ class StringContextTest { @Test def fIf() = { val res = f"${if (true) 2.5 else 2.5}%.2f" - assertEquals("2.50", res) + val expected = formatUsingCurrentLocale(2.50) + assertEquals(expected, res) } + @Test def fIfNot() = { val res = f"${if (false) 2.5 else 3.5}%.2f" - assertEquals("3.50", res) + val expected = formatUsingCurrentLocale(3.50) + assertEquals(expected, res) } + @Test def fHeteroArgs() = { val res = f"${3.14}%.2f rounds to ${3}%d" - assertEquals("3.14 rounds to 3", res) + val expected = formatUsingCurrentLocale(3.14) + " rounds to 3" + assertEquals(expected, res) } + + // Use this method to avoid problems with a locale-dependent decimal mark. + // The string interpolation is not used here intentionally as this method is used to test string interpolation. + private def formatUsingCurrentLocale(number: Double, decimalPlaces: Int = 2) = ("%." + decimalPlaces + "f").format(number) } diff --git a/test/junit/scala/collection/IterableViewLikeTest.scala b/test/junit/scala/collection/IterableViewLikeTest.scala index 55da02744b..435a43c215 100644 --- a/test/junit/scala/collection/IterableViewLikeTest.scala +++ b/test/junit/scala/collection/IterableViewLikeTest.scala @@ -4,6 +4,7 @@ import org.junit.Assert._ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 +import language.postfixOps @RunWith(classOf[JUnit4]) class IterableViewLikeTest { @@ -12,6 +13,7 @@ class IterableViewLikeTest { def hasCorrectDropAndTakeMethods() { val iter = Iterable(1, 2, 3) + import scala.language.postfixOps assertEquals(Iterable.empty[Int], iter.view take Int.MinValue force) assertEquals(Iterable.empty[Int], iter.view takeRight Int.MinValue force) assertEquals(iter, iter.view drop Int.MinValue force) diff --git a/test/junit/scala/collection/ParallelConsistencyTest.scala b/test/junit/scala/collection/ParallelConsistencyTest.scala new file mode 100644 index 0000000000..da96362413 --- /dev/null +++ b/test/junit/scala/collection/ParallelConsistencyTest.scala @@ -0,0 +1,44 @@ +package scala.collection.immutable + +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(classOf[JUnit4]) +class ParallelConsistencyTest { + + private val theSeq = Seq(1,2,3) + + // This collection will throw an exception if you do anything but call .length or .seq + private val mustCallSeq: collection.GenSeq[Int] = new collection.parallel.ParSeq[Int] { + def length = 3 + + // This method is surely sequential & safe -- want all access to go through here + def seq = theSeq + + def notSeq = throw new Exception("Access to parallel collection not via .seq") + + // These methods could possibly be used dangerously explicitly or internally + // (apply could also be used safely; if it is, do test with mustCallSeq) + def apply(i: Int) = notSeq + def splitter = notSeq + } + + // Test Vector ++ with a small parallel collection concatenation (SI-9072). + @Test + def testPlusPlus(): Unit = { + assert((Vector.empty ++ mustCallSeq) == theSeq, "Vector ++ unsafe with parallel vectors") + } + + // SI-9126, 1 of 2 + @Test + def testTranspose(): Unit = { + assert(List(mustCallSeq).transpose.flatten == theSeq, "Transposing inner parallel collection unsafe") + } + + // SI-9126, 2 of 2 + @Test + def testList_flatMap(): Unit = { + assert(List(1).flatMap(_ => mustCallSeq) == theSeq, "List#flatMap on inner parallel collection unsafe") + } +} diff --git a/test/junit/scala/collection/immutable/StringLikeTest.scala b/test/junit/scala/collection/immutable/StringLikeTest.scala new file mode 100644 index 0000000000..3722bdfe4d --- /dev/null +++ b/test/junit/scala/collection/immutable/StringLikeTest.scala @@ -0,0 +1,37 @@ +package scala.collection.immutable + +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +import scala.tools.testing.AssertUtil +import scala.util.Random + +/* Test for SI-8988 */ +@RunWith(classOf[JUnit4]) +class StringLikeTest { + @Test + def testStringSplitWithChar: Unit = { + val chars = (0 to 255).map(_.toChar) + def randString = Random.nextString(30) + + for (c <- chars) { + val s = randString + val jString = new java.lang.String(s) + + // make sure we can match a literal character done by Java's split + val jSplit = jString.split("\\Q" + c.toString + "\\E") + val sSplit = s.split(c) + AssertUtil.assertSameElements(jSplit, sSplit, s"Not same result as Java split for char $c in string $s") + } + } + + @Test + def testSplitEdgeCases: Unit = { + AssertUtil.assertSameElements("abcd".split('d'), Array("abc")) // not Array("abc", "") + AssertUtil.assertSameElements("abccc".split('c'), Array("ab")) // not Array("ab", "", "", "") + AssertUtil.assertSameElements("xxx".split('x'), Array[String]()) // not Array("", "", "", "") + AssertUtil.assertSameElements("".split('x'), Array("")) // not Array() + AssertUtil.assertSameElements("--ch--omp--".split("-"), Array("", "", "ch", "", "omp")) // All the cases! + } +} diff --git a/test/junit/scala/collection/mutable/BitSetTest.scala b/test/junit/scala/collection/mutable/BitSetTest.scala index 8d164b50d4..d56cc45601 100644 --- a/test/junit/scala/collection/mutable/BitSetTest.scala +++ b/test/junit/scala/collection/mutable/BitSetTest.scala @@ -19,4 +19,13 @@ class BitSetTest { bitSet &~= bitSet assert(bitSet.toBitMask.length == size, "Capacity of bitset changed after &~=") } + + @Test def test_SI8917() { + val bigBitSet = BitSet(1, 100, 10000) + val littleBitSet = BitSet(100) + bigBitSet &= littleBitSet + assert(!(bigBitSet contains 10000), "&= not applied to the full bitset") + littleBitSet &= bigBitSet + assert(littleBitSet.toBitMask.length < bigBitSet.toBitMask.length, "Needlessly extended the size of bitset on &=") + } } diff --git a/test/junit/scala/collection/mutable/LinkedHashMapTest.scala b/test/junit/scala/collection/mutable/LinkedHashMapTest.scala new file mode 100644 index 0000000000..37dcd028a5 --- /dev/null +++ b/test/junit/scala/collection/mutable/LinkedHashMapTest.scala @@ -0,0 +1,25 @@ +package scala.collection.mutable + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.{ Assert, Test } + +import scala.collection.mutable + +/* Test for SI-9095 */ +@RunWith(classOf[JUnit4]) +class LinkedHashMapTest { + class TestClass extends mutable.LinkedHashMap[String, Int] { + def lastItemRef = lastEntry + } + + @Test + def testClear: Unit = { + val lhm = new TestClass + Seq("a" -> 8, "b" -> 9).foreach(kv => lhm.put(kv._1, kv._2)) + + Assert.assertNotNull(lhm.lastItemRef) + lhm.clear() + Assert.assertNull(lhm.lastItemRef) + } +} diff --git a/test/junit/scala/collection/mutable/LinkedHashSetTest.scala b/test/junit/scala/collection/mutable/LinkedHashSetTest.scala new file mode 100644 index 0000000000..b419ad37ec --- /dev/null +++ b/test/junit/scala/collection/mutable/LinkedHashSetTest.scala @@ -0,0 +1,25 @@ +package scala.collection.mutable + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.{ Assert, Test } + +import scala.collection.mutable + +/* Test for SI-9095 */ +@RunWith(classOf[JUnit4]) +class LinkedHashSetTest { + class TestClass extends mutable.LinkedHashSet[String] { + def lastItemRef = lastEntry + } + + @Test + def testClear: Unit = { + val lhs = new TestClass + Seq("a", "b").foreach(k => lhs.add(k)) + + Assert.assertNotNull(lhs.lastItemRef) + lhs.clear() + Assert.assertNull(lhs.lastItemRef) + } +} diff --git a/test/junit/scala/collection/mutable/MutableListTest.scala b/test/junit/scala/collection/mutable/MutableListTest.scala new file mode 100644 index 0000000000..ac6d30def0 --- /dev/null +++ b/test/junit/scala/collection/mutable/MutableListTest.scala @@ -0,0 +1,37 @@ +package scala.collection.mutable + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import org.junit.Assert._ + +import scala.tools.testing.AssertUtil._ + +@RunWith(classOf[JUnit4]) +class MutableListTest { + + // Tests SI-8976 + @Test def tailIteratorMustTerminateAtLength(): Unit = { + val is = MutableList(1,2,3) + val tl = is.tail + assertEquals(tl.length, tl.iterator.length) + is += 5 + assertEquals(tl.length, tl.iterator.length) + assertSameElements(tl, tl.iterator) + } + @Test def iteratorMustFailEventually(): Unit = assertThrows[NoSuchElementException] { + MutableList[Unit]().iterator.next() + } + // was: Root empty iterator held reference + @Test def iteratorMustNotHoldOntoLast(): Unit = { + val is = MutableList(Some(1), Some(2)) + val it = is.iterator + val x = Some(3) + is += x + assertNotReachable(x, it) { + it.next() + it.next() + } + assertTrue(it.isEmpty) + } +} diff --git a/test/junit/scala/reflect/ClassTag.scala b/test/junit/scala/reflect/ClassTag.scala new file mode 100644 index 0000000000..90cc981fc1 --- /dev/null +++ b/test/junit/scala/reflect/ClassTag.scala @@ -0,0 +1,29 @@ +package scala.reflect + +import org.junit.Test +import org.junit.Assert._ +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +import scala.tools.testing.AssertUtil._ + +class Misc + +@RunWith(classOf[JUnit4]) +class ClassTagTest { + def checkNotString[A: ClassTag](a: Any) = a match { case x: String => false case x: A => true case _ => false } + def checkNotInt[A: ClassTag](a: Any) = a match { case x: Int => false case x: A => true case _ => false } + def checkNotLong[A: ClassTag](a: Any) = a match { case x: Long => false case x: A => true case _ => false } + + @Test def checkMisc = assertTrue(checkNotString[Misc](new Misc)) + @Test def checkString = assertTrue(checkNotInt[String] ("woele")) + @Test def checkByte = assertTrue(checkNotInt[Byte] (0.toByte)) + @Test def checkShort = assertTrue(checkNotInt[Short] (0.toShort)) + @Test def checkChar = assertTrue(checkNotInt[Char] (0.toChar)) + @Test def checkInt = assertTrue(checkNotLong[Int] (0.toInt)) + @Test def checkLong = assertTrue(checkNotInt[Long] (0.toLong)) + @Test def checkFloat = assertTrue(checkNotInt[Float] (0.toFloat)) + @Test def checkDouble = assertTrue(checkNotInt[Double] (0.toDouble)) + @Test def checkBoolean = assertTrue(checkNotInt[Boolean](false)) + @Test def checkUnit = assertTrue(checkNotInt[Unit] ({})) +}
\ No newline at end of file diff --git a/test/junit/scala/tools/nsc/backend/jvm/BTypesTest.scala b/test/junit/scala/tools/nsc/backend/jvm/BTypesTest.scala index 2347e8288e..6ada0e20fb 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/BTypesTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/BTypesTest.scala @@ -7,35 +7,41 @@ import org.junit.Test import scala.tools.asm.Opcodes import org.junit.Assert._ -@RunWith(classOf[JUnit4]) -class BTypesTest { - val settings = new Settings() - settings.processArgumentString("-usejavacp") - val g: Global = new Global(settings) - val run = new g.Run() // initializes some compiler internals - import g.{definitions => d, Symbol} +import scala.tools.nsc.backend.jvm.CodeGenTools._ +import scala.tools.testing.ClearAfterClass - def duringBackend[T](f: => T) = g.exitingDelambdafy(f) +object BTypesTest extends ClearAfterClass.Clearable { + var compiler = { + val comp = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:none") + new comp.Run() // initializes some of the compiler + comp.exitingDelambdafy(comp.scalaPrimitives.init()) // needed: it's only done when running the backend, and we don't actually run the compiler + comp.exitingDelambdafy(comp.genBCode.bTypes.initializeCoreBTypes()) + comp + } + def clear(): Unit = { compiler = null } +} - val btypes = new BTypesFromSymbols[g.type](g) - import btypes._ - duringBackend(btypes.initializeCoreBTypes()) +@RunWith(classOf[JUnit4]) +class BTypesTest extends ClearAfterClass { + ClearAfterClass.stateToClear = BTypesTest - def classBTypeFromSymbol(sym: Symbol) = duringBackend(btypes.classBTypeFromSymbol(sym)) + val compiler = BTypesTest.compiler + import compiler.genBCode.bTypes._ - val jlo = d.ObjectClass - val jls = d.StringClass + def classBTFS(sym: compiler.Symbol) = compiler.exitingDelambdafy(classBTypeFromSymbol(sym)) - val o = classBTypeFromSymbol(jlo) - val s = classBTypeFromSymbol(jls) - val oArr = ArrayBType(o) - val method = MethodBType(List(oArr, INT, DOUBLE, s), UNIT) + def jlo = compiler.definitions.ObjectClass + def jls = compiler.definitions.StringClass + def o = classBTFS(jlo) + def s = classBTFS(jls) + def oArr = ArrayBType(o) + def method = MethodBType(List(oArr, INT, DOUBLE, s), UNIT) @Test def classBTypesEquality() { - val s1 = classBTypeFromSymbol(jls) - val s2 = classBTypeFromSymbol(jls) - val o = classBTypeFromSymbol(jlo) + val s1 = classBTFS(jls) + val s2 = classBTFS(jls) + val o = classBTFS(jlo) assertEquals(s1, s2) assertEquals(s1.hashCode, s2.hashCode) assert(s1 != o) @@ -53,7 +59,7 @@ class BTypesTest { assert(FLOAT.typedOpcode(Opcodes.IALOAD) == Opcodes.FALOAD) assert(LONG.typedOpcode(Opcodes.IALOAD) == Opcodes.LALOAD) assert(DOUBLE.typedOpcode(Opcodes.IALOAD) == Opcodes.DALOAD) - assert(classBTypeFromSymbol(jls).typedOpcode(Opcodes.IALOAD) == Opcodes.AALOAD) + assert(classBTFS(jls).typedOpcode(Opcodes.IALOAD) == Opcodes.AALOAD) assert(UNIT.typedOpcode(Opcodes.IRETURN) == Opcodes.RETURN) assert(BOOL.typedOpcode(Opcodes.IRETURN) == Opcodes.IRETURN) @@ -64,7 +70,7 @@ class BTypesTest { assert(FLOAT.typedOpcode(Opcodes.IRETURN) == Opcodes.FRETURN) assert(LONG.typedOpcode(Opcodes.IRETURN) == Opcodes.LRETURN) assert(DOUBLE.typedOpcode(Opcodes.IRETURN) == Opcodes.DRETURN) - assert(classBTypeFromSymbol(jls).typedOpcode(Opcodes.IRETURN) == Opcodes.ARETURN) + assert(classBTFS(jls).typedOpcode(Opcodes.IRETURN) == Opcodes.ARETURN) } @Test diff --git a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala index c1c5a71b83..5d5215d887 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala @@ -2,16 +2,20 @@ package scala.tools.nsc.backend.jvm import org.junit.Assert._ +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.{AbstractInsnNode, LabelNode, ClassNode, MethodNode} +import scala.tools.asm.tree.{ClassNode, MethodNode} import scala.tools.cmd.CommandLineParser import scala.tools.nsc.backend.jvm.opt.LocalOpt -import scala.tools.nsc.settings.{MutableSettings, ScalaSettings} +import scala.tools.nsc.io.AbstractFile +import scala.tools.nsc.reporters.StoreReporter +import scala.tools.nsc.settings.MutableSettings import scala.tools.nsc.{Settings, Global} import scala.tools.partest.ASMConverters import scala.collection.JavaConverters._ +import scala.tools.testing.TempDir object CodeGenTools { import ASMConverters._ @@ -40,38 +44,104 @@ object CodeGenTools { } def newCompiler(defaultArgs: String = "-usejavacp", extraArgs: String = ""): Global = { + val compiler = newCompilerWithoutVirtualOutdir(defaultArgs, extraArgs) + resetOutput(compiler) + compiler + } + + def newCompilerWithoutVirtualOutdir(defaultArgs: String = "-usejavacp", extraArgs: String = ""): Global = { val settings = new Settings() val args = (CommandLineParser tokenize defaultArgs) ++ (CommandLineParser tokenize extraArgs) settings.processArguments(args, processAll = true) - val compiler = new Global(settings) - resetOutput(compiler) - compiler + new Global(settings, new StoreReporter) } - def compile(compiler: Global)(code: String): List[(String, Array[Byte])] = { + def newRun(compiler: Global): compiler.Run = { compiler.reporter.reset() resetOutput(compiler) - val run = new compiler.Run() - run.compileSources(List(new BatchSourceFile("unitTestSource.scala", code))) - val outDir = compiler.settings.outputDirs.getSingleOutput.get - (for (f <- outDir.iterator if !f.isDirectory) yield (f.name, f.toByteArray)).toList + new compiler.Run() + } + + def reporter(compiler: Global) = compiler.reporter.asInstanceOf[StoreReporter] + + def makeSourceFile(code: String, filename: String): BatchSourceFile = new BatchSourceFile(filename, code) + + def getGeneratedClassfiles(outDir: AbstractFile): List[(String, Array[Byte])] = { + def files(dir: AbstractFile): List[(String, Array[Byte])] = { + val res = ListBuffer.empty[(String, Array[Byte])] + for (f <- dir.iterator) { + if (!f.isDirectory) res += ((f.name, f.toByteArray)) + else if (f.name != "." && f.name != "..") res ++= files(f) + } + res.toList + } + files(outDir) + } + + def checkReport(compiler: Global, allowMessage: StoreReporter#Info => Boolean = _ => false): Unit = { + val disallowed = reporter(compiler).infos.toList.filter(!allowMessage(_)) // toList prevents an infer-non-wildcard-existential warning. + if (disallowed.nonEmpty) { + val msg = disallowed.mkString("\n") + assert(false, "The compiler issued non-allowed warnings or errors:\n" + msg) + } + } + + def compile(compiler: Global)(scalaCode: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[(String, Array[Byte])] = { + val run = newRun(compiler) + run.compileSources(makeSourceFile(scalaCode, "unitTestSource.scala") :: javaCode.map(p => makeSourceFile(p._1, p._2))) + checkReport(compiler, allowMessage) + getGeneratedClassfiles(compiler.settings.outputDirs.getSingleOutput.get) + } + + /** + * Compile multiple Scala files separately into a single output directory. + * + * Note that a new compiler instance is created for compiling each file because symbols survive + * across runs. This makes separate compilation slower. + * + * The output directory is a physical directory, I have not figured out if / how it's possible to + * add a VirtualDirectory to the classpath of a compiler. + */ + def compileSeparately(codes: List[String], extraArgs: String = "", allowMessage: StoreReporter#Info => Boolean = _ => false, afterEach: AbstractFile => Unit = _ => ()): List[(String, Array[Byte])] = { + val outDir = AbstractFile.getDirectory(TempDir.createTempDir()) + val outDirPath = outDir.canonicalPath + val argsWithOutDir = extraArgs + s" -d $outDirPath -cp $outDirPath" + + for (code <- codes) { + val compiler = newCompilerWithoutVirtualOutdir(extraArgs = argsWithOutDir) + new compiler.Run().compileSources(List(makeSourceFile(code, "unitTestSource.scala"))) + checkReport(compiler, allowMessage) + afterEach(outDir) + } + + val classfiles = getGeneratedClassfiles(outDir) + outDir.delete() + classfiles + } + + def compileClassesSeparately(codes: List[String], extraArgs: String = "", allowMessage: StoreReporter#Info => Boolean = _ => false, afterEach: AbstractFile => Unit = _ => ()) = { + readAsmClasses(compileSeparately(codes, extraArgs, allowMessage, afterEach)) + } + + def readAsmClasses(classfiles: List[(String, Array[Byte])]) = { + classfiles.map(p => AsmUtils.readClass(p._2)).sortBy(_.name) } - def compileClasses(compiler: Global)(code: String): List[ClassNode] = { - compile(compiler)(code).map(p => AsmUtils.readClass(p._2)).sortBy(_.name) + def compileClasses(compiler: Global)(code: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[ClassNode] = { + readAsmClasses(compile(compiler)(code, javaCode, allowMessage)) } - def compileMethods(compiler: Global)(code: String): List[MethodNode] = { - compileClasses(compiler)(s"class C { $code }").head.methods.asScala.toList.filterNot(_.name == "<init>") + def compileMethods(compiler: Global)(code: String, allowMessage: StoreReporter#Info => Boolean = _ => false): List[MethodNode] = { + compileClasses(compiler)(s"class C { $code }", allowMessage = allowMessage).head.methods.asScala.toList.filterNot(_.name == "<init>") } - def singleMethodInstructions(compiler: Global)(code: String): List[Instruction] = { - val List(m) = compileMethods(compiler)(code) + def singleMethodInstructions(compiler: Global)(code: String, allowMessage: StoreReporter#Info => Boolean = _ => false): List[Instruction] = { + val List(m) = compileMethods(compiler)(code, allowMessage = allowMessage) instructionsFromMethod(m) } - def singleMethod(compiler: Global)(code: String): Method = { - val List(m) = compileMethods(compiler)(code) + def singleMethod(compiler: Global)(code: String, allowMessage: StoreReporter#Info => Boolean = _ => false): Method = { + val List(m) = compileMethods(compiler)(code, allowMessage = allowMessage) convertMethod(m) } diff --git a/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala b/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala index 89900291ca..4086f7dd7b 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala @@ -7,10 +7,18 @@ import org.junit.Assert._ import CodeGenTools._ import scala.tools.asm.Opcodes._ import scala.tools.partest.ASMConverters._ +import scala.tools.testing.ClearAfterClass + +object DirectCompileTest extends ClearAfterClass.Clearable { + var compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:method") + def clear(): Unit = { compiler = null } +} @RunWith(classOf[JUnit4]) -class DirectCompileTest { - val compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:method") +class DirectCompileTest extends ClearAfterClass { + ClearAfterClass.stateToClear = DirectCompileTest + + val compiler = DirectCompileTest.compiler @Test def testCompile(): Unit = { @@ -70,4 +78,21 @@ class DirectCompileTest { Label(11) )) } + + @Test + def testSeparateCompilation(): Unit = { + val codeA = "class A { def f = 1 }" + val codeB = "class B extends A { def g = f }" + val List(a, b) = compileClassesSeparately(List(codeA, codeB)) + val ins = getSingleMethod(b, "g").instructions + assert(ins exists { + case Invoke(_, "B", "f", _, _) => true + case _ => false + }, ins) + } + + @Test + def compileErroneous(): Unit = { + compileClasses(compiler)("class C { def f: String = 1 }", allowMessage = _.msg contains "type mismatch") + } } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala index 2975bd060d..1b6c080234 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala @@ -15,11 +15,14 @@ import CodeGenTools._ import scala.tools.partest.ASMConverters import ASMConverters._ +import BackendReporting._ + import scala.collection.convert.decorateAsScala._ @RunWith(classOf[JUnit4]) class BTypesFromClassfileTest { - val compiler = newCompiler(extraArgs = "-Ybackend:GenBCode") + // inliner enabled -> inlineInfos are collected (and compared) in ClassBTypes + val compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:inline-global") import compiler._ import definitions._ @@ -29,6 +32,7 @@ class BTypesFromClassfileTest { def duringBackend[T](f: => T) = compiler.exitingDelambdafy(f) val run = new compiler.Run() // initializes some of the compiler + duringBackend(compiler.scalaPrimitives.init()) // needed: it's only done when running the backend, and we don't actually run the compiler duringBackend(bTypes.initializeCoreBTypes()) def clearCache() = bTypes.classBTypeFromInternalName.clear() @@ -37,7 +41,7 @@ class BTypesFromClassfileTest { if (checked(fromSym.internalName)) checked else { assert(fromSym == fromClassfile, s"$fromSym != $fromClassfile") - sameInfo(fromSym.info, fromClassfile.info, checked + fromSym.internalName) + sameInfo(fromSym.info.get, fromClassfile.info.get, checked + fromSym.internalName) } } @@ -57,8 +61,12 @@ class BTypesFromClassfileTest { else (fromSym.flags | ACC_PRIVATE | ACC_PUBLIC) == (fromClassfile.flags | ACC_PRIVATE | ACC_PUBLIC) }, s"class flags differ\n$fromSym\n$fromClassfile") - val chk1 = sameBTypes(fromSym.superClass, fromClassfile.superClass, checked) + // we don't compare InlineInfos in this test: in both cases (from symbol and from classfile) they + // are actually created by looking at the classfile members, not the symbol's. InlineInfos are only + // built from symbols for classes that are being compiled, which is not the case here. Instead + // there's a separate InlineInfoTest. + val chk1 = sameBTypes(fromSym.superClass, fromClassfile.superClass, checked) val chk2 = sameBTypes(fromSym.interfaces, fromClassfile.interfaces, chk1) // The fromSym info has only member classes, no local or anonymous. The symbol is read from the @@ -67,7 +75,7 @@ class BTypesFromClassfileTest { // and anonymous classes as members of the outer class. But not for unpickled symbols). // The fromClassfile info has all nested classes, including anonymous and local. So we filter // them out: member classes are identified by having the `outerName` defined. - val memberClassesFromClassfile = fromClassfile.nestedClasses.filter(_.info.nestedInfo.get.outerName.isDefined) + val memberClassesFromClassfile = fromClassfile.nestedClasses.filter(_.info.get.nestedInfo.get.outerName.isDefined) // Sorting is required: the backend sorts all InnerClass entries by internalName before writing // them to the classfile (to make it deterministic: the entries are collected in a Set during // code generation). diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala new file mode 100644 index 0000000000..9fda034a04 --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala @@ -0,0 +1,152 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import scala.collection.generic.Clearable +import scala.tools.asm.Opcodes._ +import org.junit.Assert._ + +import scala.tools.asm.tree._ +import scala.tools.asm.tree.analysis._ +import scala.tools.nsc.reporters.StoreReporter +import scala.tools.testing.AssertUtil._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import AsmUtils._ +import BackendReporting._ + +import scala.collection.convert.decorateAsScala._ + +@RunWith(classOf[JUnit4]) +class CallGraphTest { + val compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:inline-global -Yopt-warnings") + import compiler.genBCode.bTypes._ + + // allows inspecting the caches after a compilation run + val notPerRun: List[Clearable] = List(classBTypeFromInternalName, byteCodeRepository.classes, callGraph.callsites) + notPerRun foreach compiler.perRunCaches.unrecordCache + + def compile(code: String, allowMessage: StoreReporter#Info => Boolean): List[ClassNode] = { + notPerRun.foreach(_.clear()) + compileClasses(compiler)(code, allowMessage = allowMessage) + } + + def callsInMethod(methodNode: MethodNode): List[MethodInsnNode] = methodNode.instructions.iterator.asScala.collect({ + case call: MethodInsnNode => call + }).toList + + @Test + def callGraphStructure(): Unit = { + val code = + """class C { + | // try-catch prevents inlining - we want to analyze the callsite + | def f1 = try { 0 } catch { case _: Throwable => 1 } + | final def f2 = try { 0 } catch { case _: Throwable => 1 } + | + | @inline def f3 = try { 0 } catch { case _: Throwable => 1 } + | @inline final def f4 = try { 0 } catch { case _: Throwable => 1 } + | + | @noinline def f5 = try { 0 } catch { case _: Throwable => 1 } + | @noinline final def f6 = try { 0 } catch { case _: Throwable => 1 } + | + | @inline @noinline def f7 = try { 0 } catch { case _: Throwable => 1 } + |} + |class D extends C { + | @inline override def f1 = try { 0 } catch { case _: Throwable => 1 } + | override final def f3 = try { 0 } catch { case _: Throwable => 1 } + |} + |object C { + | def g1 = try { 0 } catch { case _: Throwable => 1 } + |} + |class Test { + | def t1(c: C) = c.f1 + c.f2 + c.f3 + c.f4 + c.f5 + c.f6 + c.f7 + C.g1 + | def t2(d: D) = d.f1 + d.f2 + d.f3 + d.f4 + d.f5 + d.f6 + d.f7 + C.g1 + |} + """.stripMargin + + // Get the ClassNodes from the code repo (don't use the unparsed ClassNodes returned by compile). + // The callGraph.callsites map is indexed by instructions of those ClassNodes. + + val ok = Set( + "D::f1()I is annotated @inline but cannot be inlined: the method is not final and may be overridden", // only one warning for D.f1: C.f1 is not annotated @inline + "C::f3()I is annotated @inline but cannot be inlined: the method is not final and may be overridden", // only one warning for C.f3: D.f3 does not have @inline (and it would also be safe to inline) + "C::f7()I is annotated @inline but cannot be inlined: the method is not final and may be overridden", // two warnings (the error message mentions C.f7 even if the receiver type is D, because f7 is inherited from C) + "operand stack at the callsite in Test::t1(LC;)I contains more values", + "operand stack at the callsite in Test::t2(LD;)I contains more values") + var msgCount = 0 + val checkMsg = (m: StoreReporter#Info) => { + msgCount += 1 + ok exists (m.msg contains _) + } + val List(cCls, cMod, dCls, testCls) = compile(code, checkMsg).map(c => byteCodeRepository.classNode(c.name).get) + assert(msgCount == 6, msgCount) + + val List(cf1, cf2, cf3, cf4, cf5, cf6, cf7) = cCls.methods.iterator.asScala.filter(_.name.startsWith("f")).toList.sortBy(_.name) + val List(df1, df3) = dCls.methods.iterator.asScala.filter(_.name.startsWith("f")).toList.sortBy(_.name) + val g1 = cMod.methods.iterator.asScala.find(_.name == "g1").get + val List(t1, t2) = testCls.methods.iterator.asScala.filter(_.name.startsWith("t")).toList.sortBy(_.name) + + val List(cf1Call, cf2Call, cf3Call, cf4Call, cf5Call, cf6Call, cf7Call, cg1Call) = callsInMethod(t1) + val List(df1Call, df2Call, df3Call, df4Call, df5Call, df6Call, df7Call, dg1Call) = callsInMethod(t2) + + def checkCallsite(callsite: callGraph.Callsite, + call: MethodInsnNode, callsiteMethod: MethodNode, target: MethodNode, calleeDeclClass: ClassBType, + safeToInline: Boolean, atInline: Boolean, atNoInline: Boolean) = try { + assert(callsite.callsiteInstruction == call) + assert(callsite.callsiteMethod == callsiteMethod) + val callee = callsite.callee.get + assert(callee.callee == target) + assert(callee.calleeDeclarationClass == calleeDeclClass) + assert(callee.safeToInline == safeToInline) + assert(callee.annotatedInline == atInline) + assert(callee.annotatedNoInline == atNoInline) + + assert(callsite.argInfos == List()) // not defined yet + } catch { + case e: Throwable => println(callsite); throw e + } + + val cClassBType = classBTypeFromClassNode(cCls) + val cMClassBType = classBTypeFromClassNode(cMod) + val dClassBType = classBTypeFromClassNode(dCls) + + checkCallsite(callGraph.callsites(cf1Call), + cf1Call, t1, cf1, cClassBType, false, false, false) + checkCallsite(callGraph.callsites(cf2Call), + cf2Call, t1, cf2, cClassBType, true, false, false) + checkCallsite(callGraph.callsites(cf3Call), + cf3Call, t1, cf3, cClassBType, false, true, false) + checkCallsite(callGraph.callsites(cf4Call), + cf4Call, t1, cf4, cClassBType, true, true, false) + checkCallsite(callGraph.callsites(cf5Call), + cf5Call, t1, cf5, cClassBType, false, false, true) + checkCallsite(callGraph.callsites(cf6Call), + cf6Call, t1, cf6, cClassBType, true, false, true) + checkCallsite(callGraph.callsites(cf7Call), + cf7Call, t1, cf7, cClassBType, false, true, true) + checkCallsite(callGraph.callsites(cg1Call), + cg1Call, t1, g1, cMClassBType, true, false, false) + + checkCallsite(callGraph.callsites(df1Call), + df1Call, t2, df1, dClassBType, false, true, false) + checkCallsite(callGraph.callsites(df2Call), + df2Call, t2, cf2, cClassBType, true, false, false) + checkCallsite(callGraph.callsites(df3Call), + df3Call, t2, df3, dClassBType, true, false, false) + checkCallsite(callGraph.callsites(df4Call), + df4Call, t2, cf4, cClassBType, true, true, false) + checkCallsite(callGraph.callsites(df5Call), + df5Call, t2, cf5, cClassBType, false, false, true) + checkCallsite(callGraph.callsites(df6Call), + df6Call, t2, cf6, cClassBType, true, false, true) + checkCallsite(callGraph.callsites(df7Call), + df7Call, t2, cf7, cClassBType, false, true, true) + checkCallsite(callGraph.callsites(dg1Call), + dg1Call, t2, g1, cMClassBType, true, false, false) + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala index fc748196d0..76492cfa23 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala @@ -17,8 +17,8 @@ class CompactLocalVariablesTest { // recurse-unreachable-jumps is required for eliminating catch blocks, in the first dce round they // are still live.only after eliminating the empty handler the catch blocks become unreachable. - val methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code,recurse-unreachable-jumps,compact-locals") - val noCompactVarsCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code,recurse-unreachable-jumps") + val methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code,compact-locals") + val noCompactVarsCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code") @Test def compactUnused(): Unit = { diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala index 7d83c54b5b..7b0504fec0 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala @@ -11,9 +11,23 @@ import org.junit.Assert._ import CodeGenTools._ import scala.tools.partest.ASMConverters import ASMConverters._ +import scala.tools.testing.ClearAfterClass + +object EmptyExceptionHandlersTest extends ClearAfterClass.Clearable { + var noOptCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:none") + var dceCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:unreachable-code") + def clear(): Unit = { + noOptCompiler = null + dceCompiler = null + } +} @RunWith(classOf[JUnit4]) -class EmptyExceptionHandlersTest { +class EmptyExceptionHandlersTest extends ClearAfterClass { + ClearAfterClass.stateToClear = EmptyExceptionHandlersTest + + val noOptCompiler = EmptyExceptionHandlersTest.noOptCompiler + val dceCompiler = EmptyExceptionHandlersTest.dceCompiler val exceptionDescriptor = "java/lang/Exception" @@ -51,9 +65,6 @@ class EmptyExceptionHandlersTest { assertTrue(convertMethod(asmMethod).handlers.isEmpty) } - val noOptCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:none") - val dceCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:unreachable-code") - @Test def eliminateUnreachableHandler(): Unit = { val code = "def f: Unit = try { } catch { case _: Exception => println(0) }; println(1)" diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala new file mode 100644 index 0000000000..57088bdd2f --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala @@ -0,0 +1,67 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import scala.collection.generic.Clearable +import org.junit.Assert._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import AsmUtils._ +import scala.tools.testing.ClearAfterClass + +import BackendReporting._ + +import scala.collection.convert.decorateAsScala._ + +object InlineInfoTest extends ClearAfterClass.Clearable { + var compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:classpath") + def clear(): Unit = { compiler = null } + + def notPerRun: List[Clearable] = List(compiler.genBCode.bTypes.classBTypeFromInternalName, compiler.genBCode.bTypes.byteCodeRepository.classes) + notPerRun foreach compiler.perRunCaches.unrecordCache +} + +@RunWith(classOf[JUnit4]) +class InlineInfoTest { + val compiler = InlineInfoTest.compiler + + def compile(code: String) = { + InlineInfoTest.notPerRun.foreach(_.clear()) + compileClasses(compiler)(code) + } + + @Test + def inlineInfosFromSymbolAndAttribute(): Unit = { + val code = + """trait T { + | @inline def f: Int + | @noinline final def g = 0 + |} + |trait U { self: T => + | @inline def f = 0 + | final def h = 0 + | final class K { + | @inline def i = 0 + | } + |} + |sealed trait V { + | @inline def j = 0 + |} + |class C extends T with U + """.stripMargin + val classes = compile(code) + val fromSyms = classes.map(c => compiler.genBCode.bTypes.classBTypeFromInternalName(c.name).info.get.inlineInfo) + + val fromAttrs = classes.map(c => { + assert(c.attrs.asScala.exists(_.isInstanceOf[InlineInfoAttribute]), c.attrs) + compiler.genBCode.bTypes.inlineInfoFromClassfile(c) + }) + + assert(fromSyms == fromAttrs) + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlineWarningTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineWarningTest.scala new file mode 100644 index 0000000000..fedc074a15 --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineWarningTest.scala @@ -0,0 +1,146 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import scala.collection.generic.Clearable +import scala.collection.mutable.ListBuffer +import scala.reflect.internal.util.BatchSourceFile +import scala.tools.asm.Opcodes._ +import org.junit.Assert._ + +import scala.tools.asm.tree._ +import scala.tools.asm.tree.analysis._ +import scala.tools.nsc.backend.jvm.opt.BytecodeUtils.AsmAnalyzer +import scala.tools.nsc.io._ +import scala.tools.nsc.reporters.StoreReporter +import scala.tools.testing.AssertUtil._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import AsmUtils._ + +import BackendReporting._ + +import scala.collection.convert.decorateAsScala._ +import scala.tools.testing.ClearAfterClass + +object InlineWarningTest extends ClearAfterClass.Clearable { + val argsNoWarn = "-Ybackend:GenBCode -Yopt:l:classpath" + val args = argsNoWarn + " -Yopt-warnings" + var compiler = newCompiler(extraArgs = args) + def clear(): Unit = { compiler = null } +} + +@RunWith(classOf[JUnit4]) +class InlineWarningTest extends ClearAfterClass { + ClearAfterClass.stateToClear = InlineWarningTest + + val compiler = InlineWarningTest.compiler + + def compile(scalaCode: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[ClassNode] = { + compileClasses(compiler)(scalaCode, javaCode, allowMessage) + } + + @Test + def nonFinal(): Unit = { + val code = + """class C { + | @inline def m1 = 1 + |} + |trait T { + | @inline def m2 = 1 + |} + |class D extends C with T + | + |class Test { + | def t1(c: C, t: T, d: D) = c.m1 + t.m2 + d.m1 + d.m2 + |} + """.stripMargin + var count = 0 + val warns = Set( + "C::m1()I is annotated @inline but cannot be inlined: the method is not final and may be overridden", + "T::m2()I is annotated @inline but cannot be inlined: the method is not final and may be overridden", + "D::m2()I is annotated @inline but cannot be inlined: the method is not final and may be overridden") + compile(code, allowMessage = i => {count += 1; warns.exists(i.msg contains _)}) + assert(count == 4, count) + } + + @Test + def traitMissingImplClass(): Unit = { + val codeA = "trait T { @inline final def f = 1 }" + val codeB = "class C { def t1(t: T) = t.f }" + + val removeImpl = (outDir: AbstractFile) => { + val f = outDir.lookupName("T$class.class", directory = false) + if (f != null) f.delete() + } + + val warn = + """T::f()I is annotated @inline but cannot be inlined: the trait method call could not be rewritten to the static implementation method. Possible reason: + |The method f(LT;)I could not be found in the class T$class or any of its parents. + |Note that the following parent classes could not be found on the classpath: T$class""".stripMargin + + var c = 0 + compileSeparately(List(codeA, codeB), extraArgs = InlineWarningTest.args, afterEach = removeImpl, allowMessage = i => {c += 1; i.msg contains warn}) + assert(c == 1, c) + + // only summary here + compileSeparately(List(codeA, codeB), extraArgs = InlineWarningTest.argsNoWarn, afterEach = removeImpl, allowMessage = _.msg contains "there was one inliner warning") + } + + @Test + def handlerNonEmptyStack(): Unit = { + val code = + """class C { + | @noinline def q = 0 + | @inline final def foo = try { q } catch { case e: Exception => 2 } + | def t1 = println(foo) // inline warning here: foo cannot be inlined on top of a non-empty stack + |} + """.stripMargin + + var c = 0 + compile(code, allowMessage = i => {c += 1; i.msg contains "operand stack at the callsite in C::t1()V contains more values"}) + assert(c == 1, c) + } + + @Test + def mixedWarnings(): Unit = { + val javaCode = + """public class A { + | public static final int bar() { return 100; } + |} + """.stripMargin + + val scalaCode = + """class B { + | @inline final def flop = A.bar + | def g = flop + |} + """.stripMargin + + val warns = List( + """failed to determine if bar should be inlined: + |The method bar()I could not be found in the class A or any of its parents. + |Note that the following parent classes are defined in Java sources (mixed compilation), no bytecode is available: A""".stripMargin, + + """B::flop()I is annotated @inline but could not be inlined: + |Failed to check if B::flop()I can be safely inlined to B without causing an IllegalAccessError. Checking instruction INVOKESTATIC A.bar ()I failed: + |The method bar()I could not be found in the class A or any of its parents. + |Note that the following parent classes are defined in Java sources (mixed compilation), no bytecode is available: A""".stripMargin) + + var c = 0 + val List(b) = compile(scalaCode, List((javaCode, "A.java")), allowMessage = i => {c += 1; warns.tail.exists(i.msg contains _)}) + assert(c == 1, c) + + // no warnings here + compileClasses(newCompiler(extraArgs = InlineWarningTest.argsNoWarn + " -Yopt-warnings:none"))(scalaCode, List((javaCode, "A.java"))) + + c = 0 + compileClasses(newCompiler(extraArgs = InlineWarningTest.argsNoWarn + " -Yopt-warnings:no-inline-mixed"))(scalaCode, List((javaCode, "A.java")), allowMessage = i => {c += 1; warns.exists(i.msg contains _)}) + assert(c == 2, c) + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala new file mode 100644 index 0000000000..b4839dcec8 --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala @@ -0,0 +1,198 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import scala.tools.asm.Opcodes._ +import org.junit.Assert._ + +import scala.tools.asm.tree._ +import scala.tools.testing.AssertUtil._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import AsmUtils._ + +import scala.collection.convert.decorateAsScala._ +import scala.tools.testing.ClearAfterClass + +object InlinerIllegalAccessTest extends ClearAfterClass.Clearable { + var compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:none") + def clear(): Unit = { compiler = null } +} + +@RunWith(classOf[JUnit4]) +class InlinerIllegalAccessTest extends ClearAfterClass { + ClearAfterClass.stateToClear = InlinerIllegalAccessTest + + val compiler = InlinerIllegalAccessTest.compiler + import compiler.genBCode.bTypes._ + + def addToRepo(cls: List[ClassNode]): Unit = for (c <- cls) byteCodeRepository.add(c, ByteCodeRepository.Classfile) + def assertEmpty(ins: Option[AbstractInsnNode]) = for (i <- ins) throw new AssertionError(textify(i)) + + @Test + def typeAccessible(): Unit = { + val code = + """package a { + | private class C { // the Scala compiler makes all classes public + | def f1 = new C // NEW a/C + | def f2 = new Array[C](0) // ANEWARRAY a/C + | def f3 = new Array[Array[C]](0) // ANEWARRAY [La/C; + | } + | class D + |} + |package b { + | class E + |} + """.stripMargin + + val allClasses = compileClasses(compiler)(code) + val List(cClass, dClass, eClass) = allClasses + assert(cClass.name == "a/C" && dClass.name == "a/D" && eClass.name == "b/E", s"${cClass.name}, ${dClass.name}, ${eClass.name}") + addToRepo(allClasses) // they are not on the compiler's classpath, so we add them manually to the code repo + + val methods = cClass.methods.asScala.filter(_.name(0) == 'f').toList + + def check(classNode: ClassNode, test: Option[AbstractInsnNode] => Unit) = { + for (m <- methods) + test(inliner.findIllegalAccess(m.instructions, classBTypeFromParsedClassfile(cClass.name), classBTypeFromParsedClassfile(classNode.name)).map(_._1)) + } + + check(cClass, assertEmpty) + check(dClass, assertEmpty) + check(eClass, assertEmpty) // C is public, so accessible in E + + byteCodeRepository.classes.clear() + classBTypeFromInternalName.clear() + + cClass.access &= ~ACC_PUBLIC // ftw + addToRepo(allClasses) + + // private classes can be accessed from the same package + check(cClass, assertEmpty) + check(dClass, assertEmpty) // accessing a private class in the same package is OK + check(eClass, { + case Some(ti: TypeInsnNode) if Set("a/C", "[La/C;")(ti.desc) => () + // MatchError otherwise + }) + } + + @Test + def memberAccessible(): Unit = { + val code = + """package a { + | class C { + | /*public*/ def a = 0 + | /*default*/ def b = 0 + | protected def c = 0 + | private def d = 0 + | + | /*public static*/ def e = 0 + | /*default static*/ def f = 0 + | protected /*static*/ def g = 0 + | private /*static*/ def h = 0 + | + | def raC = a + | def rbC = b + | def rcC = c + | def rdC = d + | def reC = e + | def rfC = f + | def rgC = g + | def rhC = h + | } + | + | class D extends C { + | def rbD = b // 1: default access b, accessed in D, declared in C. can be inlined into any class in the same package as C. + | def rcD = c // 2: protected c, accessed in D. can be inlined into C, D or E, but not into F (F and D are unrelated). + | + | def rfD = f // 1 + | def rgD = g // 2 + | } + | class E extends D + | + | class F extends C + | + | class G + |} + | + |package b { + | class H extends a.C + | class I + |} + """.stripMargin + + val allClasses = compileClasses(compiler)(code) + val List(cCl, dCl, eCl, fCl, gCl, hCl, iCl) = allClasses + addToRepo(allClasses) + + // set flags that Scala scala doesn't (default access, static) - a hacky way to test all access modes. + val names = ('a' to 'h').map(_.toString).toSet + val List(a, b, c, d, e, f, g, h) = cCl.methods.asScala.toList.filter(m => names(m.name)) + + def checkAccess(a: MethodNode, expected: Int): Unit = { + assert((a.access & (ACC_STATIC | ACC_PUBLIC | ACC_PROTECTED | ACC_PRIVATE)) == expected, s"${a.name}, ${a.access}") + } + + checkAccess(a, ACC_PUBLIC) + b.access &= ~ACC_PUBLIC; checkAccess(b, 0) // make it default access + c.access &= ~ACC_PUBLIC; c.access |= ACC_PROTECTED; checkAccess(c, ACC_PROTECTED) // make it protected - scalac actually never emits PROTECTED in bytecode, see javaFlags in BTypesFromSymbols + checkAccess(d, ACC_PRIVATE) + + e.access |= ACC_STATIC; checkAccess(e, ACC_STATIC | ACC_PUBLIC) + f.access &= ~ACC_PUBLIC; f.access |= ACC_STATIC; checkAccess(f, ACC_STATIC) + g.access &= ~ACC_PUBLIC; g.access |= (ACC_STATIC | ACC_PROTECTED); checkAccess(g, ACC_STATIC | ACC_PROTECTED) + h.access |= ACC_STATIC; checkAccess(h, ACC_STATIC | ACC_PRIVATE) + + val List(raC, rbC, rcC, rdC, reC, rfC, rgC, rhC) = cCl.methods.asScala.toList.filter(_.name(0) == 'r').sortBy(_.name) + + val List(rbD, rcD, rfD, rgD) = dCl.methods.asScala.toList.filter(_.name(0) == 'r').sortBy(_.name) + + def check(method: MethodNode, decl: ClassNode, dest: ClassNode, test: Option[AbstractInsnNode] => Unit): Unit = { + test(inliner.findIllegalAccess(method.instructions, classBTypeFromParsedClassfile(decl.name), classBTypeFromParsedClassfile(dest.name)).map(_._1)) + } + + val cOrDOwner = (_: Option[AbstractInsnNode] @unchecked) match { + case Some(mi: MethodInsnNode) if Set("a/C", "a/D")(mi.owner) => () + // MatchError otherwise + } + + // PUBLIC + + // public methods allowed everywhere + for (m <- Set(raC, reC); c <- allClasses) check(m, cCl, c, assertEmpty) + + // DEFAULT ACCESS + + // default access OK in same package + for ((m, declCls) <- Set((rbC, cCl), (rfC, cCl), (rbD, dCl), (rfD, dCl)); c <- allClasses) { + if (c.name startsWith "a/") check(m, declCls, c, assertEmpty) + else check(m, declCls, c, cOrDOwner) + } + + // PROTECTED + + // protected accessed in same class, or protected static accessed in subclass(rgD). + // can be inlined to subclasses, and classes in the same package (gCl) + for ((m, declCls) <- Set((rcC, cCl), (rgC, cCl), (rgD, dCl)); c <- Set(cCl, dCl, eCl, fCl, gCl, hCl)) check(m, declCls, c, assertEmpty) + + // protected in non-subclass and different package + for (m <- Set(rcC, rgC)) check(m, cCl, iCl, cOrDOwner) + + // non-static protected accessed in subclass (rcD). can be inlined to related class, or classes in the same package + for (c <- Set(cCl, dCl, eCl, fCl, gCl)) check(rcD, dCl, c, assertEmpty) + + // rcD cannot be inlined into non-related classes, if the declaration and destination are not in the same package + for (c <- Set(hCl, iCl)) check(rcD, dCl, c, cOrDOwner) + + // PRIVATE + + // privated method accesses can only be inlined in the same class + for (m <- Set(rdC, rhC)) check(m, cCl, cCl, assertEmpty) + for (m <- Set(rdC, rhC); c <- allClasses.tail) check(m, cCl, c, cOrDOwner) + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala new file mode 100644 index 0000000000..5c9bd1c188 --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala @@ -0,0 +1,115 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import scala.tools.asm.Opcodes._ +import org.junit.Assert._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import AsmUtils._ + +import scala.collection.convert.decorateAsScala._ + +object InlinerSeparateCompilationTest { + val args = "-Ybackend:GenBCode -Yopt:l:classpath" +} + +@RunWith(classOf[JUnit4]) +class InlinerSeparateCompilationTest { + import InlinerSeparateCompilationTest._ + import InlinerTest.{listStringLines, assertInvoke, assertNoInvoke} + + @Test + def inlnieMixedinMember(): Unit = { + val codeA = + """trait T { + | @inline def f = 0 + |} + |object O extends T { + | @inline def g = 1 + |} + """.stripMargin + + val codeB = + """class C { + | def t1(t: T) = t.f + | def t2 = O.f + | def t3 = O.g + |} + """.stripMargin + + val warn = "T::f()I is annotated @inline but cannot be inlined: the method is not final and may be overridden" + val List(c, o, oMod, t, tCls) = compileClassesSeparately(List(codeA, codeB), args + " -Yopt-warnings", _.msg contains warn) + assertInvoke(getSingleMethod(c, "t1"), "T", "f") + assertNoInvoke(getSingleMethod(c, "t2")) + assertNoInvoke(getSingleMethod(c, "t3")) + } + + @Test + def inlineSealedMember(): Unit = { + val codeA = + """sealed trait T { + | @inline def f = 1 + |} + """.stripMargin + + val codeB = + """class C { + | def t1(t: T) = t.f + |} + """.stripMargin + + val List(c, t, tCls) = compileClassesSeparately(List(codeA, codeB), args) + assertNoInvoke(getSingleMethod(c, "t1")) + } + + @Test + def inlineInheritedMember(): Unit = { + val codeA = + """trait T { + | @inline final def f = 1 + |} + |trait U extends T { + | @inline final def g = f + |} + """.stripMargin + + val codeB = + """class C extends U { + | def t1 = this.f + | def t2 = this.g + | def t3(t: T) = t.f + |} + """.stripMargin + + val List(c, t, tCls, u, uCls) = compileClassesSeparately(List(codeA, codeB), args) + for (m <- List("t1", "t2", "t3")) assertNoInvoke(getSingleMethod(c, m)) + } + + @Test + def inlineWithSelfType(): Unit = { + val assembly = + """trait Assembly extends T { + | @inline final def g = 1 + | @inline final def n = m + |} + """.stripMargin + + val codeA = + s"""trait T { self: Assembly => + | @inline final def f = g + | @inline final def m = 1 + |} + |$assembly + """.stripMargin + + val List(a, aCls, t, tCls) = compileClassesSeparately(List(codeA, assembly), args) + assertNoInvoke(getSingleMethod(tCls, "f")) + assertNoInvoke(getSingleMethod(aCls, "n")) + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala new file mode 100644 index 0000000000..39fb28570e --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -0,0 +1,953 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import scala.collection.generic.Clearable +import scala.collection.mutable.ListBuffer +import scala.reflect.internal.util.BatchSourceFile +import scala.tools.asm.Opcodes._ +import org.junit.Assert._ + +import scala.tools.asm.tree._ +import scala.tools.asm.tree.analysis._ +import scala.tools.nsc.backend.jvm.opt.BytecodeUtils.AsmAnalyzer +import scala.tools.nsc.io._ +import scala.tools.nsc.reporters.StoreReporter +import scala.tools.testing.AssertUtil._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import AsmUtils._ + +import BackendReporting._ + +import scala.collection.convert.decorateAsScala._ +import scala.tools.testing.ClearAfterClass + +object InlinerTest extends ClearAfterClass.Clearable { + val args = "-Ybackend:GenBCode -Yopt:l:classpath -Yopt-warnings" + var compiler = newCompiler(extraArgs = args) + + // allows inspecting the caches after a compilation run + def notPerRun: List[Clearable] = List(compiler.genBCode.bTypes.classBTypeFromInternalName, compiler.genBCode.bTypes.byteCodeRepository.classes, compiler.genBCode.bTypes.callGraph.callsites) + notPerRun foreach compiler.perRunCaches.unrecordCache + + def clear(): Unit = { compiler = null } + + implicit class listStringLines[T](val l: List[T]) extends AnyVal { + def stringLines = l.mkString("\n") + } + + def assertNoInvoke(m: Method): Unit = assertNoInvoke(m.instructions) + def assertNoInvoke(ins: List[Instruction]): Unit = { + assert(!ins.exists(_.isInstanceOf[Invoke]), ins.stringLines) + } + + def assertInvoke(m: Method, receiver: String, method: String): Unit = assertInvoke(m.instructions, receiver, method) + def assertInvoke(l: List[Instruction], receiver: String, method: String): Unit = { + assert(l.exists { + case Invoke(_, `receiver`, `method`, _, _) => true + case _ => false + }, l.stringLines) + } +} + +@RunWith(classOf[JUnit4]) +class InlinerTest extends ClearAfterClass { + ClearAfterClass.stateToClear = InlinerTest + + import InlinerTest.{listStringLines, assertInvoke, assertNoInvoke} + + val compiler = InlinerTest.compiler + import compiler.genBCode.bTypes._ + + def compile(scalaCode: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[ClassNode] = { + InlinerTest.notPerRun.foreach(_.clear()) + compileClasses(compiler)(scalaCode, javaCode, allowMessage) + } + + def checkCallsite(callsite: callGraph.Callsite, callee: MethodNode) = { + assert(callsite.callsiteMethod.instructions.contains(callsite.callsiteInstruction), instructionsFromMethod(callsite.callsiteMethod)) + + val callsiteClassNode = byteCodeRepository.classNode(callsite.callsiteClass.internalName).get + assert(callsiteClassNode.methods.contains(callsite.callsiteMethod), callsiteClassNode.methods.asScala.map(_.name).toList) + + assert(callsite.callee.get.callee == callee, callsite.callee.get.callee.name) + } + + // inline first invocation of f into g in class C + def inlineTest(code: String, mod: ClassNode => Unit = _ => ()): (MethodNode, Option[CannotInlineWarning]) = { + val List(cls) = compile(code) + mod(cls) + val clsBType = classBTypeFromParsedClassfile(cls.name) + + val List(f, g) = cls.methods.asScala.filter(m => Set("f", "g")(m.name)).toList.sortBy(_.name) + val fCall = g.instructions.iterator.asScala.collect({ case i: MethodInsnNode if i.name == "f" => i }).next() + + val analyzer = new AsmAnalyzer(g, clsBType.internalName) + + val r = inliner.inline( + fCall, + analyzer.frameAt(fCall).getStackSize, + g, + clsBType, + f, + clsBType, + receiverKnownNotNull = true, + keepLineNumbers = true) + (g, r) + } + + @Test + def simpleInlineOK(): Unit = { + val code = + """class C { + | def f = 1 + | def g = f + f + |} + """.stripMargin + + val (g, _) = inlineTest(code) + + val gConv = convertMethod(g) + assertSameCode(gConv.instructions.dropNonOp, + List( + VarOp(ALOAD, 0), VarOp(ASTORE, 1), // store this + Op(ICONST_1), VarOp(ISTORE, 2), Jump(GOTO, Label(10)), // store return value + Label(10), VarOp(ILOAD, 2), // load return value + VarOp(ALOAD, 0), Invoke(INVOKEVIRTUAL, "C", "f", "()I", false), Op(IADD), Op(IRETURN))) + + // line numbers are kept, so there's a line 2 (from the inlined f) + assert(gConv.instructions exists { + case LineNumber(2, _) => true + case _ => false + }, gConv.instructions.filter(_.isInstanceOf[LineNumber])) + + assert(gConv.localVars.map(_.name).sorted == List("f_this", "this"), gConv.localVars) + assert(g.maxStack == 2 && g.maxLocals == 3, s"${g.maxLocals} - ${g.maxStack}") + } + + @Test + def nothingTypedOK(): Unit = { + val code = + """class C { + | def f: Nothing = ??? + | def g: Int = { f; 1 } + |} + """.stripMargin + + // On the bytecode level, methods of type Nothing have return type Nothing$. + // This can be treated like any other result object. + + // See also discussion around ATHROW in BCodeBodyBuilder + + val (g, _) = inlineTest(code) + val expectedInlined = List( + VarOp(ALOAD, 0), VarOp(ASTORE, 1), // store this + Field(GETSTATIC, "scala/Predef$", "MODULE$", "Lscala/Predef$;"), Invoke(INVOKEVIRTUAL, "scala/Predef$", "$qmark$qmark$qmark", "()Lscala/runtime/Nothing$;", false)) // inlined call to ??? + + assertSameCode(convertMethod(g).instructions.dropNonOp.take(4), expectedInlined) + + localOpt.methodOptimizations(g, "C") + assertSameCode(convertMethod(g).instructions.dropNonOp, + expectedInlined ++ List(VarOp(ASTORE, 2), VarOp(ALOAD, 2), Op(ATHROW))) + } + + @Test + def synchronizedNoInline(): Unit = { + val code = + """class C { + | def f: Int = 0 + | def g: Int = f + |} + """.stripMargin + + val (_, can) = inlineTest(code, cls => { + val f = cls.methods.asScala.find(_.name == "f").get + f.access |= ACC_SYNCHRONIZED + }) + assert(can.get.isInstanceOf[SynchronizedMethod], can) + } + + @Test + def tryCatchOK(): Unit = { + val code = + """class C { + | def f: Int = try { 1 } catch { case _: Exception => 2 } + | def g = f + 1 + |} + """.stripMargin + val (_, r) = inlineTest(code) + assert(r.isEmpty, r) + } + + @Test + def tryCatchNoInline(): Unit = { + // cannot inline f: there's a value on g's stack. if f throws and enters the handler, all values + // on the stack are removed, including the one of g's stack that we still need. + val code = + """class C { + | def f: Int = try { 1 } catch { case _: Exception => 2 } + | def g = println(f) + |} + """.stripMargin + val (_, r) = inlineTest(code) + assert(r.get.isInstanceOf[MethodWithHandlerCalledOnNonEmptyStack], r) + } + + @Test + def illegalAccessNoInline(): Unit = { + val code = + """package a { + | class C { + | private def f: Int = 0 + | def g: Int = f + | } + |} + |package b { + | class D { + | def h(c: a.C): Int = c.g + 1 + | } + |} + """.stripMargin + + val List(c, d) = compile(code) + + val cTp = classBTypeFromParsedClassfile(c.name) + val dTp = classBTypeFromParsedClassfile(d.name) + + val g = c.methods.asScala.find(_.name == "g").get + val h = d.methods.asScala.find(_.name == "h").get + val gCall = h.instructions.iterator.asScala.collect({ + case m: MethodInsnNode if m.name == "g" => m + }).next() + + val analyzer = new AsmAnalyzer(h, dTp.internalName) + + val r = inliner.inline( + gCall, + analyzer.frameAt(gCall).getStackSize, + h, + dTp, + g, + cTp, + receiverKnownNotNull = true, + keepLineNumbers = true) + + assert(r.get.isInstanceOf[IllegalAccessInstruction], r) + } + + @Test + def inlineSimpleAtInline(): Unit = { + val code = + """class C { + | @inline final def f = 0 + | final def g = 1 + | + | def test = f + g + |} + """.stripMargin + val List(cCls) = compile(code) + val instructions = getSingleMethod(cCls, "test").instructions + assert(instructions.contains(Op(ICONST_0)), instructions.stringLines) + assert(!instructions.contains(Op(ICONST_1)), instructions) + } + + @Test + def cyclicInline(): Unit = { + val code = + """class C { + | @inline final def f: Int = g + | @inline final def g: Int = f + |} + """.stripMargin + val List(c) = compile(code) + val methods @ List(_, g) = c.methods.asScala.filter(_.name.length == 1).toList + val List(fIns, gIns) = methods.map(instructionsFromMethod(_).dropNonOp) + val invokeG = Invoke(INVOKEVIRTUAL, "C", "g", "()I", false) + assert(fIns contains invokeG, fIns) // no inlining into f, that request is elided + assert(gIns contains invokeG, gIns) // f is inlined into g, g invokes itself recursively + + assert(callGraph.callsites.size == 3, callGraph.callsites) + for (callsite <- callGraph.callsites.values if methods.contains(callsite.callsiteMethod)) { + checkCallsite(callsite, g) + } + } + + @Test + def cyclicInline2(): Unit = { + val code = + """class C { + | @inline final def h: Int = f + | @inline final def f: Int = g + g + | @inline final def g: Int = h + |} + """.stripMargin + val List(c) = compile(code) + val methods @ List(f, g, h) = c.methods.asScala.filter(_.name.length == 1).sortBy(_.name).toList + val List(fIns, gIns, hIns) = methods.map(instructionsFromMethod(_).dropNonOp) + val invokeG = Invoke(INVOKEVIRTUAL, "C", "g", "()I", false) + assert(fIns.count(_ == invokeG) == 2, fIns) // no inlining into f, these requests are elided + assert(gIns.count(_ == invokeG) == 2, gIns) + assert(hIns.count(_ == invokeG) == 2, hIns) + + assert(callGraph.callsites.size == 7, callGraph.callsites) + for (callsite <- callGraph.callsites.values if methods.contains(callsite.callsiteMethod)) { + checkCallsite(callsite, g) + } + } + + @Test + def arraycopy(): Unit = { + // also tests inlining of a void-returning method (no return value on the stack) + val code = + """// can't use the `compat.Platform.arraycopy` from the std lib for now, because the classfile doesn't have a ScalaInlineInfo attribute + |object Platform { + | @inline def arraycopy(src: AnyRef, srcPos: Int, dest: AnyRef, destPos: Int, length: Int) { + | System.arraycopy(src, srcPos, dest, destPos, length) + | } + |} + |class C { + | def f(src: AnyRef, srcPos: Int, dest: AnyRef, destPos: Int, length: Int): Unit = { + | Platform.arraycopy(src, srcPos, dest, destPos, length) + | } + |} + """.stripMargin + val List(c, _, _) = compile(code) + val ins = getSingleMethod(c, "f").instructions + val invokeSysArraycopy = Invoke(INVOKESTATIC, "java/lang/System", "arraycopy", "(Ljava/lang/Object;ILjava/lang/Object;II)V", false) + assert(ins contains invokeSysArraycopy, ins.stringLines) + } + + @Test + def arrayMemberMethod(): Unit = { + // This used to crash when building the call graph. The `owner` field of the MethodInsnNode + // for the invocation of `clone` is not an internal name, but a full array descriptor + // [Ljava.lang.Object; - the documentation in the ASM library didn't mention that possibility. + val code = + """class C { + | def f(a: Array[Object]) = { + | a.clone() + | } + |} + """.stripMargin + val List(c) = compile(code) + assert(callGraph.callsites.values exists (_.callsiteInstruction.name == "clone")) + } + + @Test + def atInlineInTrait(): Unit = { + val code = + """trait T { + | @inline final def f = 0 + |} + |class C { + | def g(t: T) = t.f + |} + """.stripMargin + val List(c, t, tClass) = compile(code) + assertNoInvoke(getSingleMethod(c, "g")) + } + + @Test + def inlinePrivateMethodWithHandler(): Unit = { + val code = + """class C { + | @inline private def f = try { 0 } catch { case _: Throwable => 1 } + | def g = f + |} + """.stripMargin + val List(c) = compile(code) + // no more invoke, f is inlined + assertNoInvoke(getSingleMethod(c, "g")) + } + + @Test + def inlineStaticCall(): Unit = { + val code = + """class C { + | def f = Integer.lowestOneBit(103) + |} + """.stripMargin + + val List(c) = compile(code) + val f = c.methods.asScala.find(_.name == "f").get + val callsiteIns = f.instructions.iterator().asScala.collect({ case c: MethodInsnNode => c }).next() + val clsBType = classBTypeFromParsedClassfile(c.name) + val analyzer = new AsmAnalyzer(f, clsBType.internalName) + + val integerClassBType = classBTypeFromInternalName("java/lang/Integer") + val lowestOneBitMethod = byteCodeRepository.methodNode(integerClassBType.internalName, "lowestOneBit", "(I)I").get._1 + + val r = inliner.inline( + callsiteIns, + analyzer.frameAt(callsiteIns).getStackSize, + f, + clsBType, + lowestOneBitMethod, + integerClassBType, + receiverKnownNotNull = false, + keepLineNumbers = false) + + assert(r.isEmpty, r) + val ins = instructionsFromMethod(f) + + // no invocations, lowestOneBit is inlined + assertNoInvoke(ins) + + // no null check when inlining a static method + ins foreach { + case Jump(IFNONNULL, _) => assert(false, ins.stringLines) + case _ => + } + } + + @Test + def maxLocalsMaxStackAfterInline(): Unit = { + val code = + """class C { + | @inline final def f1(x: Int): Int = { + | val a = x + 1 + | math.max(a, math.min(10, a - 1)) + | } + | + | @inline final def f2(x: Int): Unit = { + | val a = x + 1 + | println(math.max(a, 10)) + | } + | + | def g1 = println(f1(32)) + | def g2 = println(f2(32)) + |} + """.stripMargin + + val List(c) = compile(code) + val ms @ List(f1, f2, g1, g2) = c.methods.asScala.filter(_.name.length == 2).toList + + // stack height at callsite of f1 is 1, so max of g1 after inlining is max of f1 + 1 + assert(g1.maxStack == 7 && f1.maxStack == 6, s"${g1.maxStack} - ${f1.maxStack}") + + // locals in f1: this, x, a + // locals in g1 after inlining: this, this-of-f1, x, a, return value + assert(g1.maxLocals == 5 && f1.maxLocals == 3, s"${g1.maxLocals} - ${f1.maxLocals}") + + // like maxStack in g1 / f1 + assert(g2.maxStack == 5 && f2.maxStack == 4, s"${g2.maxStack} - ${f2.maxStack}") + + // like maxLocals for g1 / f1, but no return value + assert(g2.maxLocals == 4 && f2.maxLocals == 3, s"${g2.maxLocals} - ${f2.maxLocals}") + } + + @Test + def mixedCompilationNoInline(): Unit = { + // The inliner checks if the invocation `A.bar` can be safely inlined. For that it needs to have + // the bytecode of the invoked method. In mixed compilation, there's no classfile available for + // A, so `flop` cannot be inlined, we cannot check if it's safe. + + val javaCode = + """public class A { + | public static final int bar() { return 100; } + |} + """.stripMargin + + val scalaCode = + """class B { + | @inline final def flop = A.bar + | def g = flop + |} + """.stripMargin + + val warn = + """B::flop()I is annotated @inline but could not be inlined: + |Failed to check if B::flop()I can be safely inlined to B without causing an IllegalAccessError. Checking instruction INVOKESTATIC A.bar ()I failed: + |The method bar()I could not be found in the class A or any of its parents. + |Note that the following parent classes are defined in Java sources (mixed compilation), no bytecode is available: A""".stripMargin + + var c = 0 + val List(b) = compile(scalaCode, List((javaCode, "A.java")), allowMessage = i => {c += 1; i.msg contains warn}) + assert(c == 1, c) + val ins = getSingleMethod(b, "g").instructions + val invokeFlop = Invoke(INVOKEVIRTUAL, "B", "flop", "()I", false) + assert(ins contains invokeFlop, ins.stringLines) + } + + @Test + def inlineFromTraits(): Unit = { + val code = + """trait T { + | @inline final def f = g + | @inline final def g = 1 + |} + | + |class C extends T { + | def t1(t: T) = t.f + | def t2(c: C) = c.f + |} + """.stripMargin + val List(c, t, tClass) = compile(code) + // both are just `return 1`, no more calls + assertNoInvoke(getSingleMethod(c, "t1")) + assertNoInvoke(getSingleMethod(c, "t2")) + } + + @Test + def inlineMixinMethods(): Unit = { + val code = + """trait T { + | @inline final def f = 1 + |} + |class C extends T + """.stripMargin + val List(c, t, tClass) = compile(code) + // the static implementaiton method is inlined into the mixin, so there's no invocation in the mixin + assertNoInvoke(getSingleMethod(c, "f")) + } + + @Test + def inlineTraitInherited(): Unit = { + val code = + """trait T { + | @inline final def f = 1 + |} + |trait U extends T { + | @inline final def g = f + |} + |class C extends U { + | def t1 = f + | def t2 = g + |} + """.stripMargin + val List(c, t, tClass, u, uClass) = compile(code) + assertNoInvoke(getSingleMethod(c, "t1")) + assertNoInvoke(getSingleMethod(c, "t2")) + } + + @Test + def virtualTraitNoInline(): Unit = { + val code = + """trait T { + | @inline def f = 1 + |} + |class C extends T { + | def t1(t: T) = t.f + | def t2 = this.f + |} + """.stripMargin + val warns = Set( + "C::f()I is annotated @inline but cannot be inlined: the method is not final and may be overridden", + "T::f()I is annotated @inline but cannot be inlined: the method is not final and may be overridden") + var count = 0 + val List(c, t, tClass) = compile(code, allowMessage = i => {count += 1; warns.exists(i.msg contains _)}) + assert(count == 2, count) + assertInvoke(getSingleMethod(c, "t1"), "T", "f") + assertInvoke(getSingleMethod(c, "t2"), "C", "f") + } + + @Test + def sealedTraitInline(): Unit = { + val code = + """sealed trait T { + | @inline def f = 1 + |} + |class C { + | def t1(t: T) = t.f + |} + """.stripMargin + val List(c, t, tClass) = compile(code) + assertNoInvoke(getSingleMethod(c, "t1")) + } + + @Test + def inlineFromObject(): Unit = { + val code = + """trait T { + | @inline def f = 0 + |} + |object O extends T { + | @inline def g = 1 + | // mixin generates `def f = T$class.f(this)`, which is inlined here (we get ICONST_0) + |} + |class C { + | def t1 = O.f // the mixin method of O is inlined, so we directly get the ICONST_0 + | def t2 = O.g // object members are inlined + | def t3(t: T) = t.f // no inlining here + |} + """.stripMargin + val warn = "T::f()I is annotated @inline but cannot be inlined: the method is not final and may be overridden" + var count = 0 + val List(c, oMirror, oModule, t, tClass) = compile(code, allowMessage = i => {count += 1; i.msg contains warn}) + assert(count == 1, count) + + assertNoInvoke(getSingleMethod(oModule, "f")) + + assertNoInvoke(getSingleMethod(c, "t1")) + assertNoInvoke(getSingleMethod(c, "t2")) + assertInvoke(getSingleMethod(c, "t3"), "T", "f") + } + + @Test + def selfTypeInline(): Unit = { + val code = + """trait T { self: Assembly => + | @inline final def f = g + | @inline final def m = 1 + |} + |trait Assembly extends T { + | @inline final def g = 1 + | @inline final def n = m // inlined. (*) + | // (*) the declaration class of m is T. the signature of T$class.m is m(LAssembly;)I. so we need the self type to build the + | // signature. then we can look up the MethodNode of T$class.m and then rewrite the INVOKEINTERFACE to INVOKESTATIC. + |} + |class C { + | def t1(a: Assembly) = a.f // like above, decl class is T, need self-type of T to rewrite the interface call to static. + | def t2(a: Assembly) = a.n + |} + """.stripMargin + + val List(assembly, assemblyClass, c, t, tClass) = compile(code) + + assertNoInvoke(getSingleMethod(tClass, "f")) + + assertNoInvoke(getSingleMethod(assemblyClass, "n")) + + assertNoInvoke(getSingleMethod(c, "t1")) + assertNoInvoke(getSingleMethod(c, "t2")) + } + + @Test + def selfTypeInline2(): Unit = { + // There are some interesting things going on here with the self types. Here's a short version: + // + // trait T1 { def f = 1 } + // trait T2a { self: T1 with T2a => // self type in the backend: T1 + // def f = 2 + // def g = f // resolved to T2a.f + // } + // trait T2b { self: T2b with T1 => // self type in the backend: T2b + // def f = 2 + // def g = f // resolved to T1.f + // } + // + // scala> val t = typeOf[T2a]; exitingMixin(t.typeOfThis.typeSymbol) // self type of T2a is T1 + // res28: $r.intp.global.Symbol = trait T1 + // + // scala> typeOf[T2a].typeOfThis.member(newTermName("f")).owner // f in T2a is resolved as T2a.f + // res29: $r.intp.global.Symbol = trait T2a + // + // scala> val t = typeOf[T2b]; exitingMixin(t.typeOfThis.typeSymbol) // self type of T2b is T1 + // res30: $r.intp.global.Symbol = trait T2b + // + // scala> typeOf[T2b].typeOfThis.member(newTermName("f")).owner // f in T2b is resolved as T1.f + // res31: $r.intp.global.Symbol = trait T1 + + val code = + """trait T1 { + | @inline def f: Int = 0 + | @inline def g1 = f // not inlined: f not final, so T1$class.g1 has an interface call T1.f + |} + | + |// erased self-type (used in impl class for `self` parameter): T1 + |trait T2a { self: T1 with T2a => + | @inline override final def f = 1 + | @inline def g2a = f // inlined: resolved as T2a.f, which is re-written to T2a$class.f, so T2a$class.g2a has ICONST_1 + |} + | + |final class Ca extends T1 with T2a { + | // mixin generates accessors like `def g1 = T1$class.g1`, the impl class method call is inlined into the accessor. + | + | def m1a = g1 // call to accessor, inlined, we get the interface call T1.f + | def m2a = g2a // call to accessor, inlined, we get ICONST_1 + | def m3a = f // call to accessor, inlined, we get ICONST_1 + | + | def m4a(t: T1) = t.f // T1.f is not final, so not inlined, interface call to T1.f + | def m5a(t: T2a) = t.f // re-written to T2a$class.f, inlined, ICONST_1 + |} + | + |// erased self-type: T2b + |trait T2b { self: T2b with T1 => + | @inline override final def f = 1 + | @inline def g2b = f // not inlined: resolved as T1.f, so T2b$class.g2b has an interface call T1.f + |} + | + |final class Cb extends T1 with T2b { + | def m1b = g1 // inlined, we get the interface call to T1.f + | def m2b = g2b // inlined, we get the interface call to T1.f + | def m3b = f // inlined, we get ICONST_1 + | + | def m4b(t: T1) = t.f // T1.f is not final, so not inlined, interface call to T1.f + | def m5b(t: T2b) = t.f // re-written to T2b$class.f, inlined, ICONST_1 + |} + """.stripMargin + + val warning = "T1::f()I is annotated @inline but cannot be inlined: the method is not final and may be overridden" + var count = 0 + val List(ca, cb, t1, t1C, t2a, t2aC, t2b, t2bC) = compile(code, allowMessage = i => {count += 1; i.msg contains warning}) + assert(count == 4, count) // see comments, f is not inlined 4 times + + val t2aCfDesc = t2aC.methods.asScala.find(_.name == "f").get.desc + assert(t2aCfDesc == "(LT1;)I", t2aCfDesc) // self-type of T2a is T1 + + val t2bCfDesc = t2bC.methods.asScala.find(_.name == "f").get.desc + assert(t2bCfDesc == "(LT2b;)I", t2bCfDesc) // self-type of T2b is T2b + + assertNoInvoke(getSingleMethod(t2aC, "g2a")) + assertInvoke(getSingleMethod(t2bC, "g2b"), "T1", "f") + + assertInvoke(getSingleMethod(ca, "m1a"), "T1", "f") + assertNoInvoke(getSingleMethod(ca, "m2a")) // no invoke, see comment on def g2a + assertNoInvoke(getSingleMethod(ca, "m3a")) + assertInvoke(getSingleMethod(ca, "m4a"), "T1", "f") + assertNoInvoke(getSingleMethod(ca, "m5a")) + + assertInvoke(getSingleMethod(cb, "m1b"), "T1", "f") + assertInvoke(getSingleMethod(cb, "m2b"), "T1", "f") // invoke, see comment on def g2b + assertNoInvoke(getSingleMethod(cb, "m3b")) + assertInvoke(getSingleMethod(cb, "m4b"), "T1", "f") + assertNoInvoke(getSingleMethod(cb, "m5b")) + } + + @Test + def finalSubclassInline(): Unit = { + val code = + """class C { + | @inline def f = 0 + | @inline final def g = 1 + |} + |final class D extends C + |object E extends C + |class T { + | def t1(d: D) = d.f + d.g + E.f + E.g // d.f can be inlined because the reciever type is D, which is final. + |} // so d.f can be resolved statically. same for E.f + """.stripMargin + val List(c, d, e, eModule, t) = compile(code) + assertNoInvoke(getSingleMethod(t, "t1")) + } + + @Test + def inlineFromNestedClasses(): Unit = { + val code = + """class C { + | trait T { @inline final def f = 1 } + | class D extends T{ + | def m(t: T) = t.f + | } + | + | def m(d: D) = d.f + |} + """.stripMargin + val List(c, d, t, tC) = compile(code) + assertNoInvoke(getSingleMethod(d, "m")) + assertNoInvoke(getSingleMethod(c, "m")) + } + + @Test + def inlineTraitCastReceiverToSelf(): Unit = { + val code = + """class C { def foo(x: Int) = x } + |trait T { self: C => + | @inline final def f(x: Int) = foo(x) + | def t1 = f(1) + | def t2(t: T) = t.f(2) + |} + """.stripMargin + val List(c, t, tc) = compile(code) + val t1 = getSingleMethod(tc, "t1") + val t2 = getSingleMethod(tc, "t2") + val cast = TypeOp(CHECKCAST, "C") + Set(t1, t2).foreach(m => assert(m.instructions.contains(cast), m.instructions)) + } + + @Test + def abstractMethodWarning(): Unit = { + val code = + """abstract class C { + | @inline def foo: Int + |} + |class T { + | def t1(c: C) = c.foo + |} + """.stripMargin + val warn = "C::foo()I is annotated @inline but cannot be inlined: the method is not final and may be overridden" + var c = 0 + compile(code, allowMessage = i => {c += 1; i.msg contains warn}) + assert(c == 1, c) + } + + @Test + def abstractFinalMethodError(): Unit = { + val code = + """abstract class C { + | @inline final def foo: Int + |} + |trait T { + | @inline final def bar: Int + |} + """.stripMargin + val err = "abstract member may not have final modifier" + var i = 0 + compile(code, allowMessage = info => {i += 1; info.msg contains err}) + assert(i == 2, i) + } + + @Test + def noInlineTraitFieldAccessors(): Unit = { + val code = + """sealed trait T { + | lazy val a = 0 + | val b = 1 + | final lazy val c = 2 + | final val d = 3 + | final val d1: Int = 3 + | + | @noinline def f = 5 // re-written to T$class + | @noinline final def g = 6 // re-written + | + | @noinline def h: Int + | @inline def i: Int + |} + | + |trait U { // not sealed + | lazy val a = 0 + | val b = 1 + | final lazy val c = 2 + | final val d = 3 + | final val d1: Int = 3 + | + | @noinline def f = 5 // not re-written (not final) + | @noinline final def g = 6 // re-written + | + | @noinline def h: Int + | @inline def i: Int + |} + | + |class C { + | def m1(t: T) = t.a + t.b + t.c + t.d1 + | def m2(t: T) = t.d // inlined by the type-checker's constant folding + | def m3(t: T) = t.f + t.g + t.h + t.i + | + | def m4(u: U) = u.a + u.b + u.c + u.d1 + | def m5(u: U) = u.d + | def m6(u: U) = u.f + u.g + u.h + u.i + |} + """.stripMargin + + val List(c, t, tClass, u, uClass) = compile(code, allowMessage = _.msg contains "i()I is annotated @inline but cannot be inlined") + val m1 = getSingleMethod(c, "m1") + assertInvoke(m1, "T", "a") + assertInvoke(m1, "T", "b") + assertInvoke(m1, "T", "c") + + assertNoInvoke(getSingleMethod(c, "m2")) + + val m3 = getSingleMethod(c, "m3") + assertInvoke(m3, "T$class", "f") + assertInvoke(m3, "T$class", "g") + assertInvoke(m3, "T", "h") + assertInvoke(m3, "T", "i") + + val m4 = getSingleMethod(c, "m4") + assertInvoke(m4, "U", "a") + assertInvoke(m4, "U", "b") + assertInvoke(m4, "U", "c") + + assertNoInvoke(getSingleMethod(c, "m5")) + + val m6 = getSingleMethod(c, "m6") + assertInvoke(m6, "U", "f") + assertInvoke(m6, "U$class", "g") + assertInvoke(m6, "U", "h") + assertInvoke(m6, "U", "i") + } + + @Test + def mixedNoCrashSI9111(): Unit = { + val javaCode = + """public final class A { + | public static final class T { } + | public static final class Inner { + | public static final class T { } + | public T newT() { return null; } + | } + |} + """.stripMargin + + val scalaCode = + """class C { + | val i = new A.Inner() + |} + """.stripMargin + + // We don't get to see the warning about SI-9111, because it is associated with the MethodInlineInfo + // of method newT, which is not actually used. + // The problem is: if we reference `newT` in the scalaCode, the scala code does not compile, + // because then SI-9111 triggers during type-checking class C, in the compiler frontend, and + // we don't even get to the backend. + // Nevertheless, the workaround for SI-9111 in BcodeAsmCommon.buildInlineInfoFromClassSymbol + // is still necessary, otherwise this test crashes. + // The warning below is the typical warning we get in mixed compilation. + val warn = + """failed to determine if <init> should be inlined: + |The method <init>()V could not be found in the class A$Inner or any of its parents. + |Note that the following parent classes could not be found on the classpath: A$Inner""".stripMargin + + var c = 0 + + compileClasses(newCompiler(extraArgs = InlinerTest.args + " -Yopt-warnings:_"))( + scalaCode, + List((javaCode, "A.java")), + allowMessage = i => {c += 1; i.msg contains warn}) + assert(c == 1, c) + } + + @Test + def inlineInvokeSpecial(): Unit = { + val code = + """class Aa { + | def f1 = 0 + |} + |class B extends Aa { + | @inline final override def f1 = 1 + super.f1 // invokespecial Aa.f1 + | + | private def f2m = 0 // public B$$f2m in bytecode + | @inline final def f2 = f2m // invokevirtual B.B$$f2m + | + | private def this(x: Int) = this() // public in bytecode + | @inline final def f3 = new B() // invokespecial B.<init>() + | @inline final def f4 = new B(1) // invokespecial B.<init>(I) + | + | def t1 = f1 // inlined + | def t2 = f2 // inlined + | def t3 = f3 // inlined + | def t4 = f4 // inlined + |} + |class T { + | def t1(b: B) = b.f1 // cannot inline: contains a super call + | def t2(b: B) = b.f2 // inlined + | def t3(b: B) = b.f3 // inlined + | def t4(b: B) = b.f4 // inlined + |} + """.stripMargin + + val warn = + """B::f1()I is annotated @inline but could not be inlined: + |The callee B::f1()I contains the instruction INVOKESPECIAL Aa.f1 ()I + |that would cause an IllegalAccessError when inlined into class T.""".stripMargin + var c = 0 + val List(a, b, t) = compile(code, allowMessage = i => {c += 1; i.msg contains warn}) + assert(c == 1, c) + + assertInvoke(getSingleMethod(b, "t1"), "Aa", "f1") + assertInvoke(getSingleMethod(b, "t2"), "B", "B$$f2m") + assertInvoke(getSingleMethod(b, "t3"), "B", "<init>") + assertInvoke(getSingleMethod(b, "t4"), "B", "<init>") + + assertInvoke(getSingleMethod(t, "t1"), "B", "f1") + assertInvoke(getSingleMethod(t, "t2"), "B", "B$$f2m") + assertInvoke(getSingleMethod(t, "t3"), "B", "<init>") + assertInvoke(getSingleMethod(t, "t4"), "B", "<init>") + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala index 5430e33d6c..1ce1b88ff2 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala @@ -13,17 +13,26 @@ import scala.tools.testing.AssertUtil._ import CodeGenTools._ import scala.tools.partest.ASMConverters import ASMConverters._ +import scala.tools.testing.ClearAfterClass + +object MethodLevelOpts extends ClearAfterClass.Clearable { + var methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:method") + def clear(): Unit = { methodOptCompiler = null } +} @RunWith(classOf[JUnit4]) -class MethodLevelOpts { - val methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:method") +class MethodLevelOpts extends ClearAfterClass { + ClearAfterClass.stateToClear = MethodLevelOpts + + val methodOptCompiler = MethodLevelOpts.methodOptCompiler def wrapInDefault(code: Instruction*) = List(Label(0), LineNumber(1, Label(0))) ::: code.toList ::: List(Label(1)) @Test def eliminateEmptyTry(): Unit = { val code = "def f = { try {} catch { case _: Throwable => 0; () }; 1 }" - assertSameCode(singleMethodInstructions(methodOptCompiler)(code), wrapInDefault(Op(ICONST_1), Op(IRETURN))) + val warn = "a pure expression does nothing in statement position" + assertSameCode(singleMethodInstructions(methodOptCompiler)(code, allowMessage = _.msg contains warn), wrapInDefault(Op(ICONST_1), Op(IRETURN))) } @Test diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/ScalaInlineInfoTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/ScalaInlineInfoTest.scala new file mode 100644 index 0000000000..f8e887426b --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/ScalaInlineInfoTest.scala @@ -0,0 +1,85 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import scala.tools.asm.Opcodes._ +import org.junit.Assert._ + +import CodeGenTools._ +import scala.tools.nsc.backend.jvm.BTypes.{MethodInlineInfo, InlineInfo} +import scala.tools.partest.ASMConverters +import ASMConverters._ +import scala.collection.convert.decorateAsScala._ + +object ScalaInlineInfoTest { + var compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:none") + def clear(): Unit = { compiler = null } +} + +@RunWith(classOf[JUnit4]) +class ScalaInlineInfoTest { + val compiler = newCompiler() + + @Test + def traitMembersInlineInfo(): Unit = { + val code = + """trait T { + | def f1 = 1 // concrete method + | private def f2 = 1 // implOnly method (does not end up in the interface) + | def f3 = { + | def nest = 0 // nested method (does not end up in the interface) + | nest + | } + | + | @inline + | def f4 = super.toString // super accessor + | + | object O // module accessor (method is generated) + | def f5 = { + | object L { val x = 0 } // nested module (just flattened out) + | L.x + | } + | + | @noinline + | def f6: Int // abstract method (not in impl class) + | + | // fields + | + | val x1 = 0 + | var y2 = 0 + | var x3: Int + | lazy val x4 = 0 + | + | final val x5 = 0 + |} + """.stripMargin + + val cs @ List(t, tl, to, tCls) = compileClasses(compiler)(code) + val List(info) = t.attrs.asScala.collect({ case a: InlineInfoAttribute => a.inlineInfo }).toList + val expect = InlineInfo( + None, // self type + false, // final class + Map( + ("O()LT$O$;", MethodInlineInfo(true, false,false,false)), + ("T$$super$toString()Ljava/lang/String;",MethodInlineInfo(false,false,false,false)), + ("T$_setter_$x1_$eq(I)V", MethodInlineInfo(false,false,false,false)), + ("f1()I", MethodInlineInfo(false,true, false,false)), + ("f3()I", MethodInlineInfo(false,true, false,false)), + ("f4()Ljava/lang/String;", MethodInlineInfo(false,true, true, false)), + ("f5()I", MethodInlineInfo(false,true, false,false)), + ("f6()I", MethodInlineInfo(false,false,false,true )), + ("x1()I", MethodInlineInfo(false,false,false,false)), + ("x3()I", MethodInlineInfo(false,false,false,false)), + ("x3_$eq(I)V", MethodInlineInfo(false,false,false,false)), + ("x4()I", MethodInlineInfo(false,false,false,false)), + ("x5()I", MethodInlineInfo(true, false,false,false)), + ("y2()I", MethodInlineInfo(false,false,false,false)), + ("y2_$eq(I)V", MethodInlineInfo(false,false,false,false))), + None // warning + ) + assert(info == expect, info) + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala index 4a45dd9138..da9853148b 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala @@ -13,9 +13,34 @@ import scala.tools.testing.AssertUtil._ import CodeGenTools._ import scala.tools.partest.ASMConverters import ASMConverters._ +import scala.tools.testing.ClearAfterClass + +object UnreachableCodeTest extends ClearAfterClass.Clearable { + // jvm-1.6 enables emitting stack map frames, which impacts the code generation wrt dead basic blocks, + // see comment in BCodeBodyBuilder + var methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:method") + var dceCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code") + var noOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:none") + + // jvm-1.5 disables computing stack map frames, and it emits dead code as-is. note that this flag triggers a deprecation warning + var noOptNoFramesCompiler = newCompiler(extraArgs = "-target:jvm-1.5 -Ybackend:GenBCode -Yopt:l:none -deprecation") + + def clear(): Unit = { + methodOptCompiler = null + dceCompiler = null + noOptCompiler = null + noOptNoFramesCompiler = null + } +} @RunWith(classOf[JUnit4]) -class UnreachableCodeTest { +class UnreachableCodeTest extends ClearAfterClass { + ClearAfterClass.stateToClear = UnreachableCodeTest + + val methodOptCompiler = UnreachableCodeTest.methodOptCompiler + val dceCompiler = UnreachableCodeTest.dceCompiler + val noOptCompiler = UnreachableCodeTest.noOptCompiler + val noOptNoFramesCompiler = UnreachableCodeTest.noOptNoFramesCompiler def assertEliminateDead(code: (Instruction, Boolean)*): Unit = { val method = genMethod()(code.map(_._1): _*) @@ -25,15 +50,6 @@ class UnreachableCodeTest { assertSameCode(nonEliminated, expectedLive) } - // jvm-1.6 enables emitting stack map frames, which impacts the code generation wrt dead basic blocks, - // see comment in BCodeBodyBuilder - val methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:method") - val dceCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code") - val noOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:none") - - // jvm-1.5 disables computing stack map frames, and it emits dead code as-is. - val noOptNoFramesCompiler = newCompiler(extraArgs = "-target:jvm-1.5 -Ybackend:GenBCode -Yopt:l:none") - @Test def basicElimination(): Unit = { assertEliminateDead( @@ -138,7 +154,8 @@ class UnreachableCodeTest { assertSameCode(noDce.dropNonOp, List(Op(ICONST_1), Op(IRETURN), Op(ATHROW), Op(ATHROW))) // when NOT computing stack map frames, ASM's ClassWriter does not replace dead code by NOP/ATHROW - val noDceNoFrames = singleMethodInstructions(noOptNoFramesCompiler)(code) + val warn = "target:jvm-1.5 is deprecated" + val noDceNoFrames = singleMethodInstructions(noOptNoFramesCompiler)(code, allowMessage = _.msg contains warn) assertSameCode(noDceNoFrames.dropNonOp, List(Op(ICONST_1), Op(IRETURN), Op(ICONST_2), Op(IRETURN))) } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala index 24a1f9d1c1..769736669b 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala @@ -12,10 +12,18 @@ import scala.collection.JavaConverters._ import CodeGenTools._ import scala.tools.partest.ASMConverters import ASMConverters._ +import scala.tools.testing.ClearAfterClass + +object UnusedLocalVariablesTest extends ClearAfterClass.Clearable { + var dceCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:unreachable-code") + def clear(): Unit = { dceCompiler = null } +} @RunWith(classOf[JUnit4]) -class UnusedLocalVariablesTest { - val dceCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:unreachable-code") +class UnusedLocalVariablesTest extends ClearAfterClass { + ClearAfterClass.stateToClear = UnusedLocalVariablesTest + + val dceCompiler = UnusedLocalVariablesTest.dceCompiler @Test def removeUnusedVar(): Unit = { diff --git a/test/junit/scala/tools/nsc/symtab/FreshNameExtractorTest.scala b/test/junit/scala/tools/nsc/symtab/FreshNameExtractorTest.scala index effbfb2f7c..7796345351 100644 --- a/test/junit/scala/tools/nsc/symtab/FreshNameExtractorTest.scala +++ b/test/junit/scala/tools/nsc/symtab/FreshNameExtractorTest.scala @@ -32,16 +32,16 @@ class FreshNameExtractorTest { val Creator = new FreshNameCreator(prefixes.head) val Extractor = new FreshNameExtractor(prefixes.tail.head) assertThrows[MatchError] { - val Extractor(_) = TermName(Creator.newName("foo")) + TermName(Creator.newName("foo")) match { case Extractor(_) => } } } - @Test @org.junit.Ignore // SI-8818 - def extractionsFailsIfNameDoesntEndWithNumber = { - val Creator = new FreshNameCreator(prefixes.head) + @Test + def `no numeric suffix? no problem!` = { + val Creator = new FreshNameCreator(prefixes.head) val Extractor = new FreshNameExtractor(prefixes.head) - assertThrows[MatchError] { - val Extractor(_) = TermName(Creator.newName("foo") + "bar") + TermName(Creator.newName("foo") + "bar") match { + case Extractor(_) => } } } diff --git a/test/junit/scala/tools/nsc/symtab/SymbolTableTest.scala b/test/junit/scala/tools/nsc/symtab/SymbolTableTest.scala index 11e955a4bb..895ad9d683 100644 --- a/test/junit/scala/tools/nsc/symtab/SymbolTableTest.scala +++ b/test/junit/scala/tools/nsc/symtab/SymbolTableTest.scala @@ -44,4 +44,9 @@ class SymbolTableTest { assertFalse("Foo should be a superclass of Foo", fooSymbol.tpe <:< barSymbol.tpe) } + @Test + def noSymbolOuterClass_t9133: Unit = { + import symbolTable._ + assert(NoSymbol.outerClass == NoSymbol) + } } diff --git a/test/junit/scala/tools/testing/AssertThrowsTest.scala b/test/junit/scala/tools/testing/AssertThrowsTest.scala index d91e450bac..76758f51d2 100644 --- a/test/junit/scala/tools/testing/AssertThrowsTest.scala +++ b/test/junit/scala/tools/testing/AssertThrowsTest.scala @@ -38,6 +38,6 @@ class AssertThrowsTest { } catch { case e: AssertionError => return } - assert(false, "assertThrows should error if the tested expression does not throw anything") + fail("assertThrows should error if the tested expression does not throw anything") } } diff --git a/test/junit/scala/tools/testing/AssertUtil.scala b/test/junit/scala/tools/testing/AssertUtil.scala index 83a637783f..d798f2e53e 100644 --- a/test/junit/scala/tools/testing/AssertUtil.scala +++ b/test/junit/scala/tools/testing/AssertUtil.scala @@ -2,31 +2,54 @@ package scala.tools package testing import org.junit.Assert -import Assert.fail +import Assert._ import scala.runtime.ScalaRunTime.stringOf import scala.collection.{ GenIterable, IterableLike } +import scala.collection.JavaConverters._ +import scala.collection.mutable +import java.lang.ref._ +import java.lang.reflect._ +import java.util.IdentityHashMap /** This module contains additional higher-level assert statements * that are ultimately based on junit.Assert primitives. */ object AssertUtil { - /** - * Check if throwable T (or a subclass) was thrown during evaluation of f, and that its message - * satisfies the `checkMessage` predicate. - * If any other exception will be re-thrown. + private final val timeout = 60 * 1000L // wait a minute + + private implicit class `ref helper`[A](val r: Reference[A]) extends AnyVal { + def isEmpty: Boolean = r.get == null + def nonEmpty: Boolean = !isEmpty + } + private implicit class `class helper`(val clazz: Class[_]) extends AnyVal { + def allFields: List[Field] = { + def loop(k: Class[_]): List[Field] = + if (k == null) Nil + else k.getDeclaredFields.toList ::: loop(k.getSuperclass) + loop(clazz) + } + } + private implicit class `field helper`(val f: Field) extends AnyVal { + def follow(o: AnyRef): AnyRef = { + f setAccessible true + f get o + } + } + + /** Check that throwable T (or a subclass) was thrown during evaluation of `body`, + * and that its message satisfies the `checkMessage` predicate. + * Any other exception is propagated. */ - def assertThrows[T <: Throwable](f: => Any, + def assertThrows[T <: Throwable](body: => Any, checkMessage: String => Boolean = s => true) (implicit manifest: Manifest[T]): Unit = { - try f - catch { - case e: Throwable if checkMessage(e.getMessage) => - val clazz = manifest.runtimeClass - if (!clazz.isAssignableFrom(e.getClass)) - throw e - else return + try { + body + fail("Expression did not throw!") + } catch { + case e: Throwable if (manifest.runtimeClass isAssignableFrom e.getClass) && + checkMessage(e.getMessage) => } - fail("Expression did not throw!") } /** JUnit-style assertion for `IterableLike.sameElements`. @@ -41,4 +64,29 @@ object AssertUtil { */ def assertSameElements[A, B >: A](expected: IterableLike[A, _], actual: Iterator[B]): Unit = assertSameElements(expected, actual.toList, "") + + /** Value is not strongly reachable from roots after body is evaluated. + */ + def assertNotReachable[A <: AnyRef](a: => A, roots: AnyRef*)(body: => Unit): Unit = { + val wkref = new WeakReference(a) + def refs(root: AnyRef): mutable.Set[AnyRef] = { + val seen = new IdentityHashMap[AnyRef, Unit] + def loop(o: AnyRef): Unit = + if (wkref.nonEmpty && o != null && !seen.containsKey(o)) { + seen.put(o, ()) + for { + f <- o.getClass.allFields + if !Modifier.isStatic(f.getModifiers) + if !f.getType.isPrimitive + if !classOf[Reference[_]].isAssignableFrom(f.getType) + } loop(f follow o) + } + loop(root) + seen.keySet.asScala + } + body + for (r <- roots if wkref.nonEmpty) { + assertFalse(s"Root $r held reference", refs(r) contains wkref.get) + } + } } diff --git a/test/junit/scala/tools/testing/AssertUtilTest.scala b/test/junit/scala/tools/testing/AssertUtilTest.scala new file mode 100644 index 0000000000..03d8815ab2 --- /dev/null +++ b/test/junit/scala/tools/testing/AssertUtilTest.scala @@ -0,0 +1,21 @@ +package scala.tools +package testing + +import org.junit.Assert._ +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import AssertUtil._ + +import java.lang.ref._ + +@RunWith(classOf[JUnit4]) +class AssertUtilTest { + + @Test def reachableIgnoresReferences(): Unit = { + class Holder[A](val ref: SoftReference[A]) + val o = new Object + val r = new SoftReference(o) + assertNotReachable(o, new Holder(r)) { } + } +} diff --git a/test/junit/scala/tools/testing/ClearAfterClass.java b/test/junit/scala/tools/testing/ClearAfterClass.java new file mode 100644 index 0000000000..232d459c4e --- /dev/null +++ b/test/junit/scala/tools/testing/ClearAfterClass.java @@ -0,0 +1,20 @@ +package scala.tools.testing; + +import org.junit.AfterClass; + +/** + * Extend this class to use JUnit's @AfterClass. This annotation only works on static methods, + * which cannot be written in Scala. + * + * Example: {@link scala.tools.nsc.backend.jvm.opt.InlinerTest} + */ +public class ClearAfterClass { + public static interface Clearable { + void clear(); + } + + public static Clearable stateToClear; + + @AfterClass + public static void clearState() { stateToClear.clear(); } +} diff --git a/test/junit/scala/tools/testing/TempDir.scala b/test/junit/scala/tools/testing/TempDir.scala new file mode 100644 index 0000000000..475de8c4a2 --- /dev/null +++ b/test/junit/scala/tools/testing/TempDir.scala @@ -0,0 +1,18 @@ +package scala.tools.testing + +import java.io.{IOException, File} + +object TempDir { + final val TEMP_DIR_ATTEMPTS = 10000 + def createTempDir(): File = { + val baseDir = new File(System.getProperty("java.io.tmpdir")) + val baseName = System.currentTimeMillis() + "-" + var c = 0 + while (c < TEMP_DIR_ATTEMPTS) { + val tempDir = new File(baseDir, baseName + c) + if (tempDir.mkdir()) return tempDir + c += 1 + } + throw new IOException(s"Failed to create directory") + } +} diff --git a/test/junit/scala/util/RandomTest.scala b/test/junit/scala/util/RandomTest.scala new file mode 100644 index 0000000000..32959675ee --- /dev/null +++ b/test/junit/scala/util/RandomTest.scala @@ -0,0 +1,15 @@ +package scala.util + +import org.junit.{ Assert, Test } + +class RandomTest { + // Test for SI-9059 + @Test def testAlphanumeric: Unit = { + def isAlphaNum(c: Char) = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') + + val items = Random.alphanumeric.take(100000) + for (c <- items) { + Assert.assertTrue(s"$c should be alphanumeric", isAlphaNum(c)) + } + } +} |