diff options
74 files changed, 4342 insertions, 517 deletions
@@ -1448,6 +1448,15 @@ TODO: <target name="test.junit.comp" depends="pack.done"> <stopwatch name="test.junit.compiler.timer"/> <mkdir dir="${test.junit.classes}"/> + <javac + debug="true" + srcdir="${test.junit.src}" + destdir="${test.junit.classes}" + classpathref="test.junit.compiler.build.path" + target="1.6" + source="1.5" + compiler="javac1.6" + includes="**/*.java"/> <scalacfork destdir="${test.junit.classes}" compilerpathref="quick.compiler.path" diff --git a/src/asm/README b/src/asm/README index 3ceac88098..58d555acde 100644 --- a/src/asm/README +++ b/src/asm/README @@ -1,4 +1,4 @@ -Version 5.0.2, SVN r1741, tags/ASM_5_0_2 +Version 5.0.3, SVN r1748, tags/ASM_5_0_3 Git SVN repo: https://github.com/lrytz/asm - git svn howto: https://github.com/lrytz/asm/issues/1 @@ -6,11 +6,16 @@ Git SVN repo: https://github.com/lrytz/asm Upgrading ASM ------------- +Check the commit history of src/asm: https://github.com/scala/scala/commits/2.11.x/src/asm. +Find the previous commit that upgraded ASM and take a look at its commit message. It should +be a squashed version of a pull request that shows the precise procedure how the last upgrade +was made. + Start by deleting all source files in src/asm/ and copy the ones from the latest ASM release. Excluded Files (don't copy): - package.html files - - org/objectweb/asm/commons + - org/objectweb/asm/commons, but keep CodeSizeEvaluator.java - org/objectweb/asm/optimizer - org/objectweb/asm/xml @@ -27,4 +32,6 @@ Re-packaging and cosmetic changes: - remove trailing whitespace find src/asm/scala/tools/asm -name '*.java' | xargs sed -i '' -e 's/[ ]*$//' -Actual changes: check the git log for [asm-cherry-pick] after the previous upgrade. +Include the actual changes that we have in our repostiory + - Include the commits labelled [asm-cherry-pick] in the non-squashed PR of the previous upgrade + - Include the changes that were added to src/asm since the last upgrade and label them [asm-cherry-pick] diff --git a/src/asm/scala/tools/asm/Label.java b/src/asm/scala/tools/asm/Label.java index c094eba408..22b6798fb5 100644 --- a/src/asm/scala/tools/asm/Label.java +++ b/src/asm/scala/tools/asm/Label.java @@ -473,7 +473,7 @@ public class Label { void addToSubroutine(final long id, final int nbSubroutines) { if ((status & VISITED) == 0) { status |= VISITED; - srcAndRefPositions = new int[(nbSubroutines - 1) / 32 + 1]; + srcAndRefPositions = new int[nbSubroutines / 32 + 1]; } srcAndRefPositions[(int) (id >>> 32)] |= (int) id; } diff --git a/src/asm/scala/tools/asm/MethodWriter.java b/src/asm/scala/tools/asm/MethodWriter.java index d30e04c625..9c72ead61d 100644 --- a/src/asm/scala/tools/asm/MethodWriter.java +++ b/src/asm/scala/tools/asm/MethodWriter.java @@ -1974,43 +1974,43 @@ public class MethodWriter extends MethodVisitor { stackMap.putByte(v); } } else { - StringBuffer buf = new StringBuffer(); + StringBuilder sb = new StringBuilder(); d >>= 28; while (d-- > 0) { - buf.append('['); + sb.append('['); } if ((t & Frame.BASE_KIND) == Frame.OBJECT) { - buf.append('L'); - buf.append(cw.typeTable[t & Frame.BASE_VALUE].strVal1); - buf.append(';'); + sb.append('L'); + sb.append(cw.typeTable[t & Frame.BASE_VALUE].strVal1); + sb.append(';'); } else { switch (t & 0xF) { case 1: - buf.append('I'); + sb.append('I'); break; case 2: - buf.append('F'); + sb.append('F'); break; case 3: - buf.append('D'); + sb.append('D'); break; case 9: - buf.append('Z'); + sb.append('Z'); break; case 10: - buf.append('B'); + sb.append('B'); break; case 11: - buf.append('C'); + sb.append('C'); break; case 12: - buf.append('S'); + sb.append('S'); break; default: - buf.append('J'); + sb.append('J'); } } - stackMap.putByte(7).putShort(cw.newClass(buf.toString())); + stackMap.putByte(7).putShort(cw.newClass(sb.toString())); } } } diff --git a/src/asm/scala/tools/asm/Type.java b/src/asm/scala/tools/asm/Type.java index 7887080dee..c8f0048588 100644 --- a/src/asm/scala/tools/asm/Type.java +++ b/src/asm/scala/tools/asm/Type.java @@ -556,11 +556,11 @@ public class Type { case DOUBLE: return "double"; case ARRAY: - StringBuffer b = new StringBuffer(getElementType().getClassName()); + StringBuilder sb = new StringBuilder(getElementType().getClassName()); for (int i = getDimensions(); i > 0; --i) { - b.append("[]"); + sb.append("[]"); } - return b.toString(); + return sb.toString(); case OBJECT: return new String(buf, off, len).replace('/', '.'); default: diff --git a/src/asm/scala/tools/asm/commons/CodeSizeEvaluator.java b/src/asm/scala/tools/asm/commons/CodeSizeEvaluator.java new file mode 100644 index 0000000000..80c07bdae0 --- /dev/null +++ b/src/asm/scala/tools/asm/commons/CodeSizeEvaluator.java @@ -0,0 +1,238 @@ +/*** + * ASM: a very small and fast Java bytecode manipulation framework + * Copyright (c) 2000-2011 INRIA, France Telecom + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holders nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ +package scala.tools.asm.commons; + +import scala.tools.asm.Handle; +import scala.tools.asm.Label; +import scala.tools.asm.MethodVisitor; +import scala.tools.asm.Opcodes; + +/** + * A {@link MethodVisitor} that can be used to approximate method size. + * + * @author Eugene Kuleshov + */ +public class CodeSizeEvaluator extends MethodVisitor implements Opcodes { + + private int minSize; + + private int maxSize; + + public CodeSizeEvaluator(final MethodVisitor mv) { + this(Opcodes.ASM5, mv); + } + + protected CodeSizeEvaluator(final int api, final MethodVisitor mv) { + super(api, mv); + } + + public int getMinSize() { + return this.minSize; + } + + public int getMaxSize() { + return this.maxSize; + } + + @Override + public void visitInsn(final int opcode) { + minSize += 1; + maxSize += 1; + if (mv != null) { + mv.visitInsn(opcode); + } + } + + @Override + public void visitIntInsn(final int opcode, final int operand) { + if (opcode == SIPUSH) { + minSize += 3; + maxSize += 3; + } else { + minSize += 2; + maxSize += 2; + } + if (mv != null) { + mv.visitIntInsn(opcode, operand); + } + } + + @Override + public void visitVarInsn(final int opcode, final int var) { + if (var < 4 && opcode != RET) { + minSize += 1; + maxSize += 1; + } else if (var >= 256) { + minSize += 4; + maxSize += 4; + } else { + minSize += 2; + maxSize += 2; + } + if (mv != null) { + mv.visitVarInsn(opcode, var); + } + } + + @Override + public void visitTypeInsn(final int opcode, final String type) { + minSize += 3; + maxSize += 3; + if (mv != null) { + mv.visitTypeInsn(opcode, type); + } + } + + @Override + public void visitFieldInsn(final int opcode, final String owner, + final String name, final String desc) { + minSize += 3; + maxSize += 3; + if (mv != null) { + mv.visitFieldInsn(opcode, owner, name, desc); + } + } + + @Deprecated + @Override + public void visitMethodInsn(final int opcode, final String owner, + final String name, final String desc) { + if (api >= Opcodes.ASM5) { + super.visitMethodInsn(opcode, owner, name, desc); + return; + } + doVisitMethodInsn(opcode, owner, name, desc, + opcode == Opcodes.INVOKEINTERFACE); + } + + @Override + public void visitMethodInsn(final int opcode, final String owner, + final String name, final String desc, final boolean itf) { + if (api < Opcodes.ASM5) { + super.visitMethodInsn(opcode, owner, name, desc, itf); + return; + } + doVisitMethodInsn(opcode, owner, name, desc, itf); + } + + private void doVisitMethodInsn(int opcode, final String owner, + final String name, final String desc, final boolean itf) { + if (opcode == INVOKEINTERFACE) { + minSize += 5; + maxSize += 5; + } else { + minSize += 3; + maxSize += 3; + } + if (mv != null) { + mv.visitMethodInsn(opcode, owner, name, desc, itf); + } + } + + @Override + public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, + Object... bsmArgs) { + minSize += 5; + maxSize += 5; + if (mv != null) { + mv.visitInvokeDynamicInsn(name, desc, bsm, bsmArgs); + } + } + + @Override + public void visitJumpInsn(final int opcode, final Label label) { + minSize += 3; + if (opcode == GOTO || opcode == JSR) { + maxSize += 5; + } else { + maxSize += 8; + } + if (mv != null) { + mv.visitJumpInsn(opcode, label); + } + } + + @Override + public void visitLdcInsn(final Object cst) { + if (cst instanceof Long || cst instanceof Double) { + minSize += 3; + maxSize += 3; + } else { + minSize += 2; + maxSize += 3; + } + if (mv != null) { + mv.visitLdcInsn(cst); + } + } + + @Override + public void visitIincInsn(final int var, final int increment) { + if (var > 255 || increment > 127 || increment < -128) { + minSize += 6; + maxSize += 6; + } else { + minSize += 3; + maxSize += 3; + } + if (mv != null) { + mv.visitIincInsn(var, increment); + } + } + + @Override + public void visitTableSwitchInsn(final int min, final int max, + final Label dflt, final Label... labels) { + minSize += 13 + labels.length * 4; + maxSize += 16 + labels.length * 4; + if (mv != null) { + mv.visitTableSwitchInsn(min, max, dflt, labels); + } + } + + @Override + public void visitLookupSwitchInsn(final Label dflt, final int[] keys, + final Label[] labels) { + minSize += 9 + keys.length * 8; + maxSize += 12 + keys.length * 8; + if (mv != null) { + mv.visitLookupSwitchInsn(dflt, keys, labels); + } + } + + @Override + public void visitMultiANewArrayInsn(final String desc, final int dims) { + minSize += 4; + maxSize += 4; + if (mv != null) { + mv.visitMultiANewArrayInsn(desc, dims); + } + } +} diff --git a/src/asm/scala/tools/asm/tree/MethodInsnNode.java b/src/asm/scala/tools/asm/tree/MethodInsnNode.java index 1ec46d473d..30c7854646 100644 --- a/src/asm/scala/tools/asm/tree/MethodInsnNode.java +++ b/src/asm/scala/tools/asm/tree/MethodInsnNode.java @@ -45,6 +45,7 @@ public class MethodInsnNode extends AbstractInsnNode { /** * The internal name of the method's owner class (see * {@link scala.tools.asm.Type#getInternalName() getInternalName}). + * For methods of arrays, e.g., clone(), the array type descriptor. */ public String owner; diff --git a/src/asm/scala/tools/asm/tree/analysis/Frame.java b/src/asm/scala/tools/asm/tree/analysis/Frame.java index 44a07ee27c..0b7f4ba53b 100644 --- a/src/asm/scala/tools/asm/tree/analysis/Frame.java +++ b/src/asm/scala/tools/asm/tree/analysis/Frame.java @@ -725,14 +725,14 @@ public class Frame<V extends Value> { */ @Override public String toString() { - StringBuffer b = new StringBuffer(); + StringBuilder sb = new StringBuilder(); for (int i = 0; i < getLocals(); ++i) { - b.append(getLocal(i)); + sb.append(getLocal(i)); } - b.append(' '); + sb.append(' '); for (int i = 0; i < getStackSize(); ++i) { - b.append(getStack(i).toString()); + sb.append(getStack(i).toString()); } - return b.toString(); + return sb.toString(); } } diff --git a/src/asm/scala/tools/asm/util/CheckClassAdapter.java b/src/asm/scala/tools/asm/util/CheckClassAdapter.java index 9909208cc4..88afdb0441 100644 --- a/src/asm/scala/tools/asm/util/CheckClassAdapter.java +++ b/src/asm/scala/tools/asm/util/CheckClassAdapter.java @@ -269,26 +269,26 @@ public class CheckClassAdapter extends ClassVisitor { for (int j = 0; j < method.instructions.size(); ++j) { method.instructions.get(j).accept(mv); - StringBuffer s = new StringBuffer(); + StringBuilder sb = new StringBuilder(); Frame<BasicValue> f = frames[j]; if (f == null) { - s.append('?'); + sb.append('?'); } else { for (int k = 0; k < f.getLocals(); ++k) { - s.append(getShortName(f.getLocal(k).toString())) + sb.append(getShortName(f.getLocal(k).toString())) .append(' '); } - s.append(" : "); + sb.append(" : "); for (int k = 0; k < f.getStackSize(); ++k) { - s.append(getShortName(f.getStack(k).toString())) + sb.append(getShortName(f.getStack(k).toString())) .append(' '); } } - while (s.length() < method.maxStack + method.maxLocals + 1) { - s.append(' '); + while (sb.length() < method.maxStack + method.maxLocals + 1) { + sb.append(' '); } pw.print(Integer.toString(j + 100000).substring(1)); - pw.print(" " + s + " : " + t.text.get(t.text.size() - 1)); + pw.print(" " + sb + " : " + t.text.get(t.text.size() - 1)); } for (int j = 0; j < method.tryCatchBlocks.size(); ++j) { method.tryCatchBlocks.get(j).accept(mv); diff --git a/src/compiler/scala/tools/nsc/Reporting.scala b/src/compiler/scala/tools/nsc/Reporting.scala index 4d7e9e753f..72a4b69536 100644 --- a/src/compiler/scala/tools/nsc/Reporting.scala +++ b/src/compiler/scala/tools/nsc/Reporting.scala @@ -26,7 +26,7 @@ trait Reporting extends scala.reflect.internal.Reporting { self: ast.Positions w protected def PerRunReporting = new PerRunReporting class PerRunReporting extends PerRunReportingBase { /** Collects for certain classes of warnings during this run. */ - private class ConditionalWarning(what: String, option: Settings#BooleanSetting) { + private class ConditionalWarning(what: String, option: Settings#BooleanSetting)(reRunFlag: String = option.name) { val warnings = mutable.LinkedHashMap[Position, String]() def warn(pos: Position, msg: String) = if (option) reporter.warning(pos, msg) @@ -37,16 +37,16 @@ trait Reporting extends scala.reflect.internal.Reporting { self: ast.Positions w val warningVerb = if (numWarnings == 1) "was" else "were" val warningCount = countElementsAsString(numWarnings, s"$what warning") - reporter.warning(NoPosition, s"there $warningVerb $warningCount; re-run with ${option.name} for details") + reporter.warning(NoPosition, s"there $warningVerb $warningCount; re-run with $reRunFlag for details") } } // This change broke sbt; I gave it the thrilling name of uncheckedWarnings0 so // as to recover uncheckedWarnings for its ever-fragile compiler interface. - private val _deprecationWarnings = new ConditionalWarning("deprecation", settings.deprecation) - private val _uncheckedWarnings = new ConditionalWarning("unchecked", settings.unchecked) - private val _featureWarnings = new ConditionalWarning("feature", settings.feature) - private val _inlinerWarnings = new ConditionalWarning("inliner", settings.YinlinerWarnings) + private val _deprecationWarnings = new ConditionalWarning("deprecation", settings.deprecation)() + private val _uncheckedWarnings = new ConditionalWarning("unchecked", settings.unchecked)() + private val _featureWarnings = new ConditionalWarning("feature", settings.feature)() + private val _inlinerWarnings = new ConditionalWarning("inliner", settings.YinlinerWarnings)(if (settings.isBCodeAskedFor) settings.YoptWarnings.name else settings.YinlinerWarnings.name) private val _allConditionalWarnings = List(_deprecationWarnings, _uncheckedWarnings, _featureWarnings, _inlinerWarnings) // TODO: remove in favor of the overload that takes a Symbol, give that argument a default (NoSymbol) diff --git a/src/compiler/scala/tools/nsc/ast/Trees.scala b/src/compiler/scala/tools/nsc/ast/Trees.scala index 3652f51153..3a502bdb7a 100644 --- a/src/compiler/scala/tools/nsc/ast/Trees.scala +++ b/src/compiler/scala/tools/nsc/ast/Trees.scala @@ -178,7 +178,7 @@ trait Trees extends scala.reflect.internal.Trees { self: Global => } } - // Finally, noone resetAllAttrs it anymore, so I'm removing it from the compiler. + // Finally, no one uses resetAllAttrs anymore, so I'm removing it from the compiler. // Even though it's with great pleasure I'm doing that, I'll leave its body here to warn future generations about what happened in the past. // // So what actually happened in the past is that we used to have two flavors of resetAttrs: resetAllAttrs and resetLocalAttrs. diff --git a/src/compiler/scala/tools/nsc/backend/jvm/AsmUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/AsmUtils.scala index d3f09217cd..0df1b2029d 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/AsmUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/AsmUtils.scala @@ -7,8 +7,8 @@ package scala.tools.nsc.backend.jvm import scala.tools.asm.tree.{InsnList, AbstractInsnNode, ClassNode, MethodNode} import java.io.{StringWriter, PrintWriter} -import scala.tools.asm.util.{TraceClassVisitor, TraceMethodVisitor, Textifier} -import scala.tools.asm.{Attribute, ClassReader} +import scala.tools.asm.util.{CheckClassAdapter, TraceClassVisitor, TraceMethodVisitor, Textifier} +import scala.tools.asm.{ClassWriter, Attribute, ClassReader} import scala.collection.convert.decorateAsScala._ import scala.tools.nsc.backend.jvm.opt.InlineInfoAttributePrototype @@ -106,4 +106,18 @@ object AsmUtils { * Returns a human-readable representation of the given instruction sequence. */ def textify(insns: InsnList): String = textify(insns.iterator().asScala) + + /** + * Run ASM's CheckClassAdapter over a class. Returns None if no problem is found, otherwise + * Some(msg) with the verifier's error message. + */ + def checkClass(classNode: ClassNode): Option[String] = { + val cw = new ClassWriter(ClassWriter.COMPUTE_MAXS) + classNode.accept(cw) + val sw = new StringWriter() + val pw = new PrintWriter(sw) + CheckClassAdapter.verify(new ClassReader(cw.toByteArray), false, pw) + val res = sw.toString + if (res.isEmpty) None else Some(res) + } } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeAsmCommon.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeAsmCommon.scala index 2ebf338f5e..162da4236a 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeAsmCommon.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeAsmCommon.scala @@ -7,7 +7,8 @@ package scala.tools.nsc package backend.jvm import scala.tools.nsc.Global -import scala.tools.nsc.backend.jvm.BTypes.{MethodInlineInfo, InlineInfo, InternalName} +import scala.tools.nsc.backend.jvm.BTypes.{InternalName, MethodInlineInfo, InlineInfo} +import BackendReporting.ClassSymbolInfoFailureSI9111 /** * This trait contains code shared between GenBCode and GenASM that depends on types defined in @@ -336,7 +337,7 @@ final class BCodeAsmCommon[G <: Global](val global: G) { val isEffectivelyFinal = classSym.isEffectivelyFinal - var warning = Option.empty[String] + var warning = Option.empty[ClassSymbolInfoFailureSI9111] // Primitive methods cannot be inlined, so there's no point in building a MethodInlineInfo. Also, some // primitive methods (e.g., `isInstanceOf`) have non-erased types, which confuses [[typeToBType]]. @@ -345,7 +346,7 @@ final class BCodeAsmCommon[G <: Global](val global: G) { if (completeSilentlyAndCheckErroneous(methodSym)) { // Happens due to SI-9111. Just don't provide any MethodInlineInfo for that method, we don't need fail the compiler. if (!classSym.isJavaDefined) devWarning("SI-9111 should only be possible for Java classes") - warning = Some(s"Failed to get the type of a method of class symbol ${classSym.fullName} due to SI-9111") + warning = Some(ClassSymbolInfoFailureSI9111(classSym.fullName)) None } else { val name = methodSym.javaSimpleName.toString // same as in genDefDef diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala index 062daa4eac..15b014bdd3 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala @@ -12,6 +12,8 @@ package jvm import scala.annotation.switch import scala.tools.asm +import GenBCode._ +import BackendReporting._ /* * @@ -92,7 +94,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { val thrownKind = tpeTK(expr) // `throw null` is valid although scala.Null (as defined in src/libray-aux) isn't a subtype of Throwable. // Similarly for scala.Nothing (again, as defined in src/libray-aux). - assert(thrownKind.isNullType || thrownKind.isNothingType || thrownKind.asClassBType.isSubtypeOf(ThrowableReference)) + assert(thrownKind.isNullType || thrownKind.isNothingType || thrownKind.asClassBType.isSubtypeOf(ThrowableReference).get) genLoad(expr, thrownKind) lineNumber(expr) emit(asm.Opcodes.ATHROW) // ICode enters here into enterIgnoreMode, we'll rely instead on DCE at ClassNode level. @@ -229,7 +231,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { if (isArithmeticOp(code)) genArithmeticOp(tree, code) else if (code == scalaPrimitives.CONCAT) genStringConcat(tree) - else if (code == scalaPrimitives.HASH) genScalaHash(receiver) + else if (code == scalaPrimitives.HASH) genScalaHash(receiver, tree.pos) else if (isArrayOp(code)) genArrayOp(tree, code, expectedType) else if (isLogicalOp(code) || isComparisonOp(code)) { val success, failure, after = new asm.Label @@ -583,7 +585,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { // if (fun.symbol.isConstructor) Static(true) else SuperCall(mix); mnode.visitVarInsn(asm.Opcodes.ALOAD, 0) genLoadArguments(args, paramTKs(app)) - genCallMethod(fun.symbol, invokeStyle, pos = app.pos) + genCallMethod(fun.symbol, invokeStyle, app.pos) generatedType = asmMethodType(fun.symbol).returnType // 'new' constructor call: Note: since constructors are @@ -613,7 +615,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { */ for (i <- args.length until dims) elemKind = ArrayBType(elemKind) } - (argsSize : @switch) match { + argsSize match { case 1 => bc newarray elemKind case _ => val descr = ('[' * argsSize) + elemKind.descriptor // denotes the same as: arrayN(elemKind, argsSize).descriptor @@ -625,7 +627,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { mnode.visitTypeInsn(asm.Opcodes.NEW, rt.internalName) bc dup generatedType genLoadArguments(args, paramTKs(app)) - genCallMethod(ctor, icodes.opcodes.Static(onInstance = true)) + genCallMethod(ctor, icodes.opcodes.Static(onInstance = true), app.pos) case _ => abort(s"Cannot instantiate $tpt of kind: $generatedType") @@ -635,7 +637,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { val nativeKind = tpeTK(expr) genLoad(expr, nativeKind) val MethodNameAndType(mname, methodType) = asmBoxTo(nativeKind) - bc.invokestatic(BoxesRunTime.internalName, mname, methodType.descriptor) + bc.invokestatic(BoxesRunTime.internalName, mname, methodType.descriptor, app.pos) generatedType = boxResultType(fun.symbol) // was toTypeKind(fun.symbol.tpe.resultType) case Apply(fun @ _, List(expr)) if currentRun.runDefinitions.isUnbox(fun.symbol) => @@ -643,7 +645,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { val boxType = unboxResultType(fun.symbol) // was toTypeKind(fun.symbol.owner.linkedClassOfClass.tpe) generatedType = boxType val MethodNameAndType(mname, methodType) = asmUnboxTo(boxType) - bc.invokestatic(BoxesRunTime.internalName, mname, methodType.descriptor) + bc.invokestatic(BoxesRunTime.internalName, mname, methodType.descriptor, app.pos) case app @ Apply(fun, args) => val sym = fun.symbol @@ -694,10 +696,10 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { // descriptor (instead of a class internal name): // invokevirtual #2; //Method "[I".clone:()Ljava/lang/Object val target: String = targetTypeKind.asRefBType.classOrArrayType - bc.invokevirtual(target, "clone", "()Ljava/lang/Object;") + bc.invokevirtual(target, "clone", "()Ljava/lang/Object;", app.pos) } else { - genCallMethod(sym, invokeStyle, hostClass, app.pos) + genCallMethod(sym, invokeStyle, app.pos, hostClass) } } // end of genNormalMethodCall() @@ -809,7 +811,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { } def adapt(from: BType, to: BType) { - if (!from.conformsTo(to)) { + if (!from.conformsTo(to).get) { to match { case UNIT => bc drop from case _ => bc.emitT2T(from, to) @@ -975,23 +977,23 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { // Optimization for expressions of the form "" + x. We can avoid the StringBuilder. case List(Literal(Constant("")), arg) => genLoad(arg, ObjectReference) - genCallMethod(String_valueOf, icodes.opcodes.Static(onInstance = false)) + genCallMethod(String_valueOf, icodes.opcodes.Static(onInstance = false), arg.pos) case concatenations => - bc.genStartConcat + bc.genStartConcat(tree.pos) for (elem <- concatenations) { val kind = tpeTK(elem) genLoad(elem, kind) - bc.genStringConcat(kind) + bc.genStringConcat(kind, elem.pos) } - bc.genEndConcat + bc.genEndConcat(tree.pos) } StringReference } - def genCallMethod(method: Symbol, style: InvokeStyle, hostClass0: Symbol = null, pos: Position = NoPosition) { + def genCallMethod(method: Symbol, style: InvokeStyle, pos: Position, hostClass0: Symbol = null) { val siteSymbol = claszSymbol val hostSymbol = if (hostClass0 == null) method.owner else hostClass0 @@ -1035,26 +1037,26 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { } if (style.isStatic) { - if (style.hasInstance) { bc.invokespecial (jowner, jname, mdescr) } - else { bc.invokestatic (jowner, jname, mdescr) } + if (style.hasInstance) { bc.invokespecial (jowner, jname, mdescr, pos) } + else { bc.invokestatic (jowner, jname, mdescr, pos) } } else if (style.isDynamic) { - if (needsInterfaceCall(receiver)) { bc.invokeinterface(jowner, jname, mdescr) } - else { bc.invokevirtual (jowner, jname, mdescr) } + if (needsInterfaceCall(receiver)) { bc.invokeinterface(jowner, jname, mdescr, pos) } + else { bc.invokevirtual (jowner, jname, mdescr, pos) } } else { assert(style.isSuper, s"An unknown InvokeStyle: $style") - bc.invokespecial(jowner, jname, mdescr) + bc.invokespecial(jowner, jname, mdescr, pos) initModule() } } // end of genCallMethod() /* Generate the scala ## method. */ - def genScalaHash(tree: Tree): BType = { + def genScalaHash(tree: Tree, applyPos: Position): BType = { genLoadModule(ScalaRunTimeModule) // TODO why load ScalaRunTimeModule if ## has InvokeStyle of Static(false) ? genLoad(tree, ObjectReference) - genCallMethod(hashMethodSym, icodes.opcodes.Static(onInstance = false)) + genCallMethod(hashMethodSym, icodes.opcodes.Static(onInstance = false), applyPos) INT } @@ -1186,8 +1188,8 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { // TODO !!!!!!!!!! isReferenceType, in the sense of TypeKind? (ie non-array, non-boxed, non-nothing, may be null) if (scalaPrimitives.isUniversalEqualityOp(code) && tpeTK(lhs).isClass) { // `lhs` has reference type - if (code == EQ) genEqEqPrimitive(lhs, rhs, success, failure) - else genEqEqPrimitive(lhs, rhs, failure, success) + if (code == EQ) genEqEqPrimitive(lhs, rhs, success, failure, tree.pos) + else genEqEqPrimitive(lhs, rhs, failure, success, tree.pos) } else if (scalaPrimitives.isComparisonOp(code)) genComparisonOp(lhs, rhs, code) @@ -1207,7 +1209,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { * @param l left-hand-side of the '==' * @param r right-hand-side of the '==' */ - def genEqEqPrimitive(l: Tree, r: Tree, success: asm.Label, failure: asm.Label) { + def genEqEqPrimitive(l: Tree, r: Tree, success: asm.Label, failure: asm.Label, pos: Position) { /* True if the equality comparison is between values that require the use of the rich equality * comparator (scala.runtime.Comparator.equals). This is the case when either side of the @@ -1231,7 +1233,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { } genLoad(l, ObjectReference) genLoad(r, ObjectReference) - genCallMethod(equalsMethod, icodes.opcodes.Static(onInstance = false)) + genCallMethod(equalsMethod, icodes.opcodes.Static(onInstance = false), pos) genCZJUMP(success, failure, icodes.NE, BOOL) } else { @@ -1247,7 +1249,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { // SI-7852 Avoid null check if L is statically non-null. genLoad(l, ObjectReference) genLoad(r, ObjectReference) - genCallMethod(Object_equals, icodes.opcodes.Dynamic) + genCallMethod(Object_equals, icodes.opcodes.Dynamic, pos) genCZJUMP(success, failure, icodes.NE, BOOL) } else { // l == r -> if (l eq null) r eq null else l.equals(r) @@ -1268,7 +1270,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { markProgramPoint(lNonNull) locals.load(eqEqTempLocal) - genCallMethod(Object_equals, icodes.opcodes.Dynamic) + genCallMethod(Object_equals, icodes.opcodes.Dynamic, pos) genCZJUMP(success, failure, icodes.NE, BOOL) } } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala index ccee230191..3b7dbc18da 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala @@ -10,6 +10,8 @@ package backend.jvm import scala.tools.asm import scala.collection.mutable import scala.tools.nsc.io.AbstractFile +import GenBCode._ +import BackendReporting._ /* * Traits encapsulating functionality to convert Scala AST Trees into ASM ClassNodes. @@ -66,7 +68,7 @@ abstract class BCodeHelpers extends BCodeIdiomatic with BytecodeWriters { override def getCommonSuperClass(inameA: String, inameB: String): String = { val a = classBTypeFromInternalName(inameA) val b = classBTypeFromInternalName(inameB) - val lub = a.jvmWiseLUB(b) + val lub = a.jvmWiseLUB(b).get val lubName = lub.internalName assert(lubName != "scala/Any") lubName // ASM caches the answer during the lifetime of a ClassWriter. We outlive that. Not sure whether caching on our side would improve things. @@ -204,12 +206,12 @@ abstract class BCodeHelpers extends BCodeIdiomatic with BytecodeWriters { * can-multi-thread */ final def addInnerClassesASM(jclass: asm.ClassVisitor, refedInnerClasses: List[ClassBType]) { - val allNestedClasses = refedInnerClasses.flatMap(_.enclosingNestedClassesChain).distinct + val allNestedClasses = refedInnerClasses.flatMap(_.enclosingNestedClassesChain.get).distinct // sorting ensures nested classes are listed after their enclosing class thus satisfying the Eclipse Java compiler for (nestedClass <- allNestedClasses.sortBy(_.internalName.toString)) { // Extract the innerClassEntry - we know it exists, enclosingNestedClassesChain only returns nested classes. - val Some(e) = nestedClass.innerClassAttributeEntry + val Some(e) = nestedClass.innerClassAttributeEntry.get jclass.visitInnerClass(e.name, e.outerName, e.innerName, e.flags) } } @@ -331,125 +333,42 @@ abstract class BCodeHelpers extends BCodeIdiomatic with BytecodeWriters { getClassBTypeAndRegisterInnerClass(classSym).internalName } - private def assertClassNotArray(sym: Symbol): Unit = { - assert(sym.isClass, sym) - assert(sym != definitions.ArrayClass || isCompilingArray, sym) - } - - private def assertClassNotArrayNotPrimitive(sym: Symbol): Unit = { - assertClassNotArray(sym) - assert(!primitiveTypeMap.contains(sym) || isCompilingPrimitive, sym) - } - /** * The ClassBType for a class symbol. If the class is nested, the ClassBType is added to the * innerClassBufferASM. * - * The class symbol scala.Nothing is mapped to the class scala.runtime.Nothing$. Similarly, - * scala.Null is mapped to scala.runtime.Null$. This is because there exist no class files - * for the Nothing / Null. If used for example as a parameter type, we use the runtime classes - * in the classfile method signature. - * - * Note that the referenced class symbol may be an implementation class. For example when - * compiling a mixed-in method that forwards to the static method in the implementation class, - * the class descriptor of the receiver (the implementation class) is obtained by creating the - * ClassBType. + * TODO: clean up the way we track referenced inner classes. + * doing it during code generation is not correct when the optimizer changes the code. */ final def getClassBTypeAndRegisterInnerClass(sym: Symbol): ClassBType = { - assertClassNotArrayNotPrimitive(sym) - - if (sym == definitions.NothingClass) RT_NOTHING - else if (sym == definitions.NullClass) RT_NULL - else { - val r = classBTypeFromSymbol(sym) - if (r.isNestedClass) innerClassBufferASM += r - r - } + val r = classBTypeFromSymbol(sym) + if (r.isNestedClass.get) innerClassBufferASM += r + r } /** - * This method returns the BType for a type reference, for example a parameter type. - * - * If the result is a ClassBType for a nested class, it is added to the innerClassBufferASM. - * - * If `t` references a class, toTypeKind ensures that the class is not an implementation class. - * See also comment on getClassBTypeAndRegisterInnerClass, which is invoked for implementation - * classes. + * The BType for a type reference. If the result is a ClassBType for a nested class, it is added + * to the innerClassBufferASM. + * TODO: clean up the way we track referenced inner classes. */ - final def toTypeKind(t: Type): BType = { - import definitions.ArrayClass - - /** - * Primitive types are represented as TypeRefs to the class symbol of, for example, scala.Int. - * The `primitiveTypeMap` maps those class symbols to the corresponding PrimitiveBType. - */ - def primitiveOrClassToBType(sym: Symbol): BType = { - assertClassNotArray(sym) - assert(!sym.isImplClass, sym) - primitiveTypeMap.getOrElse(sym, getClassBTypeAndRegisterInnerClass(sym)) - } - - /** - * When compiling Array.scala, the type parameter T is not erased and shows up in method - * signatures, e.g. `def apply(i: Int): T`. A TyperRef to T is replaced by ObjectReference. - */ - def nonClassTypeRefToBType(sym: Symbol): ClassBType = { - assert(sym.isType && isCompilingArray, sym) - ObjectReference - } - - t.dealiasWiden match { - case TypeRef(_, ArrayClass, List(arg)) => ArrayBType(toTypeKind(arg)) // Array type such as Array[Int] (kept by erasure) - case TypeRef(_, sym, _) if !sym.isClass => nonClassTypeRefToBType(sym) // See comment on nonClassTypeRefToBType - case TypeRef(_, sym, _) => primitiveOrClassToBType(sym) // Common reference to a type such as scala.Int or java.lang.String - case ClassInfoType(_, _, sym) => primitiveOrClassToBType(sym) // We get here, for example, for genLoadModule, which invokes toTypeKind(moduleClassSymbol.info) - - /* AnnotatedType should (probably) be eliminated by erasure. However we know it happens for - * meta-annotated annotations (@(ann @getter) val x = 0), so we don't emit a warning. - * The type in the AnnotationInfo is an AnnotatedTpe. Tested in jvm/annotations.scala. - */ - case a @ AnnotatedType(_, t) => - debuglog(s"typeKind of annotated type $a") - toTypeKind(t) - - /* ExistentialType should (probably) be eliminated by erasure. We know they get here for - * classOf constants: - * class C[T] - * class T { final val k = classOf[C[_]] } - */ - case e @ ExistentialType(_, t) => - debuglog(s"typeKind of existential type $e") - toTypeKind(t) - - /* The cases below should probably never occur. They are kept for now to avoid introducing - * new compiler crashes, but we added a warning. The compiler / library bootstrap and the - * test suite don't produce any warning. - */ - - case tp => - currentUnit.warning(tp.typeSymbol.pos, - s"an unexpected type representation reached the compiler backend while compiling $currentUnit: $tp. " + - "If possible, please file a bug on issues.scala-lang.org.") - - tp match { - case ThisType(ArrayClass) => ObjectReference // was introduced in 9b17332f11 to fix SI-999, but this code is not reached in its test, or any other test - case ThisType(sym) => getClassBTypeAndRegisterInnerClass(sym) - case SingleType(_, sym) => primitiveOrClassToBType(sym) - case ConstantType(_) => toTypeKind(t.underlying) - case RefinedType(parents, _) => parents.map(toTypeKind(_).asClassBType).reduceLeft((a, b) => a.jvmWiseLUB(b)) - } - } + final def toTypeKind(t: Type): BType = typeToBType(t) match { + case c: ClassBType if c.isNestedClass.get => + innerClassBufferASM += c + c + case r => r } - /* - * must-single-thread + /** + * Class components that are nested classes are added to the innerClassBufferASM. + * TODO: clean up the way we track referenced inner classes. */ final def asmMethodType(msym: Symbol): MethodBType = { - assert(msym.isMethod, s"not a method-symbol: $msym") - val resT: BType = - if (msym.isClassConstructor || msym.isConstructor) UNIT - else toTypeKind(msym.tpe.resultType) - MethodBType(msym.tpe.paramTypes map toTypeKind, resT) + val r = methodBTypeFromSymbol(msym) + (r.returnType :: r.argumentTypes) foreach { + case c: ClassBType if c.isNestedClass.get => innerClassBufferASM += c + case _ => + } + r } /** @@ -796,7 +715,7 @@ abstract class BCodeHelpers extends BCodeIdiomatic with BytecodeWriters { val mirrorClass = new asm.tree.ClassNode mirrorClass.visit( classfileVersion, - bType.info.flags, + bType.info.get.flags, bType.internalName, null /* no java-generic-signature */, ObjectReference.internalName, @@ -812,7 +731,7 @@ abstract class BCodeHelpers extends BCodeIdiomatic with BytecodeWriters { addForwarders(isRemote(moduleClass), mirrorClass, bType.internalName, moduleClass) - innerClassBufferASM ++= bType.info.nestedClasses + innerClassBufferASM ++= bType.info.get.nestedClasses addInnerClassesASM(mirrorClass, innerClassBufferASM.toList) mirrorClass.visitEnd() @@ -928,7 +847,7 @@ abstract class BCodeHelpers extends BCodeIdiomatic with BytecodeWriters { constructor.visitMaxs(0, 0) // just to follow protocol, dummy arguments constructor.visitEnd() - innerClassBufferASM ++= classBTypeFromSymbol(cls).info.nestedClasses + innerClassBufferASM ++= classBTypeFromSymbol(cls).info.get.nestedClasses addInnerClassesASM(beanInfoClass, innerClassBufferASM.toList) beanInfoClass.visitEnd() diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeIdiomatic.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeIdiomatic.scala index c3db28151b..9993357eee 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeIdiomatic.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeIdiomatic.scala @@ -10,6 +10,8 @@ package backend.jvm import scala.tools.asm import scala.annotation.switch import scala.collection.mutable +import GenBCode._ +import scala.tools.asm.tree.MethodInsnNode /* * A high-level facade to the ASM API for bytecode generation. @@ -42,9 +44,6 @@ abstract class BCodeIdiomatic extends SubComponent { val StringBuilderClassName = "scala/collection/mutable/StringBuilder" - val CLASS_CONSTRUCTOR_NAME = "<clinit>" - val INSTANCE_CONSTRUCTOR_NAME = "<init>" - val EMPTY_STRING_ARRAY = Array.empty[String] val EMPTY_INT_ARRAY = Array.empty[Int] val EMPTY_LABEL_ARRAY = Array.empty[asm.Label] @@ -107,7 +106,7 @@ abstract class BCodeIdiomatic extends SubComponent { */ abstract class JCodeMethodN { - def jmethod: asm.MethodVisitor + def jmethod: asm.tree.MethodNode import asm.Opcodes; import icodes.opcodes.{ Static, Dynamic, SuperCall } @@ -207,20 +206,21 @@ abstract class BCodeIdiomatic extends SubComponent { /* * can-multi-thread */ - final def genStartConcat { + final def genStartConcat(pos: Position): Unit = { jmethod.visitTypeInsn(Opcodes.NEW, StringBuilderClassName) jmethod.visitInsn(Opcodes.DUP) invokespecial( StringBuilderClassName, INSTANCE_CONSTRUCTOR_NAME, - "()V" + "()V", + pos ) } /* * can-multi-thread */ - final def genStringConcat(el: BType) { + final def genStringConcat(el: BType, pos: Position): Unit = { val jtype = if (el.isArray || el.isClass) ObjectReference @@ -228,14 +228,14 @@ abstract class BCodeIdiomatic extends SubComponent { val bt = MethodBType(List(jtype), StringBuilderReference) - invokevirtual(StringBuilderClassName, "append", bt.descriptor) + invokevirtual(StringBuilderClassName, "append", bt.descriptor, pos) } /* * can-multi-thread */ - final def genEndConcat { - invokevirtual(StringBuilderClassName, "toString", "()Ljava/lang/String;") + final def genEndConcat(pos: Position): Unit = { + invokevirtual(StringBuilderClassName, "toString", "()Ljava/lang/String;", pos) } /* @@ -391,20 +391,26 @@ abstract class BCodeIdiomatic extends SubComponent { final def rem(tk: BType) { emitPrimitive(JCodeMethodN.remOpcodes, tk) } // can-multi-thread // can-multi-thread - final def invokespecial(owner: String, name: String, desc: String) { - jmethod.visitMethodInsn(Opcodes.INVOKESPECIAL, owner, name, desc, false) + final def invokespecial(owner: String, name: String, desc: String, pos: Position) { + addInvoke(Opcodes.INVOKESPECIAL, owner, name, desc, false, pos) } // can-multi-thread - final def invokestatic(owner: String, name: String, desc: String) { - jmethod.visitMethodInsn(Opcodes.INVOKESTATIC, owner, name, desc, false) + final def invokestatic(owner: String, name: String, desc: String, pos: Position) { + addInvoke(Opcodes.INVOKESTATIC, owner, name, desc, false, pos) } // can-multi-thread - final def invokeinterface(owner: String, name: String, desc: String) { - jmethod.visitMethodInsn(Opcodes.INVOKEINTERFACE, owner, name, desc, true) + final def invokeinterface(owner: String, name: String, desc: String, pos: Position) { + addInvoke(Opcodes.INVOKEINTERFACE, owner, name, desc, true, pos) } // can-multi-thread - final def invokevirtual(owner: String, name: String, desc: String) { - jmethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, owner, name, desc, false) + final def invokevirtual(owner: String, name: String, desc: String, pos: Position) { + addInvoke(Opcodes.INVOKEVIRTUAL, owner, name, desc, false, pos) + } + + private def addInvoke(opcode: Int, owner: String, name: String, desc: String, itf: Boolean, pos: Position) = { + val node = new MethodInsnNode(opcode, owner, name, desc, itf) + jmethod.instructions.add(node) + if (settings.YoptInlinerEnabled) callsitePositions(node) = pos } // can-multi-thread diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala index 142c901c21..2a06c62e37 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala @@ -4,18 +4,17 @@ */ -package scala -package tools.nsc +package scala.tools.nsc package backend package jvm import scala.collection.{ mutable, immutable } +import scala.tools.nsc.backend.jvm.opt.ByteCodeRepository import scala.tools.nsc.symtab._ -import scala.annotation.switch import scala.tools.asm -import scala.tools.asm.util.{TraceMethodVisitor, ASMifier} -import java.io.PrintWriter +import GenBCode._ +import BackendReporting._ /* * @@ -101,6 +100,8 @@ abstract class BCodeSkelBuilder extends BCodeHelpers { isCZRemote = isRemote(claszSymbol) thisName = internalName(claszSymbol) + val classBType = classBTypeFromSymbol(claszSymbol) + cnode = new asm.tree.ClassNode() initJClass(cnode) @@ -118,16 +119,21 @@ abstract class BCodeSkelBuilder extends BCodeHelpers { addClassFields() - innerClassBufferASM ++= classBTypeFromSymbol(claszSymbol).info.nestedClasses + innerClassBufferASM ++= classBType.info.get.nestedClasses gen(cd.impl) addInnerClassesASM(cnode, innerClassBufferASM.toList) + cnode.visitAttribute(classBType.inlineInfoAttribute.get) + if (AsmUtils.traceClassEnabled && cnode.name.contains(AsmUtils.traceClassPattern)) AsmUtils.traceClass(cnode) - cnode.innerClasses - assert(cd.symbol == claszSymbol, "Someone messed up BCodePhase.claszSymbol during genPlainClass().") + if (settings.YoptInlinerEnabled) { + // The inliner needs to find all classes in the code repo, also those being compiled + byteCodeRepository.add(cnode, ByteCodeRepository.CompilationUnit) + } + assert(cd.symbol == claszSymbol, "Someone messed up BCodePhase.claszSymbol during genPlainClass().") } // end of method genPlainClass() /* @@ -137,9 +143,9 @@ abstract class BCodeSkelBuilder extends BCodeHelpers { val ps = claszSymbol.info.parents val superClass: String = if (ps.isEmpty) ObjectReference.internalName else internalName(ps.head.typeSymbol) - val interfaceNames = classBTypeFromSymbol(claszSymbol).info.interfaces map { + val interfaceNames = classBTypeFromSymbol(claszSymbol).info.get.interfaces map { case classBType => - if (classBType.isNestedClass) { innerClassBufferASM += classBType } + if (classBType.isNestedClass.get) { innerClassBufferASM += classBType } classBType.internalName } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala index ec26ef9b48..d8a17e975e 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala @@ -6,18 +6,23 @@ package scala.tools.nsc package backend.jvm +import scala.annotation.switch +import scala.collection.concurrent.TrieMap +import scala.reflect.internal.util.Position import scala.tools.asm import asm.Opcodes -import scala.tools.asm.tree.{InnerClassNode, ClassNode} -import opt.ByteCodeRepository +import scala.tools.asm.tree.{MethodInsnNode, InnerClassNode, ClassNode} +import scala.tools.nsc.backend.jvm.BTypes.{InlineInfo, MethodInlineInfo} +import scala.tools.nsc.backend.jvm.BackendReporting._ +import scala.tools.nsc.backend.jvm.opt._ import scala.collection.convert.decorateAsScala._ /** - * The BTypes component defines The BType class hierarchy. BTypes encapsulate all type information + * The BTypes component defines The BType class hierarchy. A BType stores all type information * that is required after building the ASM nodes. This includes optimizations, generation of * InnerClass attributes and generation of stack map frames. * - * This representation is immutable and independent of the compiler data structures, hence it can + * The representation is immutable and independent of the compiler data structures, hence it can * be queried by concurrent threads. */ abstract class BTypes { @@ -34,9 +39,24 @@ abstract class BTypes { */ val byteCodeRepository: ByteCodeRepository + val inliner: Inliner[this.type] + + val callGraph: CallGraph[this.type] + + val backendReporting: BackendReporting + // Allows to define per-run caches here and in the CallGraph component, which don't have a global def recordPerRunCache[T <: collection.generic.Clearable](cache: T): T + // When building the call graph, we need to know if global inlining is allowed (the component doesn't have a global) + def inlineGlobalEnabled: Boolean + + // When the inliner is not enabled, there's no point in adding InlineInfos to all ClassBTypes + def inlinerEnabled: Boolean + + // Settings that define what kind of optimizer warnings are emitted. + def warnSettings: WarnSettings + /** * A map from internal names to ClassBTypes. Every ClassBType is added to this map on its * construction. @@ -48,13 +68,57 @@ abstract class BTypes { * Concurrent because stack map frames are computed when in the class writer, which might run * on multiple classes concurrently. */ - val classBTypeFromInternalName: collection.concurrent.Map[InternalName, ClassBType] = recordPerRunCache(collection.concurrent.TrieMap.empty[InternalName, ClassBType]) + val classBTypeFromInternalName: collection.concurrent.Map[InternalName, ClassBType] = recordPerRunCache(TrieMap.empty) + + /** + * Store the position of every MethodInsnNode during code generation. This allows each callsite + * in the call graph to remember its source position, which is required for inliner warnings. + */ + val callsitePositions: collection.concurrent.Map[MethodInsnNode, Position] = recordPerRunCache(TrieMap.empty) + + /** + * Contains the internal names of all classes that are defined in Java source files of the current + * compilation run (mixed compilation). Used for more detailed error reporting. + */ + val javaDefinedClasses: collection.mutable.Set[InternalName] = recordPerRunCache(collection.mutable.Set.empty) + + /** + * Obtain the BType for a type descriptor or internal name. For class descriptors, the ClassBType + * is constructed by parsing the corresponding classfile. + * + * Some JVM operations use either a full descriptor or only an internal name. Example: + * ANEWARRAY java/lang/String // a new array of strings (internal name for the String class) + * ANEWARRAY [Ljava/lang/String; // a new array of array of string (full descriptor for the String class) + * + * This method supports both descriptors and internal names. + */ + def bTypeForDescriptorOrInternalNameFromClassfile(desc: String): BType = (desc(0): @switch) match { + case 'V' => UNIT + case 'Z' => BOOL + case 'C' => CHAR + case 'B' => BYTE + case 'S' => SHORT + case 'I' => INT + case 'F' => FLOAT + case 'J' => LONG + case 'D' => DOUBLE + case '[' => ArrayBType(bTypeForDescriptorOrInternalNameFromClassfile(desc.substring(1))) + case 'L' if desc.last == ';' => classBTypeFromParsedClassfile(desc.substring(1, desc.length - 1)) + case _ => classBTypeFromParsedClassfile(desc) + } /** - * Parse the classfile for `internalName` and construct the [[ClassBType]]. + * Parse the classfile for `internalName` and construct the [[ClassBType]]. If the classfile cannot + * be found in the `byteCodeRepository`, the `info` of the resulting ClassBType is undefined. */ def classBTypeFromParsedClassfile(internalName: InternalName): ClassBType = { - classBTypeFromClassNode(byteCodeRepository.classNode(internalName)) + classBTypeFromInternalName.getOrElse(internalName, { + val res = ClassBType(internalName) + byteCodeRepository.classNode(internalName) match { + case Left(msg) => res.info = Left(NoClassBTypeInfoMissingBytecode(msg)); res + case Right(c) => setClassInfoFromParsedClassfile(c, res) + } + }) } /** @@ -62,11 +126,11 @@ abstract class BTypes { */ def classBTypeFromClassNode(classNode: ClassNode): ClassBType = { classBTypeFromInternalName.getOrElse(classNode.name, { - setClassInfo(classNode, ClassBType(classNode.name)) + setClassInfoFromParsedClassfile(classNode, ClassBType(classNode.name)) }) } - private def setClassInfo(classNode: ClassNode, classBType: ClassBType): ClassBType = { + private def setClassInfoFromParsedClassfile(classNode: ClassNode, classBType: ClassBType): ClassBType = { val superClass = classNode.superName match { case null => assert(classNode.name == ObjectReference.internalName, s"class with missing super type: ${classNode.name}") @@ -89,11 +153,13 @@ abstract class BTypes { * For local and anonymous classes, innerClassNode.outerName is null. Such classes are required * to have an EnclosingMethod attribute declaring the outer class. So we keep those local and * anonymous classes whose outerClass is classNode.name. - * */ def nestedInCurrentClass(innerClassNode: InnerClassNode): Boolean = { (innerClassNode.outerName != null && innerClassNode.outerName == classNode.name) || - (innerClassNode.outerName == null && byteCodeRepository.classNode(innerClassNode.name).outerClass == classNode.name) + (innerClassNode.outerName == null && { + val classNodeForInnerClass = byteCodeRepository.classNode(innerClassNode.name).get // TODO: don't get here, but set the info to Left at the end + classNodeForInnerClass.outerClass == classNode.name + }) } val nestedClasses: List[ClassBType] = classNode.innerClasses.asScala.collect({ @@ -116,11 +182,58 @@ abstract class BTypes { val staticFlag = (innerEntry.access & Opcodes.ACC_STATIC) != 0 NestedInfo(enclosingClass, Option(innerEntry.outerName), Option(innerEntry.innerName), staticFlag) } - classBType.info = ClassInfo(superClass, interfaces, flags, nestedClasses, nestedInfo) + + val inlineInfo = inlineInfoFromClassfile(classNode) + + classBType.info = Right(ClassInfo(superClass, interfaces, flags, nestedClasses, nestedInfo, inlineInfo)) classBType } /** + * Build the InlineInfo for a class. For Scala classes, the information is stored in the + * ScalaInlineInfo attribute. If the attribute is missing, the InlineInfo is built using the + * metadata available in the classfile (ACC_FINAL flags, etc). + */ + def inlineInfoFromClassfile(classNode: ClassNode): InlineInfo = { + def fromClassfileAttribute: Option[InlineInfo] = { + if (classNode.attrs == null) None + else classNode.attrs.asScala.collect({ case a: InlineInfoAttribute => a}).headOption.map(_.inlineInfo) + } + + def fromClassfileWithoutAttribute = { + val warning = { + val isScala = classNode.attrs != null && classNode.attrs.asScala.exists(a => a.`type` == BTypes.ScalaAttributeName || a.`type` == BTypes.ScalaSigAttributeName) + if (isScala) Some(NoInlineInfoAttribute(classNode.name)) + else None + } + // when building MethodInlineInfos for the members of a ClassSymbol, we exclude those methods + // in scalaPrimitives. This is necessary because some of them have non-erased types, which would + // require special handling. Excluding is OK because they are never inlined. + // Here we are parsing from a classfile and we don't need to do anything special. Many of these + // primitives don't even exist, for example Any.isInstanceOf. + val methodInfos = classNode.methods.asScala.map(methodNode => { + val info = MethodInlineInfo( + effectivelyFinal = BytecodeUtils.isFinalMethod(methodNode), + traitMethodWithStaticImplementation = false, + annotatedInline = false, + annotatedNoInline = false) + (methodNode.name + methodNode.desc, info) + }).toMap + InlineInfo( + traitImplClassSelfType = None, + isEffectivelyFinal = BytecodeUtils.isFinalClass(classNode), + methodInfos = methodInfos, + warning) + } + + // The InlineInfo is built from the classfile (not from the symbol) for all classes that are NOT + // being compiled. For those classes, the info is only needed if the inliner is enabled, othewise + // we can save the memory. + if (!inlinerEnabled) BTypes.EmptyInlineInfo + else fromClassfileAttribute getOrElse fromClassfileWithoutAttribute + } + + /** * A BType is either a primitive type, a ClassBType, an ArrayBType of one of these, or a MethodType * referring to BTypes. */ @@ -184,7 +297,7 @@ abstract class BTypes { * promotions (e.g. BYTE to INT). Its operation can be visualized more easily in terms of the * Java bytecode type hierarchy. */ - final def conformsTo(other: BType): Boolean = { + final def conformsTo(other: BType): Either[NoClassBTypeInfo, Boolean] = tryEither(Right({ assert(isRef || isPrimitive, s"conformsTo cannot handle $this") assert(other.isRef || other.isPrimitive, s"conformsTo cannot handle $other") @@ -192,7 +305,7 @@ abstract class BTypes { case ArrayBType(component) => if (other == ObjectReference || other == jlCloneableReference || other == jioSerializableReference) true else other match { - case ArrayBType(otherComponoent) => component.conformsTo(otherComponoent) + case ArrayBType(otherComponoent) => component.conformsTo(otherComponoent).orThrow case _ => false } @@ -201,7 +314,7 @@ abstract class BTypes { if (other.isBoxed) this == other else if (other == ObjectReference) true else other match { - case otherClassType: ClassBType => classType.isSubtypeOf(otherClassType) // e.g., java/lang/Double conforms to java/lang/Number + case otherClassType: ClassBType => classType.isSubtypeOf(otherClassType).orThrow // e.g., java/lang/Double conforms to java/lang/Number case _ => false } } else if (isNullType) { @@ -211,7 +324,7 @@ abstract class BTypes { } else if (isNothingType) { true } else other match { - case otherClassType: ClassBType => classType.isSubtypeOf(otherClassType) + case otherClassType: ClassBType => classType.isSubtypeOf(otherClassType).orThrow // case ArrayBType(_) => this.isNullType // documentation only, because `if (isNullType)` above covers this case case _ => // isNothingType || // documentation only, because `if (isNothingType)` above covers this case @@ -226,7 +339,7 @@ abstract class BTypes { assert(isPrimitive && other.isPrimitive, s"Expected primitive types $this - $other") this == other } - } + })) /** * Compute the upper bound of two types. @@ -245,7 +358,7 @@ abstract class BTypes { ObjectReference case _: MethodBType => - throw new AssertionError(s"unexpected method type when computing maxType: $this") + assertionError(s"unexpected method type when computing maxType: $this") } /** @@ -336,7 +449,7 @@ abstract class BTypes { */ final def maxValueType(other: BType): BType = { - def uncomparable: Nothing = throw new AssertionError(s"Cannot compute maxValueType: $this, $other") + def uncomparable: Nothing = assertionError(s"Cannot compute maxValueType: $this, $other") if (!other.isPrimitive && !other.isNothingType) uncomparable @@ -665,6 +778,21 @@ abstract class BTypes { /** * A ClassBType represents a class or interface type. The necessary information to build a * ClassBType is extracted from compiler symbols and types, see BTypesFromSymbols. + * + * The `info` field contains either the class information on an error message why the info could + * not be computed. There are two reasons for an erroneous info: + * 1. The ClassBType was built from a class symbol that stems from a java source file, and the + * symbol's type could not be completed successfully (SI-9111) + * 2. The ClassBType should be built from a classfile, but the class could not be found on the + * compilation classpath. + * + * Note that all ClassBTypes required in a non-optimzied run are built during code generation from + * the class symbols referenced by the ASTs, so they have a valid info. Therefore the backend + * often invokes `info.get` (which asserts the info to exist) when reading data from the ClassBType. + * + * The inliner on the other hand uses ClassBTypes that are built from classfiles, which may have + * a missing info. In order not to crash the compiler unnecessarily, the inliner does not force + * infos using `get`, but it reports inliner warnings for missing infos that prevent inlining. */ final case class ClassBType(internalName: InternalName) extends RefBType { /** @@ -674,14 +802,14 @@ abstract class BTypes { * B.info.nestedInfo.outerClass == A * A.info.nestedClasses contains B */ - private var _info: ClassInfo = null + private var _info: Either[NoClassBTypeInfo, ClassInfo] = null - def info: ClassInfo = { + def info: Either[NoClassBTypeInfo, ClassInfo] = { assert(_info != null, s"ClassBType.info not yet assigned: $this") _info } - def info_=(i: ClassInfo): Unit = { + def info_=(i: Either[NoClassBTypeInfo, ClassInfo]): Unit = { assert(_info == null, s"Cannot set ClassBType.info multiple times: $this") _info = i checkInfoConsistency() @@ -690,27 +818,29 @@ abstract class BTypes { classBTypeFromInternalName(internalName) = this private def checkInfoConsistency(): Unit = { + if (info.isLeft) return + // we assert some properties. however, some of the linked ClassBType (members, superClass, // interfaces) may not yet have an `_info` (initialization of cyclic structures). so we do a - // best-effort verification. - def ifInit(c: ClassBType)(p: ClassBType => Boolean): Boolean = c._info == null || p(c) + // best-effort verification. also we don't report an error if the info is a Left. + def ifInit(c: ClassBType)(p: ClassBType => Boolean): Boolean = c._info == null || c.info.isLeft || p(c) def isJLO(t: ClassBType) = t.internalName == ObjectReference.internalName assert(!ClassBType.isInternalPhantomType(internalName), s"Cannot create ClassBType for phantom type $this") assert( - if (info.superClass.isEmpty) { isJLO(this) || (isCompilingPrimitive && ClassBType.hasNoSuper(internalName)) } - else if (isInterface) isJLO(info.superClass.get) - else !isJLO(this) && ifInit(info.superClass.get)(!_.isInterface), - s"Invalid superClass in $this: ${info.superClass}" + if (info.get.superClass.isEmpty) { isJLO(this) || (isCompilingPrimitive && ClassBType.hasNoSuper(internalName)) } + else if (isInterface.get) isJLO(info.get.superClass.get) + else !isJLO(this) && ifInit(info.get.superClass.get)(!_.isInterface.get), + s"Invalid superClass in $this: ${info.get.superClass}" ) assert( - info.interfaces.forall(c => ifInit(c)(_.isInterface)), - s"Invalid interfaces in $this: ${info.interfaces}" + info.get.interfaces.forall(c => ifInit(c)(_.isInterface.get)), + s"Invalid interfaces in $this: ${info.get.interfaces}" ) - assert(info.nestedClasses.forall(c => ifInit(c)(_.isNestedClass)), info.nestedClasses) + assert(info.get.nestedClasses.forall(c => ifInit(c)(_.isNestedClass.get)), info.get.nestedClasses) } /** @@ -718,20 +848,37 @@ abstract class BTypes { */ def simpleName: String = internalName.split("/").last - def isInterface = (info.flags & asm.Opcodes.ACC_INTERFACE) != 0 + def isInterface: Either[NoClassBTypeInfo, Boolean] = info.map(i => (i.flags & asm.Opcodes.ACC_INTERFACE) != 0) + + def superClassesTransitive: Either[NoClassBTypeInfo, List[ClassBType]] = info.flatMap(i => i.superClass match { + case None => Right(Nil) + case Some(sc) => sc.superClassesTransitive.map(sc :: _) + }) - def superClassesTransitive: List[ClassBType] = info.superClass match { - case None => Nil - case Some(sc) => sc :: sc.superClassesTransitive + /** + * The prefix of the internal name until the last '/', or the empty string. + */ + def packageInternalName: String = { + val name = internalName + name.lastIndexOf('/') match { + case -1 => "" + case i => name.substring(0, i) + } } - def isNestedClass = info.nestedInfo.isDefined + def isPublic: Either[NoClassBTypeInfo, Boolean] = info.map(i => (i.flags & asm.Opcodes.ACC_PUBLIC) != 0) + + def isNestedClass: Either[NoClassBTypeInfo, Boolean] = info.map(_.nestedInfo.isDefined) - def enclosingNestedClassesChain: List[ClassBType] = - if (isNestedClass) this :: info.nestedInfo.get.enclosingClass.enclosingNestedClassesChain - else Nil + def enclosingNestedClassesChain: Either[NoClassBTypeInfo, List[ClassBType]] = { + isNestedClass.flatMap(isNested => { + // if isNested is true, we know that info.get is defined, and nestedInfo.get is also defined. + if (isNested) info.get.nestedInfo.get.enclosingClass.enclosingNestedClassesChain.map(this :: _) + else Right(Nil) + }) + } - def innerClassAttributeEntry: Option[InnerClassEntry] = info.nestedInfo map { + def innerClassAttributeEntry: Either[NoClassBTypeInfo, Option[InnerClassEntry]] = info.map(i => i.nestedInfo map { case NestedInfo(_, outerName, innerName, isStaticNestedClass) => InnerClassEntry( internalName, @@ -739,28 +886,39 @@ abstract class BTypes { innerName.orNull, GenBCode.mkFlags( // the static flag in the InnerClass table has a special meaning, see InnerClass comment - info.flags & ~Opcodes.ACC_STATIC, + i.flags & ~Opcodes.ACC_STATIC, if (isStaticNestedClass) Opcodes.ACC_STATIC else 0 ) & ClassBType.INNER_CLASSES_FLAGS ) - } + }) - def isSubtypeOf(other: ClassBType): Boolean = { - if (this == other) return true + def inlineInfoAttribute: Either[NoClassBTypeInfo, InlineInfoAttribute] = info.map(i => { + // InlineInfos are serialized for classes being compiled. For those the info was built by + // buildInlineInfoFromClassSymbol, which only adds a warning under SI-9111, which in turn + // only happens for class symbols of java source files. + // we could put this assertion into InlineInfoAttribute, but it is more safe to put it here + // where it affect only GenBCode, and not add any assertion to GenASM in 2.11.6. + assert(i.inlineInfo.warning.isEmpty, i.inlineInfo.warning) + InlineInfoAttribute(i.inlineInfo) + }) - if (isInterface) { - if (other == ObjectReference) return true // interfaces conform to Object - if (!other.isInterface) return false // this is an interface, the other is some class other than object. interfaces cannot extend classes, so the result is false. + def isSubtypeOf(other: ClassBType): Either[NoClassBTypeInfo, Boolean] = try { + if (this == other) return Right(true) + if (isInterface.orThrow) { + if (other == ObjectReference) return Right(true) // interfaces conform to Object + if (!other.isInterface.orThrow) return Right(false) // this is an interface, the other is some class other than object. interfaces cannot extend classes, so the result is false. // else: this and other are both interfaces. continue to (*) } else { - val sc = info.superClass - if (sc.isDefined && sc.get.isSubtypeOf(other)) return true // the superclass of this class conforms to other - if (!other.isInterface) return false // this and other are both classes, and the superclass of this does not conform + val sc = info.orThrow.superClass + if (sc.isDefined && sc.get.isSubtypeOf(other).orThrow) return Right(true) // the superclass of this class conforms to other + if (!other.isInterface.orThrow) return Right(false) // this and other are both classes, and the superclass of this does not conform // else: this is a class, the other is an interface. continue to (*) } // (*) check if some interface of this class conforms to other. - info.interfaces.exists(_.isSubtypeOf(other)) + Right(info.orThrow.interfaces.exists(_.isSubtypeOf(other).orThrow)) + } catch { + case Invalid(noInfo: NoClassBTypeInfo) => Left(noInfo) } /** @@ -770,34 +928,36 @@ abstract class BTypes { * http://comments.gmane.org/gmane.comp.java.vm.languages/2293 * https://issues.scala-lang.org/browse/SI-3872 */ - def jvmWiseLUB(other: ClassBType): ClassBType = { + def jvmWiseLUB(other: ClassBType): Either[NoClassBTypeInfo, ClassBType] = { def isNotNullOrNothing(c: ClassBType) = !c.isNullType && !c.isNothingType assert(isNotNullOrNothing(this) && isNotNullOrNothing(other), s"jvmWiseLub for null or nothing: $this - $other") - val res: ClassBType = (this.isInterface, other.isInterface) match { - case (true, true) => - // exercised by test/files/run/t4761.scala - if (other.isSubtypeOf(this)) this - else if (this.isSubtypeOf(other)) other - else ObjectReference - - case (true, false) => - if (other.isSubtypeOf(this)) this else ObjectReference - - case (false, true) => - if (this.isSubtypeOf(other)) other else ObjectReference + tryEither { + val res: ClassBType = (this.isInterface.orThrow, other.isInterface.orThrow) match { + case (true, true) => + // exercised by test/files/run/t4761.scala + if (other.isSubtypeOf(this).orThrow) this + else if (this.isSubtypeOf(other).orThrow) other + else ObjectReference + + case (true, false) => + if (other.isSubtypeOf(this).orThrow) this else ObjectReference + + case (false, true) => + if (this.isSubtypeOf(other).orThrow) other else ObjectReference + + case _ => + // 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.orThrow, other :: other.superClassesTransitive.orThrow) + } - case _ => - // 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) + assert(isNotNullOrNothing(res), s"jvmWiseLub computed: $res") + Right(res) } - - assert(isNotNullOrNothing(res), s"jvmWiseLub computed: $res") - res } private def firstCommonSuffix(as: List[ClassBType], bs: List[ClassBType]): ClassBType = { @@ -859,9 +1019,11 @@ abstract class BTypes { * @param nestedClasses Classes nested in this class. Those need to be added to the * InnerClass table, see the InnerClass spec summary above. * @param nestedInfo If this describes a nested class, information for the InnerClass table. + * @param inlineInfo Information about this class for the inliner. */ final case class ClassInfo(superClass: Option[ClassBType], interfaces: List[ClassBType], flags: Int, - nestedClasses: List[ClassBType], nestedInfo: Option[NestedInfo]) + nestedClasses: List[ClassBType], nestedInfo: Option[NestedInfo], + inlineInfo: InlineInfo) /** * Information required to add a class to an InnerClass table. @@ -962,11 +1124,15 @@ object BTypes { * @param methodInfos The [[MethodInlineInfo]]s for the methods declared in this class. * The map is indexed by the string s"$name$descriptor" (to * disambiguate overloads). + * + * @param warning Contains an warning message if an error occured when building this + * InlineInfo, for example if some classfile could not be found on + * the classpath. This warning can be reported later by the inliner. */ final case class InlineInfo(traitImplClassSelfType: Option[InternalName], isEffectivelyFinal: Boolean, methodInfos: Map[String, MethodInlineInfo], - warning: Option[String]) + warning: Option[ClassInlineInfoWarning]) val EmptyInlineInfo = InlineInfo(None, false, Map.empty, None) @@ -984,4 +1150,8 @@ object BTypes { traitMethodWithStaticImplementation: Boolean, annotatedInline: Boolean, annotatedNoInline: Boolean) + + // no static way (without symbol table instance) to get to nme.ScalaATTR / ScalaSignatureATTR + val ScalaAttributeName = "Scala" + val ScalaSigAttributeName = "ScalaSig" }
\ No newline at end of file diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala index 2af665d31c..eeb6ed24a2 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala @@ -7,10 +7,9 @@ package scala.tools.nsc package backend.jvm import scala.tools.asm -import opt.ByteCodeRepository -import scala.tools.asm.tree.ClassNode -import scala.tools.nsc.backend.jvm.opt.ByteCodeRepository.Source -import BTypes.InternalName +import scala.tools.nsc.backend.jvm.opt.{CallGraph, Inliner, ByteCodeRepository} +import scala.tools.nsc.backend.jvm.BTypes.{InlineInfo, MethodInlineInfo, InternalName} +import BackendReporting._ /** * This class mainly contains the method classBTypeFromSymbol, which extracts the necessary @@ -36,7 +35,13 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { val coreBTypes = new CoreBTypesProxy[this.type](this) import coreBTypes._ - val byteCodeRepository = new ByteCodeRepository(global.classPath, recordPerRunCache(collection.concurrent.TrieMap.empty[InternalName, (ClassNode, Source)])) + val byteCodeRepository = new ByteCodeRepository(global.classPath, javaDefinedClasses, recordPerRunCache(collection.concurrent.TrieMap.empty)) + + val inliner: Inliner[this.type] = new Inliner(this) + + val callGraph: CallGraph[this.type] = new CallGraph(this) + + val backendReporting: BackendReporting = new BackendReportingImpl(global) final def initializeCoreBTypes(): Unit = { coreBTypes.setBTypes(new CoreBTypes[this.type](this)) @@ -44,6 +49,20 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { def recordPerRunCache[T <: collection.generic.Clearable](cache: T): T = perRunCaches.recordCache(cache) + def inlineGlobalEnabled: Boolean = settings.YoptInlineGlobal + + def inlinerEnabled: Boolean = settings.YoptInlinerEnabled + + def warnSettings: WarnSettings = { + val c = settings.YoptWarningsChoices + // cannot extract settings.YoptWarnings into a local val due to some dependent typing issue. + WarnSettings( + !settings.YoptWarnings.isSetByUser || settings.YoptWarnings.contains(c.atInlineFailedSummary.name) || settings.YoptWarnings.contains(c.atInlineFailed.name), + settings.YoptWarnings.contains(c.noInlineMixed.name), + settings.YoptWarnings.contains(c.noInlineMissingBytecode.name), + settings.YoptWarnings.contains(c.noInlineMissingScalaInlineInfoAttr.name)) + } + // helpers that need access to global. // TODO @lry create a separate component, they don't belong to BTypesFromSymbols @@ -76,22 +95,131 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { // end helpers /** - * The ClassBType for a class symbol `sym`. + * The ClassBType for a class symbol `classSym`. + * + * The class symbol scala.Nothing is mapped to the class scala.runtime.Nothing$. Similarly, + * scala.Null is mapped to scala.runtime.Null$. This is because there exist no class files + * for the Nothing / Null. If used for example as a parameter type, we use the runtime classes + * in the classfile method signature. + * + * Note that the referenced class symbol may be an implementation class. For example when + * compiling a mixed-in method that forwards to the static method in the implementation class, + * the class descriptor of the receiver (the implementation class) is obtained by creating the + * ClassBType. */ final def classBTypeFromSymbol(classSym: Symbol): ClassBType = { assert(classSym != NoSymbol, "Cannot create ClassBType from NoSymbol") assert(classSym.isClass, s"Cannot create ClassBType from non-class symbol $classSym") - assert( - (!primitiveTypeMap.contains(classSym) || isCompilingPrimitive) && - (classSym != NothingClass && classSym != NullClass), - s"Cannot create ClassBType for special class symbol ${classSym.fullName}") + assertClassNotArrayNotPrimitive(classSym) + assert(!primitiveTypeMap.contains(classSym) || isCompilingPrimitive, s"Cannot create ClassBType for primitive class symbol $classSym") + if (classSym == NothingClass) RT_NOTHING + else if (classSym == NullClass) RT_NULL + else { + val internalName = classSym.javaBinaryName.toString + classBTypeFromInternalName.getOrElse(internalName, { + // The new ClassBType is added to the map in its constructor, before we set its info. This + // allows initializing cyclic dependencies, see the comment on variable ClassBType._info. + val res = ClassBType(internalName) + if (completeSilentlyAndCheckErroneous(classSym)) { + res.info = Left(NoClassBTypeInfoClassSymbolInfoFailedSI9111(classSym.fullName)) + res + } else { + setClassInfo(classSym, res) + } + }) + } + } - val internalName = classSym.javaBinaryName.toString - classBTypeFromInternalName.getOrElse(internalName, { - // The new ClassBType is added to the map in its constructor, before we set its info. This - // allows initializing cyclic dependencies, see the comment on variable ClassBType._info. - setClassInfo(classSym, ClassBType(internalName)) - }) + /** + * Builds a [[MethodBType]] for a method symbol. + */ + final def methodBTypeFromSymbol(methodSymbol: Symbol): MethodBType = { + assert(methodSymbol.isMethod, s"not a method-symbol: $methodSymbol") + val resultType: BType = + if (methodSymbol.isClassConstructor || methodSymbol.isConstructor) UNIT + else typeToBType(methodSymbol.tpe.resultType) + MethodBType(methodSymbol.tpe.paramTypes map typeToBType, resultType) + } + + /** + * This method returns the BType for a type reference, for example a parameter type. + * + * If `t` references a class, typeToBType ensures that the class is not an implementation class. + * See also comment on classBTypeFromSymbol, which is invoked for implementation classes. + */ + final def typeToBType(t: Type): BType = { + import definitions.ArrayClass + + /** + * Primitive types are represented as TypeRefs to the class symbol of, for example, scala.Int. + * The `primitiveTypeMap` maps those class symbols to the corresponding PrimitiveBType. + */ + def primitiveOrClassToBType(sym: Symbol): BType = { + assertClassNotArray(sym) + assert(!sym.isImplClass, sym) + primitiveTypeMap.getOrElse(sym, classBTypeFromSymbol(sym)) + } + + /** + * When compiling Array.scala, the type parameter T is not erased and shows up in method + * signatures, e.g. `def apply(i: Int): T`. A TyperRef to T is replaced by ObjectReference. + */ + def nonClassTypeRefToBType(sym: Symbol): ClassBType = { + assert(sym.isType && isCompilingArray, sym) + ObjectReference + } + + t.dealiasWiden match { + case TypeRef(_, ArrayClass, List(arg)) => ArrayBType(typeToBType(arg)) // Array type such as Array[Int] (kept by erasure) + case TypeRef(_, sym, _) if !sym.isClass => nonClassTypeRefToBType(sym) // See comment on nonClassTypeRefToBType + case TypeRef(_, sym, _) => primitiveOrClassToBType(sym) // Common reference to a type such as scala.Int or java.lang.String + case ClassInfoType(_, _, sym) => primitiveOrClassToBType(sym) // We get here, for example, for genLoadModule, which invokes typeToBType(moduleClassSymbol.info) + + /* AnnotatedType should (probably) be eliminated by erasure. However we know it happens for + * meta-annotated annotations (@(ann @getter) val x = 0), so we don't emit a warning. + * The type in the AnnotationInfo is an AnnotatedTpe. Tested in jvm/annotations.scala. + */ + case a @ AnnotatedType(_, t) => + debuglog(s"typeKind of annotated type $a") + typeToBType(t) + + /* ExistentialType should (probably) be eliminated by erasure. We know they get here for + * classOf constants: + * class C[T] + * class T { final val k = classOf[C[_]] } + */ + case e @ ExistentialType(_, t) => + debuglog(s"typeKind of existential type $e") + typeToBType(t) + + /* The cases below should probably never occur. They are kept for now to avoid introducing + * new compiler crashes, but we added a warning. The compiler / library bootstrap and the + * test suite don't produce any warning. + */ + + case tp => + currentUnit.warning(tp.typeSymbol.pos, + s"an unexpected type representation reached the compiler backend while compiling $currentUnit: $tp. " + + "If possible, please file a bug on issues.scala-lang.org.") + + tp match { + case ThisType(ArrayClass) => ObjectReference // was introduced in 9b17332f11 to fix SI-999, but this code is not reached in its test, or any other test + case ThisType(sym) => classBTypeFromSymbol(sym) + case SingleType(_, sym) => primitiveOrClassToBType(sym) + case ConstantType(_) => typeToBType(t.underlying) + case RefinedType(parents, _) => parents.map(typeToBType(_).asClassBType).reduceLeft((a, b) => a.jvmWiseLUB(b).get) + } + } + } + + def assertClassNotArray(sym: Symbol): Unit = { + assert(sym.isClass, sym) + assert(sym != definitions.ArrayClass || isCompilingArray, sym) + } + + def assertClassNotArrayNotPrimitive(sym: Symbol): Unit = { + assertClassNotArray(sym) + assert(!primitiveTypeMap.contains(sym) || isCompilingPrimitive, sym) } private def setClassInfo(classSym: Symbol, classBType: ClassBType): ClassBType = { @@ -215,7 +343,9 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { val nestedInfo = buildNestedInfo(classSym) - classBType.info = ClassInfo(superClass, interfaces, flags, nestedClasses, nestedInfo) + val inlineInfo = buildInlineInfo(classSym, classBType.internalName) + + classBType.info = Right(ClassInfo(superClass, interfaces, flags, nestedClasses, nestedInfo, inlineInfo)) classBType } @@ -271,6 +401,40 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { } /** + * Build the InlineInfo for a ClassBType from the class symbol. + * + * Note that the InlineInfo is only built from the symbolic information for classes that are being + * compiled. For all other classes we delegate to inlineInfoFromClassfile. The reason is that + * mixed-in methods are only added to class symbols being compiled, but not to other classes + * extending traits. Creating the InlineInfo from the symbol would prevent these mixins from being + * inlined. + * + * So for classes being compiled, the InlineInfo is created here and stored in the ScalaInlineInfo + * classfile attribute. + */ + private def buildInlineInfo(classSym: Symbol, internalName: InternalName): InlineInfo = { + def buildFromSymbol = buildInlineInfoFromClassSymbol(classSym, classBTypeFromSymbol(_).internalName, methodBTypeFromSymbol(_).descriptor) + + // phase travel required, see implementation of `compiles`. for nested classes, it checks if the + // enclosingTopLevelClass is being compiled. after flatten, all classes are considered top-level, + // so `compiles` would return `false`. + if (exitingPickler(currentRun.compiles(classSym))) buildFromSymbol // InlineInfo required for classes being compiled, we have to create the classfile attribute + else if (!inlinerEnabled) BTypes.EmptyInlineInfo // For other classes, we need the InlineInfo only inf the inliner is enabled. + else { + // For classes not being compiled, the InlineInfo is read from the classfile attribute. This + // fixes an issue with mixed-in methods: the mixin phase enters mixin methods only to class + // symbols being compiled. For non-compiled classes, we could not build MethodInlineInfos + // for those mixin members, which prevents inlining. + byteCodeRepository.classNode(internalName) match { + case Right(classNode) => + inlineInfoFromClassfile(classNode) + case Left(missingClass) => + InlineInfo(None, false, Map.empty, Some(ClassNotFoundWhenBuildingInlineInfoFromSymbol(missingClass))) + } + } + } + + /** * For top-level objects without a companion class, the compilere generates a mirror class with * static forwarders (Java compat). There's no symbol for the mirror class, but we still need a * ClassBType (its info.nestedClasses will hold the InnerClass entries, see comment in BTypes). @@ -282,13 +446,13 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { val c = ClassBType(internalName) // class info consistent with BCodeHelpers.genMirrorClass val nested = exitingPickler(memberClassesForInnerClassTable(moduleClassSym)) map classBTypeFromSymbol - c.info = ClassInfo( + c.info = Right(ClassInfo( superClass = Some(ObjectReference), interfaces = Nil, flags = asm.Opcodes.ACC_SUPER | asm.Opcodes.ACC_PUBLIC | asm.Opcodes.ACC_FINAL, nestedClasses = nested, - nestedInfo = None - ) + nestedInfo = None, + InlineInfo(None, true, Map.empty, None))) // no InlineInfo needed, scala never invokes methods on the mirror class c }) } @@ -322,8 +486,8 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { } // legacy, to be removed when the @remote annotation gets removed - final def isRemote(s: Symbol) = (s hasAnnotation definitions.RemoteAttr) - final def hasPublicBitSet(flags: Int) = ((flags & asm.Opcodes.ACC_PUBLIC) != 0) + final def isRemote(s: Symbol) = s hasAnnotation definitions.RemoteAttr + final def hasPublicBitSet(flags: Int) = (flags & asm.Opcodes.ACC_PUBLIC) != 0 /** * Return the Java modifiers for the given symbol. diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BackendReporting.scala b/src/compiler/scala/tools/nsc/backend/jvm/BackendReporting.scala new file mode 100644 index 0000000000..a06fb4bab8 --- /dev/null +++ b/src/compiler/scala/tools/nsc/backend/jvm/BackendReporting.scala @@ -0,0 +1,265 @@ +package scala.tools.nsc +package backend.jvm + +import scala.tools.asm.tree.{AbstractInsnNode, MethodNode} +import scala.tools.nsc.backend.jvm.BTypes.InternalName +import scala.reflect.internal.util.Position + +/** + * Interface for emitting inline warnings. The interface is required because the implementation + * depends on Global, which is not available in BTypes (only in BTypesFromSymbols). + */ +sealed abstract class BackendReporting { + def inlinerWarning(pos: Position, message: String): Unit +} + +final class BackendReportingImpl(val global: Global) extends BackendReporting { + import global._ + + def inlinerWarning(pos: Position, message: String): Unit = { + currentRun.reporting.inlinerWarning(pos, message) + } +} + +/** + * Utilities for error reporting. + * + * Defines some tools to make error reporting with Either easier. Would be subsumed by a right-biased + * Either in the standard library (or scalaz \/) (Validation is different, it accumulates multiple + * errors). + */ +object BackendReporting { + def methodSignature(classInternalName: InternalName, name: String, desc: String) = { + classInternalName + "::" + name + desc + } + + def methodSignature(classInternalName: InternalName, method: MethodNode): String = { + methodSignature(classInternalName, method.name, method.desc) + } + + def assertionError(message: String): Nothing = throw new AssertionError(message) + + implicit class RightBiasedEither[A, B](val v: Either[A, B]) extends AnyVal { + def map[U](f: B => U) = v.right.map(f) + def flatMap[BB](f: B => Either[A, BB]) = v.right.flatMap(f) + def filter(f: B => Boolean)(implicit empty: A): Either[A, B] = v match { + case Left(_) => v + case Right(e) => if (f(e)) v else Left(empty) // scalaz.\/ requires an implicit Monoid m to get m.empty + } + def foreach[U](f: B => U) = v.right.foreach(f) + + def getOrElse[BB >: B](alt: => BB): BB = v.right.getOrElse(alt) + + /** + * Get the value, fail with an assertion if this is an error. + */ + def get: B = { + assert(v.isRight, v.left.get) + v.right.get + } + + /** + * Get the right value of an `Either` by throwing a potential error message. Can simplify the + * implementation of methods that act on multiple `Either` instances. Instead of flat-mapping, + * the first error can be collected as + * + * tryEither { + * eitherOne.orThrow .... eitherTwo.orThrow ... eitherThree.orThrow + * } + */ + def orThrow: B = v match { + case Left(m) => throw Invalid(m) + case Right(t) => t + } + } + + case class Invalid[A](e: A) extends Exception + + /** + * See documentation of orThrow above. + */ + def tryEither[A, B](op: => Either[A, B]): Either[A, B] = try { op } catch { case Invalid(e) => Left(e.asInstanceOf[A]) } + + final case class WarnSettings(atInlineFailed: Boolean, noInlineMixed: Boolean, noInlineMissingBytecode: Boolean, noInlineMissingScalaInlineInfoAttr: Boolean) + + sealed trait OptimizerWarning { + def emitWarning(settings: WarnSettings): Boolean + } + + // Method filter in RightBiasedEither requires an implicit empty value. Taking the value here + // in scope allows for-comprehensions that desugar into filter calls (for example when using a + // tuple de-constructor). + implicit object emptyOptimizerWarning extends OptimizerWarning { + def emitWarning(settings: WarnSettings): Boolean = false + } + + sealed trait MissingBytecodeWarning extends OptimizerWarning { + override def toString = this match { + case ClassNotFound(internalName, definedInJavaSource) => + s"The classfile for $internalName could not be found on the compilation classpath." + { + if (definedInJavaSource) "\nThe class is defined in a Java source file that is being compiled (mixed compilation), therefore no bytecode is available." + else "" + } + + case MethodNotFound(name, descriptor, ownerInternalName, missingClasses) => + val (javaDef, others) = missingClasses.partition(_.definedInJavaSource) + s"The method $name$descriptor could not be found in the class $ownerInternalName or any of its parents." + + (if (others.isEmpty) "" else others.map(_.internalName).mkString("\nNote that the following parent classes could not be found on the classpath: ", ", ", "")) + + (if (javaDef.isEmpty) "" else javaDef.map(_.internalName).mkString("\nNote that the following parent classes are defined in Java sources (mixed compilation), no bytecode is available: ", ",", "")) + + case FieldNotFound(name, descriptor, ownerInternalName, missingClass) => + s"The field node $name$descriptor could not be found because the classfile $ownerInternalName cannot be found on the classpath." + + missingClass.map(c => s" Reason:\n$c").getOrElse("") + } + + def emitWarning(settings: WarnSettings): Boolean = this match { + case ClassNotFound(_, javaDefined) => + if (javaDefined) settings.noInlineMixed + else settings.noInlineMissingBytecode + + case m @ MethodNotFound(_, _, _, missing) => + if (m.isArrayMethod) false + else settings.noInlineMissingBytecode || missing.exists(_.emitWarning(settings)) + + case FieldNotFound(_, _, _, missing) => + settings.noInlineMissingBytecode || missing.exists(_.emitWarning(settings)) + } + } + + case class ClassNotFound(internalName: InternalName, definedInJavaSource: Boolean) extends MissingBytecodeWarning + case class MethodNotFound(name: String, descriptor: String, ownerInternalNameOrArrayDescriptor: InternalName, missingClasses: List[ClassNotFound]) extends MissingBytecodeWarning { + def isArrayMethod = ownerInternalNameOrArrayDescriptor.charAt(0) == '[' + } + case class FieldNotFound(name: String, descriptor: String, ownerInternalName: InternalName, missingClass: Option[ClassNotFound]) extends MissingBytecodeWarning + + sealed trait NoClassBTypeInfo extends OptimizerWarning { + override def toString = this match { + case NoClassBTypeInfoMissingBytecode(cause) => + cause.toString + + case NoClassBTypeInfoClassSymbolInfoFailedSI9111(classFullName) => + s"Failed to get the type of class symbol $classFullName due to SI-9111." + } + + def emitWarning(settings: WarnSettings): Boolean = this match { + case NoClassBTypeInfoMissingBytecode(cause) => cause.emitWarning(settings) + case NoClassBTypeInfoClassSymbolInfoFailedSI9111(_) => settings.noInlineMissingBytecode + } + } + + case class NoClassBTypeInfoMissingBytecode(cause: MissingBytecodeWarning) extends NoClassBTypeInfo + case class NoClassBTypeInfoClassSymbolInfoFailedSI9111(classFullName: String) extends NoClassBTypeInfo + + /** + * Used in the CallGraph for nodes where an issue occurred determining the callee information. + */ + sealed trait CalleeInfoWarning extends OptimizerWarning { + def declarationClass: InternalName + def name: String + def descriptor: String + + def warningMessageSignature = BackendReporting.methodSignature(declarationClass, name, descriptor) + + override def toString = this match { + case MethodInlineInfoIncomplete(_, _, _, cause) => + s"The inline information for $warningMessageSignature may be incomplete:\n" + cause + + case MethodInlineInfoMissing(_, _, _, cause) => + s"No inline information for method $warningMessageSignature could be found." + + cause.map(" Possible reason:\n" + _).getOrElse("") + + case MethodInlineInfoError(_, _, _, cause) => + s"Error while computing the inline information for method $warningMessageSignature:\n" + cause + + case RewriteTraitCallToStaticImplMethodFailed(_, _, _, cause) => + cause.toString + } + + def emitWarning(settings: WarnSettings): Boolean = this match { + case MethodInlineInfoIncomplete(_, _, _, cause) => cause.emitWarning(settings) + + case MethodInlineInfoMissing(_, _, _, Some(cause)) => cause.emitWarning(settings) + case MethodInlineInfoMissing(_, _, _, None) => settings.noInlineMissingBytecode + + case MethodInlineInfoError(_, _, _, cause) => cause.emitWarning(settings) + + case RewriteTraitCallToStaticImplMethodFailed(_, _, _, cause) => cause.emitWarning(settings) + } + } + + case class MethodInlineInfoIncomplete(declarationClass: InternalName, name: String, descriptor: String, cause: ClassInlineInfoWarning) extends CalleeInfoWarning + case class MethodInlineInfoMissing(declarationClass: InternalName, name: String, descriptor: String, cause: Option[ClassInlineInfoWarning]) extends CalleeInfoWarning + case class MethodInlineInfoError(declarationClass: InternalName, name: String, descriptor: String, cause: NoClassBTypeInfo) extends CalleeInfoWarning + case class RewriteTraitCallToStaticImplMethodFailed(declarationClass: InternalName, name: String, descriptor: String, cause: OptimizerWarning) extends CalleeInfoWarning + + sealed trait CannotInlineWarning extends OptimizerWarning { + def calleeDeclarationClass: InternalName + def name: String + def descriptor: String + + def calleeMethodSig = BackendReporting.methodSignature(calleeDeclarationClass, name, descriptor) + + override def toString = this match { + case IllegalAccessInstruction(_, _, _, callsiteClass, instruction) => + s"The callee $calleeMethodSig contains the instruction ${AsmUtils.textify(instruction)}" + + s"\nthat would cause an IllegalAccessError when inlined into class $callsiteClass." + + case IllegalAccessCheckFailed(_, _, _, callsiteClass, instruction, cause) => + s"Failed to check if $calleeMethodSig can be safely inlined to $callsiteClass without causing an IllegalAccessError. Checking instruction ${AsmUtils.textify(instruction)} failed:\n" + cause + + case MethodWithHandlerCalledOnNonEmptyStack(_, _, _, callsiteClass, callsiteName, callsiteDesc) => + s"""The operand stack at the callsite in ${BackendReporting.methodSignature(callsiteClass, callsiteName, callsiteDesc)} contains more values than the + |arguments expected by the callee $calleeMethodSig. These values would be discarded + |when entering an exception handler declared in the inlined method.""".stripMargin + + case SynchronizedMethod(_, _, _) => + s"Method $calleeMethodSig cannot be inlined because it is synchronized." + } + + def emitWarning(settings: WarnSettings): Boolean = this match { + case _: IllegalAccessInstruction | _: MethodWithHandlerCalledOnNonEmptyStack | _: SynchronizedMethod => + settings.atInlineFailed + + case IllegalAccessCheckFailed(_, _, _, _, _, cause) => + cause.emitWarning(settings) + } + } + case class IllegalAccessInstruction(calleeDeclarationClass: InternalName, name: String, descriptor: String, + callsiteClass: InternalName, instruction: AbstractInsnNode) extends CannotInlineWarning + case class IllegalAccessCheckFailed(calleeDeclarationClass: InternalName, name: String, descriptor: String, + callsiteClass: InternalName, instruction: AbstractInsnNode, cause: OptimizerWarning) extends CannotInlineWarning + case class MethodWithHandlerCalledOnNonEmptyStack(calleeDeclarationClass: InternalName, name: String, descriptor: String, + callsiteClass: InternalName, callsiteName: String, callsiteDesc: String) extends CannotInlineWarning + case class SynchronizedMethod(calleeDeclarationClass: InternalName, name: String, descriptor: String) extends CannotInlineWarning + + /** + * Used in the InlineInfo of a ClassBType, when some issue occurred obtaining the inline information. + */ + sealed trait ClassInlineInfoWarning extends OptimizerWarning { + override def toString = this match { + case NoInlineInfoAttribute(internalName) => + s"The Scala classfile $internalName does not have a ScalaInlineInfo attribute." + + case ClassSymbolInfoFailureSI9111(classFullName) => + s"Failed to get the type of a method of class symbol $classFullName due to SI-9111." + + case ClassNotFoundWhenBuildingInlineInfoFromSymbol(missingClass) => + s"Failed to build the inline information: $missingClass." + + case UnknownScalaInlineInfoVersion(internalName, version) => + s"Cannot read ScalaInlineInfo version $version in classfile $internalName. Use a more recent compiler." + } + + def emitWarning(settings: WarnSettings): Boolean = this match { + case NoInlineInfoAttribute(_) => settings.noInlineMissingScalaInlineInfoAttr + case ClassNotFoundWhenBuildingInlineInfoFromSymbol(cause) => cause.emitWarning(settings) + case ClassSymbolInfoFailureSI9111(_) => settings.noInlineMissingBytecode + case UnknownScalaInlineInfoVersion(_, _) => settings.noInlineMissingScalaInlineInfoAttr + } + } + + case class NoInlineInfoAttribute(internalName: InternalName) extends ClassInlineInfoWarning + case class ClassSymbolInfoFailureSI9111(classFullName: String) extends ClassInlineInfoWarning + case class ClassNotFoundWhenBuildingInlineInfoFromSymbol(missingClass: ClassNotFound) extends ClassInlineInfoWarning + case class UnknownScalaInlineInfoVersion(internalName: InternalName, version: Int) extends ClassInlineInfoWarning +} diff --git a/src/compiler/scala/tools/nsc/backend/jvm/CoreBTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/CoreBTypes.scala index 246235f395..492fe3ae79 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/CoreBTypes.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/CoreBTypes.scala @@ -99,10 +99,9 @@ class CoreBTypes[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: BTFS) { * * Therefore, when RT_NOTHING or RT_NULL are to be emitted, a mapping is needed: the internal * names of NothingClass and NullClass can't be emitted as-is. - * TODO @lry Once there's a 2.11.3 starr, use the commented argument list. The current starr crashes on the type literal `scala.runtime.Nothing$` */ - lazy val RT_NOTHING : ClassBType = classBTypeFromSymbol(rootMirror.getRequiredClass("scala.runtime.Nothing$")) // (requiredClass[scala.runtime.Nothing$]) - lazy val RT_NULL : ClassBType = classBTypeFromSymbol(rootMirror.getRequiredClass("scala.runtime.Null$")) // (requiredClass[scala.runtime.Null$]) + lazy val RT_NOTHING : ClassBType = classBTypeFromSymbol(requiredClass[scala.runtime.Nothing$]) + lazy val RT_NULL : ClassBType = classBTypeFromSymbol(requiredClass[scala.runtime.Null$]) lazy val ObjectReference : ClassBType = classBTypeFromSymbol(ObjectClass) lazy val objArrayReference : ArrayBType = ArrayBType(ObjectReference) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/GenASM.scala b/src/compiler/scala/tools/nsc/backend/jvm/GenASM.scala index 62268e1907..86a69b92ea 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/GenASM.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/GenASM.scala @@ -533,7 +533,7 @@ abstract class GenASM extends SubComponent with BytecodeWriters { self => } bytecodeWriter.writeClass(label, jclassName, arr, outF) } catch { - case e: java.lang.RuntimeException if e != null && (e.getMessage contains "too large!") => + case e: java.lang.RuntimeException if e.getMessage != null && (e.getMessage contains "too large!") => reporter.error(sym.pos, s"Could not write class $jclassName because it exceeds JVM code size limits. ${e.getMessage}") } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala b/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala index d5e95c47cf..be1595dc29 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala @@ -217,11 +217,26 @@ abstract class GenBCode extends BCodeSyncAndTry { class Worker2 { lazy val localOpt = new LocalOpt(settings) + def runGlobalOptimizations(): Unit = { + import scala.collection.convert.decorateAsScala._ + q2.asScala foreach { + case Item2(_, _, plain, _, _) => + // skip mirror / bean: wd don't inline into tem, and they are not used in the plain class + if (plain != null) { + localOpt.minimalRemoveUnreachableCode(plain) + callGraph.addClass(plain) + } + } + bTypes.inliner.runInliner() + } + def localOptimizations(classNode: ClassNode): Unit = { BackendStats.timed(BackendStats.methodOptTimer)(localOpt.methodOptimizations(classNode)) } def run() { + if (settings.YoptInlinerEnabled) runGlobalOptimizations() + while (true) { val item = q2.poll if (item.isPoison) { @@ -269,7 +284,12 @@ abstract class GenBCode extends BCodeSyncAndTry { var arrivalPos = 0 - /* + /** + * The `run` method is overridden because the backend has a different data flow than the default + * phase: the backend does not transform compilation units one by one, but on all units in the + * same run. This allows cross-unit optimizations and running some stages of the backend + * concurrently on multiple units. + * * A run of the BCodePhase phase comprises: * * (a) set-up steps (most notably supporting maps in `BCodeTypes`, @@ -287,6 +307,10 @@ abstract class GenBCode extends BCodeSyncAndTry { arrivalPos = 0 // just in case scalaPrimitives.init() bTypes.initializeCoreBTypes() + bTypes.javaDefinedClasses.clear() + bTypes.javaDefinedClasses ++= currentRun.symSource collect { + case (sym, _) if sym.isJavaDefined => sym.javaBinaryName.toString + } Statistics.stopTimer(BackendStats.bcodeInitTimer, initStart) // initBytecodeWriter invokes fullName, thus we have to run it before the typer-dependent thread is activated. @@ -410,4 +434,7 @@ object GenBCode { final val PublicStatic = asm.Opcodes.ACC_PUBLIC | asm.Opcodes.ACC_STATIC final val PublicStaticFinal = asm.Opcodes.ACC_PUBLIC | asm.Opcodes.ACC_STATIC | asm.Opcodes.ACC_FINAL + + val CLASS_CONSTRUCTOR_NAME = "<clinit>" + val INSTANCE_CONSTRUCTOR_NAME = "<init>" } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala index 7b424d2107..607b7145d6 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala @@ -10,11 +10,14 @@ package opt import scala.tools.asm import asm.tree._ import scala.collection.convert.decorateAsScala._ +import scala.tools.asm.Attribute +import scala.tools.nsc.backend.jvm.BackendReporting._ import scala.tools.nsc.io.AbstractFile import scala.tools.nsc.util.ClassFileLookup -import OptimizerReporting._ +import BytecodeUtils._ import ByteCodeRepository._ import BTypes.InternalName +import java.util.concurrent.atomic.AtomicLong /** * The ByteCodeRepository provides utilities to read the bytecode of classfiles from the compilation @@ -24,58 +27,125 @@ import BTypes.InternalName * @param classes Cache for parsed ClassNodes. Also stores the source of the bytecode: * [[Classfile]] if read from `classPath`, [[CompilationUnit]] if the bytecode * corresponds to a class being compiled. + * The `Long` field encodes the age of the node in the map, which allows removing + * old entries when the map grows too large. + * For Java classes in mixed compilation, the map contains an error message: no + * ClassNode is generated by the backend and also no classfile that could be parsed. */ -class ByteCodeRepository(val classPath: ClassFileLookup[AbstractFile], val classes: collection.concurrent.Map[InternalName, (ClassNode, Source)]) { +class ByteCodeRepository(val classPath: ClassFileLookup[AbstractFile], val isJavaSourceDefined: InternalName => Boolean, val classes: collection.concurrent.Map[InternalName, Either[ClassNotFound, (ClassNode, Source, Long)]]) { + + private val maxCacheSize = 1500 + private val targetSize = 500 + + private val idCounter = new AtomicLong(0) + + /** + * Prevent the code repository from growing too large. Profiling reveals that the average size + * of a ClassNode is about 30 kb. I observed having 17k+ classes in the cache, i.e., 500 mb. + * + * We can only remove classes with `Source == Classfile`, those can be parsed again if requested. + */ + private def limitCacheSize(): Unit = { + if (classes.count(c => c._2.isRight && c._2.right.get._2 == Classfile) > maxCacheSize) { + val removeId = idCounter.get - targetSize + val toRemove = classes.iterator.collect({ + case (name, Right((_, Classfile, id))) if id < removeId => name + }).toList + toRemove foreach classes.remove + } + } + + def add(classNode: ClassNode, source: Source) = { + classes(classNode.name) = Right((classNode, source, idCounter.incrementAndGet())) + } + /** * The class node and source for an internal name. If the class node is not yet available, it is * parsed from the classfile on the compile classpath. */ - def classNodeAndSource(internalName: InternalName): (ClassNode, Source) = { - classes.getOrElseUpdate(internalName, (parseClass(internalName), Classfile)) + def classNodeAndSource(internalName: InternalName): Either[ClassNotFound, (ClassNode, Source)] = { + val r = classes.getOrElseUpdate(internalName, { + limitCacheSize() + parseClass(internalName).map((_, Classfile, idCounter.incrementAndGet())) + }) + r.map(v => (v._1, v._2)) } /** * The class node for an internal name. If the class node is not yet available, it is parsed from * the classfile on the compile classpath. */ - def classNode(internalName: InternalName) = classNodeAndSource(internalName)._1 + def classNode(internalName: InternalName): Either[ClassNotFound, ClassNode] = classNodeAndSource(internalName).map(_._1) /** * The field node for a field matching `name` and `descriptor`, accessed in class `classInternalName`. * The declaration of the field may be in one of the superclasses. * - * @return The [[FieldNode]] of the requested field and the [[InternalName]] of its declaring class. + * @return The [[FieldNode]] of the requested field and the [[InternalName]] of its declaring + * class, or an error message if the field could not be found */ - def fieldNode(classInternalName: InternalName, name: String, descriptor: String): Option[(FieldNode, InternalName)] = { - val c = classNode(classInternalName) - c.fields.asScala.find(f => f.name == name && f.desc == descriptor).map((_, classInternalName)) orElse { - Option(c.superName).flatMap(n => fieldNode(n, name, descriptor)) + def fieldNode(classInternalName: InternalName, name: String, descriptor: String): Either[FieldNotFound, (FieldNode, InternalName)] = { + def fieldNodeImpl(parent: InternalName): Either[FieldNotFound, (FieldNode, InternalName)] = { + def msg = s"The field node $name$descriptor could not be found in class $classInternalName or any of its superclasses." + classNode(parent) match { + case Left(e) => Left(FieldNotFound(name, descriptor, classInternalName, Some(e))) + case Right(c) => + c.fields.asScala.find(f => f.name == name && f.desc == descriptor) match { + case Some(f) => Right((f, parent)) + case None => + if (c.superName == null) Left(FieldNotFound(name, descriptor, classInternalName, None)) + else fieldNode(c.superName, name, descriptor) + } + } } + fieldNodeImpl(classInternalName) } /** * The method node for a method matching `name` and `descriptor`, accessed in class `classInternalName`. * The declaration of the method may be in one of the parents. * - * @return The [[MethodNode]] of the requested method and the [[InternalName]] of its declaring class. + * @return The [[MethodNode]] of the requested method and the [[InternalName]] of its declaring + * class, or an error message if the method could not be found. */ - def methodNode(classInternalName: InternalName, name: String, descriptor: String): Option[(MethodNode, InternalName)] = { - val c = classNode(classInternalName) - c.methods.asScala.find(m => m.name == name && m.desc == descriptor).map((_, classInternalName)) orElse { - val parents = Option(c.superName) ++ c.interfaces.asScala - // `view` to stop at the first result - parents.view.flatMap(methodNode(_, name, descriptor)).headOption + def methodNode(ownerInternalNameOrArrayDescriptor: String, name: String, descriptor: String): Either[MethodNotFound, (MethodNode, InternalName)] = { + // on failure, returns a list of class names that could not be found on the classpath + def methodNodeImpl(ownerInternalName: InternalName): Either[List[ClassNotFound], (MethodNode, InternalName)] = { + classNode(ownerInternalName) match { + case Left(e) => Left(List(e)) + case Right(c) => + c.methods.asScala.find(m => m.name == name && m.desc == descriptor) match { + case Some(m) => Right((m, ownerInternalName)) + case None => findInParents(Option(c.superName) ++: c.interfaces.asScala.toList, Nil) + } + } } + + // find the MethodNode in one of the parent classes + def findInParents(parents: List[InternalName], failedClasses: List[ClassNotFound]): Either[List[ClassNotFound], (MethodNode, InternalName)] = parents match { + case x :: xs => methodNodeImpl(x).left.flatMap(failed => findInParents(xs, failed ::: failedClasses)) + case Nil => Left(failedClasses) + } + + // In a MethodInsnNode, the `owner` field may be an array descriptor, for exmple when invoking `clone`. We don't have a method node to return in this case. + if (ownerInternalNameOrArrayDescriptor.charAt(0) == '[') + Left(MethodNotFound(name, descriptor, ownerInternalNameOrArrayDescriptor, Nil)) + else + methodNodeImpl(ownerInternalNameOrArrayDescriptor).left.map(MethodNotFound(name, descriptor, ownerInternalNameOrArrayDescriptor, _)) } - private def parseClass(internalName: InternalName): ClassNode = { + private def parseClass(internalName: InternalName): Either[ClassNotFound, ClassNode] = { val fullName = internalName.replace('/', '.') classPath.findClassFile(fullName) map { classFile => val classNode = new asm.tree.ClassNode() val classReader = new asm.ClassReader(classFile.toByteArray) + + // Passing the InlineInfoAttributePrototype makes the ClassReader invoke the specific `read` + // method of the InlineInfoAttribute class, instead of putting the byte array into a generic + // Attribute. // We don't need frames when inlining, but we want to keep the local variable table, so we // don't use SKIP_DEBUG. - classReader.accept(classNode, asm.ClassReader.SKIP_FRAMES) + classReader.accept(classNode, Array[Attribute](InlineInfoAttributePrototype), asm.ClassReader.SKIP_FRAMES) // SKIP_FRAMES leaves line number nodes. Remove them because they are not correct after // inlining. // TODO: we need to remove them also for classes that are not parsed from classfiles, why not simplify and do it once when inlining? @@ -85,18 +155,9 @@ class ByteCodeRepository(val classPath: ClassFileLookup[AbstractFile], val class // https://jcp.org/aboutJava/communityprocess/final/jsr045/index.html removeLineNumberNodes(classNode) classNode - } getOrElse { - inlineFailure(s"Class file for class $fullName not found.") - } - } - - private def removeLineNumberNodes(classNode: ClassNode): Unit = { - for (method <- classNode.methods.asScala) { - val iter = method.instructions.iterator() - while (iter.hasNext) iter.next() match { - case _: LineNumberNode => iter.remove() - case _ => - } + } match { + case Some(node) => Right(node) + case None => Left(ClassNotFound(internalName, isJavaSourceDefined(internalName))) } } } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala index 6b4047c0a7..14e8cccc60 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala @@ -10,9 +10,14 @@ package opt import scala.annotation.{tailrec, switch} import scala.collection.mutable import scala.reflect.internal.util.Collections._ -import scala.tools.asm.Opcodes +import scala.tools.asm.tree.analysis._ +import scala.tools.asm.{MethodWriter, ClassWriter, Label, Opcodes} import scala.tools.asm.tree._ import scala.collection.convert.decorateAsScala._ +import GenBCode._ +import scala.collection.convert.decorateAsScala._ +import scala.collection.convert.decorateAsJava._ +import scala.tools.nsc.backend.jvm.BTypes._ object BytecodeUtils { @@ -68,6 +73,20 @@ object BytecodeUtils { def isExecutable(instruction: AbstractInsnNode): Boolean = instruction.getOpcode >= 0 + def isConstructor(methodNode: MethodNode): Boolean = { + methodNode.name == INSTANCE_CONSTRUCTOR_NAME || methodNode.name == CLASS_CONSTRUCTOR_NAME + } + + def isStaticMethod(methodNode: MethodNode): Boolean = (methodNode.access & Opcodes.ACC_STATIC) != 0 + + def isAbstractMethod(methodNode: MethodNode): Boolean = (methodNode.access & Opcodes.ACC_ABSTRACT) != 0 + + def isSynchronizedMethod(methodNode: MethodNode): Boolean = (methodNode.access & Opcodes.ACC_SYNCHRONIZED) != 0 + + def isFinalClass(classNode: ClassNode): Boolean = (classNode.access & Opcodes.ACC_FINAL) != 0 + + def isFinalMethod(methodNode: MethodNode): Boolean = (methodNode.access & (Opcodes.ACC_FINAL | Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC)) != 0 + def nextExecutableInstruction(instruction: AbstractInsnNode, alsoKeep: AbstractInsnNode => Boolean = Set()): Option[AbstractInsnNode] = { var result = instruction do { result = result.getNext } @@ -181,4 +200,115 @@ object BytecodeUtils { if (handler.end == from) handler.end = to } } + + /** + * In order to run an Analyzer, the maxLocals / maxStack fields need to be available. The ASM + * framework only computes these values during bytecode generation. + * + * Since 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. + */ + 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 + } + + def removeLineNumberNodes(classNode: ClassNode): Unit = { + for (m <- classNode.methods.asScala) removeLineNumberNodes(m.instructions) + } + + def removeLineNumberNodes(instructions: InsnList): Unit = { + val iter = instructions.iterator() + while (iter.hasNext) iter.next() match { + case _: LineNumberNode => iter.remove() + case _ => + } + } + + def cloneLabels(methodNode: MethodNode): Map[LabelNode, LabelNode] = { + methodNode.instructions.iterator().asScala.collect({ + case labelNode: LabelNode => (labelNode, newLabelNode) + }).toMap + } + + /** + * Create a new [[LabelNode]] with a correctly associated [[Label]]. + */ + def newLabelNode: LabelNode = { + val label = new Label + val labelNode = new LabelNode(label) + label.info = labelNode + labelNode + } + + /** + * Clone the instructions in `methodNode` into a new [[InsnList]], mapping labels according to + * the `labelMap`. Returns the new instruction list and a map from old to new instructions. + */ + def cloneInstructions(methodNode: MethodNode, labelMap: Map[LabelNode, LabelNode]): (InsnList, Map[AbstractInsnNode, AbstractInsnNode]) = { + val javaLabelMap = labelMap.asJava + val result = new InsnList + var map = Map.empty[AbstractInsnNode, AbstractInsnNode] + for (ins <- methodNode.instructions.iterator.asScala) { + val cloned = ins.clone(javaLabelMap) + result add cloned + map += ((ins, cloned)) + } + (result, map) + } + + /** + * Clone the local variable descriptors of `methodNode` and map their `start` and `end` labels + * according to the `labelMap`. + */ + def cloneLocalVariableNodes(methodNode: MethodNode, labelMap: Map[LabelNode, LabelNode], prefix: String): List[LocalVariableNode] = { + methodNode.localVariables.iterator().asScala.map(localVariable => new LocalVariableNode( + prefix + localVariable.name, + localVariable.desc, + localVariable.signature, + labelMap(localVariable.start), + labelMap(localVariable.end), + localVariable.index + )).toList + } + + /** + * Clone the local try/catch blocks of `methodNode` and map their `start` and `end` and `handler` + * labels according to the `labelMap`. + */ + def cloneTryCatchBlockNodes(methodNode: MethodNode, labelMap: Map[LabelNode, LabelNode]): List[TryCatchBlockNode] = { + methodNode.tryCatchBlocks.iterator().asScala.map(tryCatch => new TryCatchBlockNode( + labelMap(tryCatch.start), + labelMap(tryCatch.end), + labelMap(tryCatch.handler), + tryCatch.`type` + )).toList + } + + /** + * A wrapper to make ASM's Analyzer a bit easier to use. + */ + class AsmAnalyzer[V <: Value](methodNode: MethodNode, classInternalName: InternalName, interpreter: Interpreter[V] = new BasicInterpreter) { + val analyzer = new Analyzer(interpreter) + analyzer.analyze(classInternalName, methodNode) + def frameAt(instruction: AbstractInsnNode): Frame[V] = analyzer.getFrames()(methodNode.instructions.indexOf(instruction)) + } + + implicit class `frame extensions`[V <: Value](val frame: Frame[V]) extends AnyVal { + def peekDown(n: Int): V = { + val topIndex = frame.getStackSize - 1 + frame.getStack(topIndex - n) + } + } } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala new file mode 100644 index 0000000000..47d32c94cb --- /dev/null +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala @@ -0,0 +1,189 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2014 LAMP/EPFL + * @author Martin Odersky + */ + +package scala.tools.nsc +package backend.jvm +package opt + +import scala.reflect.internal.util.{NoPosition, Position} +import scala.tools.asm.tree._ +import scala.collection.convert.decorateAsScala._ +import scala.tools.nsc.backend.jvm.BTypes.{MethodInlineInfo, InternalName} +import scala.tools.nsc.backend.jvm.BackendReporting._ +import scala.tools.nsc.backend.jvm.opt.BytecodeUtils.AsmAnalyzer +import ByteCodeRepository.{Source, CompilationUnit} + +class CallGraph[BT <: BTypes](val btypes: BT) { + import btypes._ + + val callsites: collection.concurrent.Map[MethodInsnNode, Callsite] = recordPerRunCache(collection.concurrent.TrieMap.empty[MethodInsnNode, Callsite]) + + def addClass(classNode: ClassNode): Unit = { + for (m <- classNode.methods.asScala; callsite <- analyzeCallsites(m, classBTypeFromClassNode(classNode))) + callsites(callsite.callsiteInstruction) = callsite + } + + def analyzeCallsites(methodNode: MethodNode, definingClass: ClassBType): List[Callsite] = { + + case class CallsiteInfo(safeToInline: Boolean, safeToRewrite: Boolean, + annotatedInline: Boolean, annotatedNoInline: Boolean, + warning: Option[CalleeInfoWarning]) + + /** + * Analyze a callsite and gather meta-data that can be used for inlining decisions. + */ + def analyzeCallsite(calleeMethodNode: MethodNode, calleeDeclarationClassBType: ClassBType, receiverTypeInternalName: InternalName, calleeSource: Source): CallsiteInfo = { + val methodSignature = calleeMethodNode.name + calleeMethodNode.desc + + try { + // The inlineInfo.methodInfos of a ClassBType holds an InlineInfo for each method *declared* + // within a class (not for inherited methods). Since we already have the classBType of the + // callee, we only check there for the methodInlineInfo, we should find it there. + calleeDeclarationClassBType.info.orThrow.inlineInfo.methodInfos.get(methodSignature) match { + case Some(methodInlineInfo) => + val canInlineFromSource = inlineGlobalEnabled || calleeSource == CompilationUnit + + val isAbstract = BytecodeUtils.isAbstractMethod(calleeMethodNode) + + // (1) A non-final method can be safe to inline if the receiver type is a final subclass. Example: + // class A { @inline def f = 1 }; object B extends A; B.f // can be inlined + // + // TODO: type analysis can render more calls statically resolved. Example˜∫ + // new A.f // can be inlined, the receiver type is known to be exactly A. + val isStaticallyResolved: Boolean = { + methodInlineInfo.effectivelyFinal || + classBTypeFromParsedClassfile(receiverTypeInternalName).info.orThrow.inlineInfo.isEffectivelyFinal // (1) + } + + val isRewritableTraitCall = isStaticallyResolved && methodInlineInfo.traitMethodWithStaticImplementation + + val warning = calleeDeclarationClassBType.info.orThrow.inlineInfo.warning.map( + MethodInlineInfoIncomplete(calleeDeclarationClassBType.internalName, calleeMethodNode.name, calleeMethodNode.desc, _)) + + // (1) For invocations of final trait methods, the callee isStaticallyResolved but also + // abstract. Such a callee is not safe to inline - it needs to be re-written to the + // static impl method first (safeToRewrite). + // (2) Final trait methods can be rewritten from the interface to the static implementation + // method to enable inlining. + CallsiteInfo( + safeToInline = canInlineFromSource && isStaticallyResolved && !isAbstract, // (1) + safeToRewrite = canInlineFromSource && isRewritableTraitCall, // (2) + annotatedInline = methodInlineInfo.annotatedInline, + annotatedNoInline = methodInlineInfo.annotatedNoInline, + warning = warning) + + case None => + val warning = MethodInlineInfoMissing(calleeDeclarationClassBType.internalName, calleeMethodNode.name, calleeMethodNode.desc, calleeDeclarationClassBType.info.orThrow.inlineInfo.warning) + CallsiteInfo(false, false, false, false, Some(warning)) + } + } catch { + case Invalid(noInfo: NoClassBTypeInfo) => + val warning = MethodInlineInfoError(calleeDeclarationClassBType.internalName, calleeMethodNode.name, calleeMethodNode.desc, noInfo) + CallsiteInfo(false, false, false, false, Some(warning)) + } + } + + // TODO: run dataflow analyses to make the call graph more precise + // - producers to get forwarded parameters (ForwardedParam) + // - typeAnalysis for more precise argument types, more precise callee + // - nullAnalysis to skip emitting the receiver-null-check when inlining + + // TODO: for now we run a basic analyzer to get the stack height at the call site. + // once we run a more elaborate analyzer (types, nullness), we can get the stack height out of there. + val analyzer = new AsmAnalyzer(methodNode, definingClass.internalName) + + methodNode.instructions.iterator.asScala.collect({ + case call: MethodInsnNode => + val callee: Either[OptimizerWarning, Callee] = for { + (method, declarationClass) <- byteCodeRepository.methodNode(call.owner, call.name, call.desc): Either[OptimizerWarning, (MethodNode, InternalName)] + (declarationClassNode, source) <- byteCodeRepository.classNodeAndSource(declarationClass): Either[OptimizerWarning, (ClassNode, Source)] + declarationClassBType = classBTypeFromClassNode(declarationClassNode) + } yield { + val CallsiteInfo(safeToInline, safeToRewrite, annotatedInline, annotatedNoInline, warning) = analyzeCallsite(method, declarationClassBType, call.owner, source) + Callee( + callee = method, + calleeDeclarationClass = declarationClassBType, + safeToInline = safeToInline, + safeToRewrite = safeToRewrite, + annotatedInline = annotatedInline, + annotatedNoInline = annotatedNoInline, + calleeInfoWarning = warning) + } + + val argInfos = if (callee.isLeft) Nil else { + // TODO: for now it's Nil, because we don't run any data flow analysis + // there's no point in using the parameter types, that doesn't add any information. + // NOTE: need to run the same analyses after inlining, to re-compute the argInfos for the + // new duplicated callsites, see Inliner.inline + Nil + } + + Callsite( + callsiteInstruction = call, + callsiteMethod = methodNode, + callsiteClass = definingClass, + callee = callee, + argInfos = argInfos, + callsiteStackHeight = analyzer.frameAt(call).getStackSize, + callsitePosition = callsitePositions.getOrElse(call, NoPosition) + ) + }).toList + } + + /** + * A callsite in the call graph. + * + * @param callsiteInstruction The invocation instruction + * @param callsiteMethod The method containing the callsite + * @param callsiteClass The class containing the callsite + * @param callee The callee, as it appears in the invocation instruction. For virtual + * calls, an override of the callee might be invoked. Also, the callee + * can be abstract. Contains a warning message if the callee MethodNode + * cannot be found in the bytecode repository. + * @param argInfos Information about the invocation receiver and arguments + * @param callsiteStackHeight The stack height at the callsite, required by the inliner + * @param callsitePosition The source position of the callsite, used for inliner warnings. + */ + final case class Callsite(callsiteInstruction: MethodInsnNode, callsiteMethod: MethodNode, callsiteClass: ClassBType, + callee: Either[OptimizerWarning, Callee], argInfos: List[ArgInfo], + callsiteStackHeight: Int, callsitePosition: Position) { + override def toString = + "Invocation of" + + s" ${callee.map(_.calleeDeclarationClass.internalName).getOrElse("?")}.${callsiteInstruction.name + callsiteInstruction.desc}" + + s"@${callsiteMethod.instructions.indexOf(callsiteInstruction)}" + + s" in ${callsiteClass.internalName}.${callsiteMethod.name}" + } + + /** + * Information about invocation arguments, obtained through data flow analysis of the callsite method. + */ + sealed trait ArgInfo + final case class ArgTypeInfo(argType: BType, isPrecise: Boolean, knownNotNull: Boolean) extends ArgInfo + final case class ForwardedParam(index: Int) extends ArgInfo + // can be extended, e.g., with constant types + + /** + * A callee in the call graph. + * + * @param callee The callee, as it appears in the invocation instruction. For + * virtual calls, an override of the callee might be invoked. Also, + * the callee can be abstract. + * @param calleeDeclarationClass The class in which the callee is declared + * @param safeToInline True if the callee can be safely inlined: it cannot be overridden, + * and the inliner settings (project / global) allow inlining it. + * @param safeToRewrite True if the callee the interface method of a concrete trait method + * that can be safely re-written to the static implementation method. + * @param annotatedInline True if the callee is annotated @inline + * @param annotatedNoInline True if the callee is annotated @noinline + * @param calleeInfoWarning An inliner warning if some information was not available while + * gathering the information about this callee. + */ + final case class Callee(callee: MethodNode, calleeDeclarationClass: ClassBType, + safeToInline: Boolean, safeToRewrite: Boolean, + annotatedInline: Boolean, annotatedNoInline: Boolean, + calleeInfoWarning: Option[CalleeInfoWarning]) { + assert(!(safeToInline && safeToRewrite), s"A callee of ${callee.name} can be either safeToInline or safeToRewrite, but not both.") + } +} diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/InlineInfoAttribute.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/InlineInfoAttribute.scala index 4812f2290f..e7dd5abc57 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/InlineInfoAttribute.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/InlineInfoAttribute.scala @@ -9,6 +9,7 @@ package opt import scala.tools.asm._ import scala.tools.nsc.backend.jvm.BTypes.{InlineInfo, MethodInlineInfo} +import scala.tools.nsc.backend.jvm.BackendReporting.UnknownScalaInlineInfoVersion /** * This attribute stores the InlineInfo for a ClassBType as an independent classfile attribute. @@ -119,7 +120,7 @@ case class InlineInfoAttribute(inlineInfo: InlineInfo) extends Attribute(InlineI InlineInfoAttribute(InlineInfo(self, isFinal, infos, None)) } else { - val msg = s"Cannot read ScalaInlineInfo version $version in classfile ${cr.getClassName}. Use a more recent compiler." + val msg = UnknownScalaInlineInfoVersion(cr.getClassName, version) InlineInfoAttribute(BTypes.EmptyInlineInfo.copy(warning = Some(msg))) } } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala new file mode 100644 index 0000000000..e14e57d3ab --- /dev/null +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala @@ -0,0 +1,648 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2014 LAMP/EPFL + * @author Martin Odersky + */ + +package scala.tools.nsc +package backend.jvm +package opt + +import scala.annotation.tailrec +import scala.tools.asm +import asm.Opcodes._ +import asm.tree._ +import scala.collection.convert.decorateAsScala._ +import scala.collection.convert.decorateAsJava._ +import AsmUtils._ +import BytecodeUtils._ +import collection.mutable +import scala.tools.asm.tree.analysis.{SourceInterpreter, Analyzer} +import BackendReporting._ +import scala.tools.nsc.backend.jvm.BTypes.InternalName + +class Inliner[BT <: BTypes](val btypes: BT) { + import btypes._ + import callGraph._ + + def runInliner(): Unit = { + rewriteFinalTraitMethodInvocations() + + for (request <- collectAndOrderInlineRequests) { + val Right(callee) = request.callee // collectAndOrderInlineRequests returns callsites with a known callee + + val r = inline(request.callsiteInstruction, request.callsiteStackHeight, request.callsiteMethod, request.callsiteClass, + callee.callee, callee.calleeDeclarationClass, + receiverKnownNotNull = false, keepLineNumbers = false) + + for (warning <- r) { + if ((callee.annotatedInline && btypes.warnSettings.atInlineFailed) || warning.emitWarning(warnSettings)) { + val annotWarn = if (callee.annotatedInline) " is annotated @inline but" else "" + val msg = s"${BackendReporting.methodSignature(callee.calleeDeclarationClass.internalName, callee.callee)}$annotWarn could not be inlined:\n$warning" + backendReporting.inlinerWarning(request.callsitePosition, msg) + } + } + } + } + + /** + * Ordering for inline requests. Required to make the inliner deterministic: + * - Always remove the same request when breaking inlining cycles + * - Perform inlinings in a consistent order + */ + object callsiteOrdering extends Ordering[Callsite] { + override def compare(x: Callsite, y: Callsite): Int = { + val cls = x.callsiteClass.internalName compareTo y.callsiteClass.internalName + if (cls != 0) return cls + + val name = x.callsiteMethod.name compareTo y.callsiteMethod.name + if (name != 0) return name + + val desc = x.callsiteMethod.desc compareTo y.callsiteMethod.desc + if (desc != 0) return desc + + def pos(c: Callsite) = c.callsiteMethod.instructions.indexOf(c.callsiteInstruction) + pos(x) - pos(y) + } + } + + /** + * Select callsites from the call graph that should be inlined. The resulting list of inlining + * requests is allowed to have cycles, and the callsites can appear in any order. + */ + def selectCallsitesForInlining: List[Callsite] = { + callsites.valuesIterator.filter({ + case callsite @ Callsite(_, _, _, Right(Callee(callee, calleeDeclClass, safeToInline, _, annotatedInline, _, warning)), _, _, pos) => + val res = doInlineCallsite(callsite) + + if (!res) { + if (annotatedInline && btypes.warnSettings.atInlineFailed) { + // if the callsite is annotated @inline, we report an inline warning even if the underlying + // reason is, for example, mixed compilation (which has a separate -Yopt-warning flag). + def initMsg = s"${BackendReporting.methodSignature(calleeDeclClass.internalName, callee)} is annotated @inline but cannot be inlined" + def warnMsg = warning.map(" Possible reason:\n" + _).getOrElse("") + if (doRewriteTraitCallsite(callsite)) + backendReporting.inlinerWarning(pos, s"$initMsg: the trait method call could not be rewritten to the static implementation method." + warnMsg) + else if (!safeToInline) + backendReporting.inlinerWarning(pos, s"$initMsg: the method is not final and may be overridden." + warnMsg) + else + backendReporting.inlinerWarning(pos, s"$initMsg." + warnMsg) + } else if (warning.isDefined && warning.get.emitWarning(warnSettings)) { + // when annotatedInline is false, and there is some warning, the callsite metadata is possibly incomplete. + backendReporting.inlinerWarning(pos, s"there was a problem determining if method ${callee.name} can be inlined: \n"+ warning.get) + } + } + + res + + case Callsite(ins, _, _, Left(warning), _, _, pos) => + if (warning.emitWarning(warnSettings)) + backendReporting.inlinerWarning(pos, s"failed to determine if ${ins.name} should be inlined:\n$warning") + false + }).toList + } + + /** + * The current inlining heuristics are simple: inline calls to methods annotated @inline. + */ + def doInlineCallsite(callsite: Callsite): Boolean = callsite match { + case Callsite(_, _, _, Right(Callee(callee, calleeDeclClass, safeToInline, _, annotatedInline, _, warning)), _, _, pos) => + annotatedInline && safeToInline + + case _ => false + } + + def rewriteFinalTraitMethodInvocations(): Unit = { + // Rewriting final trait method callsites to the implementation class enables inlining. + // We cannot just iterate over the values of the `callsites` map because the rewrite changes the + // map. Therefore we first copy the values to a list. + callsites.values.toList.foreach(rewriteFinalTraitMethodInvocation) + } + + /** + * True for statically resolved trait callsites that should be rewritten to the static implementation method. + */ + def doRewriteTraitCallsite(callsite: Callsite) = callsite.callee match { + case Right(Callee(callee, calleeDeclarationClass, safeToInline, true, annotatedInline, annotatedNoInline, infoWarning)) => true + case _ => false + } + + /** + * Rewrite the INVOKEINTERFACE callsite of a final trait method invocation to INVOKESTATIC of the + * corresponding method in the implementation class. This enables inlining final trait methods. + * + * In a final trait method callsite, the callee is safeToInline and the callee method is abstract + * (the receiver type is the interface, so the method is abstract). + */ + def rewriteFinalTraitMethodInvocation(callsite: Callsite): Unit = { + if (doRewriteTraitCallsite(callsite)) { + val Right(Callee(callee, calleeDeclarationClass, _, _, annotatedInline, annotatedNoInline, infoWarning)) = callsite.callee + + val traitMethodArgumentTypes = asm.Type.getArgumentTypes(callee.desc) + + val implClassInternalName = calleeDeclarationClass.internalName + "$class" + + val selfParamTypeV: Either[OptimizerWarning, ClassBType] = calleeDeclarationClass.info.map(_.inlineInfo.traitImplClassSelfType match { + case Some(internalName) => classBTypeFromParsedClassfile(internalName) + case None => calleeDeclarationClass + }) + + def implClassMethodV(implMethodDescriptor: String): Either[OptimizerWarning, MethodNode] = { + byteCodeRepository.methodNode(implClassInternalName, callee.name, implMethodDescriptor).map(_._1) + } + + // The rewrite reading the implementation class and the implementation method from the bytecode + // repository. If either of the two fails, the rewrite is not performed. + val res = for { + selfParamType <- selfParamTypeV + implMethodDescriptor = asm.Type.getMethodDescriptor(asm.Type.getReturnType(callee.desc), selfParamType.toASMType +: traitMethodArgumentTypes: _*) + implClassMethod <- implClassMethodV(implMethodDescriptor) + implClassBType = classBTypeFromParsedClassfile(implClassInternalName) + selfTypeOk <- calleeDeclarationClass.isSubtypeOf(selfParamType) + } yield { + + // The self parameter type may be incompatible with the trait type. + // trait T { self: S => def foo = 1 } + // The $self parameter type of T$class.foo is S, which may be unrelated to T. If we re-write + // a call to T.foo to T$class.foo, we need to cast the receiver to S, otherwise we get a + // VerifyError. We run a `SourceInterpreter` to find all producer instructions of the + // receiver value and add a cast to the self type after each. + if (!selfTypeOk) { + val analyzer = new AsmAnalyzer(callsite.callsiteMethod, callsite.callsiteClass.internalName, new SourceInterpreter) + val receiverValue = analyzer.frameAt(callsite.callsiteInstruction).peekDown(traitMethodArgumentTypes.length) + for (i <- receiverValue.insns.asScala) { + val cast = new TypeInsnNode(CHECKCAST, selfParamType.internalName) + callsite.callsiteMethod.instructions.insert(i, cast) + } + } + + val newCallsiteInstruction = new MethodInsnNode(INVOKESTATIC, implClassInternalName, callee.name, implMethodDescriptor, false) + callsite.callsiteMethod.instructions.insert(callsite.callsiteInstruction, newCallsiteInstruction) + callsite.callsiteMethod.instructions.remove(callsite.callsiteInstruction) + + callGraph.callsites.remove(callsite.callsiteInstruction) + val staticCallsite = Callsite( + callsiteInstruction = newCallsiteInstruction, + callsiteMethod = callsite.callsiteMethod, + callsiteClass = callsite.callsiteClass, + callee = Right(Callee( + callee = implClassMethod, + calleeDeclarationClass = implClassBType, + safeToInline = true, + safeToRewrite = false, + annotatedInline = annotatedInline, + annotatedNoInline = annotatedNoInline, + calleeInfoWarning = infoWarning)), + argInfos = Nil, + callsiteStackHeight = callsite.callsiteStackHeight, + callsitePosition = callsite.callsitePosition + ) + callGraph.callsites(newCallsiteInstruction) = staticCallsite + } + + for (warning <- res.left) { + val Right(callee) = callsite.callee + val newCallee = callee.copy(calleeInfoWarning = Some(RewriteTraitCallToStaticImplMethodFailed(calleeDeclarationClass.internalName, callee.callee.name, callee.callee.desc, warning))) + callGraph.callsites(callsite.callsiteInstruction) = callsite.copy(callee = Right(newCallee)) + } + } + } + + /** + * Returns the callsites that can be inlined. Ensures that the returned inline request graph does + * not contain cycles. + * + * The resulting list is sorted such that the leaves of the inline request graph are on the left. + * Once these leaves are inlined, the successive elements will be leaves, etc. + */ + private def collectAndOrderInlineRequests: List[Callsite] = { + val requests = selectCallsitesForInlining + + // This map is an index to look up the inlining requests for a method. The value sets are mutable + // to allow removing elided requests (to break inlining cycles). The map itself is mutable to + // allow efficient building: requests.groupBy would build values as List[Callsite] that need to + // be transformed to mutable sets. + val inlineRequestsForMethod: mutable.Map[MethodNode, mutable.Set[Callsite]] = mutable.HashMap.empty.withDefaultValue(mutable.HashSet.empty) + for (r <- requests) inlineRequestsForMethod.getOrElseUpdate(r.callsiteMethod, mutable.HashSet.empty) += r + + /** + * Break cycles in the inline request graph by removing callsites. + * + * The list `requests` is traversed left-to-right, removing those callsites that are part of a + * cycle. Elided callsites are also removed from the `inlineRequestsForMethod` map. + */ + def breakInlineCycles(requests: List[Callsite]): List[Callsite] = { + // is there a path of inline requests from start to goal? + def isReachable(start: MethodNode, goal: MethodNode): Boolean = { + @tailrec def reachableImpl(check: List[MethodNode], visited: Set[MethodNode]): Boolean = check match { + case x :: xs => + if (x == goal) true + else if (visited(x)) reachableImpl(xs, visited) + else { + val callees = inlineRequestsForMethod(x).map(_.callee.get.callee) + reachableImpl(xs ::: callees.toList, visited + x) + } + + case Nil => + false + } + reachableImpl(List(start), Set.empty) + } + + val result = new mutable.ListBuffer[Callsite]() + // sort the inline requests to ensure that removing requests is deterministic + for (r <- requests.sorted(callsiteOrdering)) { + // is there a chain of inlining requests that would inline the callsite method into the callee? + if (isReachable(r.callee.get.callee, r.callsiteMethod)) + inlineRequestsForMethod(r.callsiteMethod) -= r + else + result += r + } + result.toList + } + + // sort the remaining inline requests such that the leaves appear first, then those requests + // that become leaves, etc. + def leavesFirst(requests: List[Callsite], visited: Set[Callsite] = Set.empty): List[Callsite] = { + if (requests.isEmpty) Nil + else { + val (leaves, others) = requests.partition(r => { + val inlineRequestsForCallee = inlineRequestsForMethod(r.callee.get.callee) + inlineRequestsForCallee.forall(visited) + }) + assert(leaves.nonEmpty, requests) + leaves ::: leavesFirst(others, visited ++ leaves) + } + } + + leavesFirst(breakInlineCycles(requests)) + } + + + /** + * Copy and adapt the instructions of a method to a callsite. + * + * Preconditions: + * - The maxLocals and maxStack values of the callsite method are correctly computed + * - The callsite method contains no unreachable basic blocks, i.e., running an [[Analyzer]] + * does not produce any `null` frames + * + * @param callsiteInstruction The invocation instruction + * @param callsiteStackHeight The stack height at the callsite + * @param callsiteMethod The method in which the invocation occurs + * @param callsiteClass The class in which the callsite method is defined + * @param callee The invoked method + * @param calleeDeclarationClass The class in which the invoked method is defined + * @param receiverKnownNotNull `true` if the receiver is known to be non-null + * @param keepLineNumbers `true` if LineNumberNodes should be copied to the call site + * @return `Some(message)` if inlining cannot be performed, `None` otherwise + */ + def inline(callsiteInstruction: MethodInsnNode, callsiteStackHeight: Int, callsiteMethod: MethodNode, callsiteClass: ClassBType, + callee: MethodNode, calleeDeclarationClass: ClassBType, + receiverKnownNotNull: Boolean, keepLineNumbers: Boolean): Option[CannotInlineWarning] = { + canInline(callsiteInstruction, callsiteStackHeight, callsiteMethod, callsiteClass, callee, calleeDeclarationClass) orElse { + // New labels for the cloned instructions + val labelsMap = cloneLabels(callee) + val (clonedInstructions, instructionMap) = cloneInstructions(callee, labelsMap) + if (!keepLineNumbers) { + removeLineNumberNodes(clonedInstructions) + } + + // local vars in the callee are shifted by the number of locals at the callsite + val localVarShift = callsiteMethod.maxLocals + clonedInstructions.iterator.asScala foreach { + case varInstruction: VarInsnNode => varInstruction.`var` += localVarShift + case _ => () + } + + // add a STORE instruction for each expected argument, including for THIS instance if any + val argStores = new InsnList + var nextLocalIndex = callsiteMethod.maxLocals + if (!isStaticMethod(callee)) { + if (!receiverKnownNotNull) { + argStores.add(new InsnNode(DUP)) + val nonNullLabel = newLabelNode + argStores.add(new JumpInsnNode(IFNONNULL, nonNullLabel)) + argStores.add(new InsnNode(ACONST_NULL)) + argStores.add(new InsnNode(ATHROW)) + argStores.add(nonNullLabel) + } + argStores.add(new VarInsnNode(ASTORE, nextLocalIndex)) + nextLocalIndex += 1 + } + + // We just use an asm.Type here, no need to create the MethodBType. + val calleAsmType = asm.Type.getMethodType(callee.desc) + + for(argTp <- calleAsmType.getArgumentTypes) { + val opc = argTp.getOpcode(ISTORE) // returns the correct xSTORE instruction for argTp + argStores.insert(new VarInsnNode(opc, nextLocalIndex)) // "insert" is "prepend" - the last argument is on the top of the stack + nextLocalIndex += argTp.getSize + } + + clonedInstructions.insert(argStores) + + // label for the exit of the inlined functions. xRETURNs are rplaced by GOTOs to this label. + val postCallLabel = newLabelNode + clonedInstructions.add(postCallLabel) + + // replace xRETURNs: + // - store the return value (if any) + // - clear the stack of the inlined method (insert DROPs) + // - load the return value + // - GOTO postCallLabel + + val returnType = calleAsmType.getReturnType + val hasReturnValue = returnType.getSort != asm.Type.VOID + val returnValueIndex = callsiteMethod.maxLocals + callee.maxLocals + nextLocalIndex += returnType.getSize + + def returnValueStore(returnInstruction: AbstractInsnNode) = { + val opc = returnInstruction.getOpcode match { + case IRETURN => ISTORE + case LRETURN => LSTORE + case FRETURN => FSTORE + case DRETURN => DSTORE + case ARETURN => ASTORE + } + new VarInsnNode(opc, returnValueIndex) + } + + // We run an interpreter to know the stack height at each xRETURN instruction and the sizes + // of the values on the stack. + val analyzer = new AsmAnalyzer(callee, calleeDeclarationClass.internalName) + + for (originalReturn <- callee.instructions.iterator().asScala if isReturn(originalReturn)) { + val frame = analyzer.frameAt(originalReturn) + var stackHeight = frame.getStackSize + + val inlinedReturn = instructionMap(originalReturn) + val returnReplacement = new InsnList + + def drop(slot: Int) = returnReplacement add getPop(frame.peekDown(slot).getSize) + + // for non-void methods, store the stack top into the return local variable + if (hasReturnValue) { + returnReplacement add returnValueStore(originalReturn) + stackHeight -= 1 + } + + // drop the rest of the stack + for (i <- 0 until stackHeight) drop(i) + + returnReplacement add new JumpInsnNode(GOTO, postCallLabel) + clonedInstructions.insert(inlinedReturn, returnReplacement) + clonedInstructions.remove(inlinedReturn) + } + + // Load instruction for the return value + if (hasReturnValue) { + val retVarLoad = { + val opc = returnType.getOpcode(ILOAD) + new VarInsnNode(opc, returnValueIndex) + } + clonedInstructions.insert(postCallLabel, retVarLoad) + } + + callsiteMethod.instructions.insert(callsiteInstruction, clonedInstructions) + callsiteMethod.instructions.remove(callsiteInstruction) + + callsiteMethod.localVariables.addAll(cloneLocalVariableNodes(callee, labelsMap, callee.name + "_").asJava) + callsiteMethod.tryCatchBlocks.addAll(cloneTryCatchBlockNodes(callee, labelsMap).asJava) + + // Add all invocation instructions that were inlined to the call graph + callee.instructions.iterator().asScala foreach { + case originalCallsiteIns: MethodInsnNode => + callGraph.callsites.get(originalCallsiteIns) match { + case Some(originalCallsite) => + val newCallsiteIns = instructionMap(originalCallsiteIns).asInstanceOf[MethodInsnNode] + callGraph.callsites(newCallsiteIns) = Callsite( + callsiteInstruction = newCallsiteIns, + callsiteMethod = callsiteMethod, + callsiteClass = callsiteClass, + callee = originalCallsite.callee, + argInfos = Nil, // TODO: re-compute argInfos for new destination (once we actually compute them) + callsiteStackHeight = callsiteStackHeight + originalCallsite.callsiteStackHeight, + callsitePosition = originalCallsite.callsitePosition + ) + + case None => + } + + case _ => + } + // Remove the elided invocation from the call graph + callGraph.callsites.remove(callsiteInstruction) + + callsiteMethod.maxLocals += returnType.getSize + callee.maxLocals + callsiteMethod.maxStack = math.max(callsiteMethod.maxStack, callee.maxStack + callsiteStackHeight) + + None + } + } + + /** + * Check whether an inling can be performed. Parmeters are described in method [[inline]]. + * @return `Some(message)` if inlining cannot be performed, `None` otherwise + */ + def canInline(callsiteInstruction: MethodInsnNode, callsiteStackHeight: Int, callsiteMethod: MethodNode, callsiteClass: ClassBType, + callee: MethodNode, calleeDeclarationClass: ClassBType): Option[CannotInlineWarning] = { + + def calleeDesc = s"${callee.name} of type ${callee.desc} in ${calleeDeclarationClass.internalName}" + def methodMismatch = s"Wrong method node for inlining ${textify(callsiteInstruction)}: $calleeDesc" + assert(callsiteInstruction.name == callee.name, methodMismatch) + assert(callsiteInstruction.desc == callee.desc, methodMismatch) + assert(!isConstructor(callee), s"Constructors cannot be inlined: $calleeDesc") + assert(!BytecodeUtils.isAbstractMethod(callee), s"Callee is abstract: $calleeDesc") + assert(callsiteMethod.instructions.contains(callsiteInstruction), s"Callsite ${textify(callsiteInstruction)} is not an instruction of $calleeDesc") + + // When an exception is thrown, the stack is cleared before jumping to the handler. When + // inlining a method that catches an exception, all values that were on the stack before the + // call (in addition to the arguments) would be cleared (SI-6157). So we don't inline methods + // with handlers in case there are values on the stack. + // Alternatively, we could save all stack values below the method arguments into locals, but + // that would be inefficient: we'd need to pop all parameters, save the values, and push the + // parameters back for the (inlined) invocation. Similarly for the result after the call. + def stackHasNonParameters: Boolean = { + val expectedArgs = asm.Type.getArgumentTypes(callsiteInstruction.desc).length + (callsiteInstruction.getOpcode match { + case INVOKEVIRTUAL | INVOKESPECIAL | INVOKEINTERFACE => 1 + case INVOKESTATIC => 0 + case INVOKEDYNAMIC => + assertionError(s"Unexpected opcode, cannot inline ${textify(callsiteInstruction)}") + }) + callsiteStackHeight > expectedArgs + } + + if (isSynchronizedMethod(callee)) { + // Could be done by locking on the receiver, wrapping the inlined code in a try and unlocking + // in finally. But it's probably not worth the effort, scala never emits synchronized methods. + Some(SynchronizedMethod(calleeDeclarationClass.internalName, callee.name, callee.desc)) + } else if (!callee.tryCatchBlocks.isEmpty && stackHasNonParameters) { + Some(MethodWithHandlerCalledOnNonEmptyStack( + calleeDeclarationClass.internalName, callee.name, callee.desc, + callsiteClass.internalName, callsiteMethod.name, callsiteMethod.desc)) + } else findIllegalAccess(callee.instructions, calleeDeclarationClass, callsiteClass) map { + case (illegalAccessIns, None) => + IllegalAccessInstruction( + calleeDeclarationClass.internalName, callee.name, callee.desc, + callsiteClass.internalName, illegalAccessIns) + + case (illegalAccessIns, Some(warning)) => + IllegalAccessCheckFailed( + calleeDeclarationClass.internalName, callee.name, callee.desc, + callsiteClass.internalName, illegalAccessIns, warning) + } + } + + /** + * Returns the first instruction in the `instructions` list that would cause a + * [[java.lang.IllegalAccessError]] when inlined into the `destinationClass`. + * + * If validity of some instruction could not be checked because an error occurred, the instruction + * is returned together with a warning message that describes the problem. + */ + def findIllegalAccess(instructions: InsnList, calleeDeclarationClass: ClassBType, destinationClass: ClassBType): Option[(AbstractInsnNode, Option[OptimizerWarning])] = { + + /** + * Check if a type is accessible to some class, as defined in JVMS 5.4.4. + * (A1) C is public + * (A2) C and D are members of the same run-time package + */ + def classIsAccessible(accessed: BType, from: ClassBType = destinationClass): Either[OptimizerWarning, Boolean] = (accessed: @unchecked) match { + // TODO: A2 requires "same run-time package", which seems to be package + classloader (JMVS 5.3.). is the below ok? + case c: ClassBType => c.isPublic.map(_ || c.packageInternalName == from.packageInternalName) + case a: ArrayBType => classIsAccessible(a.elementType, from) + case _: PrimitiveBType => Right(true) + } + + /** + * Check if a member reference is accessible from the [[destinationClass]], as defined in the + * JVMS 5.4.4. Note that the class name in a field / method reference is not necessarily the + * class in which the member is declared: + * + * class A { def f = 0 }; class B extends A { f } + * + * The INVOKEVIRTUAL instruction uses a method reference "B.f ()I". Therefore this method has + * two parameters: + * + * @param memberDeclClass The class in which the member is declared (A) + * @param memberRefClass The class used in the member reference (B) + * + * JVMS 5.4.4 summary: A field or method R is accessible to a class D (destinationClass) iff + * (B1) R is public + * (B2) R is protected, declared in C (memberDeclClass) and D is a subclass of C. + * If R is not static, R must contain a symbolic reference to a class T (memberRefClass), + * such that T is either a subclass of D, a superclass of D, or D itself. + * (B3) R is either protected or has default access and declared by a class in the same + * run-time package as D. + * (B4) R is private and is declared in D. + */ + def memberIsAccessible(memberFlags: Int, memberDeclClass: ClassBType, memberRefClass: ClassBType): Either[OptimizerWarning, Boolean] = { + // TODO: B3 requires "same run-time package", which seems to be package + classloader (JMVS 5.3.). is the below ok? + def samePackageAsDestination = memberDeclClass.packageInternalName == destinationClass.packageInternalName + + val key = (ACC_PUBLIC | ACC_PROTECTED | ACC_PRIVATE) & memberFlags + key match { + case ACC_PUBLIC => // B1 + Right(true) + + case ACC_PROTECTED => // B2 + tryEither { + val condB2 = destinationClass.isSubtypeOf(memberDeclClass).orThrow && { + val isStatic = (ACC_STATIC & memberFlags) != 0 + isStatic || memberRefClass.isSubtypeOf(destinationClass).orThrow || destinationClass.isSubtypeOf(memberRefClass).orThrow + } + Right(condB2 || samePackageAsDestination) // B3 (protected) + } + + case 0 => // B3 (default access) + Right(samePackageAsDestination) + + case ACC_PRIVATE => // B4 + Right(memberDeclClass == destinationClass) + } + } + + /** + * Check if `instruction` can be transplanted to `destinationClass`. + * + * If the instruction references a class, method or field that cannot be found in the + * byteCodeRepository, it is considered as not legal. This is known to happen in mixed + * compilation: for Java classes there is no classfile that could be parsed, nor does the + * compiler generate any bytecode. + * + * Returns a warning message describing the problem if checking the legality for the instruction + * failed. + */ + def isLegal(instruction: AbstractInsnNode): Either[OptimizerWarning, Boolean] = instruction match { + case ti: TypeInsnNode => + // NEW, ANEWARRAY, CHECKCAST or INSTANCEOF. For these instructions, the reference + // "must be a symbolic reference to a class, array, or interface type" (JVMS 6), so + // it can be an internal name, or a full array descriptor. + classIsAccessible(bTypeForDescriptorOrInternalNameFromClassfile(ti.desc)) + + case ma: MultiANewArrayInsnNode => + // "a symbolic reference to a class, array, or interface type" + classIsAccessible(bTypeForDescriptorOrInternalNameFromClassfile(ma.desc)) + + case fi: FieldInsnNode => + val fieldRefClass = classBTypeFromParsedClassfile(fi.owner) + for { + (fieldNode, fieldDeclClassNode) <- byteCodeRepository.fieldNode(fieldRefClass.internalName, fi.name, fi.desc): Either[OptimizerWarning, (FieldNode, InternalName)] + fieldDeclClass = classBTypeFromParsedClassfile(fieldDeclClassNode) + res <- memberIsAccessible(fieldNode.access, fieldDeclClass, fieldRefClass) + } yield { + res + } + + case mi: MethodInsnNode => + if (mi.owner.charAt(0) == '[') Right(true) // array methods are accessible + else { + def canInlineCall(opcode: Int, methodFlags: Int, methodDeclClass: ClassBType, methodRefClass: ClassBType): Either[OptimizerWarning, Boolean] = { + opcode match { + case INVOKESPECIAL if mi.name != GenBCode.INSTANCE_CONSTRUCTOR_NAME => + // invokespecial is used for private method calls, super calls and instance constructor calls. + // private method and super calls can only be inlined into the same class. + Right(destinationClass == calleeDeclarationClass) + + case _ => // INVOKEVIRTUAL, INVOKESTATIC, INVOKEINTERFACE and INVOKESPECIAL of constructors + memberIsAccessible(methodFlags, methodDeclClass, methodRefClass) + } + } + + val methodRefClass = classBTypeFromParsedClassfile(mi.owner) + for { + (methodNode, methodDeclClassNode) <- byteCodeRepository.methodNode(methodRefClass.internalName, mi.name, mi.desc): Either[OptimizerWarning, (MethodNode, InternalName)] + methodDeclClass = classBTypeFromParsedClassfile(methodDeclClassNode) + res <- canInlineCall(mi.getOpcode, methodNode.access, methodDeclClass, methodRefClass) + } yield { + res + } + } + + case ivd: InvokeDynamicInsnNode => + // TODO @lry check necessary conditions to inline an indy, instead of giving up + Right(false) + + case ci: LdcInsnNode => ci.cst match { + case t: asm.Type => classIsAccessible(bTypeForDescriptorOrInternalNameFromClassfile(t.getInternalName)) + case _ => Right(true) + } + + case _ => Right(true) + } + + val it = instructions.iterator.asScala + @tailrec def find: Option[(AbstractInsnNode, Option[OptimizerWarning])] = { + if (!it.hasNext) None // all instructions are legal + else { + val i = it.next() + isLegal(i) match { + case Left(warning) => Some((i, Some(warning))) // checking isLegal for i failed + case Right(false) => Some((i, None)) // an illegal instruction was found + case _ => find + } + } + } + find + } +} diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala index 87ad715e4d..f6cfc5598b 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -8,10 +8,11 @@ package backend.jvm package opt import scala.annotation.switch -import scala.tools.asm.{Opcodes, MethodWriter, ClassWriter} -import scala.tools.asm.tree.analysis.{Analyzer, BasicValue, BasicInterpreter} +import scala.tools.asm.Opcodes +import scala.tools.asm.tree.analysis.{Analyzer, BasicInterpreter} import scala.tools.asm.tree._ import scala.collection.convert.decorateAsScala._ +import scala.tools.nsc.backend.jvm.BTypes.InternalName import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ import scala.tools.nsc.settings.ScalaSettings @@ -48,6 +49,45 @@ import scala.tools.nsc.settings.ScalaSettings */ class LocalOpt(settings: ScalaSettings) { /** + * Remove unreachable code from all methods of `classNode`. See of its overload. + * + * @param classNode The class to optimize + * @return `true` if unreachable code was removed from any method + */ + def minimalRemoveUnreachableCode(classNode: ClassNode): Boolean = { + classNode.methods.asScala.foldLeft(false) { + case (changed, method) => minimalRemoveUnreachableCode(method, classNode.name) || changed + } + } + + /** + * Remove unreachable code from a method. + * + * This implementation only removes instructions that are unreachable for an ASM analyzer / + * interpreter. This ensures that future analyses will not produce `null` frames. The inliner + * and call graph builder depend on this property. + */ + def minimalRemoveUnreachableCode(method: MethodNode, ownerClassName: InternalName): Boolean = { + if (method.instructions.size == 0) return false // fast path for abstract methods + + // For correctness, after removing unreachable code, we have to eliminate empty exception + // handlers, see scaladoc of def methodOptimizations. Removing an live handler may render more + // code unreachable and therefore requires running another round. + def removalRound(): Boolean = { + val (codeRemoved, liveLabels) = removeUnreachableCodeImpl(method, ownerClassName) + if (codeRemoved) { + val liveHandlerRemoved = removeEmptyExceptionHandlers(method).exists(h => liveLabels(h.start)) + if (liveHandlerRemoved) removalRound() + } + codeRemoved + } + + val codeRemoved = removalRound() + if (codeRemoved) removeUnusedLocalVariableNodes(method)() + codeRemoved + } + + /** * Remove unreachable instructions from all (non-abstract) methods and apply various other * cleanups to the bytecode. * @@ -73,7 +113,7 @@ class LocalOpt(settings: ScalaSettings) { * * Returns `true` if the bytecode of `method` was changed. */ - private def methodOptimizations(method: MethodNode, ownerClassName: String): Boolean = { + def methodOptimizations(method: MethodNode, ownerClassName: InternalName): Boolean = { if (method.instructions.size == 0) return false // fast path for abstract methods // unreachable-code also removes unused local variable nodes and empty exception handlers. @@ -102,9 +142,7 @@ class LocalOpt(settings: ScalaSettings) { // This triggers "ClassFormatError: Illegal exception table range in class file C". Similar // for local variables in dead blocks. Maybe that's a bug in the ASM framework. - var recurse = true - var codeHandlersOrJumpsChanged = false - while (recurse) { + def removalRound(): Boolean = { // unreachable-code, empty-handlers and simplify-jumps run until reaching a fixpoint (see doc on class LocalOpt) val (codeRemoved, handlersRemoved, liveHandlerRemoved) = if (settings.YoptUnreachableCode) { val (codeRemoved, liveLabels) = removeUnreachableCodeImpl(method, ownerClassName) @@ -116,15 +154,18 @@ class LocalOpt(settings: ScalaSettings) { val jumpsChanged = if (settings.YoptSimplifyJumps) simplifyJumps(method) else false - codeHandlersOrJumpsChanged ||= (codeRemoved || handlersRemoved || jumpsChanged) + // Eliminating live handlers and simplifying jump instructions may render more code + // unreachable, so we need to run another round. + if (liveHandlerRemoved || jumpsChanged) removalRound() - // The doc comment of class LocalOpt explains why we recurse if jumpsChanged || liveHandlerRemoved - recurse = settings.YoptRecurseUnreachableJumps && (jumpsChanged || liveHandlerRemoved) + codeRemoved || handlersRemoved || jumpsChanged } + val codeHandlersOrJumpsChanged = removalRound() + // (*) Removing stale local variable descriptors is required for correctness of unreachable-code val localsRemoved = - if (settings.YoptCompactLocals) compactLocalVariables(method) + if (settings.YoptCompactLocals) compactLocalVariables(method) // also removes unused else if (settings.YoptUnreachableCode) removeUnusedLocalVariableNodes(method)() // (*) else false @@ -146,10 +187,10 @@ class LocalOpt(settings: ScalaSettings) { * * TODO: rewrite, don't use computeMaxLocalsMaxStack (runs a ClassWriter) / Analyzer. Too slow. */ - def removeUnreachableCodeImpl(method: MethodNode, ownerClassName: String): (Boolean, Set[LabelNode]) = { + def removeUnreachableCodeImpl(method: MethodNode, ownerClassName: InternalName): (Boolean, Set[LabelNode]) = { // The data flow analysis requires the maxLocals / maxStack fields of the method to be computed. computeMaxLocalsMaxStack(method) - val a = new Analyzer[BasicValue](new BasicInterpreter) + val a = new Analyzer(new BasicInterpreter) a.analyze(ownerClassName, method) val frames = a.getFrames @@ -319,29 +360,6 @@ class LocalOpt(settings: ScalaSettings) { } /** - * In order to run an Analyzer, the maxLocals / maxStack fields need to be available. The ASM - * framework only computes these values during bytecode generation. - * - * Since 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 - } - - /** * Removes LineNumberNodes that don't describe any executable instructions. * * This method expects (and asserts) that the `start` label of each LineNumberNode is the diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/OptimizerReporting.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/OptimizerReporting.scala deleted file mode 100644 index 7002e43d98..0000000000 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/OptimizerReporting.scala +++ /dev/null @@ -1,24 +0,0 @@ -/* NSC -- new Scala compiler - * Copyright 2005-2014 LAMP/EPFL - * @author Martin Odersky - */ - -package scala.tools.nsc -package backend.jvm - -import scala.tools.asm -import asm.tree._ - -/** - * Reporting utilities used in the optimizer. - */ -object OptimizerReporting { - def methodSignature(className: String, methodName: String, methodDescriptor: String): String = { - className + "::" + methodName + methodDescriptor - } - - def methodSignature(className: String, method: MethodNode): String = methodSignature(className, method.name, method.desc) - - def inlineFailure(reason: String): Nothing = MissingRequirementError.signal(reason) - def assertionError(message: String): Nothing = throw new AssertionError(message) -} diff --git a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala index d7f4cca615..d273995e6e 100644 --- a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala @@ -218,23 +218,24 @@ trait ScalaSettings extends AbsScalaSettings object YoptChoices extends MultiChoiceEnumeration { val unreachableCode = Choice("unreachable-code", "Eliminate unreachable code, exception handlers protecting no instructions, debug information of eliminated variables.") val simplifyJumps = Choice("simplify-jumps", "Simplify branching instructions, eliminate unnecessary ones.") - val recurseUnreachableJumps = Choice("recurse-unreachable-jumps", "Recursively apply unreachable-code and simplify-jumps (if enabled) until reaching a fixpoint.") val emptyLineNumbers = Choice("empty-line-numbers", "Eliminate unnecessary line number information.") val emptyLabels = Choice("empty-labels", "Eliminate and collapse redundant labels in the bytecode.") val compactLocals = Choice("compact-locals", "Eliminate empty slots in the sequence of local variables.") + val inlineProject = Choice("inline-project", "Inline only methods defined in the files being compiled") + val inlineGlobal = Choice("inline-global", "Inline methods from any source, including classfiles on the compile classpath") 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(unreachableCode, simplifyJumps, recurseUnreachableJumps, emptyLineNumbers, emptyLabels, compactLocals) + private val methodChoices = List(unreachableCode, simplifyJumps, emptyLineNumbers, emptyLabels, compactLocals) val lMethod = Choice("l:method", "Enable intra-method optimizations: "+ methodChoices.mkString(","), expandsTo = methodChoices) - private val projectChoices = List(lMethod) + private val projectChoices = List(lMethod, inlineProject) val lProject = Choice("l:project", "Enable cross-method optimizations within the current project: "+ projectChoices.mkString(","), expandsTo = projectChoices) - private val classpathChoices = List(lProject) + private val classpathChoices = List(lProject, inlineGlobal) val lClasspath = Choice("l:classpath", "Enable cross-method optimizations across the entire classpath: "+ classpathChoices.mkString(","), expandsTo = classpathChoices) } @@ -247,11 +248,33 @@ trait ScalaSettings extends AbsScalaSettings def YoptNone = Yopt.isSetByUser && Yopt.value.isEmpty def YoptUnreachableCode = !Yopt.isSetByUser || Yopt.contains(YoptChoices.unreachableCode) def YoptSimplifyJumps = Yopt.contains(YoptChoices.simplifyJumps) - def YoptRecurseUnreachableJumps = Yopt.contains(YoptChoices.recurseUnreachableJumps) def YoptEmptyLineNumbers = Yopt.contains(YoptChoices.emptyLineNumbers) def YoptEmptyLabels = Yopt.contains(YoptChoices.emptyLabels) def YoptCompactLocals = Yopt.contains(YoptChoices.compactLocals) + def YoptInlineProject = Yopt.contains(YoptChoices.inlineProject) + def YoptInlineGlobal = Yopt.contains(YoptChoices.inlineGlobal) + def YoptInlinerEnabled = YoptInlineProject || YoptInlineGlobal + + object YoptWarningsChoices extends MultiChoiceEnumeration { + val none = Choice("none" , "No optimizer warnings.") + val atInlineFailedSummary = Choice("at-inline-failed-summary" , "One-line summary if there were @inline method calls that could not be inlined.") + val atInlineFailed = Choice("at-inline-failed" , "A detailed warning for each @inline method call that could not be inlined.") + val noInlineMixed = Choice("no-inline-mixed" , "In mixed compilation, warn at callsites methods defined in java sources (the inlining decision cannot be made without bytecode).") + val noInlineMissingBytecode = Choice("no-inline-missing-bytecode" , "Warn if an inlining decision cannot be made because a the bytecode of a class or member cannot be found on the compilation classpath.") + val noInlineMissingScalaInlineInfoAttr = Choice("no-inline-missing-attribute", "Warn if an inlining decision cannot be made because a Scala classfile does not have a ScalaInlineInfo attribute.") + } + + val YoptWarnings = MultiChoiceSetting( + name = "-Yopt-warnings", + helpArg = "warnings", + descr = "Enable optimizer warnings", + domain = YoptWarningsChoices, + default = Some(List(YoptWarningsChoices.atInlineFailed.name))) withPostSetHook (self => { + if (self.value subsetOf Set(YoptWarningsChoices.none, YoptWarningsChoices.atInlineFailedSummary)) YinlinerWarnings.value = false + else YinlinerWarnings.value = true + }) + 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, jvm = Value } diff --git a/src/compiler/scala/tools/nsc/transform/Delambdafy.scala b/src/compiler/scala/tools/nsc/transform/Delambdafy.scala index 1f832ba81e..94e88589f5 100644 --- a/src/compiler/scala/tools/nsc/transform/Delambdafy.scala +++ b/src/compiler/scala/tools/nsc/transform/Delambdafy.scala @@ -255,6 +255,8 @@ abstract class Delambdafy extends Transform with TypingTransformers with ast.Tre val name = unit.freshTypeName(s"$oldClassPart$suffix".replace("$anon", "$nestedInAnon")) val lambdaClass = pkg newClassSymbol(name, originalFunction.pos, FINAL | SYNTHETIC) addAnnotation SerialVersionUIDAnnotation + // make sure currentRun.compiles(lambdaClass) is true (AddInterfaces does the same for trait impl classes) + currentRun.symSource(lambdaClass) = funOwner.sourceFile 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) diff --git a/src/compiler/scala/tools/nsc/typechecker/Typers.scala b/src/compiler/scala/tools/nsc/typechecker/Typers.scala index 3a85d16f55..758425aad5 100644 --- a/src/compiler/scala/tools/nsc/typechecker/Typers.scala +++ b/src/compiler/scala/tools/nsc/typechecker/Typers.scala @@ -2053,7 +2053,7 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper case acc => acc } ownAcc match { - case acc: TermSymbol if !acc.isVariable => + case acc: TermSymbol if !acc.isVariable && !isByNameParamType(acc.info) => debuglog(s"$acc has alias ${alias.fullLocationString}") acc setAlias alias case _ => diff --git a/src/library/scala/collection/SeqLike.scala b/src/library/scala/collection/SeqLike.scala index 329273df5b..66fce0f902 100644 --- a/src/library/scala/collection/SeqLike.scala +++ b/src/library/scala/collection/SeqLike.scala @@ -447,9 +447,11 @@ trait SeqLike[+A, +Repr] extends Any with IterableLike[A, Repr] with GenSeqLike[ def diff[B >: A](that: GenSeq[B]): Repr = { val occ = occCounts(that.seq) val b = newBuilder - for (x <- this) - if (occ(x) == 0) b += x - else occ(x) -= 1 + for (x <- this) { + val ox = occ(x) // Avoid multiple map lookups + if (ox == 0) b += x + else occ(x) = ox - 1 + } b.result() } @@ -476,11 +478,13 @@ trait SeqLike[+A, +Repr] extends Any with IterableLike[A, Repr] with GenSeqLike[ def intersect[B >: A](that: GenSeq[B]): Repr = { val occ = occCounts(that.seq) val b = newBuilder - for (x <- this) - if (occ(x) > 0) { + for (x <- this) { + val ox = occ(x) // Avoid multiple map lookups + if (ox > 0) { b += x - occ(x) -= 1 + occ(x) = ox - 1 } + } b.result() } @@ -509,22 +513,35 @@ trait SeqLike[+A, +Repr] extends Any with IterableLike[A, Repr] with GenSeqLike[ def patch[B >: A, That](from: Int, patch: GenSeq[B], replaced: Int)(implicit bf: CanBuildFrom[Repr, B, That]): That = { val b = bf(repr) - val (prefix, rest) = this.splitAt(from) - b ++= toCollection(prefix) + var i = 0 + val it = this.iterator + while (i < from && it.hasNext) { + b += it.next() + i += 1 + } b ++= patch.seq - b ++= toCollection(rest).view drop replaced + i = replaced + while (i > 0 && it.hasNext) { + it.next() + i -= 1 + } + while (it.hasNext) b += it.next() b.result() } def updated[B >: A, That](index: Int, elem: B)(implicit bf: CanBuildFrom[Repr, B, That]): That = { if (index < 0) throw new IndexOutOfBoundsException(index.toString) val b = bf(repr) - val (prefix, rest) = this.splitAt(index) - val restColl = toCollection(rest) - if (restColl.isEmpty) throw new IndexOutOfBoundsException(index.toString) - b ++= toCollection(prefix) + var i = 0 + val it = this.iterator + while (i < index && it.hasNext) { + b += it.next() + i += 1 + } + if (!it.hasNext) throw new IndexOutOfBoundsException(index.toString) b += elem - b ++= restColl.view.tail + it.next() + while (it.hasNext) b += it.next() b.result() } @@ -544,8 +561,9 @@ trait SeqLike[+A, +Repr] extends Any with IterableLike[A, Repr] with GenSeqLike[ def padTo[B >: A, That](len: Int, elem: B)(implicit bf: CanBuildFrom[Repr, B, That]): That = { val b = bf(repr) - b.sizeHint(length max len) - var diff = len - length + val L = length + b.sizeHint(math.max(L, len)) + var diff = len - L b ++= thisCollection while (diff > 0) { b += elem @@ -617,16 +635,23 @@ trait SeqLike[+A, +Repr] extends Any with IterableLike[A, Repr] with GenSeqLike[ */ def sorted[B >: A](implicit ord: Ordering[B]): Repr = { val len = this.length - val arr = new ArraySeq[A](len) - var i = 0 - for (x <- this) { - arr(i) = x - i += 1 - } - java.util.Arrays.sort(arr.array, ord.asInstanceOf[Ordering[Object]]) val b = newBuilder - b.sizeHint(len) - for (x <- arr) b += x + if (len == 1) b ++= this + else if (len > 1) { + b.sizeHint(len) + val arr = new Array[AnyRef](len) // Previously used ArraySeq for more compact but slower code + var i = 0 + for (x <- this) { + arr(i) = x.asInstanceOf[AnyRef] + i += 1 + } + java.util.Arrays.sort(arr, ord.asInstanceOf[Ordering[Object]]) + i = 0 + while (i < arr.length) { + b += arr(i).asInstanceOf[A] + i += 1 + } + } b.result() } diff --git a/src/reflect/scala/reflect/internal/SymbolTable.scala b/src/reflect/scala/reflect/internal/SymbolTable.scala index 01e4cdf367..ef63078f90 100644 --- a/src/reflect/scala/reflect/internal/SymbolTable.scala +++ b/src/reflect/scala/reflect/internal/SymbolTable.scala @@ -355,6 +355,14 @@ abstract class SymbolTable extends macros.Universe cache } + /** + * Removes a cache from the per-run caches. This is useful for testing: it allows running the + * compiler and then inspect the state of a cache. + */ + def unrecordCache[T <: Clearable](cache: T): Unit = { + caches = caches.filterNot(_.get eq cache) + } + def clearAll() = { debuglog("Clearing " + caches.size + " caches.") caches foreach (ref => Option(ref.get).foreach(_.clear)) diff --git a/test/files/pos/t9111-inliner-workaround.flags b/test/files/pos/t9111-inliner-workaround.flags new file mode 100644 index 0000000000..63b5558cfd --- /dev/null +++ b/test/files/pos/t9111-inliner-workaround.flags @@ -0,0 +1 @@ +-Ybackend:GenBCode -Yopt:l:classpath
\ No newline at end of file diff --git a/test/files/pos/t9111-inliner-workaround/A_1.java b/test/files/pos/t9111-inliner-workaround/A_1.java new file mode 100644 index 0000000000..bc60b68ea6 --- /dev/null +++ b/test/files/pos/t9111-inliner-workaround/A_1.java @@ -0,0 +1,13 @@ +public class A_1 { + public static class T { } + + public static class Inner { + public static class T { } + + public void foo(T t) { } + + public T t = null; + + public class Deeper extends T { } + } +} diff --git a/test/files/pos/t9111-inliner-workaround/Test_1.scala b/test/files/pos/t9111-inliner-workaround/Test_1.scala new file mode 100644 index 0000000000..1a00fff833 --- /dev/null +++ b/test/files/pos/t9111-inliner-workaround/Test_1.scala @@ -0,0 +1,10 @@ +object Test extends App { + println(new A_1.Inner()) + + // Accessing foo or Deeper triggers the error of SI-9111. + // However, when not referring to those definitions, compilation should + // succeed, also if the inliner is enabled. + + // println(i.foo(null)) + // new i.Deeper() +} diff --git a/test/files/run/bcodeInlinerMixed.flags b/test/files/run/bcodeInlinerMixed.flags new file mode 100644 index 0000000000..63b5558cfd --- /dev/null +++ b/test/files/run/bcodeInlinerMixed.flags @@ -0,0 +1 @@ +-Ybackend:GenBCode -Yopt:l:classpath
\ No newline at end of file diff --git a/test/files/run/bcodeInlinerMixed/A_1.java b/test/files/run/bcodeInlinerMixed/A_1.java new file mode 100644 index 0000000000..44d7d88eeb --- /dev/null +++ b/test/files/run/bcodeInlinerMixed/A_1.java @@ -0,0 +1,3 @@ +public class A_1 { + public static final int bar() { return 100; } +} diff --git a/test/files/run/bcodeInlinerMixed/B_1.scala b/test/files/run/bcodeInlinerMixed/B_1.scala new file mode 100644 index 0000000000..2aadeccb82 --- /dev/null +++ b/test/files/run/bcodeInlinerMixed/B_1.scala @@ -0,0 +1,20 @@ +// Partest does proper mixed compilation: +// 1. scalac *.scala *.java +// 2. javac *.java +// 3. scalc *.scala +// +// In the second scalc round, the classfile for A_1 is on the classpath. +// Therefore the inliner has access to the bytecode of `bar`, which means +// it can verify that the invocation to `bar` can be safely inlined. +// +// So both callsites of `flop` are inlined. +// +// In a single mixed compilation, `flop` cannot be inlined, see JUnit InlinerTest.scala, def mixedCompilationNoInline. + +class B { + @inline final def flop = A_1.bar + def g = flop +} +class C { + def h(b: B) = b.flop +} diff --git a/test/files/run/bcodeInlinerMixed/Test.scala b/test/files/run/bcodeInlinerMixed/Test.scala new file mode 100644 index 0000000000..c8c7a9fe2a --- /dev/null +++ b/test/files/run/bcodeInlinerMixed/Test.scala @@ -0,0 +1,16 @@ +import scala.tools.partest.{BytecodeTest, ASMConverters} +import ASMConverters._ + +object Test extends BytecodeTest { + def show: Unit = { + val gIns = instructionsFromMethod(getMethod(loadClassNode("B"), "g")) + val hIns = instructionsFromMethod(getMethod(loadClassNode("C"), "h")) + // val invocation = Invoke(INVOKESTATIC, A_1, bar, ()I, false) + for (i <- List(gIns, hIns)) { + assert(i exists { + case Invoke(_, _, "bar", "()I", _) => true + case _ => false + }, i mkString "\n") + } + } +} diff --git a/test/files/run/colltest1.scala b/test/files/run/colltest1.scala index e0ec378585..de8780a050 100644 --- a/test/files/run/colltest1.scala +++ b/test/files/run/colltest1.scala @@ -1,5 +1,5 @@ /* - * filter: inliner warnings; re-run with -Yinline-warnings for details + * filter: inliner warnings; re-run with */ import scala.collection._ import scala.language.postfixOps diff --git a/test/files/run/compiler-asSeenFrom.scala b/test/files/run/compiler-asSeenFrom.scala index 677dd40ddc..a60c2e8925 100644 --- a/test/files/run/compiler-asSeenFrom.scala +++ b/test/files/run/compiler-asSeenFrom.scala @@ -1,5 +1,5 @@ /* - * filter: inliner warning; re-run with -Yinline-warnings for details + * filter: inliner warning; re-run with */ import scala.tools.nsc._ import scala.tools.partest.DirectTest diff --git a/test/files/run/existentials-in-compiler.scala b/test/files/run/existentials-in-compiler.scala index dfc7048b31..e516eddf95 100644 --- a/test/files/run/existentials-in-compiler.scala +++ b/test/files/run/existentials-in-compiler.scala @@ -1,5 +1,5 @@ /* - * filter: inliner warnings; re-run with -Yinline-warnings for details + * filter: inliner warnings; re-run with */ import scala.tools.nsc._ import scala.tools.partest.CompilerTest diff --git a/test/files/run/is-valid-num.scala b/test/files/run/is-valid-num.scala index 4ab2fac8dd..156121cab5 100644 --- a/test/files/run/is-valid-num.scala +++ b/test/files/run/is-valid-num.scala @@ -1,5 +1,5 @@ /* - * filter: inliner warnings; re-run with -Yinline-warnings for details + * filter: inliner warnings; re-run with */ object Test { def x = BigInt("10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") diff --git a/test/files/run/iterator-from.scala b/test/files/run/iterator-from.scala index e2ca5864ea..e7ba1aeb28 100644 --- a/test/files/run/iterator-from.scala +++ b/test/files/run/iterator-from.scala @@ -1,5 +1,5 @@ /* This file tests iteratorFrom, keysIteratorFrom, and valueIteratorFrom on various sorted sets and maps - * filter: inliner warnings; re-run with -Yinline-warnings for details + * filter: inliner warnings; re-run with */ import scala.util.{Random => R} diff --git a/test/files/run/mapConserve.scala b/test/files/run/mapConserve.scala index f52af3b9f4..c17754283a 100644 --- a/test/files/run/mapConserve.scala +++ b/test/files/run/mapConserve.scala @@ -1,5 +1,5 @@ /* - * filter: inliner warnings; re-run with -Yinline-warnings for details + * filter: inliner warnings; re-run with */ import scala.annotation.tailrec import scala.collection.mutable.ListBuffer diff --git a/test/files/run/pc-conversions.scala b/test/files/run/pc-conversions.scala index 5fecac9d94..d4ae305aa7 100644 --- a/test/files/run/pc-conversions.scala +++ b/test/files/run/pc-conversions.scala @@ -1,5 +1,5 @@ /* - * filter: inliner warning; re-run with -Yinline-warnings for details + * filter: inliner warning; re-run with */ import collection._ diff --git a/test/files/run/stringinterpolation_macro-run.scala b/test/files/run/stringinterpolation_macro-run.scala index e18375d521..ae7c0e5d7a 100644 --- a/test/files/run/stringinterpolation_macro-run.scala +++ b/test/files/run/stringinterpolation_macro-run.scala @@ -1,5 +1,5 @@ /* - * filter: inliner warnings; re-run with -Yinline-warnings for details + * filter: inliner warnings; re-run with */ object Test extends App { diff --git a/test/files/run/synchronized.check b/test/files/run/synchronized.check index eab191b4ed..9add05ea0c 100644 --- a/test/files/run/synchronized.check +++ b/test/files/run/synchronized.check @@ -1,4 +1,8 @@ +#partest !-Ybackend:GenBCode warning: there were 14 inliner warnings; re-run with -Yinline-warnings for details +#partest -Ybackend:GenBCode +warning: there were 14 inliner warnings; re-run with -Yopt-warnings for details +#partest .|. c1.f1: OK .|. c1.fi: OK .|... c1.fv: OK diff --git a/test/files/run/t7096.scala b/test/files/run/t7096.scala index 872562dd4d..f723d70abe 100644 --- a/test/files/run/t7096.scala +++ b/test/files/run/t7096.scala @@ -1,5 +1,5 @@ /* - * filter: inliner warning; re-run with -Yinline-warnings for details + * filter: inliner warning; re-run with */ import scala.tools.partest._ import scala.tools.nsc._ diff --git a/test/files/run/t7582.check b/test/files/run/t7582.check index cd951d8d4f..2a11210000 100644 --- a/test/files/run/t7582.check +++ b/test/files/run/t7582.check @@ -1,2 +1,6 @@ +#partest !-Ybackend:GenBCode warning: there was one inliner warning; re-run with -Yinline-warnings for details +#partest -Ybackend:GenBCode +warning: there was one inliner warning; re-run with -Yopt-warnings for details +#partest 2 diff --git a/test/files/run/t7582b.check b/test/files/run/t7582b.check index cd951d8d4f..2a11210000 100644 --- a/test/files/run/t7582b.check +++ b/test/files/run/t7582b.check @@ -1,2 +1,6 @@ +#partest !-Ybackend:GenBCode warning: there was one inliner warning; re-run with -Yinline-warnings for details +#partest -Ybackend:GenBCode +warning: there was one inliner warning; re-run with -Yopt-warnings for details +#partest 2 diff --git a/test/files/run/t9223.scala b/test/files/run/t9223.scala new file mode 100644 index 0000000000..78767b158d --- /dev/null +++ b/test/files/run/t9223.scala @@ -0,0 +1,8 @@ +class X(val x: String) +class Y(y: => String) extends X(y) { def f = y } + +object Test { + def main(args: Array[String]): Unit = { + assert(new Y("hi").f == "hi") + } +} diff --git a/test/files/run/t9223b.scala b/test/files/run/t9223b.scala new file mode 100644 index 0000000000..2afc7ddfe0 --- /dev/null +++ b/test/files/run/t9223b.scala @@ -0,0 +1,8 @@ +class X(x: => String) { def xx = x } +class Y(y: String) extends X(y) { def f = y } + +object Test { + def main(args: Array[String]): Unit = { + assert(new Y("hi").f == "hi") + } +} diff --git a/test/junit/scala/collection/IterableViewLikeTest.scala b/test/junit/scala/collection/IterableViewLikeTest.scala index ab09c4930b..435a43c215 100644 --- a/test/junit/scala/collection/IterableViewLikeTest.scala +++ b/test/junit/scala/collection/IterableViewLikeTest.scala @@ -13,6 +13,7 @@ class IterableViewLikeTest { def hasCorrectDropAndTakeMethods() { val iter = Iterable(1, 2, 3) + import scala.language.postfixOps assertEquals(Iterable.empty[Int], iter.view take Int.MinValue force) assertEquals(Iterable.empty[Int], iter.view takeRight Int.MinValue force) assertEquals(iter, iter.view drop Int.MinValue force) diff --git a/test/junit/scala/tools/nsc/backend/jvm/BTypesTest.scala b/test/junit/scala/tools/nsc/backend/jvm/BTypesTest.scala index 2347e8288e..6ada0e20fb 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/BTypesTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/BTypesTest.scala @@ -7,35 +7,41 @@ import org.junit.Test import scala.tools.asm.Opcodes import org.junit.Assert._ -@RunWith(classOf[JUnit4]) -class BTypesTest { - val settings = new Settings() - settings.processArgumentString("-usejavacp") - val g: Global = new Global(settings) - val run = new g.Run() // initializes some compiler internals - import g.{definitions => d, Symbol} +import scala.tools.nsc.backend.jvm.CodeGenTools._ +import scala.tools.testing.ClearAfterClass - def duringBackend[T](f: => T) = g.exitingDelambdafy(f) +object BTypesTest extends ClearAfterClass.Clearable { + var compiler = { + val comp = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:none") + new comp.Run() // initializes some of the compiler + comp.exitingDelambdafy(comp.scalaPrimitives.init()) // needed: it's only done when running the backend, and we don't actually run the compiler + comp.exitingDelambdafy(comp.genBCode.bTypes.initializeCoreBTypes()) + comp + } + def clear(): Unit = { compiler = null } +} - val btypes = new BTypesFromSymbols[g.type](g) - import btypes._ - duringBackend(btypes.initializeCoreBTypes()) +@RunWith(classOf[JUnit4]) +class BTypesTest extends ClearAfterClass { + ClearAfterClass.stateToClear = BTypesTest - def classBTypeFromSymbol(sym: Symbol) = duringBackend(btypes.classBTypeFromSymbol(sym)) + val compiler = BTypesTest.compiler + import compiler.genBCode.bTypes._ - val jlo = d.ObjectClass - val jls = d.StringClass + def classBTFS(sym: compiler.Symbol) = compiler.exitingDelambdafy(classBTypeFromSymbol(sym)) - val o = classBTypeFromSymbol(jlo) - val s = classBTypeFromSymbol(jls) - val oArr = ArrayBType(o) - val method = MethodBType(List(oArr, INT, DOUBLE, s), UNIT) + def jlo = compiler.definitions.ObjectClass + def jls = compiler.definitions.StringClass + def o = classBTFS(jlo) + def s = classBTFS(jls) + def oArr = ArrayBType(o) + def method = MethodBType(List(oArr, INT, DOUBLE, s), UNIT) @Test def classBTypesEquality() { - val s1 = classBTypeFromSymbol(jls) - val s2 = classBTypeFromSymbol(jls) - val o = classBTypeFromSymbol(jlo) + val s1 = classBTFS(jls) + val s2 = classBTFS(jls) + val o = classBTFS(jlo) assertEquals(s1, s2) assertEquals(s1.hashCode, s2.hashCode) assert(s1 != o) @@ -53,7 +59,7 @@ class BTypesTest { assert(FLOAT.typedOpcode(Opcodes.IALOAD) == Opcodes.FALOAD) assert(LONG.typedOpcode(Opcodes.IALOAD) == Opcodes.LALOAD) assert(DOUBLE.typedOpcode(Opcodes.IALOAD) == Opcodes.DALOAD) - assert(classBTypeFromSymbol(jls).typedOpcode(Opcodes.IALOAD) == Opcodes.AALOAD) + assert(classBTFS(jls).typedOpcode(Opcodes.IALOAD) == Opcodes.AALOAD) assert(UNIT.typedOpcode(Opcodes.IRETURN) == Opcodes.RETURN) assert(BOOL.typedOpcode(Opcodes.IRETURN) == Opcodes.IRETURN) @@ -64,7 +70,7 @@ class BTypesTest { assert(FLOAT.typedOpcode(Opcodes.IRETURN) == Opcodes.FRETURN) assert(LONG.typedOpcode(Opcodes.IRETURN) == Opcodes.LRETURN) assert(DOUBLE.typedOpcode(Opcodes.IRETURN) == Opcodes.DRETURN) - assert(classBTypeFromSymbol(jls).typedOpcode(Opcodes.IRETURN) == Opcodes.ARETURN) + assert(classBTFS(jls).typedOpcode(Opcodes.IRETURN) == Opcodes.ARETURN) } @Test diff --git a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala index c1c5a71b83..5d5215d887 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala @@ -2,16 +2,20 @@ package scala.tools.nsc.backend.jvm import org.junit.Assert._ +import scala.collection.mutable.ListBuffer import scala.reflect.internal.util.BatchSourceFile import scala.reflect.io.VirtualDirectory import scala.tools.asm.Opcodes -import scala.tools.asm.tree.{AbstractInsnNode, LabelNode, ClassNode, MethodNode} +import scala.tools.asm.tree.{ClassNode, MethodNode} import scala.tools.cmd.CommandLineParser import scala.tools.nsc.backend.jvm.opt.LocalOpt -import scala.tools.nsc.settings.{MutableSettings, ScalaSettings} +import scala.tools.nsc.io.AbstractFile +import scala.tools.nsc.reporters.StoreReporter +import scala.tools.nsc.settings.MutableSettings import scala.tools.nsc.{Settings, Global} import scala.tools.partest.ASMConverters import scala.collection.JavaConverters._ +import scala.tools.testing.TempDir object CodeGenTools { import ASMConverters._ @@ -40,38 +44,104 @@ object CodeGenTools { } def newCompiler(defaultArgs: String = "-usejavacp", extraArgs: String = ""): Global = { + val compiler = newCompilerWithoutVirtualOutdir(defaultArgs, extraArgs) + resetOutput(compiler) + compiler + } + + def newCompilerWithoutVirtualOutdir(defaultArgs: String = "-usejavacp", extraArgs: String = ""): Global = { val settings = new Settings() val args = (CommandLineParser tokenize defaultArgs) ++ (CommandLineParser tokenize extraArgs) settings.processArguments(args, processAll = true) - val compiler = new Global(settings) - resetOutput(compiler) - compiler + new Global(settings, new StoreReporter) } - def compile(compiler: Global)(code: String): List[(String, Array[Byte])] = { + def newRun(compiler: Global): compiler.Run = { compiler.reporter.reset() resetOutput(compiler) - val run = new compiler.Run() - run.compileSources(List(new BatchSourceFile("unitTestSource.scala", code))) - val outDir = compiler.settings.outputDirs.getSingleOutput.get - (for (f <- outDir.iterator if !f.isDirectory) yield (f.name, f.toByteArray)).toList + new compiler.Run() + } + + def reporter(compiler: Global) = compiler.reporter.asInstanceOf[StoreReporter] + + def makeSourceFile(code: String, filename: String): BatchSourceFile = new BatchSourceFile(filename, code) + + def getGeneratedClassfiles(outDir: AbstractFile): List[(String, Array[Byte])] = { + def files(dir: AbstractFile): List[(String, Array[Byte])] = { + val res = ListBuffer.empty[(String, Array[Byte])] + for (f <- dir.iterator) { + if (!f.isDirectory) res += ((f.name, f.toByteArray)) + else if (f.name != "." && f.name != "..") res ++= files(f) + } + res.toList + } + files(outDir) + } + + def checkReport(compiler: Global, allowMessage: StoreReporter#Info => Boolean = _ => false): Unit = { + val disallowed = reporter(compiler).infos.toList.filter(!allowMessage(_)) // toList prevents an infer-non-wildcard-existential warning. + if (disallowed.nonEmpty) { + val msg = disallowed.mkString("\n") + assert(false, "The compiler issued non-allowed warnings or errors:\n" + msg) + } + } + + def compile(compiler: Global)(scalaCode: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[(String, Array[Byte])] = { + val run = newRun(compiler) + run.compileSources(makeSourceFile(scalaCode, "unitTestSource.scala") :: javaCode.map(p => makeSourceFile(p._1, p._2))) + checkReport(compiler, allowMessage) + getGeneratedClassfiles(compiler.settings.outputDirs.getSingleOutput.get) + } + + /** + * Compile multiple Scala files separately into a single output directory. + * + * Note that a new compiler instance is created for compiling each file because symbols survive + * across runs. This makes separate compilation slower. + * + * The output directory is a physical directory, I have not figured out if / how it's possible to + * add a VirtualDirectory to the classpath of a compiler. + */ + def compileSeparately(codes: List[String], extraArgs: String = "", allowMessage: StoreReporter#Info => Boolean = _ => false, afterEach: AbstractFile => Unit = _ => ()): List[(String, Array[Byte])] = { + val outDir = AbstractFile.getDirectory(TempDir.createTempDir()) + val outDirPath = outDir.canonicalPath + val argsWithOutDir = extraArgs + s" -d $outDirPath -cp $outDirPath" + + for (code <- codes) { + val compiler = newCompilerWithoutVirtualOutdir(extraArgs = argsWithOutDir) + new compiler.Run().compileSources(List(makeSourceFile(code, "unitTestSource.scala"))) + checkReport(compiler, allowMessage) + afterEach(outDir) + } + + val classfiles = getGeneratedClassfiles(outDir) + outDir.delete() + classfiles + } + + def compileClassesSeparately(codes: List[String], extraArgs: String = "", allowMessage: StoreReporter#Info => Boolean = _ => false, afterEach: AbstractFile => Unit = _ => ()) = { + readAsmClasses(compileSeparately(codes, extraArgs, allowMessage, afterEach)) + } + + def readAsmClasses(classfiles: List[(String, Array[Byte])]) = { + classfiles.map(p => AsmUtils.readClass(p._2)).sortBy(_.name) } - def compileClasses(compiler: Global)(code: String): List[ClassNode] = { - compile(compiler)(code).map(p => AsmUtils.readClass(p._2)).sortBy(_.name) + def compileClasses(compiler: Global)(code: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[ClassNode] = { + readAsmClasses(compile(compiler)(code, javaCode, allowMessage)) } - def compileMethods(compiler: Global)(code: String): List[MethodNode] = { - compileClasses(compiler)(s"class C { $code }").head.methods.asScala.toList.filterNot(_.name == "<init>") + def compileMethods(compiler: Global)(code: String, allowMessage: StoreReporter#Info => Boolean = _ => false): List[MethodNode] = { + compileClasses(compiler)(s"class C { $code }", allowMessage = allowMessage).head.methods.asScala.toList.filterNot(_.name == "<init>") } - def singleMethodInstructions(compiler: Global)(code: String): List[Instruction] = { - val List(m) = compileMethods(compiler)(code) + def singleMethodInstructions(compiler: Global)(code: String, allowMessage: StoreReporter#Info => Boolean = _ => false): List[Instruction] = { + val List(m) = compileMethods(compiler)(code, allowMessage = allowMessage) instructionsFromMethod(m) } - def singleMethod(compiler: Global)(code: String): Method = { - val List(m) = compileMethods(compiler)(code) + def singleMethod(compiler: Global)(code: String, allowMessage: StoreReporter#Info => Boolean = _ => false): Method = { + val List(m) = compileMethods(compiler)(code, allowMessage = allowMessage) convertMethod(m) } diff --git a/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala b/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala index 89900291ca..4086f7dd7b 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala @@ -7,10 +7,18 @@ import org.junit.Assert._ import CodeGenTools._ import scala.tools.asm.Opcodes._ import scala.tools.partest.ASMConverters._ +import scala.tools.testing.ClearAfterClass + +object DirectCompileTest extends ClearAfterClass.Clearable { + var compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:method") + def clear(): Unit = { compiler = null } +} @RunWith(classOf[JUnit4]) -class DirectCompileTest { - val compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:method") +class DirectCompileTest extends ClearAfterClass { + ClearAfterClass.stateToClear = DirectCompileTest + + val compiler = DirectCompileTest.compiler @Test def testCompile(): Unit = { @@ -70,4 +78,21 @@ class DirectCompileTest { Label(11) )) } + + @Test + def testSeparateCompilation(): Unit = { + val codeA = "class A { def f = 1 }" + val codeB = "class B extends A { def g = f }" + val List(a, b) = compileClassesSeparately(List(codeA, codeB)) + val ins = getSingleMethod(b, "g").instructions + assert(ins exists { + case Invoke(_, "B", "f", _, _) => true + case _ => false + }, ins) + } + + @Test + def compileErroneous(): Unit = { + compileClasses(compiler)("class C { def f: String = 1 }", allowMessage = _.msg contains "type mismatch") + } } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala index 2975bd060d..1b6c080234 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala @@ -15,11 +15,14 @@ import CodeGenTools._ import scala.tools.partest.ASMConverters import ASMConverters._ +import BackendReporting._ + import scala.collection.convert.decorateAsScala._ @RunWith(classOf[JUnit4]) class BTypesFromClassfileTest { - val compiler = newCompiler(extraArgs = "-Ybackend:GenBCode") + // inliner enabled -> inlineInfos are collected (and compared) in ClassBTypes + val compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:inline-global") import compiler._ import definitions._ @@ -29,6 +32,7 @@ class BTypesFromClassfileTest { def duringBackend[T](f: => T) = compiler.exitingDelambdafy(f) val run = new compiler.Run() // initializes some of the compiler + duringBackend(compiler.scalaPrimitives.init()) // needed: it's only done when running the backend, and we don't actually run the compiler duringBackend(bTypes.initializeCoreBTypes()) def clearCache() = bTypes.classBTypeFromInternalName.clear() @@ -37,7 +41,7 @@ class BTypesFromClassfileTest { if (checked(fromSym.internalName)) checked else { assert(fromSym == fromClassfile, s"$fromSym != $fromClassfile") - sameInfo(fromSym.info, fromClassfile.info, checked + fromSym.internalName) + sameInfo(fromSym.info.get, fromClassfile.info.get, checked + fromSym.internalName) } } @@ -57,8 +61,12 @@ class BTypesFromClassfileTest { else (fromSym.flags | ACC_PRIVATE | ACC_PUBLIC) == (fromClassfile.flags | ACC_PRIVATE | ACC_PUBLIC) }, s"class flags differ\n$fromSym\n$fromClassfile") - val chk1 = sameBTypes(fromSym.superClass, fromClassfile.superClass, checked) + // we don't compare InlineInfos in this test: in both cases (from symbol and from classfile) they + // are actually created by looking at the classfile members, not the symbol's. InlineInfos are only + // built from symbols for classes that are being compiled, which is not the case here. Instead + // there's a separate InlineInfoTest. + val chk1 = sameBTypes(fromSym.superClass, fromClassfile.superClass, checked) val chk2 = sameBTypes(fromSym.interfaces, fromClassfile.interfaces, chk1) // The fromSym info has only member classes, no local or anonymous. The symbol is read from the @@ -67,7 +75,7 @@ class BTypesFromClassfileTest { // and anonymous classes as members of the outer class. But not for unpickled symbols). // The fromClassfile info has all nested classes, including anonymous and local. So we filter // them out: member classes are identified by having the `outerName` defined. - val memberClassesFromClassfile = fromClassfile.nestedClasses.filter(_.info.nestedInfo.get.outerName.isDefined) + val memberClassesFromClassfile = fromClassfile.nestedClasses.filter(_.info.get.nestedInfo.get.outerName.isDefined) // Sorting is required: the backend sorts all InnerClass entries by internalName before writing // them to the classfile (to make it deterministic: the entries are collected in a Set during // code generation). diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala new file mode 100644 index 0000000000..9fda034a04 --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala @@ -0,0 +1,152 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import scala.collection.generic.Clearable +import scala.tools.asm.Opcodes._ +import org.junit.Assert._ + +import scala.tools.asm.tree._ +import scala.tools.asm.tree.analysis._ +import scala.tools.nsc.reporters.StoreReporter +import scala.tools.testing.AssertUtil._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import AsmUtils._ +import BackendReporting._ + +import scala.collection.convert.decorateAsScala._ + +@RunWith(classOf[JUnit4]) +class CallGraphTest { + val compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:inline-global -Yopt-warnings") + import compiler.genBCode.bTypes._ + + // allows inspecting the caches after a compilation run + val notPerRun: List[Clearable] = List(classBTypeFromInternalName, byteCodeRepository.classes, callGraph.callsites) + notPerRun foreach compiler.perRunCaches.unrecordCache + + def compile(code: String, allowMessage: StoreReporter#Info => Boolean): List[ClassNode] = { + notPerRun.foreach(_.clear()) + compileClasses(compiler)(code, allowMessage = allowMessage) + } + + def callsInMethod(methodNode: MethodNode): List[MethodInsnNode] = methodNode.instructions.iterator.asScala.collect({ + case call: MethodInsnNode => call + }).toList + + @Test + def callGraphStructure(): Unit = { + val code = + """class C { + | // try-catch prevents inlining - we want to analyze the callsite + | def f1 = try { 0 } catch { case _: Throwable => 1 } + | final def f2 = try { 0 } catch { case _: Throwable => 1 } + | + | @inline def f3 = try { 0 } catch { case _: Throwable => 1 } + | @inline final def f4 = try { 0 } catch { case _: Throwable => 1 } + | + | @noinline def f5 = try { 0 } catch { case _: Throwable => 1 } + | @noinline final def f6 = try { 0 } catch { case _: Throwable => 1 } + | + | @inline @noinline def f7 = try { 0 } catch { case _: Throwable => 1 } + |} + |class D extends C { + | @inline override def f1 = try { 0 } catch { case _: Throwable => 1 } + | override final def f3 = try { 0 } catch { case _: Throwable => 1 } + |} + |object C { + | def g1 = try { 0 } catch { case _: Throwable => 1 } + |} + |class Test { + | def t1(c: C) = c.f1 + c.f2 + c.f3 + c.f4 + c.f5 + c.f6 + c.f7 + C.g1 + | def t2(d: D) = d.f1 + d.f2 + d.f3 + d.f4 + d.f5 + d.f6 + d.f7 + C.g1 + |} + """.stripMargin + + // Get the ClassNodes from the code repo (don't use the unparsed ClassNodes returned by compile). + // The callGraph.callsites map is indexed by instructions of those ClassNodes. + + val ok = Set( + "D::f1()I is annotated @inline but cannot be inlined: the method is not final and may be overridden", // only one warning for D.f1: C.f1 is not annotated @inline + "C::f3()I is annotated @inline but cannot be inlined: the method is not final and may be overridden", // only one warning for C.f3: D.f3 does not have @inline (and it would also be safe to inline) + "C::f7()I is annotated @inline but cannot be inlined: the method is not final and may be overridden", // two warnings (the error message mentions C.f7 even if the receiver type is D, because f7 is inherited from C) + "operand stack at the callsite in Test::t1(LC;)I contains more values", + "operand stack at the callsite in Test::t2(LD;)I contains more values") + var msgCount = 0 + val checkMsg = (m: StoreReporter#Info) => { + msgCount += 1 + ok exists (m.msg contains _) + } + val List(cCls, cMod, dCls, testCls) = compile(code, checkMsg).map(c => byteCodeRepository.classNode(c.name).get) + assert(msgCount == 6, msgCount) + + val List(cf1, cf2, cf3, cf4, cf5, cf6, cf7) = cCls.methods.iterator.asScala.filter(_.name.startsWith("f")).toList.sortBy(_.name) + val List(df1, df3) = dCls.methods.iterator.asScala.filter(_.name.startsWith("f")).toList.sortBy(_.name) + val g1 = cMod.methods.iterator.asScala.find(_.name == "g1").get + val List(t1, t2) = testCls.methods.iterator.asScala.filter(_.name.startsWith("t")).toList.sortBy(_.name) + + val List(cf1Call, cf2Call, cf3Call, cf4Call, cf5Call, cf6Call, cf7Call, cg1Call) = callsInMethod(t1) + val List(df1Call, df2Call, df3Call, df4Call, df5Call, df6Call, df7Call, dg1Call) = callsInMethod(t2) + + def checkCallsite(callsite: callGraph.Callsite, + call: MethodInsnNode, callsiteMethod: MethodNode, target: MethodNode, calleeDeclClass: ClassBType, + safeToInline: Boolean, atInline: Boolean, atNoInline: Boolean) = try { + assert(callsite.callsiteInstruction == call) + assert(callsite.callsiteMethod == callsiteMethod) + val callee = callsite.callee.get + assert(callee.callee == target) + assert(callee.calleeDeclarationClass == calleeDeclClass) + assert(callee.safeToInline == safeToInline) + assert(callee.annotatedInline == atInline) + assert(callee.annotatedNoInline == atNoInline) + + assert(callsite.argInfos == List()) // not defined yet + } catch { + case e: Throwable => println(callsite); throw e + } + + val cClassBType = classBTypeFromClassNode(cCls) + val cMClassBType = classBTypeFromClassNode(cMod) + val dClassBType = classBTypeFromClassNode(dCls) + + checkCallsite(callGraph.callsites(cf1Call), + cf1Call, t1, cf1, cClassBType, false, false, false) + checkCallsite(callGraph.callsites(cf2Call), + cf2Call, t1, cf2, cClassBType, true, false, false) + checkCallsite(callGraph.callsites(cf3Call), + cf3Call, t1, cf3, cClassBType, false, true, false) + checkCallsite(callGraph.callsites(cf4Call), + cf4Call, t1, cf4, cClassBType, true, true, false) + checkCallsite(callGraph.callsites(cf5Call), + cf5Call, t1, cf5, cClassBType, false, false, true) + checkCallsite(callGraph.callsites(cf6Call), + cf6Call, t1, cf6, cClassBType, true, false, true) + checkCallsite(callGraph.callsites(cf7Call), + cf7Call, t1, cf7, cClassBType, false, true, true) + checkCallsite(callGraph.callsites(cg1Call), + cg1Call, t1, g1, cMClassBType, true, false, false) + + checkCallsite(callGraph.callsites(df1Call), + df1Call, t2, df1, dClassBType, false, true, false) + checkCallsite(callGraph.callsites(df2Call), + df2Call, t2, cf2, cClassBType, true, false, false) + checkCallsite(callGraph.callsites(df3Call), + df3Call, t2, df3, dClassBType, true, false, false) + checkCallsite(callGraph.callsites(df4Call), + df4Call, t2, cf4, cClassBType, true, true, false) + checkCallsite(callGraph.callsites(df5Call), + df5Call, t2, cf5, cClassBType, false, false, true) + checkCallsite(callGraph.callsites(df6Call), + df6Call, t2, cf6, cClassBType, true, false, true) + checkCallsite(callGraph.callsites(df7Call), + df7Call, t2, cf7, cClassBType, false, true, true) + checkCallsite(callGraph.callsites(dg1Call), + dg1Call, t2, g1, cMClassBType, true, false, false) + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala index fc748196d0..76492cfa23 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala @@ -17,8 +17,8 @@ class CompactLocalVariablesTest { // recurse-unreachable-jumps is required for eliminating catch blocks, in the first dce round they // are still live.only after eliminating the empty handler the catch blocks become unreachable. - val methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code,recurse-unreachable-jumps,compact-locals") - val noCompactVarsCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code,recurse-unreachable-jumps") + val methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code,compact-locals") + val noCompactVarsCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code") @Test def compactUnused(): Unit = { diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala index 7d83c54b5b..7b0504fec0 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala @@ -11,9 +11,23 @@ import org.junit.Assert._ import CodeGenTools._ import scala.tools.partest.ASMConverters import ASMConverters._ +import scala.tools.testing.ClearAfterClass + +object EmptyExceptionHandlersTest extends ClearAfterClass.Clearable { + var noOptCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:none") + var dceCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:unreachable-code") + def clear(): Unit = { + noOptCompiler = null + dceCompiler = null + } +} @RunWith(classOf[JUnit4]) -class EmptyExceptionHandlersTest { +class EmptyExceptionHandlersTest extends ClearAfterClass { + ClearAfterClass.stateToClear = EmptyExceptionHandlersTest + + val noOptCompiler = EmptyExceptionHandlersTest.noOptCompiler + val dceCompiler = EmptyExceptionHandlersTest.dceCompiler val exceptionDescriptor = "java/lang/Exception" @@ -51,9 +65,6 @@ class EmptyExceptionHandlersTest { assertTrue(convertMethod(asmMethod).handlers.isEmpty) } - val noOptCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:none") - val dceCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:unreachable-code") - @Test def eliminateUnreachableHandler(): Unit = { val code = "def f: Unit = try { } catch { case _: Exception => println(0) }; println(1)" diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala new file mode 100644 index 0000000000..57088bdd2f --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala @@ -0,0 +1,67 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import scala.collection.generic.Clearable +import org.junit.Assert._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import AsmUtils._ +import scala.tools.testing.ClearAfterClass + +import BackendReporting._ + +import scala.collection.convert.decorateAsScala._ + +object InlineInfoTest extends ClearAfterClass.Clearable { + var compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:classpath") + def clear(): Unit = { compiler = null } + + def notPerRun: List[Clearable] = List(compiler.genBCode.bTypes.classBTypeFromInternalName, compiler.genBCode.bTypes.byteCodeRepository.classes) + notPerRun foreach compiler.perRunCaches.unrecordCache +} + +@RunWith(classOf[JUnit4]) +class InlineInfoTest { + val compiler = InlineInfoTest.compiler + + def compile(code: String) = { + InlineInfoTest.notPerRun.foreach(_.clear()) + compileClasses(compiler)(code) + } + + @Test + def inlineInfosFromSymbolAndAttribute(): Unit = { + val code = + """trait T { + | @inline def f: Int + | @noinline final def g = 0 + |} + |trait U { self: T => + | @inline def f = 0 + | final def h = 0 + | final class K { + | @inline def i = 0 + | } + |} + |sealed trait V { + | @inline def j = 0 + |} + |class C extends T with U + """.stripMargin + val classes = compile(code) + val fromSyms = classes.map(c => compiler.genBCode.bTypes.classBTypeFromInternalName(c.name).info.get.inlineInfo) + + val fromAttrs = classes.map(c => { + assert(c.attrs.asScala.exists(_.isInstanceOf[InlineInfoAttribute]), c.attrs) + compiler.genBCode.bTypes.inlineInfoFromClassfile(c) + }) + + assert(fromSyms == fromAttrs) + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlineWarningTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineWarningTest.scala new file mode 100644 index 0000000000..fedc074a15 --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineWarningTest.scala @@ -0,0 +1,146 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import scala.collection.generic.Clearable +import scala.collection.mutable.ListBuffer +import scala.reflect.internal.util.BatchSourceFile +import scala.tools.asm.Opcodes._ +import org.junit.Assert._ + +import scala.tools.asm.tree._ +import scala.tools.asm.tree.analysis._ +import scala.tools.nsc.backend.jvm.opt.BytecodeUtils.AsmAnalyzer +import scala.tools.nsc.io._ +import scala.tools.nsc.reporters.StoreReporter +import scala.tools.testing.AssertUtil._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import AsmUtils._ + +import BackendReporting._ + +import scala.collection.convert.decorateAsScala._ +import scala.tools.testing.ClearAfterClass + +object InlineWarningTest extends ClearAfterClass.Clearable { + val argsNoWarn = "-Ybackend:GenBCode -Yopt:l:classpath" + val args = argsNoWarn + " -Yopt-warnings" + var compiler = newCompiler(extraArgs = args) + def clear(): Unit = { compiler = null } +} + +@RunWith(classOf[JUnit4]) +class InlineWarningTest extends ClearAfterClass { + ClearAfterClass.stateToClear = InlineWarningTest + + val compiler = InlineWarningTest.compiler + + def compile(scalaCode: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[ClassNode] = { + compileClasses(compiler)(scalaCode, javaCode, allowMessage) + } + + @Test + def nonFinal(): Unit = { + val code = + """class C { + | @inline def m1 = 1 + |} + |trait T { + | @inline def m2 = 1 + |} + |class D extends C with T + | + |class Test { + | def t1(c: C, t: T, d: D) = c.m1 + t.m2 + d.m1 + d.m2 + |} + """.stripMargin + var count = 0 + val warns = Set( + "C::m1()I is annotated @inline but cannot be inlined: the method is not final and may be overridden", + "T::m2()I is annotated @inline but cannot be inlined: the method is not final and may be overridden", + "D::m2()I is annotated @inline but cannot be inlined: the method is not final and may be overridden") + compile(code, allowMessage = i => {count += 1; warns.exists(i.msg contains _)}) + assert(count == 4, count) + } + + @Test + def traitMissingImplClass(): Unit = { + val codeA = "trait T { @inline final def f = 1 }" + val codeB = "class C { def t1(t: T) = t.f }" + + val removeImpl = (outDir: AbstractFile) => { + val f = outDir.lookupName("T$class.class", directory = false) + if (f != null) f.delete() + } + + val warn = + """T::f()I is annotated @inline but cannot be inlined: the trait method call could not be rewritten to the static implementation method. Possible reason: + |The method f(LT;)I could not be found in the class T$class or any of its parents. + |Note that the following parent classes could not be found on the classpath: T$class""".stripMargin + + var c = 0 + compileSeparately(List(codeA, codeB), extraArgs = InlineWarningTest.args, afterEach = removeImpl, allowMessage = i => {c += 1; i.msg contains warn}) + assert(c == 1, c) + + // only summary here + compileSeparately(List(codeA, codeB), extraArgs = InlineWarningTest.argsNoWarn, afterEach = removeImpl, allowMessage = _.msg contains "there was one inliner warning") + } + + @Test + def handlerNonEmptyStack(): Unit = { + val code = + """class C { + | @noinline def q = 0 + | @inline final def foo = try { q } catch { case e: Exception => 2 } + | def t1 = println(foo) // inline warning here: foo cannot be inlined on top of a non-empty stack + |} + """.stripMargin + + var c = 0 + compile(code, allowMessage = i => {c += 1; i.msg contains "operand stack at the callsite in C::t1()V contains more values"}) + assert(c == 1, c) + } + + @Test + def mixedWarnings(): Unit = { + val javaCode = + """public class A { + | public static final int bar() { return 100; } + |} + """.stripMargin + + val scalaCode = + """class B { + | @inline final def flop = A.bar + | def g = flop + |} + """.stripMargin + + val warns = List( + """failed to determine if bar should be inlined: + |The method bar()I could not be found in the class A or any of its parents. + |Note that the following parent classes are defined in Java sources (mixed compilation), no bytecode is available: A""".stripMargin, + + """B::flop()I is annotated @inline but could not be inlined: + |Failed to check if B::flop()I can be safely inlined to B without causing an IllegalAccessError. Checking instruction INVOKESTATIC A.bar ()I failed: + |The method bar()I could not be found in the class A or any of its parents. + |Note that the following parent classes are defined in Java sources (mixed compilation), no bytecode is available: A""".stripMargin) + + var c = 0 + val List(b) = compile(scalaCode, List((javaCode, "A.java")), allowMessage = i => {c += 1; warns.tail.exists(i.msg contains _)}) + assert(c == 1, c) + + // no warnings here + compileClasses(newCompiler(extraArgs = InlineWarningTest.argsNoWarn + " -Yopt-warnings:none"))(scalaCode, List((javaCode, "A.java"))) + + c = 0 + compileClasses(newCompiler(extraArgs = InlineWarningTest.argsNoWarn + " -Yopt-warnings:no-inline-mixed"))(scalaCode, List((javaCode, "A.java")), allowMessage = i => {c += 1; warns.exists(i.msg contains _)}) + assert(c == 2, c) + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala new file mode 100644 index 0000000000..b4839dcec8 --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala @@ -0,0 +1,198 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import scala.tools.asm.Opcodes._ +import org.junit.Assert._ + +import scala.tools.asm.tree._ +import scala.tools.testing.AssertUtil._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import AsmUtils._ + +import scala.collection.convert.decorateAsScala._ +import scala.tools.testing.ClearAfterClass + +object InlinerIllegalAccessTest extends ClearAfterClass.Clearable { + var compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:none") + def clear(): Unit = { compiler = null } +} + +@RunWith(classOf[JUnit4]) +class InlinerIllegalAccessTest extends ClearAfterClass { + ClearAfterClass.stateToClear = InlinerIllegalAccessTest + + val compiler = InlinerIllegalAccessTest.compiler + import compiler.genBCode.bTypes._ + + def addToRepo(cls: List[ClassNode]): Unit = for (c <- cls) byteCodeRepository.add(c, ByteCodeRepository.Classfile) + def assertEmpty(ins: Option[AbstractInsnNode]) = for (i <- ins) throw new AssertionError(textify(i)) + + @Test + def typeAccessible(): Unit = { + val code = + """package a { + | private class C { // the Scala compiler makes all classes public + | def f1 = new C // NEW a/C + | def f2 = new Array[C](0) // ANEWARRAY a/C + | def f3 = new Array[Array[C]](0) // ANEWARRAY [La/C; + | } + | class D + |} + |package b { + | class E + |} + """.stripMargin + + val allClasses = compileClasses(compiler)(code) + val List(cClass, dClass, eClass) = allClasses + assert(cClass.name == "a/C" && dClass.name == "a/D" && eClass.name == "b/E", s"${cClass.name}, ${dClass.name}, ${eClass.name}") + addToRepo(allClasses) // they are not on the compiler's classpath, so we add them manually to the code repo + + val methods = cClass.methods.asScala.filter(_.name(0) == 'f').toList + + def check(classNode: ClassNode, test: Option[AbstractInsnNode] => Unit) = { + for (m <- methods) + test(inliner.findIllegalAccess(m.instructions, classBTypeFromParsedClassfile(cClass.name), classBTypeFromParsedClassfile(classNode.name)).map(_._1)) + } + + check(cClass, assertEmpty) + check(dClass, assertEmpty) + check(eClass, assertEmpty) // C is public, so accessible in E + + byteCodeRepository.classes.clear() + classBTypeFromInternalName.clear() + + cClass.access &= ~ACC_PUBLIC // ftw + addToRepo(allClasses) + + // private classes can be accessed from the same package + check(cClass, assertEmpty) + check(dClass, assertEmpty) // accessing a private class in the same package is OK + check(eClass, { + case Some(ti: TypeInsnNode) if Set("a/C", "[La/C;")(ti.desc) => () + // MatchError otherwise + }) + } + + @Test + def memberAccessible(): Unit = { + val code = + """package a { + | class C { + | /*public*/ def a = 0 + | /*default*/ def b = 0 + | protected def c = 0 + | private def d = 0 + | + | /*public static*/ def e = 0 + | /*default static*/ def f = 0 + | protected /*static*/ def g = 0 + | private /*static*/ def h = 0 + | + | def raC = a + | def rbC = b + | def rcC = c + | def rdC = d + | def reC = e + | def rfC = f + | def rgC = g + | def rhC = h + | } + | + | class D extends C { + | def rbD = b // 1: default access b, accessed in D, declared in C. can be inlined into any class in the same package as C. + | def rcD = c // 2: protected c, accessed in D. can be inlined into C, D or E, but not into F (F and D are unrelated). + | + | def rfD = f // 1 + | def rgD = g // 2 + | } + | class E extends D + | + | class F extends C + | + | class G + |} + | + |package b { + | class H extends a.C + | class I + |} + """.stripMargin + + val allClasses = compileClasses(compiler)(code) + val List(cCl, dCl, eCl, fCl, gCl, hCl, iCl) = allClasses + addToRepo(allClasses) + + // set flags that Scala scala doesn't (default access, static) - a hacky way to test all access modes. + val names = ('a' to 'h').map(_.toString).toSet + val List(a, b, c, d, e, f, g, h) = cCl.methods.asScala.toList.filter(m => names(m.name)) + + def checkAccess(a: MethodNode, expected: Int): Unit = { + assert((a.access & (ACC_STATIC | ACC_PUBLIC | ACC_PROTECTED | ACC_PRIVATE)) == expected, s"${a.name}, ${a.access}") + } + + checkAccess(a, ACC_PUBLIC) + b.access &= ~ACC_PUBLIC; checkAccess(b, 0) // make it default access + c.access &= ~ACC_PUBLIC; c.access |= ACC_PROTECTED; checkAccess(c, ACC_PROTECTED) // make it protected - scalac actually never emits PROTECTED in bytecode, see javaFlags in BTypesFromSymbols + checkAccess(d, ACC_PRIVATE) + + e.access |= ACC_STATIC; checkAccess(e, ACC_STATIC | ACC_PUBLIC) + f.access &= ~ACC_PUBLIC; f.access |= ACC_STATIC; checkAccess(f, ACC_STATIC) + g.access &= ~ACC_PUBLIC; g.access |= (ACC_STATIC | ACC_PROTECTED); checkAccess(g, ACC_STATIC | ACC_PROTECTED) + h.access |= ACC_STATIC; checkAccess(h, ACC_STATIC | ACC_PRIVATE) + + val List(raC, rbC, rcC, rdC, reC, rfC, rgC, rhC) = cCl.methods.asScala.toList.filter(_.name(0) == 'r').sortBy(_.name) + + val List(rbD, rcD, rfD, rgD) = dCl.methods.asScala.toList.filter(_.name(0) == 'r').sortBy(_.name) + + def check(method: MethodNode, decl: ClassNode, dest: ClassNode, test: Option[AbstractInsnNode] => Unit): Unit = { + test(inliner.findIllegalAccess(method.instructions, classBTypeFromParsedClassfile(decl.name), classBTypeFromParsedClassfile(dest.name)).map(_._1)) + } + + val cOrDOwner = (_: Option[AbstractInsnNode] @unchecked) match { + case Some(mi: MethodInsnNode) if Set("a/C", "a/D")(mi.owner) => () + // MatchError otherwise + } + + // PUBLIC + + // public methods allowed everywhere + for (m <- Set(raC, reC); c <- allClasses) check(m, cCl, c, assertEmpty) + + // DEFAULT ACCESS + + // default access OK in same package + for ((m, declCls) <- Set((rbC, cCl), (rfC, cCl), (rbD, dCl), (rfD, dCl)); c <- allClasses) { + if (c.name startsWith "a/") check(m, declCls, c, assertEmpty) + else check(m, declCls, c, cOrDOwner) + } + + // PROTECTED + + // protected accessed in same class, or protected static accessed in subclass(rgD). + // can be inlined to subclasses, and classes in the same package (gCl) + for ((m, declCls) <- Set((rcC, cCl), (rgC, cCl), (rgD, dCl)); c <- Set(cCl, dCl, eCl, fCl, gCl, hCl)) check(m, declCls, c, assertEmpty) + + // protected in non-subclass and different package + for (m <- Set(rcC, rgC)) check(m, cCl, iCl, cOrDOwner) + + // non-static protected accessed in subclass (rcD). can be inlined to related class, or classes in the same package + for (c <- Set(cCl, dCl, eCl, fCl, gCl)) check(rcD, dCl, c, assertEmpty) + + // rcD cannot be inlined into non-related classes, if the declaration and destination are not in the same package + for (c <- Set(hCl, iCl)) check(rcD, dCl, c, cOrDOwner) + + // PRIVATE + + // privated method accesses can only be inlined in the same class + for (m <- Set(rdC, rhC)) check(m, cCl, cCl, assertEmpty) + for (m <- Set(rdC, rhC); c <- allClasses.tail) check(m, cCl, c, cOrDOwner) + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala new file mode 100644 index 0000000000..5c9bd1c188 --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala @@ -0,0 +1,115 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import scala.tools.asm.Opcodes._ +import org.junit.Assert._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import AsmUtils._ + +import scala.collection.convert.decorateAsScala._ + +object InlinerSeparateCompilationTest { + val args = "-Ybackend:GenBCode -Yopt:l:classpath" +} + +@RunWith(classOf[JUnit4]) +class InlinerSeparateCompilationTest { + import InlinerSeparateCompilationTest._ + import InlinerTest.{listStringLines, assertInvoke, assertNoInvoke} + + @Test + def inlnieMixedinMember(): Unit = { + val codeA = + """trait T { + | @inline def f = 0 + |} + |object O extends T { + | @inline def g = 1 + |} + """.stripMargin + + val codeB = + """class C { + | def t1(t: T) = t.f + | def t2 = O.f + | def t3 = O.g + |} + """.stripMargin + + val warn = "T::f()I is annotated @inline but cannot be inlined: the method is not final and may be overridden" + val List(c, o, oMod, t, tCls) = compileClassesSeparately(List(codeA, codeB), args + " -Yopt-warnings", _.msg contains warn) + assertInvoke(getSingleMethod(c, "t1"), "T", "f") + assertNoInvoke(getSingleMethod(c, "t2")) + assertNoInvoke(getSingleMethod(c, "t3")) + } + + @Test + def inlineSealedMember(): Unit = { + val codeA = + """sealed trait T { + | @inline def f = 1 + |} + """.stripMargin + + val codeB = + """class C { + | def t1(t: T) = t.f + |} + """.stripMargin + + val List(c, t, tCls) = compileClassesSeparately(List(codeA, codeB), args) + assertNoInvoke(getSingleMethod(c, "t1")) + } + + @Test + def inlineInheritedMember(): Unit = { + val codeA = + """trait T { + | @inline final def f = 1 + |} + |trait U extends T { + | @inline final def g = f + |} + """.stripMargin + + val codeB = + """class C extends U { + | def t1 = this.f + | def t2 = this.g + | def t3(t: T) = t.f + |} + """.stripMargin + + val List(c, t, tCls, u, uCls) = compileClassesSeparately(List(codeA, codeB), args) + for (m <- List("t1", "t2", "t3")) assertNoInvoke(getSingleMethod(c, m)) + } + + @Test + def inlineWithSelfType(): Unit = { + val assembly = + """trait Assembly extends T { + | @inline final def g = 1 + | @inline final def n = m + |} + """.stripMargin + + val codeA = + s"""trait T { self: Assembly => + | @inline final def f = g + | @inline final def m = 1 + |} + |$assembly + """.stripMargin + + val List(a, aCls, t, tCls) = compileClassesSeparately(List(codeA, assembly), args) + assertNoInvoke(getSingleMethod(tCls, "f")) + assertNoInvoke(getSingleMethod(aCls, "n")) + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala new file mode 100644 index 0000000000..39fb28570e --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -0,0 +1,953 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import scala.collection.generic.Clearable +import scala.collection.mutable.ListBuffer +import scala.reflect.internal.util.BatchSourceFile +import scala.tools.asm.Opcodes._ +import org.junit.Assert._ + +import scala.tools.asm.tree._ +import scala.tools.asm.tree.analysis._ +import scala.tools.nsc.backend.jvm.opt.BytecodeUtils.AsmAnalyzer +import scala.tools.nsc.io._ +import scala.tools.nsc.reporters.StoreReporter +import scala.tools.testing.AssertUtil._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import AsmUtils._ + +import BackendReporting._ + +import scala.collection.convert.decorateAsScala._ +import scala.tools.testing.ClearAfterClass + +object InlinerTest extends ClearAfterClass.Clearable { + val args = "-Ybackend:GenBCode -Yopt:l:classpath -Yopt-warnings" + var compiler = newCompiler(extraArgs = args) + + // allows inspecting the caches after a compilation run + def notPerRun: List[Clearable] = List(compiler.genBCode.bTypes.classBTypeFromInternalName, compiler.genBCode.bTypes.byteCodeRepository.classes, compiler.genBCode.bTypes.callGraph.callsites) + notPerRun foreach compiler.perRunCaches.unrecordCache + + def clear(): Unit = { compiler = null } + + implicit class listStringLines[T](val l: List[T]) extends AnyVal { + def stringLines = l.mkString("\n") + } + + def assertNoInvoke(m: Method): Unit = assertNoInvoke(m.instructions) + def assertNoInvoke(ins: List[Instruction]): Unit = { + assert(!ins.exists(_.isInstanceOf[Invoke]), ins.stringLines) + } + + def assertInvoke(m: Method, receiver: String, method: String): Unit = assertInvoke(m.instructions, receiver, method) + def assertInvoke(l: List[Instruction], receiver: String, method: String): Unit = { + assert(l.exists { + case Invoke(_, `receiver`, `method`, _, _) => true + case _ => false + }, l.stringLines) + } +} + +@RunWith(classOf[JUnit4]) +class InlinerTest extends ClearAfterClass { + ClearAfterClass.stateToClear = InlinerTest + + import InlinerTest.{listStringLines, assertInvoke, assertNoInvoke} + + val compiler = InlinerTest.compiler + import compiler.genBCode.bTypes._ + + def compile(scalaCode: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[ClassNode] = { + InlinerTest.notPerRun.foreach(_.clear()) + compileClasses(compiler)(scalaCode, javaCode, allowMessage) + } + + def checkCallsite(callsite: callGraph.Callsite, callee: MethodNode) = { + assert(callsite.callsiteMethod.instructions.contains(callsite.callsiteInstruction), instructionsFromMethod(callsite.callsiteMethod)) + + val callsiteClassNode = byteCodeRepository.classNode(callsite.callsiteClass.internalName).get + assert(callsiteClassNode.methods.contains(callsite.callsiteMethod), callsiteClassNode.methods.asScala.map(_.name).toList) + + assert(callsite.callee.get.callee == callee, callsite.callee.get.callee.name) + } + + // inline first invocation of f into g in class C + def inlineTest(code: String, mod: ClassNode => Unit = _ => ()): (MethodNode, Option[CannotInlineWarning]) = { + val List(cls) = compile(code) + mod(cls) + val clsBType = classBTypeFromParsedClassfile(cls.name) + + val List(f, g) = cls.methods.asScala.filter(m => Set("f", "g")(m.name)).toList.sortBy(_.name) + val fCall = g.instructions.iterator.asScala.collect({ case i: MethodInsnNode if i.name == "f" => i }).next() + + val analyzer = new AsmAnalyzer(g, clsBType.internalName) + + val r = inliner.inline( + fCall, + analyzer.frameAt(fCall).getStackSize, + g, + clsBType, + f, + clsBType, + receiverKnownNotNull = true, + keepLineNumbers = true) + (g, r) + } + + @Test + def simpleInlineOK(): Unit = { + val code = + """class C { + | def f = 1 + | def g = f + f + |} + """.stripMargin + + val (g, _) = inlineTest(code) + + val gConv = convertMethod(g) + assertSameCode(gConv.instructions.dropNonOp, + List( + VarOp(ALOAD, 0), VarOp(ASTORE, 1), // store this + Op(ICONST_1), VarOp(ISTORE, 2), Jump(GOTO, Label(10)), // store return value + Label(10), VarOp(ILOAD, 2), // load return value + VarOp(ALOAD, 0), Invoke(INVOKEVIRTUAL, "C", "f", "()I", false), Op(IADD), Op(IRETURN))) + + // line numbers are kept, so there's a line 2 (from the inlined f) + assert(gConv.instructions exists { + case LineNumber(2, _) => true + case _ => false + }, gConv.instructions.filter(_.isInstanceOf[LineNumber])) + + assert(gConv.localVars.map(_.name).sorted == List("f_this", "this"), gConv.localVars) + assert(g.maxStack == 2 && g.maxLocals == 3, s"${g.maxLocals} - ${g.maxStack}") + } + + @Test + def nothingTypedOK(): Unit = { + val code = + """class C { + | def f: Nothing = ??? + | def g: Int = { f; 1 } + |} + """.stripMargin + + // On the bytecode level, methods of type Nothing have return type Nothing$. + // This can be treated like any other result object. + + // See also discussion around ATHROW in BCodeBodyBuilder + + val (g, _) = inlineTest(code) + val expectedInlined = List( + VarOp(ALOAD, 0), VarOp(ASTORE, 1), // store this + Field(GETSTATIC, "scala/Predef$", "MODULE$", "Lscala/Predef$;"), Invoke(INVOKEVIRTUAL, "scala/Predef$", "$qmark$qmark$qmark", "()Lscala/runtime/Nothing$;", false)) // inlined call to ??? + + assertSameCode(convertMethod(g).instructions.dropNonOp.take(4), expectedInlined) + + localOpt.methodOptimizations(g, "C") + assertSameCode(convertMethod(g).instructions.dropNonOp, + expectedInlined ++ List(VarOp(ASTORE, 2), VarOp(ALOAD, 2), Op(ATHROW))) + } + + @Test + def synchronizedNoInline(): Unit = { + val code = + """class C { + | def f: Int = 0 + | def g: Int = f + |} + """.stripMargin + + val (_, can) = inlineTest(code, cls => { + val f = cls.methods.asScala.find(_.name == "f").get + f.access |= ACC_SYNCHRONIZED + }) + assert(can.get.isInstanceOf[SynchronizedMethod], can) + } + + @Test + def tryCatchOK(): Unit = { + val code = + """class C { + | def f: Int = try { 1 } catch { case _: Exception => 2 } + | def g = f + 1 + |} + """.stripMargin + val (_, r) = inlineTest(code) + assert(r.isEmpty, r) + } + + @Test + def tryCatchNoInline(): Unit = { + // cannot inline f: there's a value on g's stack. if f throws and enters the handler, all values + // on the stack are removed, including the one of g's stack that we still need. + val code = + """class C { + | def f: Int = try { 1 } catch { case _: Exception => 2 } + | def g = println(f) + |} + """.stripMargin + val (_, r) = inlineTest(code) + assert(r.get.isInstanceOf[MethodWithHandlerCalledOnNonEmptyStack], r) + } + + @Test + def illegalAccessNoInline(): Unit = { + val code = + """package a { + | class C { + | private def f: Int = 0 + | def g: Int = f + | } + |} + |package b { + | class D { + | def h(c: a.C): Int = c.g + 1 + | } + |} + """.stripMargin + + val List(c, d) = compile(code) + + val cTp = classBTypeFromParsedClassfile(c.name) + val dTp = classBTypeFromParsedClassfile(d.name) + + val g = c.methods.asScala.find(_.name == "g").get + val h = d.methods.asScala.find(_.name == "h").get + val gCall = h.instructions.iterator.asScala.collect({ + case m: MethodInsnNode if m.name == "g" => m + }).next() + + val analyzer = new AsmAnalyzer(h, dTp.internalName) + + val r = inliner.inline( + gCall, + analyzer.frameAt(gCall).getStackSize, + h, + dTp, + g, + cTp, + receiverKnownNotNull = true, + keepLineNumbers = true) + + assert(r.get.isInstanceOf[IllegalAccessInstruction], r) + } + + @Test + def inlineSimpleAtInline(): Unit = { + val code = + """class C { + | @inline final def f = 0 + | final def g = 1 + | + | def test = f + g + |} + """.stripMargin + val List(cCls) = compile(code) + val instructions = getSingleMethod(cCls, "test").instructions + assert(instructions.contains(Op(ICONST_0)), instructions.stringLines) + assert(!instructions.contains(Op(ICONST_1)), instructions) + } + + @Test + def cyclicInline(): Unit = { + val code = + """class C { + | @inline final def f: Int = g + | @inline final def g: Int = f + |} + """.stripMargin + val List(c) = compile(code) + val methods @ List(_, g) = c.methods.asScala.filter(_.name.length == 1).toList + val List(fIns, gIns) = methods.map(instructionsFromMethod(_).dropNonOp) + val invokeG = Invoke(INVOKEVIRTUAL, "C", "g", "()I", false) + assert(fIns contains invokeG, fIns) // no inlining into f, that request is elided + assert(gIns contains invokeG, gIns) // f is inlined into g, g invokes itself recursively + + assert(callGraph.callsites.size == 3, callGraph.callsites) + for (callsite <- callGraph.callsites.values if methods.contains(callsite.callsiteMethod)) { + checkCallsite(callsite, g) + } + } + + @Test + def cyclicInline2(): Unit = { + val code = + """class C { + | @inline final def h: Int = f + | @inline final def f: Int = g + g + | @inline final def g: Int = h + |} + """.stripMargin + val List(c) = compile(code) + val methods @ List(f, g, h) = c.methods.asScala.filter(_.name.length == 1).sortBy(_.name).toList + val List(fIns, gIns, hIns) = methods.map(instructionsFromMethod(_).dropNonOp) + val invokeG = Invoke(INVOKEVIRTUAL, "C", "g", "()I", false) + assert(fIns.count(_ == invokeG) == 2, fIns) // no inlining into f, these requests are elided + assert(gIns.count(_ == invokeG) == 2, gIns) + assert(hIns.count(_ == invokeG) == 2, hIns) + + assert(callGraph.callsites.size == 7, callGraph.callsites) + for (callsite <- callGraph.callsites.values if methods.contains(callsite.callsiteMethod)) { + checkCallsite(callsite, g) + } + } + + @Test + def arraycopy(): Unit = { + // also tests inlining of a void-returning method (no return value on the stack) + val code = + """// can't use the `compat.Platform.arraycopy` from the std lib for now, because the classfile doesn't have a ScalaInlineInfo attribute + |object Platform { + | @inline def arraycopy(src: AnyRef, srcPos: Int, dest: AnyRef, destPos: Int, length: Int) { + | System.arraycopy(src, srcPos, dest, destPos, length) + | } + |} + |class C { + | def f(src: AnyRef, srcPos: Int, dest: AnyRef, destPos: Int, length: Int): Unit = { + | Platform.arraycopy(src, srcPos, dest, destPos, length) + | } + |} + """.stripMargin + val List(c, _, _) = compile(code) + val ins = getSingleMethod(c, "f").instructions + val invokeSysArraycopy = Invoke(INVOKESTATIC, "java/lang/System", "arraycopy", "(Ljava/lang/Object;ILjava/lang/Object;II)V", false) + assert(ins contains invokeSysArraycopy, ins.stringLines) + } + + @Test + def arrayMemberMethod(): Unit = { + // This used to crash when building the call graph. The `owner` field of the MethodInsnNode + // for the invocation of `clone` is not an internal name, but a full array descriptor + // [Ljava.lang.Object; - the documentation in the ASM library didn't mention that possibility. + val code = + """class C { + | def f(a: Array[Object]) = { + | a.clone() + | } + |} + """.stripMargin + val List(c) = compile(code) + assert(callGraph.callsites.values exists (_.callsiteInstruction.name == "clone")) + } + + @Test + def atInlineInTrait(): Unit = { + val code = + """trait T { + | @inline final def f = 0 + |} + |class C { + | def g(t: T) = t.f + |} + """.stripMargin + val List(c, t, tClass) = compile(code) + assertNoInvoke(getSingleMethod(c, "g")) + } + + @Test + def inlinePrivateMethodWithHandler(): Unit = { + val code = + """class C { + | @inline private def f = try { 0 } catch { case _: Throwable => 1 } + | def g = f + |} + """.stripMargin + val List(c) = compile(code) + // no more invoke, f is inlined + assertNoInvoke(getSingleMethod(c, "g")) + } + + @Test + def inlineStaticCall(): Unit = { + val code = + """class C { + | def f = Integer.lowestOneBit(103) + |} + """.stripMargin + + val List(c) = compile(code) + val f = c.methods.asScala.find(_.name == "f").get + val callsiteIns = f.instructions.iterator().asScala.collect({ case c: MethodInsnNode => c }).next() + val clsBType = classBTypeFromParsedClassfile(c.name) + val analyzer = new AsmAnalyzer(f, clsBType.internalName) + + val integerClassBType = classBTypeFromInternalName("java/lang/Integer") + val lowestOneBitMethod = byteCodeRepository.methodNode(integerClassBType.internalName, "lowestOneBit", "(I)I").get._1 + + val r = inliner.inline( + callsiteIns, + analyzer.frameAt(callsiteIns).getStackSize, + f, + clsBType, + lowestOneBitMethod, + integerClassBType, + receiverKnownNotNull = false, + keepLineNumbers = false) + + assert(r.isEmpty, r) + val ins = instructionsFromMethod(f) + + // no invocations, lowestOneBit is inlined + assertNoInvoke(ins) + + // no null check when inlining a static method + ins foreach { + case Jump(IFNONNULL, _) => assert(false, ins.stringLines) + case _ => + } + } + + @Test + def maxLocalsMaxStackAfterInline(): Unit = { + val code = + """class C { + | @inline final def f1(x: Int): Int = { + | val a = x + 1 + | math.max(a, math.min(10, a - 1)) + | } + | + | @inline final def f2(x: Int): Unit = { + | val a = x + 1 + | println(math.max(a, 10)) + | } + | + | def g1 = println(f1(32)) + | def g2 = println(f2(32)) + |} + """.stripMargin + + val List(c) = compile(code) + val ms @ List(f1, f2, g1, g2) = c.methods.asScala.filter(_.name.length == 2).toList + + // stack height at callsite of f1 is 1, so max of g1 after inlining is max of f1 + 1 + assert(g1.maxStack == 7 && f1.maxStack == 6, s"${g1.maxStack} - ${f1.maxStack}") + + // locals in f1: this, x, a + // locals in g1 after inlining: this, this-of-f1, x, a, return value + assert(g1.maxLocals == 5 && f1.maxLocals == 3, s"${g1.maxLocals} - ${f1.maxLocals}") + + // like maxStack in g1 / f1 + assert(g2.maxStack == 5 && f2.maxStack == 4, s"${g2.maxStack} - ${f2.maxStack}") + + // like maxLocals for g1 / f1, but no return value + assert(g2.maxLocals == 4 && f2.maxLocals == 3, s"${g2.maxLocals} - ${f2.maxLocals}") + } + + @Test + def mixedCompilationNoInline(): Unit = { + // The inliner checks if the invocation `A.bar` can be safely inlined. For that it needs to have + // the bytecode of the invoked method. In mixed compilation, there's no classfile available for + // A, so `flop` cannot be inlined, we cannot check if it's safe. + + val javaCode = + """public class A { + | public static final int bar() { return 100; } + |} + """.stripMargin + + val scalaCode = + """class B { + | @inline final def flop = A.bar + | def g = flop + |} + """.stripMargin + + val warn = + """B::flop()I is annotated @inline but could not be inlined: + |Failed to check if B::flop()I can be safely inlined to B without causing an IllegalAccessError. Checking instruction INVOKESTATIC A.bar ()I failed: + |The method bar()I could not be found in the class A or any of its parents. + |Note that the following parent classes are defined in Java sources (mixed compilation), no bytecode is available: A""".stripMargin + + var c = 0 + val List(b) = compile(scalaCode, List((javaCode, "A.java")), allowMessage = i => {c += 1; i.msg contains warn}) + assert(c == 1, c) + val ins = getSingleMethod(b, "g").instructions + val invokeFlop = Invoke(INVOKEVIRTUAL, "B", "flop", "()I", false) + assert(ins contains invokeFlop, ins.stringLines) + } + + @Test + def inlineFromTraits(): Unit = { + val code = + """trait T { + | @inline final def f = g + | @inline final def g = 1 + |} + | + |class C extends T { + | def t1(t: T) = t.f + | def t2(c: C) = c.f + |} + """.stripMargin + val List(c, t, tClass) = compile(code) + // both are just `return 1`, no more calls + assertNoInvoke(getSingleMethod(c, "t1")) + assertNoInvoke(getSingleMethod(c, "t2")) + } + + @Test + def inlineMixinMethods(): Unit = { + val code = + """trait T { + | @inline final def f = 1 + |} + |class C extends T + """.stripMargin + val List(c, t, tClass) = compile(code) + // the static implementaiton method is inlined into the mixin, so there's no invocation in the mixin + assertNoInvoke(getSingleMethod(c, "f")) + } + + @Test + def inlineTraitInherited(): Unit = { + val code = + """trait T { + | @inline final def f = 1 + |} + |trait U extends T { + | @inline final def g = f + |} + |class C extends U { + | def t1 = f + | def t2 = g + |} + """.stripMargin + val List(c, t, tClass, u, uClass) = compile(code) + assertNoInvoke(getSingleMethod(c, "t1")) + assertNoInvoke(getSingleMethod(c, "t2")) + } + + @Test + def virtualTraitNoInline(): Unit = { + val code = + """trait T { + | @inline def f = 1 + |} + |class C extends T { + | def t1(t: T) = t.f + | def t2 = this.f + |} + """.stripMargin + val warns = Set( + "C::f()I is annotated @inline but cannot be inlined: the method is not final and may be overridden", + "T::f()I is annotated @inline but cannot be inlined: the method is not final and may be overridden") + var count = 0 + val List(c, t, tClass) = compile(code, allowMessage = i => {count += 1; warns.exists(i.msg contains _)}) + assert(count == 2, count) + assertInvoke(getSingleMethod(c, "t1"), "T", "f") + assertInvoke(getSingleMethod(c, "t2"), "C", "f") + } + + @Test + def sealedTraitInline(): Unit = { + val code = + """sealed trait T { + | @inline def f = 1 + |} + |class C { + | def t1(t: T) = t.f + |} + """.stripMargin + val List(c, t, tClass) = compile(code) + assertNoInvoke(getSingleMethod(c, "t1")) + } + + @Test + def inlineFromObject(): Unit = { + val code = + """trait T { + | @inline def f = 0 + |} + |object O extends T { + | @inline def g = 1 + | // mixin generates `def f = T$class.f(this)`, which is inlined here (we get ICONST_0) + |} + |class C { + | def t1 = O.f // the mixin method of O is inlined, so we directly get the ICONST_0 + | def t2 = O.g // object members are inlined + | def t3(t: T) = t.f // no inlining here + |} + """.stripMargin + val warn = "T::f()I is annotated @inline but cannot be inlined: the method is not final and may be overridden" + var count = 0 + val List(c, oMirror, oModule, t, tClass) = compile(code, allowMessage = i => {count += 1; i.msg contains warn}) + assert(count == 1, count) + + assertNoInvoke(getSingleMethod(oModule, "f")) + + assertNoInvoke(getSingleMethod(c, "t1")) + assertNoInvoke(getSingleMethod(c, "t2")) + assertInvoke(getSingleMethod(c, "t3"), "T", "f") + } + + @Test + def selfTypeInline(): Unit = { + val code = + """trait T { self: Assembly => + | @inline final def f = g + | @inline final def m = 1 + |} + |trait Assembly extends T { + | @inline final def g = 1 + | @inline final def n = m // inlined. (*) + | // (*) the declaration class of m is T. the signature of T$class.m is m(LAssembly;)I. so we need the self type to build the + | // signature. then we can look up the MethodNode of T$class.m and then rewrite the INVOKEINTERFACE to INVOKESTATIC. + |} + |class C { + | def t1(a: Assembly) = a.f // like above, decl class is T, need self-type of T to rewrite the interface call to static. + | def t2(a: Assembly) = a.n + |} + """.stripMargin + + val List(assembly, assemblyClass, c, t, tClass) = compile(code) + + assertNoInvoke(getSingleMethod(tClass, "f")) + + assertNoInvoke(getSingleMethod(assemblyClass, "n")) + + assertNoInvoke(getSingleMethod(c, "t1")) + assertNoInvoke(getSingleMethod(c, "t2")) + } + + @Test + def selfTypeInline2(): Unit = { + // There are some interesting things going on here with the self types. Here's a short version: + // + // trait T1 { def f = 1 } + // trait T2a { self: T1 with T2a => // self type in the backend: T1 + // def f = 2 + // def g = f // resolved to T2a.f + // } + // trait T2b { self: T2b with T1 => // self type in the backend: T2b + // def f = 2 + // def g = f // resolved to T1.f + // } + // + // scala> val t = typeOf[T2a]; exitingMixin(t.typeOfThis.typeSymbol) // self type of T2a is T1 + // res28: $r.intp.global.Symbol = trait T1 + // + // scala> typeOf[T2a].typeOfThis.member(newTermName("f")).owner // f in T2a is resolved as T2a.f + // res29: $r.intp.global.Symbol = trait T2a + // + // scala> val t = typeOf[T2b]; exitingMixin(t.typeOfThis.typeSymbol) // self type of T2b is T1 + // res30: $r.intp.global.Symbol = trait T2b + // + // scala> typeOf[T2b].typeOfThis.member(newTermName("f")).owner // f in T2b is resolved as T1.f + // res31: $r.intp.global.Symbol = trait T1 + + val code = + """trait T1 { + | @inline def f: Int = 0 + | @inline def g1 = f // not inlined: f not final, so T1$class.g1 has an interface call T1.f + |} + | + |// erased self-type (used in impl class for `self` parameter): T1 + |trait T2a { self: T1 with T2a => + | @inline override final def f = 1 + | @inline def g2a = f // inlined: resolved as T2a.f, which is re-written to T2a$class.f, so T2a$class.g2a has ICONST_1 + |} + | + |final class Ca extends T1 with T2a { + | // mixin generates accessors like `def g1 = T1$class.g1`, the impl class method call is inlined into the accessor. + | + | def m1a = g1 // call to accessor, inlined, we get the interface call T1.f + | def m2a = g2a // call to accessor, inlined, we get ICONST_1 + | def m3a = f // call to accessor, inlined, we get ICONST_1 + | + | def m4a(t: T1) = t.f // T1.f is not final, so not inlined, interface call to T1.f + | def m5a(t: T2a) = t.f // re-written to T2a$class.f, inlined, ICONST_1 + |} + | + |// erased self-type: T2b + |trait T2b { self: T2b with T1 => + | @inline override final def f = 1 + | @inline def g2b = f // not inlined: resolved as T1.f, so T2b$class.g2b has an interface call T1.f + |} + | + |final class Cb extends T1 with T2b { + | def m1b = g1 // inlined, we get the interface call to T1.f + | def m2b = g2b // inlined, we get the interface call to T1.f + | def m3b = f // inlined, we get ICONST_1 + | + | def m4b(t: T1) = t.f // T1.f is not final, so not inlined, interface call to T1.f + | def m5b(t: T2b) = t.f // re-written to T2b$class.f, inlined, ICONST_1 + |} + """.stripMargin + + val warning = "T1::f()I is annotated @inline but cannot be inlined: the method is not final and may be overridden" + var count = 0 + val List(ca, cb, t1, t1C, t2a, t2aC, t2b, t2bC) = compile(code, allowMessage = i => {count += 1; i.msg contains warning}) + assert(count == 4, count) // see comments, f is not inlined 4 times + + val t2aCfDesc = t2aC.methods.asScala.find(_.name == "f").get.desc + assert(t2aCfDesc == "(LT1;)I", t2aCfDesc) // self-type of T2a is T1 + + val t2bCfDesc = t2bC.methods.asScala.find(_.name == "f").get.desc + assert(t2bCfDesc == "(LT2b;)I", t2bCfDesc) // self-type of T2b is T2b + + assertNoInvoke(getSingleMethod(t2aC, "g2a")) + assertInvoke(getSingleMethod(t2bC, "g2b"), "T1", "f") + + assertInvoke(getSingleMethod(ca, "m1a"), "T1", "f") + assertNoInvoke(getSingleMethod(ca, "m2a")) // no invoke, see comment on def g2a + assertNoInvoke(getSingleMethod(ca, "m3a")) + assertInvoke(getSingleMethod(ca, "m4a"), "T1", "f") + assertNoInvoke(getSingleMethod(ca, "m5a")) + + assertInvoke(getSingleMethod(cb, "m1b"), "T1", "f") + assertInvoke(getSingleMethod(cb, "m2b"), "T1", "f") // invoke, see comment on def g2b + assertNoInvoke(getSingleMethod(cb, "m3b")) + assertInvoke(getSingleMethod(cb, "m4b"), "T1", "f") + assertNoInvoke(getSingleMethod(cb, "m5b")) + } + + @Test + def finalSubclassInline(): Unit = { + val code = + """class C { + | @inline def f = 0 + | @inline final def g = 1 + |} + |final class D extends C + |object E extends C + |class T { + | def t1(d: D) = d.f + d.g + E.f + E.g // d.f can be inlined because the reciever type is D, which is final. + |} // so d.f can be resolved statically. same for E.f + """.stripMargin + val List(c, d, e, eModule, t) = compile(code) + assertNoInvoke(getSingleMethod(t, "t1")) + } + + @Test + def inlineFromNestedClasses(): Unit = { + val code = + """class C { + | trait T { @inline final def f = 1 } + | class D extends T{ + | def m(t: T) = t.f + | } + | + | def m(d: D) = d.f + |} + """.stripMargin + val List(c, d, t, tC) = compile(code) + assertNoInvoke(getSingleMethod(d, "m")) + assertNoInvoke(getSingleMethod(c, "m")) + } + + @Test + def inlineTraitCastReceiverToSelf(): Unit = { + val code = + """class C { def foo(x: Int) = x } + |trait T { self: C => + | @inline final def f(x: Int) = foo(x) + | def t1 = f(1) + | def t2(t: T) = t.f(2) + |} + """.stripMargin + val List(c, t, tc) = compile(code) + val t1 = getSingleMethod(tc, "t1") + val t2 = getSingleMethod(tc, "t2") + val cast = TypeOp(CHECKCAST, "C") + Set(t1, t2).foreach(m => assert(m.instructions.contains(cast), m.instructions)) + } + + @Test + def abstractMethodWarning(): Unit = { + val code = + """abstract class C { + | @inline def foo: Int + |} + |class T { + | def t1(c: C) = c.foo + |} + """.stripMargin + val warn = "C::foo()I is annotated @inline but cannot be inlined: the method is not final and may be overridden" + var c = 0 + compile(code, allowMessage = i => {c += 1; i.msg contains warn}) + assert(c == 1, c) + } + + @Test + def abstractFinalMethodError(): Unit = { + val code = + """abstract class C { + | @inline final def foo: Int + |} + |trait T { + | @inline final def bar: Int + |} + """.stripMargin + val err = "abstract member may not have final modifier" + var i = 0 + compile(code, allowMessage = info => {i += 1; info.msg contains err}) + assert(i == 2, i) + } + + @Test + def noInlineTraitFieldAccessors(): Unit = { + val code = + """sealed trait T { + | lazy val a = 0 + | val b = 1 + | final lazy val c = 2 + | final val d = 3 + | final val d1: Int = 3 + | + | @noinline def f = 5 // re-written to T$class + | @noinline final def g = 6 // re-written + | + | @noinline def h: Int + | @inline def i: Int + |} + | + |trait U { // not sealed + | lazy val a = 0 + | val b = 1 + | final lazy val c = 2 + | final val d = 3 + | final val d1: Int = 3 + | + | @noinline def f = 5 // not re-written (not final) + | @noinline final def g = 6 // re-written + | + | @noinline def h: Int + | @inline def i: Int + |} + | + |class C { + | def m1(t: T) = t.a + t.b + t.c + t.d1 + | def m2(t: T) = t.d // inlined by the type-checker's constant folding + | def m3(t: T) = t.f + t.g + t.h + t.i + | + | def m4(u: U) = u.a + u.b + u.c + u.d1 + | def m5(u: U) = u.d + | def m6(u: U) = u.f + u.g + u.h + u.i + |} + """.stripMargin + + val List(c, t, tClass, u, uClass) = compile(code, allowMessage = _.msg contains "i()I is annotated @inline but cannot be inlined") + val m1 = getSingleMethod(c, "m1") + assertInvoke(m1, "T", "a") + assertInvoke(m1, "T", "b") + assertInvoke(m1, "T", "c") + + assertNoInvoke(getSingleMethod(c, "m2")) + + val m3 = getSingleMethod(c, "m3") + assertInvoke(m3, "T$class", "f") + assertInvoke(m3, "T$class", "g") + assertInvoke(m3, "T", "h") + assertInvoke(m3, "T", "i") + + val m4 = getSingleMethod(c, "m4") + assertInvoke(m4, "U", "a") + assertInvoke(m4, "U", "b") + assertInvoke(m4, "U", "c") + + assertNoInvoke(getSingleMethod(c, "m5")) + + val m6 = getSingleMethod(c, "m6") + assertInvoke(m6, "U", "f") + assertInvoke(m6, "U$class", "g") + assertInvoke(m6, "U", "h") + assertInvoke(m6, "U", "i") + } + + @Test + def mixedNoCrashSI9111(): Unit = { + val javaCode = + """public final class A { + | public static final class T { } + | public static final class Inner { + | public static final class T { } + | public T newT() { return null; } + | } + |} + """.stripMargin + + val scalaCode = + """class C { + | val i = new A.Inner() + |} + """.stripMargin + + // We don't get to see the warning about SI-9111, because it is associated with the MethodInlineInfo + // of method newT, which is not actually used. + // The problem is: if we reference `newT` in the scalaCode, the scala code does not compile, + // because then SI-9111 triggers during type-checking class C, in the compiler frontend, and + // we don't even get to the backend. + // Nevertheless, the workaround for SI-9111 in BcodeAsmCommon.buildInlineInfoFromClassSymbol + // is still necessary, otherwise this test crashes. + // The warning below is the typical warning we get in mixed compilation. + val warn = + """failed to determine if <init> should be inlined: + |The method <init>()V could not be found in the class A$Inner or any of its parents. + |Note that the following parent classes could not be found on the classpath: A$Inner""".stripMargin + + var c = 0 + + compileClasses(newCompiler(extraArgs = InlinerTest.args + " -Yopt-warnings:_"))( + scalaCode, + List((javaCode, "A.java")), + allowMessage = i => {c += 1; i.msg contains warn}) + assert(c == 1, c) + } + + @Test + def inlineInvokeSpecial(): Unit = { + val code = + """class Aa { + | def f1 = 0 + |} + |class B extends Aa { + | @inline final override def f1 = 1 + super.f1 // invokespecial Aa.f1 + | + | private def f2m = 0 // public B$$f2m in bytecode + | @inline final def f2 = f2m // invokevirtual B.B$$f2m + | + | private def this(x: Int) = this() // public in bytecode + | @inline final def f3 = new B() // invokespecial B.<init>() + | @inline final def f4 = new B(1) // invokespecial B.<init>(I) + | + | def t1 = f1 // inlined + | def t2 = f2 // inlined + | def t3 = f3 // inlined + | def t4 = f4 // inlined + |} + |class T { + | def t1(b: B) = b.f1 // cannot inline: contains a super call + | def t2(b: B) = b.f2 // inlined + | def t3(b: B) = b.f3 // inlined + | def t4(b: B) = b.f4 // inlined + |} + """.stripMargin + + val warn = + """B::f1()I is annotated @inline but could not be inlined: + |The callee B::f1()I contains the instruction INVOKESPECIAL Aa.f1 ()I + |that would cause an IllegalAccessError when inlined into class T.""".stripMargin + var c = 0 + val List(a, b, t) = compile(code, allowMessage = i => {c += 1; i.msg contains warn}) + assert(c == 1, c) + + assertInvoke(getSingleMethod(b, "t1"), "Aa", "f1") + assertInvoke(getSingleMethod(b, "t2"), "B", "B$$f2m") + assertInvoke(getSingleMethod(b, "t3"), "B", "<init>") + assertInvoke(getSingleMethod(b, "t4"), "B", "<init>") + + assertInvoke(getSingleMethod(t, "t1"), "B", "f1") + assertInvoke(getSingleMethod(t, "t2"), "B", "B$$f2m") + assertInvoke(getSingleMethod(t, "t3"), "B", "<init>") + assertInvoke(getSingleMethod(t, "t4"), "B", "<init>") + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala index 5430e33d6c..1ce1b88ff2 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala @@ -13,17 +13,26 @@ import scala.tools.testing.AssertUtil._ import CodeGenTools._ import scala.tools.partest.ASMConverters import ASMConverters._ +import scala.tools.testing.ClearAfterClass + +object MethodLevelOpts extends ClearAfterClass.Clearable { + var methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:method") + def clear(): Unit = { methodOptCompiler = null } +} @RunWith(classOf[JUnit4]) -class MethodLevelOpts { - val methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:method") +class MethodLevelOpts extends ClearAfterClass { + ClearAfterClass.stateToClear = MethodLevelOpts + + val methodOptCompiler = MethodLevelOpts.methodOptCompiler def wrapInDefault(code: Instruction*) = List(Label(0), LineNumber(1, Label(0))) ::: code.toList ::: List(Label(1)) @Test def eliminateEmptyTry(): Unit = { val code = "def f = { try {} catch { case _: Throwable => 0; () }; 1 }" - assertSameCode(singleMethodInstructions(methodOptCompiler)(code), wrapInDefault(Op(ICONST_1), Op(IRETURN))) + val warn = "a pure expression does nothing in statement position" + assertSameCode(singleMethodInstructions(methodOptCompiler)(code, allowMessage = _.msg contains warn), wrapInDefault(Op(ICONST_1), Op(IRETURN))) } @Test diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala index 4a45dd9138..da9853148b 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala @@ -13,9 +13,34 @@ import scala.tools.testing.AssertUtil._ import CodeGenTools._ import scala.tools.partest.ASMConverters import ASMConverters._ +import scala.tools.testing.ClearAfterClass + +object UnreachableCodeTest extends ClearAfterClass.Clearable { + // jvm-1.6 enables emitting stack map frames, which impacts the code generation wrt dead basic blocks, + // see comment in BCodeBodyBuilder + var methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:method") + var dceCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code") + var noOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:none") + + // jvm-1.5 disables computing stack map frames, and it emits dead code as-is. note that this flag triggers a deprecation warning + var noOptNoFramesCompiler = newCompiler(extraArgs = "-target:jvm-1.5 -Ybackend:GenBCode -Yopt:l:none -deprecation") + + def clear(): Unit = { + methodOptCompiler = null + dceCompiler = null + noOptCompiler = null + noOptNoFramesCompiler = null + } +} @RunWith(classOf[JUnit4]) -class UnreachableCodeTest { +class UnreachableCodeTest extends ClearAfterClass { + ClearAfterClass.stateToClear = UnreachableCodeTest + + val methodOptCompiler = UnreachableCodeTest.methodOptCompiler + val dceCompiler = UnreachableCodeTest.dceCompiler + val noOptCompiler = UnreachableCodeTest.noOptCompiler + val noOptNoFramesCompiler = UnreachableCodeTest.noOptNoFramesCompiler def assertEliminateDead(code: (Instruction, Boolean)*): Unit = { val method = genMethod()(code.map(_._1): _*) @@ -25,15 +50,6 @@ class UnreachableCodeTest { assertSameCode(nonEliminated, expectedLive) } - // jvm-1.6 enables emitting stack map frames, which impacts the code generation wrt dead basic blocks, - // see comment in BCodeBodyBuilder - val methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:method") - val dceCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code") - val noOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:none") - - // jvm-1.5 disables computing stack map frames, and it emits dead code as-is. - val noOptNoFramesCompiler = newCompiler(extraArgs = "-target:jvm-1.5 -Ybackend:GenBCode -Yopt:l:none") - @Test def basicElimination(): Unit = { assertEliminateDead( @@ -138,7 +154,8 @@ class UnreachableCodeTest { assertSameCode(noDce.dropNonOp, List(Op(ICONST_1), Op(IRETURN), Op(ATHROW), Op(ATHROW))) // when NOT computing stack map frames, ASM's ClassWriter does not replace dead code by NOP/ATHROW - val noDceNoFrames = singleMethodInstructions(noOptNoFramesCompiler)(code) + val warn = "target:jvm-1.5 is deprecated" + val noDceNoFrames = singleMethodInstructions(noOptNoFramesCompiler)(code, allowMessage = _.msg contains warn) assertSameCode(noDceNoFrames.dropNonOp, List(Op(ICONST_1), Op(IRETURN), Op(ICONST_2), Op(IRETURN))) } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala index 24a1f9d1c1..769736669b 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala @@ -12,10 +12,18 @@ import scala.collection.JavaConverters._ import CodeGenTools._ import scala.tools.partest.ASMConverters import ASMConverters._ +import scala.tools.testing.ClearAfterClass + +object UnusedLocalVariablesTest extends ClearAfterClass.Clearable { + var dceCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:unreachable-code") + def clear(): Unit = { dceCompiler = null } +} @RunWith(classOf[JUnit4]) -class UnusedLocalVariablesTest { - val dceCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:unreachable-code") +class UnusedLocalVariablesTest extends ClearAfterClass { + ClearAfterClass.stateToClear = UnusedLocalVariablesTest + + val dceCompiler = UnusedLocalVariablesTest.dceCompiler @Test def removeUnusedVar(): Unit = { diff --git a/test/junit/scala/tools/testing/ClearAfterClass.java b/test/junit/scala/tools/testing/ClearAfterClass.java new file mode 100644 index 0000000000..232d459c4e --- /dev/null +++ b/test/junit/scala/tools/testing/ClearAfterClass.java @@ -0,0 +1,20 @@ +package scala.tools.testing; + +import org.junit.AfterClass; + +/** + * Extend this class to use JUnit's @AfterClass. This annotation only works on static methods, + * which cannot be written in Scala. + * + * Example: {@link scala.tools.nsc.backend.jvm.opt.InlinerTest} + */ +public class ClearAfterClass { + public static interface Clearable { + void clear(); + } + + public static Clearable stateToClear; + + @AfterClass + public static void clearState() { stateToClear.clear(); } +} diff --git a/test/junit/scala/tools/testing/TempDir.scala b/test/junit/scala/tools/testing/TempDir.scala new file mode 100644 index 0000000000..475de8c4a2 --- /dev/null +++ b/test/junit/scala/tools/testing/TempDir.scala @@ -0,0 +1,18 @@ +package scala.tools.testing + +import java.io.{IOException, File} + +object TempDir { + final val TEMP_DIR_ATTEMPTS = 10000 + def createTempDir(): File = { + val baseDir = new File(System.getProperty("java.io.tmpdir")) + val baseName = System.currentTimeMillis() + "-" + var c = 0 + while (c < TEMP_DIR_ATTEMPTS) { + val tempDir = new File(baseDir, baseName + c) + if (tempDir.mkdir()) return tempDir + c += 1 + } + throw new IOException(s"Failed to create directory") + } +} |