diff options
55 files changed, 1614 insertions, 176 deletions
diff --git a/bincompat-backward.whitelist.conf b/bincompat-backward.whitelist.conf index 1c48887b63..6c98dc62a1 100644 --- a/bincompat-backward.whitelist.conf +++ b/bincompat-backward.whitelist.conf @@ -194,6 +194,19 @@ filter { { matchName="scala.collection.immutable.Stream.filteredTail" problemName=MissingMethodProblem + }, + // https://github.com/scala/scala/pull/3848 -- SI-8680 + { + matchName="scala.collection.immutable.Stream.scala$collection$immutable$Stream$$loop$6" + problemName=MissingMethodProblem + }, + { + matchName="scala.collection.immutable.Stream.scala$collection$immutable$Stream$$loop$5" + problemName=MissingMethodProblem + }, + { + matchName="scala.collection.immutable.Stream.scala$collection$immutable$Stream$$loop$4" + problemName=MissingMethodProblem } ] } diff --git a/bincompat-forward.whitelist.conf b/bincompat-forward.whitelist.conf index ec80e7cce9..87a59f2d53 100644 --- a/bincompat-forward.whitelist.conf +++ b/bincompat-forward.whitelist.conf @@ -368,6 +368,27 @@ filter { { matchName="scala.reflect.io.AbstractFile.filterImpl" problemName=MissingMethodProblem + }, + // https://github.com/scala/scala/pull/3848 -- SI-8680 + { + matchName="scala.collection.immutable.Stream.scala$collection$immutable$Stream$$loop$6" + problemName=MissingMethodProblem + }, + { + matchName="scala.collection.immutable.Stream.scala$collection$immutable$Stream$$loop$5" + problemName=MissingMethodProblem + }, + { + matchName="scala.collection.immutable.Stream.scala$collection$immutable$Stream$$loop$4" + problemName=MissingMethodProblem + }, + { + matchName="scala.collection.immutable.Stream.scala$collection$immutable$Stream$$loop$3" + problemName=MissingMethodProblem + }, + { + matchName="scala.collection.immutable.Stream.scala$collection$immutable$Stream$$loop$2" + problemName=MissingMethodProblem } ] } @@ -970,6 +970,7 @@ TODO: <pathelement location="${test.junit.classes}"/> <path refid="quick.compiler.build.path"/> <path refid="quick.repl.build.path"/> + <path refid="quick.partest-extras.build.path"/> <path refid="junit.classpath"/> </path> @@ -1512,7 +1513,12 @@ TODO: <!-- =========================================================================== BINARY COMPATIBILITY TESTING ============================================================================ --> - <target name="bc.init" depends="init" unless="maven-deps-done-mima"> + <target name="bc.init" depends="init" if="test.bc.skip"> + <!-- if test.bc.skip is set, make sure that pc.prepare is not executed either --> + <property name="maven-deps-done-mima" value="true"/> + </target> + + <target name="bc.prepare" depends="bc.init" unless="maven-deps-done-mima"> <property name="bc-reference-version" value="2.11.0"/> <property name="bc-build.dir" value="${build.dir}/bc"/> @@ -1530,7 +1536,7 @@ TODO: </target> <target name="test.bc-opt" description="Optimized version of test.bc."> <optimized name="test.bc"/></target> - <target name="test.bc" depends="bc.init, pack.lib, pack.reflect"> + <target name="test.bc" depends="bc.prepare, pack.lib, pack.reflect" unless="test.bc.skip"> <bc.check project="library"/> <bc.check project="reflect"/> </target> diff --git a/src/asm/scala/tools/asm/MethodWriter.java b/src/asm/scala/tools/asm/MethodWriter.java index 0c4130e499..d30e04c625 100644 --- a/src/asm/scala/tools/asm/MethodWriter.java +++ b/src/asm/scala/tools/asm/MethodWriter.java @@ -37,7 +37,7 @@ package scala.tools.asm; * @author Eric Bruneton * @author Eugene Kuleshov */ -class MethodWriter extends MethodVisitor { +public class MethodWriter extends MethodVisitor { /** * Pseudo access flag used to denote constructors. @@ -235,11 +235,19 @@ class MethodWriter extends MethodVisitor { */ private int maxStack; + public int getMaxStack() { + return maxStack; + } + /** * Maximum number of local variables for this method. */ private int maxLocals; + public int getMaxLocals() { + return maxLocals; + } + /** * Number of local variables in the current stack map frame. */ diff --git a/src/compiler/scala/tools/reflect/quasiquotes/Holes.scala b/src/compiler/scala/reflect/quasiquotes/Holes.scala index 68cc728eb3..38b05f9d4b 100644 --- a/src/compiler/scala/tools/reflect/quasiquotes/Holes.scala +++ b/src/compiler/scala/reflect/quasiquotes/Holes.scala @@ -1,4 +1,4 @@ -package scala.tools.reflect +package scala.reflect package quasiquotes import scala.collection.{immutable, mutable} diff --git a/src/compiler/scala/tools/reflect/quasiquotes/Parsers.scala b/src/compiler/scala/reflect/quasiquotes/Parsers.scala index 392b7fc881..007bac27da 100644 --- a/src/compiler/scala/tools/reflect/quasiquotes/Parsers.scala +++ b/src/compiler/scala/reflect/quasiquotes/Parsers.scala @@ -1,4 +1,4 @@ -package scala.tools.reflect +package scala.reflect package quasiquotes import scala.tools.nsc.ast.parser.{Parsers => ScalaParser} diff --git a/src/compiler/scala/tools/reflect/quasiquotes/Placeholders.scala b/src/compiler/scala/reflect/quasiquotes/Placeholders.scala index b287971815..a5b42f8a1f 100644 --- a/src/compiler/scala/tools/reflect/quasiquotes/Placeholders.scala +++ b/src/compiler/scala/reflect/quasiquotes/Placeholders.scala @@ -1,4 +1,4 @@ -package scala.tools.reflect +package scala.reflect package quasiquotes import java.util.UUID.randomUUID diff --git a/src/compiler/scala/tools/reflect/quasiquotes/Quasiquotes.scala b/src/compiler/scala/reflect/quasiquotes/Quasiquotes.scala index b33069181c..72e6000e9f 100644 --- a/src/compiler/scala/tools/reflect/quasiquotes/Quasiquotes.scala +++ b/src/compiler/scala/reflect/quasiquotes/Quasiquotes.scala @@ -1,4 +1,4 @@ -package scala.tools.reflect +package scala.reflect package quasiquotes import scala.reflect.macros.runtime.Context diff --git a/src/compiler/scala/tools/reflect/quasiquotes/Reifiers.scala b/src/compiler/scala/reflect/quasiquotes/Reifiers.scala index 95113d5b00..07becdc3c6 100644 --- a/src/compiler/scala/tools/reflect/quasiquotes/Reifiers.scala +++ b/src/compiler/scala/reflect/quasiquotes/Reifiers.scala @@ -1,4 +1,4 @@ -package scala.tools.reflect +package scala.reflect package quasiquotes import java.lang.UnsupportedOperationException diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeAsmCommon.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeAsmCommon.scala index e5b4c4a6c2..0c0d726630 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeAsmCommon.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeAsmCommon.scala @@ -22,14 +22,11 @@ final class BCodeAsmCommon[G <: Global](val global: G) { * null. */ def isAnonymousOrLocalClass(classSym: Symbol): Boolean = { - def isDelambdafyLambdaClass(classSym: Symbol): Boolean = { - classSym.isAnonymousFunction && (settings.Ydelambdafy.value == "method") - } - assert(classSym.isClass, s"not a class: $classSym") - - !isDelambdafyLambdaClass(classSym) && - (classSym.isAnonymousClass || !classSym.originalOwner.isClass) + val res = (classSym.isAnonymousClass || !classSym.originalOwner.isClass) + // lambda classes are always top-level classes. + if (res) assert(!classSym.isDelambdafyFunction) + res } /** @@ -81,7 +78,7 @@ final class BCodeAsmCommon[G <: Global](val global: G) { final case class EnclosingMethodEntry(owner: String, name: String, methodDescriptor: String) /** - * If data for emitting an EnclosingMethod attribute. None if `classSym` is a member class (not + * Data for emitting an EnclosingMethod attribute. None if `classSym` is a member class (not * an anonymous or local class). See doc in BTypes. * * The class is parametrized by two functions to obtain a bytecode class descriptor for a class diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala index 397171049f..daf36ce374 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala @@ -9,7 +9,6 @@ package tools.nsc package backend package jvm -import scala.collection.{ mutable, immutable } import scala.annotation.switch import scala.tools.asm @@ -283,9 +282,10 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { val Local(tk, _, idx, isSynth) = locals.getOrMakeLocal(sym) if (rhs == EmptyTree) { emitZeroOf(tk) } else { genLoad(rhs, tk) } + val localVarStart = currProgramPoint() bc.store(idx, tk) if (!isSynth) { // there are case <synthetic> ValDef's emitted by patmat - varsInScope ::= (sym -> currProgramPoint()) + varsInScope ::= (sym -> localVarStart) } generatedType = UNIT @@ -815,7 +815,51 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { case _ => bc.emitT2T(from, to) } } else if (from.isNothingType) { - emit(asm.Opcodes.ATHROW) // ICode enters here into enterIgnoreMode, we'll rely instead on DCE at ClassNode level. + /* There are two possibilities for from.isNothingType: emitting a "throw e" expressions and + * loading a (phantom) value of type Nothing. + * + * The Nothing type in Scala's type system does not exist in the JVM. In bytecode, Nothing + * is mapped to scala.runtime.Nothing$. To the JVM, a call to Predef.??? looks like it would + * return an object of type Nothing$. We need to do something with that phantom object on + * the stack. "Phantom" because it never exists: such methods always throw, but the JVM does + * not know that. + * + * Note: The two verifiers (old: type inference, new: type checking) have different + * requirements. Very briefly: + * + * Old (http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.10.2.1): at + * each program point, no matter what branches were taken to get there + * - Stack is same size and has same typed values + * - Local and stack values need to have consistent types + * - In practice, the old verifier seems to ignore unreachable code and accept any + * instructions after an ATHROW. For example, there can be another ATHROW (without + * loading another throwable first). + * + * New (http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.10.1) + * - Requires consistent stack map frames. GenBCode generates stack frames if -target:jvm-1.6 + * or higher. + * - In practice: the ASM library computes stack map frames for us (ClassWriter). Emitting + * correct frames after an ATHROW is probably complex, so ASM uses the following strategy: + * - Every time when generating an ATHROW, a new basic block is started. + * - During classfile writing, such basic blocks are found to be dead: no branches go there + * - Eliminating dead code would probably require complex shifts in the output byte buffer + * - But there's an easy solution: replace all code in the dead block with with + * `nop; nop; ... nop; athrow`, making sure the bytecode size stays the same + * - The corresponding stack frame can be easily generated: on entering a dead the block, + * the frame requires a single Throwable on the stack. + * - Since there are no branches to the dead block, the frame requirements are never violated. + * + * To summarize the above: it does matter what we emit after an ATHROW. + * + * NOW: if we end up here because we emitted a load of a (phantom) value of type Nothing$, + * there was no ATHROW emitted. So, we have to make the verifier happy and do something + * with that value. Since Nothing$ extends Throwable, the easiest is to just emit an ATHROW. + * + * If we ended up here because we generated a "throw e" expression, we know the last + * emitted instruction was an ATHROW. As explained above, it is OK to emit a second ATHROW, + * the verifiers will be happy. + */ + emit(asm.Opcodes.ATHROW) } else if (from.isNullType) { bc drop from emit(asm.Opcodes.ACONST_NULL) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala index 5670715cd3..14bffd67ab 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala @@ -782,14 +782,9 @@ abstract class BCodeHelpers extends BCodeIdiomatic with BytecodeWriters { } } // end of trait BCClassGen - /* basic functionality for class file building of plain, mirror, and beaninfo classes. */ - abstract class JBuilder extends BCInnerClassGen { - - } // end of class JBuilder - /* functionality for building plain and mirror classes */ abstract class JCommonBuilder - extends JBuilder + extends BCInnerClassGen with BCAnnotGen with BCForwardersGen with BCPickles { } @@ -851,7 +846,7 @@ abstract class BCodeHelpers extends BCodeIdiomatic with BytecodeWriters { } // end of class JMirrorBuilder /* builder of bean info classes */ - class JBeanInfoBuilder extends JBuilder { + class JBeanInfoBuilder extends BCInnerClassGen { /* * Generate a bean info class that describes the given class. diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala index 4592031a31..03bc32061b 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala @@ -346,6 +346,13 @@ abstract class BCodeSkelBuilder extends BCodeHelpers { /* * Bookkeeping for method-local vars and method-params. + * + * TODO: use fewer slots. local variable slots are never re-used in separate blocks. + * In the following example, x and y could use the same slot. + * def foo() = { + * { val x = 1 } + * { val y = "a" } + * } */ object locals { diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala index 7bf61b4f51..53ac5bfdc7 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala @@ -713,6 +713,8 @@ abstract class BTypes { // TODO @lry I don't really understand the reasoning here. // Both this and other are classes. The code takes (transitively) all superclasses and // finds the first common one. + // MOST LIKELY the answer can be found here, see the comments and links by Miguel: + // - https://issues.scala-lang.org/browse/SI-3872 firstCommonSuffix(this :: this.superClassesTransitive, other :: other.superClassesTransitive) } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BackendStats.scala b/src/compiler/scala/tools/nsc/backend/jvm/BackendStats.scala new file mode 100644 index 0000000000..4b9383c67c --- /dev/null +++ b/src/compiler/scala/tools/nsc/backend/jvm/BackendStats.scala @@ -0,0 +1,24 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2014 LAMP/EPFL + * @author Martin Odersky + */ + +package scala.tools.nsc +package backend.jvm + +import scala.reflect.internal.util.Statistics + +object BackendStats { + import Statistics.{newTimer, newSubTimer} + val bcodeTimer = newTimer("time in backend", "jvm") + + val bcodeInitTimer = newSubTimer("bcode initialization", bcodeTimer) + val bcodeGenStat = newSubTimer("code generation", bcodeTimer) + val bcodeDceTimer = newSubTimer("dead code elimination", bcodeTimer) + val bcodeWriteTimer = newSubTimer("classfile writing", bcodeTimer) + + def timed[T](timer: Statistics.Timer)(body: => T): T = { + val start = Statistics.startTimer(timer) + try body finally Statistics.stopTimer(timer, start) + } +} diff --git a/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala b/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala index 0a7c894a69..ba94a9c44c 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala @@ -11,8 +11,10 @@ package jvm import scala.collection.{ mutable, immutable } import scala.annotation.switch +import scala.reflect.internal.util.Statistics import scala.tools.asm +import scala.tools.asm.tree.ClassNode /* * Prepare in-memory representations of classfiles using the ASM Tree API, and serialize them to disk. @@ -213,6 +215,14 @@ abstract class GenBCode extends BCodeSyncAndTry { * - converting the plain ClassNode to byte array and placing it on queue-3 */ class Worker2 { + def localOptimizations(classNode: ClassNode): Unit = { + def dce(): Boolean = BackendStats.timed(BackendStats.bcodeDceTimer) { + if (settings.YoptUnreachableCode) opt.LocalOpt.removeUnreachableCode(classNode) + else false + } + + dce() + } def run() { while (true) { @@ -222,8 +232,10 @@ abstract class GenBCode extends BCodeSyncAndTry { return } else { - try { addToQ3(item) } - catch { + try { + localOptimizations(item.plain) + addToQ3(item) + } catch { case ex: Throwable => ex.printStackTrace() error(s"Error while emitting ${item.plain.name}\n${ex.getMessage}") @@ -272,10 +284,13 @@ abstract class GenBCode extends BCodeSyncAndTry { * */ override def run() { + val bcodeStart = Statistics.startTimer(BackendStats.bcodeTimer) + val initStart = Statistics.startTimer(BackendStats.bcodeInitTimer) arrivalPos = 0 // just in case scalaPrimitives.init() bTypes.intializeCoreBTypes() + Statistics.stopTimer(BackendStats.bcodeInitTimer, initStart) // initBytecodeWriter invokes fullName, thus we have to run it before the typer-dependent thread is activated. bytecodeWriter = initBytecodeWriter(cleanup.getEntryPoints) @@ -287,6 +302,7 @@ abstract class GenBCode extends BCodeSyncAndTry { // closing output files. bytecodeWriter.close() + Statistics.stopTimer(BackendStats.bcodeTimer, bcodeStart) /* TODO Bytecode can be verified (now that all classfiles have been written to disk) * @@ -312,9 +328,15 @@ abstract class GenBCode extends BCodeSyncAndTry { private def buildAndSendToDisk(needsOutFolder: Boolean) { feedPipeline1() + val genStart = Statistics.startTimer(BackendStats.bcodeGenStat) (new Worker1(needsOutFolder)).run() + Statistics.stopTimer(BackendStats.bcodeGenStat, genStart) + (new Worker2).run() + + val writeStart = Statistics.startTimer(BackendStats.bcodeWriteTimer) drainQ3() + Statistics.stopTimer(BackendStats.bcodeWriteTimer, writeStart) } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala new file mode 100644 index 0000000000..3acd2d6154 --- /dev/null +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -0,0 +1,190 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2014 LAMP/EPFL + * @author Martin Odersky + */ + +package scala.tools.nsc +package backend.jvm +package opt + +import scala.tools.asm.{Opcodes, MethodWriter, ClassWriter} +import scala.tools.asm.tree.analysis.{Analyzer, BasicValue, BasicInterpreter} +import scala.tools.asm.tree._ +import scala.collection.convert.decorateAsScala._ +import scala.collection.{ mutable => m } + +/** + * Intra-Method optimizations. + */ +object LocalOpt { + /** + * Remove unreachable instructions from all (non-abstract) methods. + * + * @param clazz The class whose methods are optimized + * @return `true` if unreachable code was elminated in some method, `false` otherwise. + */ + def removeUnreachableCode(clazz: ClassNode): Boolean = { + clazz.methods.asScala.foldLeft(false) { + case (changed, method) => removeUnreachableCode(method, clazz.name) || changed + } + } + + /** + * Remove unreachable code from a method. + * We rely on dead code elimination provided by the ASM framework, as described in the ASM User + * Guide (http://asm.ow2.org/index.html), Section 8.2.1. It runs a data flow analysis, which only + * computes Frame information for reachable instructions. Instructions for which no Frame data is + * available after the analyis are unreachable. + * + * TODO doc: it also removes empty handlers, unused local vars + * + * Returns `true` if dead code in `method` has been eliminated. + */ + private def removeUnreachableCode(method: MethodNode, ownerClassName: String): Boolean = { + if (method.instructions.size == 0) return false // fast path for abstract methods + + val codeRemoved = removeUnreachableCodeImpl(method, ownerClassName) + + // unreachable-code also removes unused local variable nodes and empty exception handlers. + // This is required for correctness: such nodes are not allowed to refer to instruction offsets + // that don't exist (because they have been eliminated). + val localsRemoved = removeUnusedLocalVariableNodes(method) + val handlersRemoved = removeEmptyExceptionHandlers(method) + + // When eliminating a handler, the catch block becomes unreachable. The recursive invocation + // removes these blocks. + // Note that invoking removeUnreachableCode*Impl* a second time is not enough: removing the dead + // catch block can render other handlers empty, which also have to be removed in turn. + if (handlersRemoved) removeUnreachableCode(method, ownerClassName) + + // assert that we can leave local variable annotations as-is + def nullOrEmpty[T](l: java.util.List[T]) = l == null || l.isEmpty + assert(nullOrEmpty(method.visibleLocalVariableAnnotations), method.visibleLocalVariableAnnotations) + assert(nullOrEmpty(method.invisibleLocalVariableAnnotations), method.invisibleLocalVariableAnnotations) + + codeRemoved || localsRemoved || handlersRemoved + } + + private def removeUnreachableCodeImpl(method: MethodNode, ownerClassName: String): Boolean = { + val initialSize = method.instructions.size + if (initialSize == 0) return false + + // The data flow analysis requires the maxLocals / maxStack fields of the method to be computed. + computeMaxLocalsMaxStack(method) + val a = new Analyzer[BasicValue](new BasicInterpreter) + a.analyze(ownerClassName, method) + val frames = a.getFrames + + var i = 0 + val itr = method.instructions.iterator() + while (itr.hasNext) { + val ins = itr.next() + // Don't remove label nodes: they might be referenced for example in a LocalVariableNode + if (frames(i) == null && !ins.isInstanceOf[LabelNode]) { + // Instruction iterators allow removing during iteration. + // Removing is O(1): instructions are doubly linked list elements. + itr.remove() + } + i += 1 + } + + method.instructions.size != initialSize + } + + /** + * Remove exception handlers that cover empty code blocks from all methods of `clazz`. + * Returns `true` if any exception handler was eliminated. + */ + def removeEmptyExceptionHandlers(clazz: ClassNode): Boolean = { + clazz.methods.asScala.foldLeft(false) { + case (changed, method) => removeEmptyExceptionHandlers(method) || changed + } + } + + /** + * Remove exception handlers that cover empty code blocks. A block is considered empty if it + * consist only of labels, frames, line numbers, nops and gotos. + * + * Note that no instructions are eliminated. + * + * @return `true` if some exception handler was eliminated. + */ + def removeEmptyExceptionHandlers(method: MethodNode): Boolean = { + /** True if there exists code between start and end. */ + def containsExecutableCode(start: AbstractInsnNode, end: LabelNode): Boolean = { + start != end && (start.getOpcode match { + // FrameNode, LabelNode and LineNumberNode have opcode == -1. + case -1 | Opcodes.NOP | Opcodes.GOTO => containsExecutableCode(start.getNext, end) + case _ => true + }) + } + + val initialNumberHandlers = method.tryCatchBlocks.size + val handlersIter = method.tryCatchBlocks.iterator() + while(handlersIter.hasNext) { + val handler = handlersIter.next() + if (!containsExecutableCode(handler.start, handler.end)) handlersIter.remove() + } + method.tryCatchBlocks.size != initialNumberHandlers + } + + /** + * Remove all non-parameter entries from the local variable table which denote variables that are + * not actually read or written. + * + * Note that each entry in the local variable table has a start, end and index. Two entries with + * the same index, but distinct start / end ranges are different variables, they may have not the + * same type or name. + * + * TODO: also re-allocate locals to occupy fewer slots after eliminating unused ones + */ + def removeUnusedLocalVariableNodes(method: MethodNode): Boolean = { + def variableIsUsed(start: AbstractInsnNode, end: LabelNode, varIndex: Int): Boolean = { + start != end && (start match { + case v: VarInsnNode => v.`var` == varIndex + case _ => variableIsUsed(start.getNext, end, varIndex) + }) + } + + val initialNumVars = method.localVariables.size + val localsIter = method.localVariables.iterator() + // The parameters and `this` (for instance methods) have the lowest indices in the local variables + // table. Note that double / long fields occupy two slots, so we sum up the sizes. Since getSize + // returns 0 for void, we have to add `max 1`. + val paramsSize = scala.tools.asm.Type.getArgumentTypes(method.desc).map(_.getSize max 1).sum + val thisSize = if ((method.access & Opcodes.ACC_STATIC) == 0) 1 else 0 + val endParamIndex = paramsSize + thisSize + while (localsIter.hasNext) { + val local = localsIter.next() + // parameters and `this` have the lowest indices, starting at 0 + val used = local.index < endParamIndex || variableIsUsed(local.start, local.end, local.index) + if (!used) + localsIter.remove() + } + method.localVariables.size == initialNumVars + } + + + /** + * In order to run an Analyzer, the maxLocals / maxStack fields need to be available. The ASM + * framework only computes these values during bytecode generation. + * + * Sicne there's currently no better way, we run a bytecode generator on the method and extract + * the computed values. This required changes to the ASM codebase: + * - the [[MethodWriter]] class was made public + * - accessors for maxLocals / maxStack were added to the MethodWriter class + * + * We could probably make this faster (and allocate less memory) by hacking the ASM framework + * more: create a subclass of MethodWriter with a /dev/null byteVector. Another option would be + * to create a separate visitor for computing those values, duplicating the functionality from the + * MethodWriter. + */ + private def computeMaxLocalsMaxStack(method: MethodNode) { + val cw = new ClassWriter(ClassWriter.COMPUTE_MAXS) + val excs = method.exceptions.asScala.toArray + val mw = cw.visitMethod(method.access, method.name, method.desc, method.signature, excs).asInstanceOf[MethodWriter] + method.accept(mw) + method.maxLocals = mw.getMaxLocals + method.maxStack = mw.getMaxStack + } +} diff --git a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala index 91b03869e5..466e397dd7 100644 --- a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala @@ -209,9 +209,35 @@ trait ScalaSettings extends AbsScalaSettings // the current standard is "inline" but we are moving towards "method" val Ydelambdafy = ChoiceSetting ("-Ydelambdafy", "strategy", "Strategy used for translating lambdas into JVM code.", List("inline", "method"), "inline") + object YoptChoices extends MultiChoiceEnumeration { + val unreachableCode = Choice("unreachable-code", "Eliminate unreachable code") + + val lNone = Choice("l:none", "Don't enable any optimizations") + + private val defaultChoices = List(unreachableCode) + val lDefault = Choice("l:default", "Enable default optimizations: "+ defaultChoices.mkString(","), expandsTo = defaultChoices) + + private val methodChoices = List(lDefault) + val lMethod = Choice("l:method", "Intra-method optimizations: "+ methodChoices.mkString(","), expandsTo = methodChoices) + + private val projectChoices = List(lMethod) + val lProject = Choice("l:project", "Cross-method optimizations within the current project: "+ projectChoices.mkString(","), expandsTo = projectChoices) + + private val classpathChoices = List(lProject) + val lClasspath = Choice("l:classpath", "Cross-method optmizations across the entire classpath: "+ classpathChoices.mkString(","), expandsTo = classpathChoices) + } + + val Yopt = MultiChoiceSetting( + name = "-Yopt", + helpArg = "optimization", + descr = "Enable optimizations", + domain = YoptChoices) + + def YoptUnreachableCode: Boolean = !Yopt.isSetByUser || Yopt.contains(YoptChoices.unreachableCode) + private def removalIn212 = "This flag is scheduled for removal in 2.12. If you have a case where you need this flag then please report a bug." - object YstatisticsPhases extends MultiChoiceEnumeration { val parser, typer, patmat, erasure, cleanup = Value } + object YstatisticsPhases extends MultiChoiceEnumeration { val parser, typer, patmat, erasure, cleanup, jvm = Value } val Ystatistics = { val description = "Print compiler statistics for specific phases" MultiChoiceSetting( diff --git a/src/compiler/scala/tools/nsc/transform/Delambdafy.scala b/src/compiler/scala/tools/nsc/transform/Delambdafy.scala index 9cc8d779a0..12e7b23f48 100644 --- a/src/compiler/scala/tools/nsc/transform/Delambdafy.scala +++ b/src/compiler/scala/tools/nsc/transform/Delambdafy.scala @@ -245,18 +245,22 @@ abstract class Delambdafy extends Transform with TypingTransformers with ast.Tre // - make `anonClass.isAnonymousClass` true. // - use `newAnonymousClassSymbol` or push the required variations into a similar factory method // - reinstate the assertion in `Erasure.resolveAnonymousBridgeClash` - val suffix = "$lambda$" + ( + val suffix = nme.DELAMBDAFY_LAMBDA_CLASS_NAME + "$" + ( if (funOwner.isPrimaryConstructor) "" else "$" + funOwner.name + "$" ) - val name = unit.freshTypeName(s"${oldClass.name.decode}$suffix") + val oldClassPart = oldClass.name.decode + // make sure the class name doesn't contain $anon, otherwsie isAnonymousClass/Function may be true + val name = unit.freshTypeName(s"$oldClassPart$suffix".replace("$anon", "$nestedInAnon")) - val anonClass = pkg newClassSymbol(name, originalFunction.pos, FINAL | SYNTHETIC) addAnnotation SerialVersionUIDAnnotation - anonClass setInfo ClassInfoType(parents, newScope, anonClass) + val lambdaClass = pkg newClassSymbol(name, originalFunction.pos, FINAL | SYNTHETIC) addAnnotation SerialVersionUIDAnnotation + lambdaClass setInfo ClassInfoType(parents, newScope, lambdaClass) + assert(!lambdaClass.isAnonymousClass && !lambdaClass.isAnonymousFunction, "anonymous class name: "+ lambdaClass.name) + assert(lambdaClass.isDelambdafyFunction, "not lambda class name: " + lambdaClass.name) val captureProxies2 = new LinkedHashMap[Symbol, TermSymbol] captures foreach {capture => - val sym = anonClass.newVariable(capture.name.toTermName, capture.pos, SYNTHETIC) + val sym = lambdaClass.newVariable(capture.name.toTermName, capture.pos, SYNTHETIC) sym setInfo capture.info captureProxies2 += ((capture, sym)) } @@ -266,30 +270,30 @@ abstract class Delambdafy extends Transform with TypingTransformers with ast.Tre val thisProxy = { val target = targetMethod(originalFunction) if (thisReferringMethods contains target) { - val sym = anonClass.newVariable(nme.FAKE_LOCAL_THIS, originalFunction.pos, SYNTHETIC) + val sym = lambdaClass.newVariable(nme.FAKE_LOCAL_THIS, originalFunction.pos, SYNTHETIC) sym.info = oldClass.tpe sym } else NoSymbol } - val decapturify = new DeCapturifyTransformer(captureProxies2, unit, oldClass, anonClass, originalFunction.symbol.pos, thisProxy) + val decapturify = new DeCapturifyTransformer(captureProxies2, unit, oldClass, lambdaClass, originalFunction.symbol.pos, thisProxy) val accessorMethod = createAccessorMethod(thisProxy, originalFunction) val decapturedFunction = decapturify.transform(originalFunction).asInstanceOf[Function] val members = (optionSymbol(thisProxy).toList ++ (captureProxies2 map (_._2))) map {member => - anonClass.info.decls enter member + lambdaClass.info.decls enter member ValDef(member, gen.mkZero(member.tpe)) setPos decapturedFunction.pos } // constructor - val constr = createConstructor(anonClass, members) + val constr = createConstructor(lambdaClass, members) // apply method with same arguments and return type as original lambda. - val applyMethodDef = createApplyMethod(anonClass, decapturedFunction, accessorMethod, thisProxy) + val applyMethodDef = createApplyMethod(lambdaClass, decapturedFunction, accessorMethod, thisProxy) - val bridgeMethod = createBridgeMethod(anonClass, originalFunction, applyMethodDef) + val bridgeMethod = createBridgeMethod(lambdaClass, originalFunction, applyMethodDef) def fulldef(sym: Symbol) = if (sym == NoSymbol) sym.toString @@ -305,7 +309,7 @@ abstract class Delambdafy extends Transform with TypingTransformers with ast.Tre val body = members ++ List(constr, applyMethodDef) ++ bridgeMethod // TODO if member fields are private this complains that they're not accessible - (localTyper.typedPos(decapturedFunction.pos)(ClassDef(anonClass, body)).asInstanceOf[ClassDef], thisProxy, accessorMethod) + (localTyper.typedPos(decapturedFunction.pos)(ClassDef(lambdaClass, body)).asInstanceOf[ClassDef], thisProxy, accessorMethod) } val (anonymousClassDef, thisProxy, accessorMethod) = makeAnonymousClass diff --git a/src/compiler/scala/tools/nsc/typechecker/Infer.scala b/src/compiler/scala/tools/nsc/typechecker/Infer.scala index 421021163e..ee2775ee26 100644 --- a/src/compiler/scala/tools/nsc/typechecker/Infer.scala +++ b/src/compiler/scala/tools/nsc/typechecker/Infer.scala @@ -544,7 +544,14 @@ trait Infer extends Checkable { val targs = solvedTypes(tvars, tparams, tparams map varianceInTypes(formals), upper = false, lubDepth(formals) max lubDepth(argtpes)) // Can warn about inferring Any/AnyVal as long as they don't appear // explicitly anywhere amongst the formal, argument, result, or expected type. - def canWarnAboutAny = !(pt :: restpe :: formals ::: argtpes exists (t => (t contains AnyClass) || (t contains AnyValClass))) + // ...or lower bound of a type param, since they're asking for it. + def canWarnAboutAny = { + val loBounds = tparams map (_.info.bounds.lo) + val hasAny = pt :: restpe :: formals ::: argtpes ::: loBounds exists (t => + (t contains AnyClass) || (t contains AnyValClass) + ) + !hasAny + } def argumentPosition(idx: Int): Position = context.tree match { case x: ValOrDefDef => x.rhs match { case Apply(fn, args) if idx < args.size => args(idx).pos diff --git a/src/compiler/scala/tools/reflect/FastTrack.scala b/src/compiler/scala/tools/reflect/FastTrack.scala index 64cf3d0847..8fed53c89f 100644 --- a/src/compiler/scala/tools/reflect/FastTrack.scala +++ b/src/compiler/scala/tools/reflect/FastTrack.scala @@ -5,7 +5,7 @@ import scala.reflect.reify.Taggers import scala.tools.nsc.typechecker.{ Analyzer, Macros } import scala.reflect.runtime.Macros.currentMirror import scala.reflect.api.Universe -import scala.tools.reflect.quasiquotes.{ Quasiquotes => QuasiquoteImpls } +import scala.reflect.quasiquotes.{ Quasiquotes => QuasiquoteImpls } /** Optimizes system macro expansions by hardwiring them directly to their implementations * bypassing standard reflective load and invoke to avoid the overhead of Java/Scala reflection. diff --git a/src/library/scala/collection/immutable/Stream.scala b/src/library/scala/collection/immutable/Stream.scala index 1f97c4c769..91a4e1c43d 100644 --- a/src/library/scala/collection/immutable/Stream.scala +++ b/src/library/scala/collection/immutable/Stream.scala @@ -176,6 +176,12 @@ import scala.language.implicitConversions * loop(1, 1) * } * }}} + * + * Note that `mkString` forces evaluation of a `Stream`, but `addString` does + * not. In both cases, a `Stream` that is or ends in a cycle + * (e.g. `lazy val s: Stream[Int] = 0 #:: s`) will convert additional trips + * through the cycle to `...`. Additionally, `addString` will display an + * un-memoized tail as `?`. * * @tparam A the type of the elements contained in this stream. * @@ -253,12 +259,22 @@ self => * @note Often we use `Stream`s to represent an infinite set or series. If * that's the case for your particular `Stream` then this function will never * return and will probably crash the VM with an `OutOfMemory` exception. + * This function will not hang on a finite cycle, however. * * @return The fully realized `Stream`. */ def force: Stream[A] = { - var these = this - while (!these.isEmpty) these = these.tail + // Use standard 2x 1x iterator trick for cycle detection ("those" is slow one) + var these, those = this + if (!these.isEmpty) these = these.tail + while (those ne these) { + if (these.isEmpty) return this + these = these.tail + if (these.isEmpty) return this + these = these.tail + if (these eq those) return this + those = those.tail + } this } @@ -309,9 +325,24 @@ self => override def toStream: Stream[A] = this - override def hasDefiniteSize = { - def loop(s: Stream[A]): Boolean = s.isEmpty || s.tailDefined && loop(s.tail) - loop(this) + override def hasDefiniteSize: Boolean = isEmpty || { + if (!tailDefined) false + else { + // Two-iterator trick (2x & 1x speed) for cycle detection. + var those = this + var these = tail + while (those ne these) { + if (these.isEmpty) return true + if (!these.tailDefined) return false + these = these.tail + if (these.isEmpty) return true + if (!these.tailDefined) return false + these = these.tail + if (those eq these) return false + those = those.tail + } + false // Cycle detected + } } /** Create a new stream which contains all elements of this stream followed by @@ -690,7 +721,8 @@ self => * `end`. Inside, the string representations of defined elements (w.r.t. * the method `toString()`) are separated by the string `sep`. The method will * not force evaluation of undefined elements. A tail of such elements will be - * represented by a `"?"` instead. + * represented by a `"?"` instead. A cyclic stream is represented by a `"..."` + * at the point where the cycle repeats. * * @param b The [[collection.mutable.StringBuilder]] factory to which we need * to add the string elements. @@ -701,16 +733,81 @@ self => * resulting string. */ override def addString(b: StringBuilder, start: String, sep: String, end: String): StringBuilder = { - def loop(pre: String, these: Stream[A]) { - if (these.isEmpty) b append end - else { - b append pre append these.head - if (these.tailDefined) loop(sep, these.tail) - else b append sep append "?" append end + b append start + if (!isEmpty) { + b append head + var cursor = this + var n = 1 + if (cursor.tailDefined) { // If tailDefined, also !isEmpty + var scout = tail + if (scout.isEmpty) { + // Single element. Bail out early. + b append end + return b + } + if ((cursor ne scout) && scout.tailDefined) { + cursor = scout + scout = scout.tail + // Use 2x 1x iterator trick for cycle detection; slow iterator can add strings + while ((cursor ne scout) && scout.tailDefined) { + b append sep append cursor.head + n += 1 + cursor = cursor.tail + scout = scout.tail + if (scout.tailDefined) scout = scout.tail + } + } + if (!scout.tailDefined) { // Not a cycle, scout hit an end + while (cursor ne scout) { + b append sep append cursor.head + n += 1 + cursor = cursor.tail + } + } + else { + // Cycle. + // If we have a prefix of length P followed by a cycle of length C, + // the scout will be at position (P%C) in the cycle when the cursor + // enters it at P. They'll then collide when the scout advances another + // C - (P%C) ahead of the cursor. + // If we run the scout P farther, then it will be at the start of + // the cycle: (C - (P%C) + (P%C)) == C == 0. So if another runner + // starts at the beginning of the prefix, they'll collide exactly at + // the start of the loop. + var runner = this + var k = 0 + while (runner ne scout) { + runner = runner.tail + scout = scout.tail + k += 1 + } + // Now runner and scout are at the beginning of the cycle. Advance + // cursor, adding to string, until it hits; then we'll have covered + // everything once. If cursor is already at beginning, we'd better + // advance one first unless runner didn't go anywhere (in which case + // we've already looped once). + if ((cursor eq scout) && (k > 0)) { + b append sep append cursor.head + n += 1 + cursor = cursor.tail + } + while (cursor ne scout) { + b append sep append cursor.head + n += 1 + cursor = cursor.tail + } + // Subtract prefix length from total length for cycle reporting. + // (Not currently used, but probably a good idea for the future.) + n -= k + } + } + if (!cursor.isEmpty) { + // Either undefined or cyclic; we can check with tailDefined + if (!cursor.tailDefined) b append sep append "?" + else b append sep append "..." } } - b append start - loop("", this) + b append end b } diff --git a/src/library/scala/collection/mutable/LongMap.scala b/src/library/scala/collection/mutable/LongMap.scala index 984ae6f7cc..ef488a3697 100644 --- a/src/library/scala/collection/mutable/LongMap.scala +++ b/src/library/scala/collection/mutable/LongMap.scala @@ -388,12 +388,14 @@ extends AbstractMap[Long, V] nextPair = anotherPair anotherPair = null } - nextPair = null + else nextPair = null ans } } override def foreach[A](f: ((Long,V)) => A) { + if ((extraKeys & 1) == 1) f((0L, zeroValue.asInstanceOf[V])) + if ((extraKeys & 2) == 2) f((Long.MinValue, minValue.asInstanceOf[V])) var i,j = 0 while (i < _keys.length & j < _size) { val k = _keys(i) @@ -403,8 +405,6 @@ extends AbstractMap[Long, V] } i += 1 } - if ((extraKeys & 1) == 1) f((0L, zeroValue.asInstanceOf[V])) - if ((extraKeys & 2) == 2) f((Long.MinValue, minValue.asInstanceOf[V])) } override def clone(): LongMap[V] = { @@ -417,6 +417,8 @@ extends AbstractMap[Long, V] /** Applies a function to all keys of this map. */ def foreachKey[A](f: Long => A) { + if ((extraKeys & 1) == 1) f(0L) + if ((extraKeys & 2) == 2) f(Long.MinValue) var i,j = 0 while (i < _keys.length & j < _size) { val k = _keys(i) @@ -426,12 +428,12 @@ extends AbstractMap[Long, V] } i += 1 } - if ((extraKeys & 1) == 1) f(0L) - if ((extraKeys & 2) == 2) f(Long.MinValue) } /** Applies a function to all values of this map. */ def foreachValue[A](f: V => A) { + if ((extraKeys & 1) == 1) f(zeroValue.asInstanceOf[V]) + if ((extraKeys & 2) == 2) f(minValue.asInstanceOf[V]) var i,j = 0 while (i < _keys.length & j < _size) { val k = _keys(i) @@ -441,8 +443,6 @@ extends AbstractMap[Long, V] } i += 1 } - if ((extraKeys & 1) == 1) f(zeroValue.asInstanceOf[V]) - if ((extraKeys & 2) == 2) f(minValue.asInstanceOf[V]) } /** Creates a new `LongMap` with different values. @@ -450,6 +450,8 @@ extends AbstractMap[Long, V] * collection immediately. */ def mapValuesNow[V1](f: V => V1): LongMap[V1] = { + val zv = if ((extraKeys & 1) == 1) f(zeroValue.asInstanceOf[V]).asInstanceOf[AnyRef] else null + val mv = if ((extraKeys & 2) == 2) f(minValue.asInstanceOf[V]).asInstanceOf[AnyRef] else null val lm = new LongMap[V1](LongMap.exceptionDefault, 1, false) val kz = java.util.Arrays.copyOf(_keys, _keys.length) val vz = new Array[AnyRef](_values.length) @@ -462,8 +464,6 @@ extends AbstractMap[Long, V] } i += 1 } - val zv = if ((extraKeys & 1) == 1) f(zeroValue.asInstanceOf[V]).asInstanceOf[AnyRef] else null - val mv = if ((extraKeys & 2) == 2) f(minValue.asInstanceOf[V]).asInstanceOf[AnyRef] else null lm.initializeTo(mask, extraKeys, zv, mv, _size, _vacant, kz, vz) lm } @@ -472,6 +472,8 @@ extends AbstractMap[Long, V] * Note: the default, if any, is not transformed. */ def transformValues(f: V => V): this.type = { + if ((extraKeys & 1) == 1) zeroValue = f(zeroValue.asInstanceOf[V]).asInstanceOf[AnyRef] + if ((extraKeys & 2) == 2) minValue = f(minValue.asInstanceOf[V]).asInstanceOf[AnyRef] var i,j = 0 while (i < _keys.length & j < _size) { val k = _keys(i) @@ -481,26 +483,8 @@ extends AbstractMap[Long, V] } i += 1 } - if ((extraKeys & 1) == 1) zeroValue = f(zeroValue.asInstanceOf[V]).asInstanceOf[AnyRef] - if ((extraKeys & 2) == 2) minValue = f(minValue.asInstanceOf[V]).asInstanceOf[AnyRef] this } - - /* - override def toString = { - val sb = new StringBuilder("LongMap(") - var n = 0 - foreach{ case (k,v) => - if (n > 0) sb ++= ", " - sb ++= k.toString - sb ++= " -> " - sb ++= v.toString - n += 1 - } - sb += ')' - sb.result - } - */ } object LongMap { diff --git a/src/library/scala/sys/package.scala b/src/library/scala/sys/package.scala index 386bd84113..e493603bc2 100644 --- a/src/library/scala/sys/package.scala +++ b/src/library/scala/sys/package.scala @@ -61,16 +61,15 @@ package object sys { def env: immutable.Map[String, String] = immutable.Map(System.getenv().asScala.toSeq: _*) /** Register a shutdown hook to be run when the VM exits. - * The newly created thread is marked as a daemon so it will not - * interfere with VM shutdown. The hook is automatically registered: - * the returned value can be ignored, but is available in case the - * Thread requires further modification. It can also be unregistered - * by calling ShutdownHookThread#remove(). + * The hook is automatically registered: the returned value can be ignored, + * but is available in case the Thread requires further modification. + * It can also be unregistered by calling ShutdownHookThread#remove(). * * Note that shutdown hooks are NOT guaranteed to be run. * * @param body the body of code to run at shutdown * @return the Thread which will run the shutdown hook. + * @see [[scala.sys.ShutdownHookThread]] */ def addShutdownHook(body: => Unit): ShutdownHookThread = ShutdownHookThread(body) diff --git a/src/partest-extras/scala/tools/partest/ASMConverters.scala b/src/partest-extras/scala/tools/partest/ASMConverters.scala index d618e086f4..67a4e8ae01 100644 --- a/src/partest-extras/scala/tools/partest/ASMConverters.scala +++ b/src/partest-extras/scala/tools/partest/ASMConverters.scala @@ -2,70 +2,216 @@ package scala.tools.partest import scala.collection.JavaConverters._ import scala.tools.asm -import asm.tree.{ClassNode, MethodNode, InsnList} +import asm.{tree => t} /** Makes using ASM from ByteCodeTests more convenient. * * Wraps ASM instructions in case classes so that equals and toString work * for the purpose of bytecode diffing and pretty printing. */ -trait ASMConverters { - // wrap ASM's instructions so we get case class-style `equals` and `toString` - object instructions { - def fromMethod(meth: MethodNode): List[Instruction] = { - val insns = meth.instructions - val asmToScala = new AsmToScala{ def labelIndex(l: asm.tree.AbstractInsnNode) = insns.indexOf(l) } - - asmToScala.mapOver(insns.iterator.asScala.toList).asInstanceOf[List[Instruction]] +object ASMConverters { + + /** + * Transform the instructions of an ASM Method into a list of [[Instruction]]s. + */ + def instructionsFromMethod(meth: t.MethodNode): List[Instruction] = new AsmToScala(meth).instructions + + def convertMethod(meth: t.MethodNode): Method = new AsmToScala(meth).method + + implicit class RichInstructionLists(val self: List[Instruction]) extends AnyVal { + def === (other: List[Instruction]) = equivalentBytecode(self, other) + + def dropLinesFrames = self.filterNot(i => i.isInstanceOf[LineNumber] || i.isInstanceOf[FrameEntry]) + + private def referencedLabels(instruction: Instruction): Set[Instruction] = instruction match { + case Jump(op, label) => Set(label) + case LookupSwitch(op, dflt, keys, labels) => (dflt :: labels).toSet + case TableSwitch(op, min, max, dflt, labels) => (dflt :: labels).toSet + case LineNumber(line, start) => Set(start) + case _ => Set.empty } - sealed abstract class Instruction { def opcode: String } - case class Field (opcode: String, desc: String, name: String, owner: String) extends Instruction - case class Incr (opcode: String, incr: Int, `var`: Int) extends Instruction - case class Op (opcode: String) extends Instruction - case class IntOp (opcode: String, operand: Int) extends Instruction - case class Jump (opcode: String, label: Label) extends Instruction - case class Ldc (opcode: String, cst: Any) extends Instruction - case class LookupSwitch (opcode: String, dflt: Label, keys: List[Integer], labels: List[Label]) extends Instruction - case class TableSwitch (opcode: String, dflt: Label, max: Int, min: Int, labels: List[Label]) extends Instruction - case class Method (opcode: String, desc: String, name: String, owner: String) extends Instruction - case class NewArray (opcode: String, desc: String, dims: Int) extends Instruction - case class TypeOp (opcode: String, desc: String) extends Instruction - case class VarOp (opcode: String, `var`: Int) extends Instruction - case class Label (offset: Int) extends Instruction { def opcode: String = "" } - case class FrameEntry (local: List[Any], stack: List[Any]) extends Instruction { def opcode: String = "" } - case class LineNumber (line: Int, start: Label) extends Instruction { def opcode: String = "" } + def dropStaleLabels = { + val definedLabels: Set[Instruction] = self.filter(_.isInstanceOf[Label]).toSet + val usedLabels: Set[Instruction] = self.flatMap(referencedLabels)(collection.breakOut) + self.filterNot(definedLabels diff usedLabels) + } + + def dropNonOp = dropLinesFrames.dropStaleLabels + } + + sealed abstract class Instruction extends Product { + def opcode: Int + + // toString such that the first field, "opcode: Int", is printed textually. + final override def toString() = { + import scala.tools.asm.util.Printer.OPCODES + def opString(op: Int) = if (OPCODES.isDefinedAt(op)) OPCODES(op) else "?" + val printOpcode = opcode != -1 + + productPrefix + ( + if (printOpcode) Iterator(opString(opcode)) ++ productIterator.drop(1) + else productIterator + ).mkString("(", ", ", ")") + } } - abstract class AsmToScala { - import instructions._ + case class Method(instructions: List[Instruction], handlers: List[ExceptionHandler], localVars: List[LocalVariable]) + + case class Field (opcode: Int, owner: String, name: String, desc: String) extends Instruction + case class Incr (opcode: Int, `var`: Int, incr: Int) extends Instruction + case class Op (opcode: Int) extends Instruction + case class IntOp (opcode: Int, operand: Int) extends Instruction + case class Jump (opcode: Int, label: Label) extends Instruction + case class Ldc (opcode: Int, cst: Any) extends Instruction + case class LookupSwitch(opcode: Int, dflt: Label, keys: List[Int], labels: List[Label]) extends Instruction + case class TableSwitch (opcode: Int, min: Int, max: Int, dflt: Label, labels: List[Label]) extends Instruction + case class Invoke (opcode: Int, owner: String, name: String, desc: String, itf: Boolean) extends Instruction + case class NewArray (opcode: Int, desc: String, dims: Int) extends Instruction + case class TypeOp (opcode: Int, desc: String) extends Instruction + case class VarOp (opcode: Int, `var`: Int) extends Instruction + case class Label (offset: Int) extends Instruction { def opcode: Int = -1 } + case class FrameEntry (`type`: Int, local: List[Any], stack: List[Any]) extends Instruction { def opcode: Int = -1 } + case class LineNumber (line: Int, start: Label) extends Instruction { def opcode: Int = -1 } + + case class ExceptionHandler(start: Label, end: Label, handler: Label, desc: Option[String]) + case class LocalVariable(name: String, desc: String, signature: Option[String], start: Label, end: Label, index: Int) + + class AsmToScala(asmMethod: t.MethodNode) { + + def instructions: List[Instruction] = asmMethod.instructions.iterator.asScala.toList map apply + + def method: Method = Method(instructions, convertHandlers(asmMethod), convertLocalVars(asmMethod)) - def labelIndex(l: asm.tree.AbstractInsnNode): Int + private def labelIndex(l: t.LabelNode): Int = asmMethod.instructions.indexOf(l) + + private def op(i: t.AbstractInsnNode): Int = i.getOpcode - def mapOver(is: List[Any]): List[Any] = is map { - case i: asm.tree.AbstractInsnNode => apply(i) + private def lst[T](xs: java.util.List[T]): List[T] = if (xs == null) Nil else xs.asScala.toList + + // Heterogenous List[Any] is used in FrameNode: type information about locals / stack values + // are stored in a List[Any] (Integer, String or LabelNode), see Javadoc of MethodNode#visitFrame. + // Opcodes (eg Opcodes.INTEGER) and Reference types (eg "java/lang/Object") are returned unchanged, + // LabelNodes are mapped to their LabelEntry. + private def mapOverFrameTypes(is: List[Any]): List[Any] = is map { + case i: t.LabelNode => applyLabel(i) case x => x } - def op(i: asm.tree.AbstractInsnNode) = if (asm.util.Printer.OPCODES.isDefinedAt(i.getOpcode)) asm.util.Printer.OPCODES(i.getOpcode) else "?" - def lst[T](xs: java.util.List[T]): List[T] = if (xs == null) Nil else xs.asScala.toList - def apply(l: asm.tree.LabelNode): Label = this(l: asm.tree.AbstractInsnNode).asInstanceOf[Label] - def apply(x: asm.tree.AbstractInsnNode): Instruction = x match { - case i: asm.tree.FieldInsnNode => Field (op(i), i.desc: String, i.name: String, i.owner: String) - case i: asm.tree.IincInsnNode => Incr (op(i), i.incr: Int, i.`var`: Int) - case i: asm.tree.InsnNode => Op (op(i)) - case i: asm.tree.IntInsnNode => IntOp (op(i), i.operand: Int) - case i: asm.tree.JumpInsnNode => Jump (op(i), this(i.label)) - case i: asm.tree.LdcInsnNode => Ldc (op(i), i.cst: Any) - case i: asm.tree.LookupSwitchInsnNode => LookupSwitch (op(i), this(i.dflt), lst(i.keys), mapOver(lst(i.labels)).asInstanceOf[List[Label]]) - case i: asm.tree.TableSwitchInsnNode => TableSwitch (op(i), this(i.dflt), i.max: Int, i.min: Int, mapOver(lst(i.labels)).asInstanceOf[List[Label]]) - case i: asm.tree.MethodInsnNode => Method (op(i), i.desc: String, i.name: String, i.owner: String) - case i: asm.tree.MultiANewArrayInsnNode => NewArray (op(i), i.desc: String, i.dims: Int) - case i: asm.tree.TypeInsnNode => TypeOp (op(i), i.desc: String) - case i: asm.tree.VarInsnNode => VarOp (op(i), i.`var`: Int) - case i: asm.tree.LabelNode => Label (labelIndex(x)) - case i: asm.tree.FrameNode => FrameEntry (mapOver(lst(i.local)), mapOver(lst(i.stack))) - case i: asm.tree.LineNumberNode => LineNumber (i.line: Int, this(i.start): Label) + // avoids some casts + private def applyLabel(l: t.LabelNode) = this(l: t.AbstractInsnNode).asInstanceOf[Label] + + private def apply(x: t.AbstractInsnNode): Instruction = x match { + case i: t.FieldInsnNode => Field (op(i), i.owner, i.name, i.desc) + case i: t.IincInsnNode => Incr (op(i), i.`var`, i.incr) + case i: t.InsnNode => Op (op(i)) + case i: t.IntInsnNode => IntOp (op(i), i.operand) + case i: t.JumpInsnNode => Jump (op(i), applyLabel(i.label)) + case i: t.LdcInsnNode => Ldc (op(i), i.cst: Any) + case i: t.LookupSwitchInsnNode => LookupSwitch (op(i), applyLabel(i.dflt), lst(i.keys) map (x => x: Int), lst(i.labels) map applyLabel) + case i: t.TableSwitchInsnNode => TableSwitch (op(i), i.min, i.max, applyLabel(i.dflt), lst(i.labels) map applyLabel) + case i: t.MethodInsnNode => Invoke (op(i), i.owner, i.name, i.desc, i.itf) + case i: t.MultiANewArrayInsnNode => NewArray (op(i), i.desc, i.dims) + case i: t.TypeInsnNode => TypeOp (op(i), i.desc) + case i: t.VarInsnNode => VarOp (op(i), i.`var`) + case i: t.LabelNode => Label (labelIndex(i)) + case i: t.FrameNode => FrameEntry (i.`type`, mapOverFrameTypes(lst(i.local)), mapOverFrameTypes(lst(i.stack))) + case i: t.LineNumberNode => LineNumber (i.line, applyLabel(i.start)) + } + + private def convertHandlers(method: t.MethodNode): List[ExceptionHandler] = { + method.tryCatchBlocks.asScala.map(h => ExceptionHandler(applyLabel(h.start), applyLabel(h.end), applyLabel(h.handler), Option(h.`type`)))(collection.breakOut) + } + + private def convertLocalVars(method: t.MethodNode): List[LocalVariable] = { + method.localVariables.asScala.map(v => LocalVariable(v.name, v.desc, Option(v.signature), applyLabel(v.start), applyLabel(v.end), v.index))(collection.breakOut) + } + } + + import collection.mutable.{Map => MMap} + + /** + * Bytecode is equal modula local variable numbering and label numbering. + */ + def equivalentBytecode(as: List[Instruction], bs: List[Instruction], varMap: MMap[Int, Int] = MMap(), labelMap: MMap[Int, Int] = MMap()): Boolean = { + def same(v1: Int, v2: Int, m: MMap[Int, Int]) = { + if (m contains v1) m(v1) == v2 + else if (m.valuesIterator contains v2) false // v2 is already associated with some different value v1 + else { m(v1) = v2; true } + } + def sameVar(v1: Int, v2: Int) = same(v1, v2, varMap) + def sameLabel(l1: Label, l2: Label) = same(l1.offset, l2.offset, labelMap) + def sameLabels(ls1: List[Label], ls2: List[Label]) = (ls1 corresponds ls2)(sameLabel) + + def sameFrameTypes(ts1: List[Any], ts2: List[Any]) = (ts1 corresponds ts2) { + case (t1: Label, t2: Label) => sameLabel(t1, t2) + case (x, y) => x == y + } + + if (as.isEmpty) bs.isEmpty + else if (bs.isEmpty) false + else ((as.head, bs.head) match { + case (VarOp(op1, v1), VarOp(op2, v2)) => op1 == op2 && sameVar(v1, v2) + case (Incr(op1, v1, inc1), Incr(op2, v2, inc2)) => op1 == op2 && sameVar(v1, v2) && inc1 == inc2 + + case (l1 @ Label(_), l2 @ Label(_)) => sameLabel(l1, l2) + case (Jump(op1, l1), Jump(op2, l2)) => op1 == op2 && sameLabel(l1, l2) + case (LookupSwitch(op1, l1, keys1, ls1), LookupSwitch(op2, l2, keys2, ls2)) => op1 == op2 && sameLabel(l1, l2) && keys1 == keys2 && sameLabels(ls1, ls2) + case (TableSwitch(op1, min1, max1, l1, ls1), TableSwitch(op2, min2, max2, l2, ls2)) => op1 == op2 && min1 == min2 && max1 == max2 && sameLabel(l1, l2) && sameLabels(ls1, ls2) + case (LineNumber(line1, l1), LineNumber(line2, l2)) => line1 == line2 && sameLabel(l1, l2) + case (FrameEntry(tp1, loc1, stk1), FrameEntry(tp2, loc2, stk2)) => tp1 == tp2 && sameFrameTypes(loc1, loc2) && sameFrameTypes(stk1, stk2) + + // this needs to go after the above. For example, Label(1) may not equal Label(1), if before + // the left 1 was associated with another right index. + case (a, b) if a == b => true + + case _ => false + }) && equivalentBytecode(as.tail, bs.tail, varMap, labelMap) + } + + def applyToMethod(method: t.MethodNode, instructions: List[Instruction]): Unit = { + val asmLabel = createLabelNodes(instructions) + instructions.foreach(visitMethod(method, _, asmLabel)) + } + + /** + * Convert back a [[Method]] to ASM land. The code is emitted into the parameter `asmMethod`. + */ + def applyToMethod(asmMethod: t.MethodNode, method: Method): Unit = { + val asmLabel = createLabelNodes(method.instructions) + method.instructions.foreach(visitMethod(asmMethod, _, asmLabel)) + method.handlers.foreach(h => asmMethod.visitTryCatchBlock(asmLabel(h.start), asmLabel(h.end), asmLabel(h.handler), h.desc.orNull)) + method.localVars.foreach(v => asmMethod.visitLocalVariable(v.name, v.desc, v.signature.orNull, asmLabel(v.start), asmLabel(v.end), v.index)) + } + + private def createLabelNodes(instructions: List[Instruction]): Map[Label, asm.Label] = { + val labels = instructions collect { + case l: Label => l } + assert(labels.distinct == labels, s"Duplicate labels in: $labels") + labels.map(l => (l, new asm.Label())).toMap + } + + private def frameTypesToAsm(l: List[Any], asmLabel: Map[Label, asm.Label]): List[Object] = l map { + case l: Label => asmLabel(l) + case x => x.asInstanceOf[Object] + } + + private def visitMethod(method: t.MethodNode, instruction: Instruction, asmLabel: Map[Label, asm.Label]): Unit = instruction match { + case Field(op, owner, name, desc) => method.visitFieldInsn(op, owner, name, desc) + case Incr(op, vr, incr) => method.visitIincInsn(vr, incr) + case Op(op) => method.visitInsn(op) + case IntOp(op, operand) => method.visitIntInsn(op, operand) + case Jump(op, label) => method.visitJumpInsn(op, asmLabel(label)) + case Ldc(op, cst) => method.visitLdcInsn(cst) + case LookupSwitch(op, dflt, keys, labels) => method.visitLookupSwitchInsn(asmLabel(dflt), keys.toArray, (labels map asmLabel).toArray) + case TableSwitch(op, min, max, dflt, labels) => method.visitTableSwitchInsn(min, max, asmLabel(dflt), (labels map asmLabel).toArray: _*) + case Invoke(op, owner, name, desc, itf) => method.visitMethodInsn(op, owner, name, desc, itf) + case NewArray(op, desc, dims) => method.visitMultiANewArrayInsn(desc, dims) + case TypeOp(op, desc) => method.visitTypeInsn(op, desc) + case VarOp(op, vr) => method.visitVarInsn(op, vr) + case l: Label => method.visitLabel(asmLabel(l)) + case FrameEntry(tp, local, stack) => method.visitFrame(tp, local.length, frameTypesToAsm(local, asmLabel).toArray, stack.length, frameTypesToAsm(stack, asmLabel).toArray) + case LineNumber(line, start) => method.visitLineNumber(line, asmLabel(start)) } -}
\ No newline at end of file +} diff --git a/src/partest-extras/scala/tools/partest/BytecodeTest.scala b/src/partest-extras/scala/tools/partest/BytecodeTest.scala index 1e4362fcde..3261cada37 100644 --- a/src/partest-extras/scala/tools/partest/BytecodeTest.scala +++ b/src/partest-extras/scala/tools/partest/BytecodeTest.scala @@ -3,7 +3,7 @@ package scala.tools.partest import scala.tools.nsc.util.JavaClassPath import scala.collection.JavaConverters._ import scala.tools.asm.{ClassWriter, ClassReader} -import scala.tools.asm.tree.{ClassNode, MethodNode, InsnList} +import scala.tools.asm.tree._ import java.io.{FileOutputStream, FileInputStream, File => JFile, InputStream} import AsmNode._ @@ -28,8 +28,8 @@ import AsmNode._ * See test/files/jvm/bytecode-test-example for an example of bytecode test. * */ -abstract class BytecodeTest extends ASMConverters { - import instructions._ +abstract class BytecodeTest { + import ASMConverters._ /** produce the output to be compared against a checkfile */ protected def show(): Unit @@ -38,8 +38,8 @@ abstract class BytecodeTest extends ASMConverters { // asserts def sameBytecode(methA: MethodNode, methB: MethodNode) = { - val isa = instructions.fromMethod(methA) - val isb = instructions.fromMethod(methB) + val isa = instructionsFromMethod(methA) + val isb = instructionsFromMethod(methB) if (isa == isb) println("bytecode identical") else diffInstructions(isa, isb) } @@ -81,18 +81,16 @@ abstract class BytecodeTest extends ASMConverters { } } - // bytecode is equal modulo local variable numbering - def equalsModuloVar(a: Instruction, b: Instruction) = (a, b) match { - case _ if a == b => true - case (VarOp(op1, _), VarOp(op2, _)) if op1 == op2 => true - case _ => false - } - - def similarBytecode(methA: MethodNode, methB: MethodNode, similar: (Instruction, Instruction) => Boolean) = { - val isa = fromMethod(methA) - val isb = fromMethod(methB) + /** + * Compare the bytecodes of two methods. + * + * For the `similar` function, you probably want to pass [[ASMConverters.equivalentBytecode]]. + */ + def similarBytecode(methA: MethodNode, methB: MethodNode, similar: (List[Instruction], List[Instruction]) => Boolean) = { + val isa = instructionsFromMethod(methA) + val isb = instructionsFromMethod(methB) if (isa == isb) println("bytecode identical") - else if ((isa, isb).zipped.forall { case (a, b) => similar(a, b) }) println("bytecode similar") + else if (similar(isa, isb)) println("bytecode similar") else diffInstructions(isa, isb) } diff --git a/src/reflect/scala/reflect/internal/StdNames.scala b/src/reflect/scala/reflect/internal/StdNames.scala index d203218c09..99ff6a10b4 100644 --- a/src/reflect/scala/reflect/internal/StdNames.scala +++ b/src/reflect/scala/reflect/internal/StdNames.scala @@ -99,17 +99,18 @@ trait StdNames { val SINGLETON_SUFFIX: String = ".type" - val ANON_CLASS_NAME: NameType = "$anon" - val ANON_FUN_NAME: NameType = "$anonfun" - val EMPTY: NameType = "" - val EMPTY_PACKAGE_NAME: NameType = "<empty>" - val IMPL_CLASS_SUFFIX = "$class" - val IMPORT: NameType = "<import>" - val MODULE_SUFFIX_NAME: NameType = MODULE_SUFFIX_STRING - val MODULE_VAR_SUFFIX: NameType = "$module" - val PACKAGE: NameType = "package" - val ROOT: NameType = "<root>" - val SPECIALIZED_SUFFIX: NameType = "$sp" + val ANON_CLASS_NAME: NameType = "$anon" + val DELAMBDAFY_LAMBDA_CLASS_NAME: NameType = "$lambda" + val ANON_FUN_NAME: NameType = "$anonfun" + val EMPTY: NameType = "" + val EMPTY_PACKAGE_NAME: NameType = "<empty>" + val IMPL_CLASS_SUFFIX = "$class" + val IMPORT: NameType = "<import>" + val MODULE_SUFFIX_NAME: NameType = MODULE_SUFFIX_STRING + val MODULE_VAR_SUFFIX: NameType = "$module" + val PACKAGE: NameType = "package" + val ROOT: NameType = "<root>" + val SPECIALIZED_SUFFIX: NameType = "$sp" // value types (and AnyRef) are all used as terms as well // as (at least) arguments to the @specialize annotation. diff --git a/src/reflect/scala/reflect/internal/Symbols.scala b/src/reflect/scala/reflect/internal/Symbols.scala index 2670faa22d..44fce2c9ab 100644 --- a/src/reflect/scala/reflect/internal/Symbols.scala +++ b/src/reflect/scala/reflect/internal/Symbols.scala @@ -789,6 +789,7 @@ trait Symbols extends api.Symbols { self: SymbolTable => isMethod && owner.isDerivedValueClass && !isParamAccessor && !isConstructor && !hasFlag(SUPERACCESSOR) && !isMacro && !isSpecialized final def isAnonymousFunction = isSynthetic && (name containsName tpnme.ANON_FUN_NAME) + final def isDelambdafyFunction = isSynthetic && (name containsName tpnme.DELAMBDAFY_LAMBDA_CLASS_NAME) final def isDefinedInPackage = effectiveOwner.isPackageClass final def needsFlatClasses = phase.flatClasses && rawowner != NoSymbol && !rawowner.isPackageClass diff --git a/src/reflect/scala/reflect/internal/Types.scala b/src/reflect/scala/reflect/internal/Types.scala index f26315c538..a95f626a0b 100644 --- a/src/reflect/scala/reflect/internal/Types.scala +++ b/src/reflect/scala/reflect/internal/Types.scala @@ -2605,7 +2605,7 @@ trait Types // derived from the existentially quantified type into the typing environment // (aka \Gamma, which tracks types for variables and constraints/kinds for types) // as a nice bonus, delaying this until we need it avoids cyclic errors - def tpars = underlying.typeSymbol.initialize.typeParams + def tpars = underlying.typeSymbolDirect.initialize.typeParams def newSkolem(quant: Symbol) = owner.newExistentialSkolem(quant, origin) def newSharpenedSkolem(quant: Symbol, tparam: Symbol): Symbol = { diff --git a/test/files/jvm/t6941/test.scala b/test/files/jvm/t6941/test.scala index 248617f71f..fceb54487f 100644 --- a/test/files/jvm/t6941/test.scala +++ b/test/files/jvm/t6941/test.scala @@ -1,4 +1,4 @@ -import scala.tools.partest.BytecodeTest +import scala.tools.partest.{BytecodeTest, ASMConverters} import scala.tools.nsc.util.JavaClassPath import java.io.InputStream @@ -10,6 +10,6 @@ import scala.collection.JavaConverters._ object Test extends BytecodeTest { def show: Unit = { val classNode = loadClassNode("SameBytecode") - similarBytecode(getMethod(classNode, "a"), getMethod(classNode, "b"), equalsModuloVar) + similarBytecode(getMethod(classNode, "a"), getMethod(classNode, "b"), ASMConverters.equivalentBytecode(_, _)) } } diff --git a/test/files/jvm/t7253/test.scala b/test/files/jvm/t7253/test.scala index 7fe08e8813..a3f1e86e65 100644 --- a/test/files/jvm/t7253/test.scala +++ b/test/files/jvm/t7253/test.scala @@ -1,4 +1,4 @@ -import scala.tools.partest.BytecodeTest +import scala.tools.partest.{BytecodeTest, ASMConverters} import scala.tools.nsc.util.JavaClassPath import java.io.InputStream @@ -8,10 +8,10 @@ import asm.tree.{ClassNode, InsnList} import scala.collection.JavaConverters._ object Test extends BytecodeTest { - import instructions._ + import ASMConverters._ def show: Unit = { - val instrBaseSeqs = Seq("ScalaClient_1", "JavaClient_1") map (name => instructions.fromMethod(getMethod(loadClassNode(name), "foo"))) + val instrBaseSeqs = Seq("ScalaClient_1", "JavaClient_1") map (name => instructionsFromMethod(getMethod(loadClassNode(name), "foo"))) val instrSeqs = instrBaseSeqs map (_ filter isInvoke) cmpInstructions(instrSeqs(0), instrSeqs(1)) } diff --git a/test/files/jvm/unreachable.flags b/test/files/jvm/unreachable.flags deleted file mode 100644 index 49f2d2c4c8..0000000000 --- a/test/files/jvm/unreachable.flags +++ /dev/null @@ -1 +0,0 @@ --Ybackend:GenASM diff --git a/test/files/neg/warn-inferred-any.check b/test/files/neg/warn-inferred-any.check index 4628033e55..8ad81d1529 100644 --- a/test/files/neg/warn-inferred-any.check +++ b/test/files/neg/warn-inferred-any.check @@ -7,6 +7,9 @@ warn-inferred-any.scala:16: warning: a type was inferred to be `AnyVal`; this ma warn-inferred-any.scala:17: warning: a type was inferred to be `AnyVal`; this may indicate a programming error. { 1l to 5l contains 5d } ^ +warn-inferred-any.scala:25: warning: a type was inferred to be `Any`; this may indicate a programming error. + def za = f(1, "one") + ^ error: No warnings can be incurred under -Xfatal-warnings. -three warnings found +four warnings found one error found diff --git a/test/files/neg/warn-inferred-any.scala b/test/files/neg/warn-inferred-any.scala index b853e6e5a8..693c33e7be 100644 --- a/test/files/neg/warn-inferred-any.scala +++ b/test/files/neg/warn-inferred-any.scala @@ -17,3 +17,11 @@ trait Ys[+A] { { 1l to 5l contains 5d } { 1l to 5l contains 5l } } + +trait Zs { + def f[A](a: A*) = 42 + def g[A >: Any](a: A*) = 42 // don't warn + + def za = f(1, "one") + def zu = g(1, "one") +} diff --git a/test/files/pos/t8844.scala b/test/files/pos/t8844.scala new file mode 100644 index 0000000000..d33c520d1b --- /dev/null +++ b/test/files/pos/t8844.scala @@ -0,0 +1,4 @@ +object Example { + type S[A] = String + def foo(s: S[_]): String = s +} diff --git a/test/files/run/delambdafyLambdaClassNames.check b/test/files/run/delambdafyLambdaClassNames.check new file mode 100644 index 0000000000..d425d15dd0 --- /dev/null +++ b/test/files/run/delambdafyLambdaClassNames.check @@ -0,0 +1 @@ +A$$nestedInAnon$1$lambda$$run$1 diff --git a/test/files/run/delambdafyLambdaClassNames.flags b/test/files/run/delambdafyLambdaClassNames.flags new file mode 100644 index 0000000000..b10233d322 --- /dev/null +++ b/test/files/run/delambdafyLambdaClassNames.flags @@ -0,0 +1 @@ +-Ybackend:GenBCode -Ydelambdafy:method
\ No newline at end of file diff --git a/test/files/run/delambdafyLambdaClassNames/A_1.scala b/test/files/run/delambdafyLambdaClassNames/A_1.scala new file mode 100644 index 0000000000..10489414b7 --- /dev/null +++ b/test/files/run/delambdafyLambdaClassNames/A_1.scala @@ -0,0 +1,5 @@ +class A { + def f = new Runnable { + def run(): Unit = List(1,2).foreach(println) + } +} diff --git a/test/files/run/delambdafyLambdaClassNames/Test.scala b/test/files/run/delambdafyLambdaClassNames/Test.scala new file mode 100644 index 0000000000..49a397d1d2 --- /dev/null +++ b/test/files/run/delambdafyLambdaClassNames/Test.scala @@ -0,0 +1,4 @@ +object Test extends App { + val c = Class.forName("A$$nestedInAnon$1$lambda$$run$1") + println(c.getName) +} diff --git a/test/files/run/nothingTypeDce.flags b/test/files/run/nothingTypeDce.flags new file mode 100644 index 0000000000..d85321ca0e --- /dev/null +++ b/test/files/run/nothingTypeDce.flags @@ -0,0 +1 @@ +-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code diff --git a/test/files/run/nothingTypeDce.scala b/test/files/run/nothingTypeDce.scala new file mode 100644 index 0000000000..5f3692fd33 --- /dev/null +++ b/test/files/run/nothingTypeDce.scala @@ -0,0 +1,63 @@ +// See comment in BCodeBodyBuilder + +// -target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code +// target enables stack map frames generation + +class C { + // can't just emit a call to ???, that returns value of type Nothing$ (not Int). + def f1: Int = ??? + + def f2: Int = throw new Error("") + + def f3(x: Boolean) = { + var y = 0 + // cannot assign an object of type Nothing$ to Int + if (x) y = ??? + else y = 1 + y + } + + def f4(x: Boolean) = { + var y = 0 + // tests that whatever is emitted after the throw is valid (what? depends on opts, presence of stack map frames) + if (x) y = throw new Error("") + else y = 1 + y + } + + def f5(x: Boolean) = { + // stack heights need to be the same. ??? looks to the jvm like returning a value of + // type Nothing$, need to drop or throw it. + println( + if (x) { ???; 10 } + else 20 + ) + } + + def f6(x: Boolean) = { + println( + if (x) { throw new Error(""); 10 } + else 20 + ) + } + + def f7(x: Boolean) = { + println( + if (x) throw new Error("") + else 20 + ) + } + + def f8(x: Boolean) = { + println( + if (x) throw new Error("") + else 20 + ) + } +} + +object Test extends App { + // creating an instance is enough to trigger bytecode verification for all methods, + // no need to invoke the methods. + new C() +} diff --git a/test/files/run/nothingTypeNoFramesNoDce.check b/test/files/run/nothingTypeNoFramesNoDce.check new file mode 100644 index 0000000000..b1d08b45ff --- /dev/null +++ b/test/files/run/nothingTypeNoFramesNoDce.check @@ -0,0 +1 @@ +warning: -target:jvm-1.5 is deprecated: use target for Java 1.6 or above. diff --git a/test/files/run/nothingTypeNoFramesNoDce.flags b/test/files/run/nothingTypeNoFramesNoDce.flags new file mode 100644 index 0000000000..a035c86179 --- /dev/null +++ b/test/files/run/nothingTypeNoFramesNoDce.flags @@ -0,0 +1 @@ +-target:jvm-1.5 -Ybackend:GenBCode -Yopt:l:none -deprecation diff --git a/test/files/run/nothingTypeNoFramesNoDce.scala b/test/files/run/nothingTypeNoFramesNoDce.scala new file mode 100644 index 0000000000..3d1298303a --- /dev/null +++ b/test/files/run/nothingTypeNoFramesNoDce.scala @@ -0,0 +1,61 @@ +// See comment in BCodeBodyBuilder + +// -target:jvm-1.5 -Ybackend:GenBCode -Yopt:l:none +// target disables stack map frame generation. in this mode, the ClssWriter just emits dead code as is. + +class C { + // can't just emit a call to ???, that returns value of type Nothing$ (not Int). + def f1: Int = ??? + + def f2: Int = throw new Error("") + + def f3(x: Boolean) = { + var y = 0 + // cannot assign an object of type Nothing$ to Int + if (x) y = ??? + else y = 1 + y + } + + def f4(x: Boolean) = { + var y = 0 + // tests that whatever is emitted after the throw is valid (what? depends on opts, presence of stack map frames) + if (x) y = throw new Error("") + else y = 1 + y + } + + def f5(x: Boolean) = { + // stack heights need to be the smae. ??? looks to the jvm like returning a value of + // type Nothing$, need to drop or throw it. + println( + if (x) { ???; 10 } + else 20 + ) + } + + def f6(x: Boolean) = { + println( + if (x) { throw new Error(""); 10 } + else 20 + ) + } + + def f7(x: Boolean) = { + println( + if (x) throw new Error("") + else 20 + ) + } + + def f8(x: Boolean) = { + println( + if (x) throw new Error("") + else 20 + ) + } +} + +object Test extends App { + new C() +} diff --git a/test/files/run/nothingTypeNoOpt.flags b/test/files/run/nothingTypeNoOpt.flags new file mode 100644 index 0000000000..b3b518051b --- /dev/null +++ b/test/files/run/nothingTypeNoOpt.flags @@ -0,0 +1 @@ +-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:none diff --git a/test/files/run/nothingTypeNoOpt.scala b/test/files/run/nothingTypeNoOpt.scala new file mode 100644 index 0000000000..5c5a20fa3b --- /dev/null +++ b/test/files/run/nothingTypeNoOpt.scala @@ -0,0 +1,61 @@ +// See comment in BCodeBodyBuilder + +// -target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:none +// target enables stack map frame generation + +class C { + // can't just emit a call to ???, that returns value of type Nothing$ (not Int). + def f1: Int = ??? + + def f2: Int = throw new Error("") + + def f3(x: Boolean) = { + var y = 0 + // cannot assign an object of type Nothing$ to Int + if (x) y = ??? + else y = 1 + y + } + + def f4(x: Boolean) = { + var y = 0 + // tests that whatever is emitted after the throw is valid (what? depends on opts, presence of stack map frames) + if (x) y = throw new Error("") + else y = 1 + y + } + + def f5(x: Boolean) = { + // stack heights need to be the smae. ??? looks to the jvm like returning a value of + // type Nothing$, need to drop or throw it. + println( + if (x) { ???; 10 } + else 20 + ) + } + + def f6(x: Boolean) = { + println( + if (x) { throw new Error(""); 10 } + else 20 + ) + } + + def f7(x: Boolean) = { + println( + if (x) throw new Error("") + else 20 + ) + } + + def f8(x: Boolean) = { + println( + if (x) throw new Error("") + else 20 + ) + } +} + +object Test extends App { + new C() +} diff --git a/test/files/run/t8680.scala b/test/files/run/t8680.scala new file mode 100644 index 0000000000..2bce09c507 --- /dev/null +++ b/test/files/run/t8680.scala @@ -0,0 +1,53 @@ +object Test extends App { + def pre(n: Int) = (-n to -1).toStream + + def cyc(m: Int) = { + lazy val s: Stream[Int] = (0 until m).toStream #::: s + s + } + + def precyc(n: Int, m: Int) = pre(n) #::: cyc(m) + + def str(s: Stream[Int]) = { + val b = new StringBuilder + s.addString(b, "", "", "") + b.toString + } + + def goal(n: Int, m: Int) = (-n until m).mkString + "..." + + // Check un-forced cyclic and non-cyclic streams + assert(str(pre(2)) == pre(2).take(1).toList.mkString + "?") + assert(str(cyc(2)) == cyc(2).take(1).toList.mkString + "?") + assert(str(precyc(2,2)) == precyc(2,2).take(1).toList.mkString + "?") + assert(!pre(2).hasDefiniteSize) + assert(!cyc(2).hasDefiniteSize) + assert(!precyc(2,2).hasDefiniteSize) + + // Check forced cyclic and non-cyclic streams + assert(str(pre(2).force) == (-2 to -1).mkString) + assert(str(cyc(2).force) == (0 until 2).mkString + "...") + assert(str(precyc(2,2).force) == (-2 until 2).mkString + "...") + assert(pre(2).force.hasDefiniteSize) + assert(!cyc(2).force.hasDefiniteSize) + assert(!precyc(2,2).force.hasDefiniteSize) + + // Special cases + assert(str(cyc(1).force) == goal(0,1)) + assert(str(precyc(1,6).force) == goal(1,6)) + assert(str(precyc(6,1).force) == goal(6,1)) + + // Make sure there are no odd/even problems + for (n <- 3 to 4; m <- 3 to 4) { + assert(precyc(n,m).mkString == goal(n,m), s"mkString $n $m") + assert(!precyc(n,m).force.hasDefiniteSize, s"hasDef $n$m") + } + + // Make sure there are no cycle/prefix modulus problems + for (i <- 6 to 8) { + assert(precyc(i,3).mkString == goal(i,3), s"mkString $i 3") + assert(precyc(3,i).mkString == goal(3,i), s"mkString 3 $i") + assert(!precyc(i,3).force.hasDefiniteSize, s"hasDef $i 3") + assert(!precyc(3,i).force.hasDefiniteSize, s"hasDef 3 $i") + } +} diff --git a/test/junit/scala/collection/SetMapConsistencyTest.scala b/test/junit/scala/collection/SetMapConsistencyTest.scala index eed6007eef..261c11a98b 100644 --- a/test/junit/scala/collection/SetMapConsistencyTest.scala +++ b/test/junit/scala/collection/SetMapConsistencyTest.scala @@ -514,4 +514,19 @@ class SetMapConsistencyTest { assert( hs.toList.toSet == hs ) assert( hs == hs.toList.toSet ) } + + @Test + def testSI8815() { + val lm = new scala.collection.mutable.LongMap[String] + lm += (Long.MinValue, "min") + lm += (-1, "neg-one") + lm += (0, "zero") + lm += (Long.MaxValue, "max") + var nit = 0 + lm.iterator.foreach(_ => nit += 1) + var nfe = 0 + lm.foreach(_ => nfe += 1) + assert(nit == 4) + assert(nfe == 4) + } } diff --git a/test/junit/scala/tools/nsc/backend/jvm/BTypesTest.scala b/test/junit/scala/tools/nsc/backend/jvm/BTypesTest.scala index cb7e7050b0..221aad6536 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/BTypesTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/BTypesTest.scala @@ -1,7 +1,6 @@ package scala.tools.nsc package backend.jvm -import scala.tools.testing.AssertUtil._ import org.junit.runner.RunWith import org.junit.runners.JUnit4 import org.junit.Test diff --git a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala new file mode 100644 index 0000000000..15bc1f427d --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala @@ -0,0 +1,79 @@ +package scala.tools.nsc.backend.jvm + +import org.junit.Assert._ + +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.cmd.CommandLineParser +import scala.tools.nsc.{Settings, Global} +import scala.tools.partest.ASMConverters +import scala.collection.JavaConverters._ + +object CodeGenTools { + import ASMConverters._ + + def genMethod( flags: Int = Opcodes.ACC_PUBLIC, + name: String = "m", + descriptor: String = "()V", + genericSignature: String = null, + throwsExceptions: Array[String] = null, + handlers: List[ExceptionHandler] = Nil, + localVars: List[LocalVariable] = Nil)(body: Instruction*): MethodNode = { + val node = new MethodNode(flags, name, descriptor, genericSignature, throwsExceptions) + applyToMethod(node, Method(body.toList, handlers, localVars)) + node + } + + def wrapInClass(method: MethodNode): ClassNode = { + val cls = new ClassNode() + cls.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "C", null, "java/lang/Object", null) + cls.methods.add(method) + cls + } + + private def resetOutput(compiler: Global): Unit = { + compiler.settings.outputDirs.setSingleOutput(new VirtualDirectory("(memory)", None)) + } + + def newCompiler(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 + } + + def compile(compiler: Global)(code: String): List[(String, Array[Byte])] = { + 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 + } + + def compileClasses(compiler: Global)(code: String): List[ClassNode] = { + compile(compiler)(code).map(p => AsmUtils.readClass(p._2)).sortBy(_.name) + } + + def compileMethods(compiler: Global)(code: String): List[MethodNode] = { + compileClasses(compiler)(s"class C { $code }").head.methods.asScala.toList.filterNot(_.name == "<init>") + } + + def singleMethodInstructions(compiler: Global)(code: String): List[Instruction] = { + val List(m) = compileMethods(compiler)(code) + instructionsFromMethod(m) + } + + def singleMethod(compiler: Global)(code: String): Method = { + val List(m) = compileMethods(compiler)(code) + convertMethod(m) + } + + def assertSameCode(actual: List[Instruction], expected: List[Instruction]): Unit = { + assertTrue(s"\nExpected: $expected\nActual : $actual", actual === expected) + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala b/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala new file mode 100644 index 0000000000..2fb5bb8052 --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala @@ -0,0 +1,81 @@ +package scala.tools.nsc.backend.jvm + +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Assert._ +import CodeGenTools._ +import scala.tools.asm.Opcodes._ +import scala.tools.partest.ASMConverters._ + +@RunWith(classOf[JUnit4]) +class DirectCompileTest { + val compiler = newCompiler(extraArgs = "-Ybackend:GenBCode") + + @Test + def testCompile(): Unit = { + val List(("C.class", bytes)) = compile(compiler)( + """ + |class C { + | def f = 1 + |} + """.stripMargin) + def s(i: Int, n: Int) = (bytes(i) & 0xff) << n + assertTrue((s(0, 24) | s(1, 16) | s(2, 8) | s(3, 0)) == 0xcafebabe) // mocha java latte machiatto surpreme dark roasted espresso + } + + @Test + def testCompileClasses(): Unit = { + val List(cClass, cModuleClass) = compileClasses(compiler)( + """ + |class C + |object C + """.stripMargin) + + assertTrue(cClass.name == "C") + assertTrue(cModuleClass.name == "C$") + + val List(dMirror, dModuleClass) = compileClasses(compiler)( + """ + |object D + """.stripMargin) + + assertTrue(dMirror.name == "D") + assertTrue(dModuleClass.name == "D$") + } + + @Test + def testCompileMethods(): Unit = { + val List(f, g) = compileMethods(compiler)( + """ + |def f = 10 + |def g = f + """.stripMargin) + assertTrue(f.name == "f") + assertTrue(g.name == "g") + + assertTrue(instructionsFromMethod(f).dropNonOp === + List(IntOp(BIPUSH, 10), Op(IRETURN))) + + assertTrue(instructionsFromMethod(g).dropNonOp === + List(VarOp(ALOAD, 0), Invoke(INVOKEVIRTUAL, "C", "f", "()I", false), Op(IRETURN))) + } + + @Test + def testDropNonOpAliveLabels(): Unit = { + val List(f) = compileMethods(compiler)("""def f(x: Int) = if (x == 0) "a" else "b"""") + assertTrue(instructionsFromMethod(f).dropNonOp === List( + VarOp(ILOAD, 1), + Op(ICONST_0), + Jump(IF_ICMPEQ, Label(6)), + Jump(GOTO, Label(10)), + Label(6), + Ldc(LDC, "a"), + Jump(GOTO, Label(13)), + Label(10), + Ldc(LDC, "b"), + Label(13), + Op(ARETURN) + )) + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala new file mode 100644 index 0000000000..57fa1a7b66 --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala @@ -0,0 +1,92 @@ +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._ + +@RunWith(classOf[JUnit4]) +class EmptyExceptionHandlersTest { + + val exceptionDescriptor = "java/lang/Exception" + + @Test + def eliminateEmpty(): Unit = { + val handlers = List(ExceptionHandler(Label(1), Label(2), Label(2), Some(exceptionDescriptor))) + val asmMethod = genMethod(handlers = handlers)( + Label(1), + Label(2), + Op(RETURN) + ) + assertTrue(convertMethod(asmMethod).handlers.length == 1) + LocalOpt.removeEmptyExceptionHandlers(asmMethod) + assertTrue(convertMethod(asmMethod).handlers.isEmpty) + } + + @Test + def eliminateHandlersGuardingNops(): Unit = { + val handlers = List(ExceptionHandler(Label(1), Label(2), Label(2), Some(exceptionDescriptor))) + val asmMethod = genMethod(handlers = handlers)( + Label(1), // nops only + Op(NOP), + Op(NOP), + Jump(GOTO, Label(3)), + Op(NOP), + Label(3), + Op(NOP), + Jump(GOTO, Label(4)), + + Label(2), // handler + Op(ACONST_NULL), + Op(ATHROW), + + Label(4), // return + Op(RETURN) + ) + assertTrue(convertMethod(asmMethod).handlers.length == 1) + LocalOpt.removeEmptyExceptionHandlers(asmMethod) + 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)" + + assertTrue(singleMethod(noOptCompiler)(code).handlers.length == 1) + val optMethod = singleMethod(dceCompiler)(code) + assertTrue(optMethod.handlers.isEmpty) + + val code2 = + """def f: Unit = { + | println(0) + | return + | try { throw new Exception("") } // removed by dce, so handler will be removed as well + | catch { case _: Exception => println(1) } + | println(2) + |}""".stripMargin + + assertTrue(singleMethod(dceCompiler)(code2).handlers.isEmpty) + } + + @Test + def keepAliveHandlers(): Unit = { + val code = + """def f: Int = { + | println(0) + | try { 1 } + | catch { case _: Exception => 2 } + |}""".stripMargin + + assertTrue(singleMethod(dceCompiler)(code).handlers.length == 1) + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala new file mode 100644 index 0000000000..a3bd7ae6fe --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala @@ -0,0 +1,217 @@ +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.testing.AssertUtil._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ + +@RunWith(classOf[JUnit4]) +class UnreachableCodeTest { + import UnreachableCodeTest._ + + // jvm-1.6 enables emitting stack map frames, which impacts the code generation wrt dead basic blocks, + // see comment in BCodeBodyBuilder + 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( + Op(ACONST_NULL), + Op(ATHROW), + Op(RETURN).dead + ) + + assertEliminateDead( + Op(RETURN) + ) + + assertEliminateDead( + Op(RETURN), + Op(ACONST_NULL).dead, + Op(ATHROW).dead + ) + } + + @Test + def eliminateNop(): Unit = { + assertEliminateDead( + // not dead, since visited by data flow analysis. need a different opt to eliminate it. + Op(NOP), + Op(RETURN), + Op(NOP).dead + ) + } + + @Test + def eliminateBranchOver(): Unit = { + assertEliminateDead( + Jump(GOTO, Label(1)), + Op(ACONST_NULL).dead, + Op(ATHROW).dead, + Label(1), + Op(RETURN) + ) + + assertEliminateDead( + Jump(GOTO, Label(1)), + Label(1), + Op(RETURN) + ) + } + + @Test + def deadLabelsRemain(): Unit = { + assertEliminateDead( + Op(RETURN), + Jump(GOTO, Label(1)).dead, + // not dead - labels may be referenced from other places in a classfile (eg exceptions table). + // will need a different opt to get rid of them + Label(1) + ) + } + + @Test + def pushPopNotEliminated(): Unit = { + assertEliminateDead( + // not dead, visited by data flow analysis. + Op(ACONST_NULL), + Op(POP), + Op(RETURN) + ) + } + + @Test + def nullnessNotConsidered(): Unit = { + assertEliminateDead( + Op(ACONST_NULL), + Jump(IFNULL, Label(1)), + Op(RETURN), // not dead + Label(1), + Op(RETURN) + ) + } + + @Test + def basicEliminationCompiler(): Unit = { + val code = "def f: Int = { return 1; 2 }" + val withDce = singleMethodInstructions(dceCompiler)(code) + assertSameCode(withDce.dropNonOp, List(Op(ICONST_1), Op(IRETURN))) + + val noDce = singleMethodInstructions(noOptCompiler)(code) + + // The emitted code is ICONST_1, IRETURN, ICONST_2, IRETURN. The latter two are dead. + // + // GenBCode puts the last IRETURN into a new basic block: it emits a label before the second + // IRETURN. This is an implementation detail, it may change; it affects the outcome of this test. + // + // During classfile writing with COMPUTE_FAMES (-target:jvm-1.6 or larger), the ClassfileWriter + // puts the ICONST_2 into a new basic block, because the preceding operation (IRETURN) ends + // the current block. We get something like + // + // L1: ICONST_1; IRETURN + // L2: ICONST_2 << dead + // L3: IRETURN << dead + // + // Finally, instructions in the dead basic blocks are replaced by ATHROW, as explained in + // a comment in BCodeBodyBuilder. + 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) + assertSameCode(noDceNoFrames.dropNonOp, List(Op(ICONST_1), Op(IRETURN), Op(ICONST_2), Op(IRETURN))) + } + + @Test + def eliminateDeadCatchBlocks(): Unit = { + val code = "def f: Int = { return 0; try { 1 } catch { case _: Exception => 2 } }" + assertSameCode(singleMethodInstructions(dceCompiler)(code).dropNonOp, + List(Op(ICONST_0), Op(IRETURN))) + + val code2 = "def f: Unit = { try { } catch { case _: Exception => () }; () }" + // DCE only removes dead basic blocks, but not NOPs, and also not useless jumps + assertSameCode(singleMethodInstructions(dceCompiler)(code2).dropNonOp, + List(Op(NOP), Jump(GOTO, Label(33)), Label(33), Op(RETURN))) + + val code3 = "def f: Unit = { try { } catch { case _: Exception => try { } catch { case _: Exception => () } }; () }" + assertSameCode(singleMethodInstructions(dceCompiler)(code3).dropNonOp, + List(Op(NOP), Jump(GOTO, Label(33)), Label(33), Op(RETURN))) + + val code4 = "def f: Unit = { try { try { } catch { case _: Exception => () } } catch { case _: Exception => () }; () }" + assertSameCode(singleMethodInstructions(dceCompiler)(code4).dropNonOp, + List(Op(NOP), Jump(GOTO, Label(4)), Label(4), Jump(GOTO, Label(7)), Label(7), Op(RETURN))) + } + + @Test // test the dce-testing tools + def metaTest(): Unit = { + assertEliminateDead() // no instructions + + assertThrows[AssertionError]( + assertEliminateDead(Op(RETURN).dead), + _.contains("Expected: List()\nActual : List(Op(RETURN))") + ) + + assertThrows[AssertionError]( + assertEliminateDead(Op(RETURN), Op(RETURN)), + _.contains("Expected: List(Op(RETURN), Op(RETURN))\nActual : List(Op(RETURN))") + ) + } + + @Test + def bytecodeEquivalence: Unit = { + assertTrue(List(VarOp(ILOAD, 1)) === + List(VarOp(ILOAD, 2))) + assertTrue(List(VarOp(ILOAD, 1), VarOp(ISTORE, 1)) === + List(VarOp(ILOAD, 2), VarOp(ISTORE, 2))) + + // the first Op will associate 1->2, then the 2->2 will fail + assertFalse(List(VarOp(ILOAD, 1), VarOp(ISTORE, 2)) === + List(VarOp(ILOAD, 2), VarOp(ISTORE, 2))) + + // will associate 1->2 and 2->1, which is OK + assertTrue(List(VarOp(ILOAD, 1), VarOp(ISTORE, 2)) === + List(VarOp(ILOAD, 2), VarOp(ISTORE, 1))) + + assertTrue(List(Label(1), Label(2), Label(1)) === + List(Label(2), Label(4), Label(2))) + assertTrue(List(LineNumber(1, Label(1)), Label(1)) === + List(LineNumber(1, Label(3)), Label(3))) + assertFalse(List(LineNumber(1, Label(1)), Label(1)) === + List(LineNumber(1, Label(3)), Label(1))) + + assertTrue(List(TableSwitch(TABLESWITCH, 1, 3, Label(4), List(Label(5), Label(6))), Label(4), Label(5), Label(6)) === + List(TableSwitch(TABLESWITCH, 1, 3, Label(9), List(Label(3), Label(4))), Label(9), Label(3), Label(4))) + + assertTrue(List(FrameEntry(F_FULL, List(INTEGER, DOUBLE, Label(3)), List("java/lang/Object", Label(4))), Label(3), Label(4)) === + List(FrameEntry(F_FULL, List(INTEGER, DOUBLE, Label(1)), List("java/lang/Object", Label(3))), Label(1), Label(3))) + } +} + +object UnreachableCodeTest { + import scala.language.implicitConversions + implicit def aliveInstruction(ins: Instruction): (Instruction, Boolean) = (ins, true) + + implicit class MortalInstruction(val ins: Instruction) extends AnyVal { + def dead: (Instruction, Boolean) = (ins, false) + } + + def assertEliminateDead(code: (Instruction, Boolean)*): Unit = { + val cls = wrapInClass(genMethod()(code.map(_._1): _*)) + LocalOpt.removeUnreachableCode(cls) + val nonEliminated = instructionsFromMethod(cls.methods.get(0)) + val expectedLive = code.filter(_._2).map(_._1).toList + assertSameCode(nonEliminated, expectedLive) + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala new file mode 100644 index 0000000000..24a1f9d1c1 --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala @@ -0,0 +1,87 @@ +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.collection.JavaConverters._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ + +@RunWith(classOf[JUnit4]) +class UnusedLocalVariablesTest { + val dceCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:unreachable-code") + + @Test + def removeUnusedVar(): Unit = { + val code = """def f(a: Long, b: String, c: Double): Unit = { return; var x = a; var y = x + 10 }""" + assertLocalVarCount(code, 4) // `this, a, b, c` + + val code2 = """def f(): Unit = { var x = if (true) return else () }""" + assertLocalVarCount(code2, 1) // x is eliminated, constant folding in scalac removes the if + + val code3 = """def f: Unit = return""" // paramless method + assertLocalVarCount(code3, 1) // this + } + + @Test + def keepUsedVar(): Unit = { + val code = """def f(a: Long, b: String, c: Double): Unit = { val x = 10 + a; val y = x + 10 }""" + assertLocalVarCount(code, 6) + + val code2 = """def f(a: Long): Unit = { var x = if (a == 0l) return else () }""" + assertLocalVarCount(code2, 3) // remains + } + + @Test + def constructorLocals(): Unit = { + val code = """class C { + | def this(a: Int) = { + | this() + | throw new Exception("") + | val y = 0 + | } + |} + |""".stripMargin + val cls = compileClasses(dceCompiler)(code).head + val m = convertMethod(cls.methods.asScala.toList.find(_.desc == "(I)V").get) + assertTrue(m.localVars.length == 2) // this, a, but not y + + + val code2 = + """class C { + | { + | throw new Exception("") + | val a = 0 + | } + |} + | + |object C { + | { + | throw new Exception("") + | val b = 1 + | } + |} + """.stripMargin + + val clss2 = compileClasses(dceCompiler)(code2) + val cls2 = clss2.find(_.name == "C").get + val companion2 = clss2.find(_.name == "C$").get + + val clsConstr = convertMethod(cls2.methods.asScala.toList.find(_.name == "<init>").get) + val companionConstr = convertMethod(companion2.methods.asScala.toList.find(_.name == "<init>").get) + + assertTrue(clsConstr.localVars.length == 1) // this + assertTrue(companionConstr.localVars.length == 1) // this + } + + def assertLocalVarCount(code: String, numVars: Int): Unit = { + assertTrue(singleMethod(dceCompiler)(code).localVars.length == numVars) + } + +} diff --git a/test/junit/scala/tools/testing/AssertThrowsTest.scala b/test/junit/scala/tools/testing/AssertThrowsTest.scala index a70519e63c..d91e450bac 100644 --- a/test/junit/scala/tools/testing/AssertThrowsTest.scala +++ b/test/junit/scala/tools/testing/AssertThrowsTest.scala @@ -31,4 +31,13 @@ class AssertThrowsTest { } }) -}
\ No newline at end of file + @Test + def errorIfNoThrow: Unit = { + try { + assertThrows[Foo] { () } + } catch { + case e: AssertionError => return + } + assert(false, "assertThrows should error if the tested expression does not throw anything") + } +} |