diff options
111 files changed, 5158 insertions, 613 deletions
@@ -29,8 +29,8 @@ PS: If you have some spare time to help out around here, we would be delighted t # Handy Links - [A wealth of documentation](http://docs.scala-lang.org) - [Scala CI](https://scala-ci.typesafe.com/) - - [Scala CI at EPFL](https://scala-webapps.epfl.ch/jenkins) - - [Download the latest nightly](https://scala-webapps.epfl.ch/jenkins/job/scala-nightly-main-master/ws/dists/latest/*zip*/latest.zip); + - [Scala CI at EPFL](https://scala-webapps.epfl.ch/jenkins/) + - [Download the latest nightly](http://www.scala-lang.org/files/archive/nightly/2.11.x/); - Scala mailing lists: - [Compiler and standard library development](https://groups.google.com/group/scala-internals) - [Users of Scala](https://groups.google.com/group/scala-user) diff --git a/build.number b/build.number index 5f8ed6d6b6..27d2d23041 100644 --- a/build.number +++ b/build.number @@ -1,7 +1,7 @@ #Tue Sep 11 19:21:09 CEST 2007 version.major=2 version.minor=11 -version.patch=6 +version.patch=7 # This is the -N part of a version. if it's 0, it's dropped from maven versions. version.bnum=0 @@ -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/scripts/jobs/integrate/bootstrap b/scripts/jobs/integrate/bootstrap index 46d610018c..d0a5b452a8 100755 --- a/scripts/jobs/integrate/bootstrap +++ b/scripts/jobs/integrate/bootstrap @@ -438,7 +438,6 @@ constructUpdatedModuleVersions() { if [ ! -z "$JLINE_VER" ] ; then updatedModuleVersions=("${updatedModuleVersions[@]}" "-Djline.version=$JLINE_VER"); fi if [ ! -z "$SCALA_BINARY_VER" ]; then updatedModuleVersions=("${updatedModuleVersions[@]}" "-Dscala.binary.version=$SCALA_BINARY_VER"); fi - if [ ! -z "$SCALA_FULL_VER" ] ; then updatedModuleVersions=("${updatedModuleVersions[@]}" "-Dscala.full.version=$SCALA_FULL_VER"); fi } # build locker (scala + modules) and quick, publishing everything to private-repo @@ -493,7 +492,10 @@ bootstrap() { cd $baseDir rm -rf build/ # must leave everything else in $baseDir for downstream jobs + # scala.full.version determines the dependency of scala-dist on the continuations plugin, + # which is fully cross-versioned (for $SCALA_VER, the version we're releasing) ant -Dstarr.version=$SCALA_VER\ + -Dscala.full.version=$SCALA_VER\ -Dextra.repo.url=$privateRepo\ -Dmaven.version.suffix=$SCALA_VER_SUFFIX\ ${updatedModuleVersions[@]} \ diff --git a/spec/01-lexical-syntax.md b/spec/01-lexical-syntax.md index 945dedf99b..3972961f58 100644 --- a/spec/01-lexical-syntax.md +++ b/spec/01-lexical-syntax.md @@ -59,7 +59,7 @@ idrest ::= {letter | digit} [‘_’ op] There are three ways to form an identifier. First, an identifier can start with a letter which can be followed by an arbitrary sequence of -letters and digits. This may be followed by underscore ‘_’ +letters and digits. This may be followed by underscore `‘_‘` characters and another string composed of either letters and digits or of operator characters. Second, an identifier can start with an operator character followed by an arbitrary sequence of operator characters. diff --git a/spec/05-classes-and-objects.md b/spec/05-classes-and-objects.md index fd20d6ae2c..8681c93193 100644 --- a/spec/05-classes-and-objects.md +++ b/spec/05-classes-and-objects.md @@ -526,7 +526,7 @@ case the member is called _qualified private_. Class-private or object-private members may not be abstract, and may not have `protected` or `override` modifiers. -#### `protected` +### `protected` The `protected` modifier applies to class member definitions. Protected members of a class can be accessed from within - the template of the defining class, @@ -557,14 +557,14 @@ legal if the prefix is `this` or `$O$.this`, for some class $O$ enclosing the reference. In addition, the restrictions for unqualified `protected` apply. -#### `override` +### `override` The `override` modifier applies to class member definitions or declarations. It is mandatory for member definitions or declarations that override some other concrete member definition in a parent class. If an `override` modifier is given, there must be at least one overridden member definition or declaration (either concrete or abstract). -#### `abstract override` +### `abstract override` The `override` modifier has an additional significance when combined with the `abstract` modifier. That modifier combination is only allowed for value members of traits. @@ -579,7 +579,7 @@ influence the concept whether a member is concrete or abstract. A member is _abstract_ if only a declaration is given for it; it is _concrete_ if a full definition is given. -#### `abstract` +### `abstract` The `abstract` modifier is used in class definitions. It is redundant for traits, and mandatory for all other classes which have incomplete members. Abstract classes cannot be @@ -592,7 +592,7 @@ The `abstract` modifier can also be used in conjunction with `override` for class member definitions. In that case the previous discussion applies. -#### `final` +### `final` The `final` modifier applies to class member definitions and to class definitions. A `final` class member definition may not be overridden in subclasses. A `final` class may not be inherited by @@ -604,13 +604,13 @@ an explicit `final` modifier, even if they are defined in a final class or object. `final` may not be applied to incomplete members, and it may not be combined in one modifier list with `sealed`. -#### `sealed` +### `sealed` The `sealed` modifier applies to class definitions. A `sealed` class may not be directly inherited, except if the inheriting template is defined in the same source file as the inherited class. However, subclasses of a sealed class can be inherited anywhere. -#### `lazy` +### `lazy` The `lazy` modifier applies to value definitions. A `lazy` value is initialized the first time it is accessed (which might never happen at all). Attempting to access a lazy value during its 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/Global.scala b/src/compiler/scala/tools/nsc/Global.scala index 1c9dbad4dd..b233acf271 100644 --- a/src/compiler/scala/tools/nsc/Global.scala +++ b/src/compiler/scala/tools/nsc/Global.scala @@ -1551,7 +1551,8 @@ class Global(var currentSettings: Settings, var reporter: Reporter) if (reporter.hasErrors) { for ((sym, file) <- symSource.iterator) { - sym.reset(new loaders.SourcefileLoader(file)) + if (file != null) + sym.reset(new loaders.SourcefileLoader(file)) if (sym.isTerm) sym.moduleClass reset loaders.moduleClassLoader } 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/icode/GenICode.scala b/src/compiler/scala/tools/nsc/backend/icode/GenICode.scala index cf52ad6636..185fd93501 100644 --- a/src/compiler/scala/tools/nsc/backend/icode/GenICode.scala +++ b/src/compiler/scala/tools/nsc/backend/icode/GenICode.scala @@ -876,13 +876,15 @@ abstract class GenICode extends SubComponent { genLoadModule(ctx, tree) generatedType = toTypeKind(sym.info) } else { - try { - val Some(l) = ctx.method.lookupLocal(sym) - ctx.bb.emit(LOAD_LOCAL(l), tree.pos) - generatedType = l.kind - } catch { - case ex: MatchError => - abort("symbol " + sym + " does not exist in " + ctx.method) + ctx.method.lookupLocal(sym) match { + case Some(l) => + ctx.bb.emit(LOAD_LOCAL(l), tree.pos) + generatedType = l.kind + case None => + val saved = settings.uniqid + settings.uniqid.value = true + try abort(s"symbol $sym does not exist in ${ctx.method}, which contains locals ${ctx.method.locals.mkString(",")}") + finally settings.uniqid.value = saved } } } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/AsmUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/AsmUtils.scala index 75aa0fc984..0df1b2029d 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/AsmUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/AsmUtils.scala @@ -7,9 +7,10 @@ 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.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 object AsmUtils { @@ -49,7 +50,7 @@ object AsmUtils { def readClass(bytes: Array[Byte]): ClassNode = { val node = new ClassNode() - new ClassReader(bytes).accept(node, 0) + new ClassReader(bytes).accept(node, Array[Attribute](InlineInfoAttributePrototype), 0) node } @@ -105,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 27827015c3..162da4236a 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeAsmCommon.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeAsmCommon.scala @@ -3,9 +3,12 @@ * @author Martin Odersky */ -package scala.tools.nsc.backend.jvm +package scala.tools.nsc +package backend.jvm import scala.tools.nsc.Global +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 @@ -300,4 +303,97 @@ final class BCodeAsmCommon[G <: Global](val global: G) { } interfaces.map(_.typeSymbol) } + + /** + * This is a hack to work around SI-9111. The completer of `methodSym` may report type errors. We + * cannot change the typer context of the completer at this point and make it silent: the context + * captured when creating the completer in the namer. However, we can temporarily replace + * global.reporter (it's a var) to store errors. + */ + def completeSilentlyAndCheckErroneous(sym: Symbol): Boolean = { + if (sym.hasCompleteInfo) false + else { + val originalReporter = global.reporter + val storeReporter = new reporters.StoreReporter() + global.reporter = storeReporter + try { + sym.info + } finally { + global.reporter = originalReporter + } + sym.isErroneous + } + } + + /** + * Build the [[InlineInfo]] for a class symbol. + */ + def buildInlineInfoFromClassSymbol(classSym: Symbol, classSymToInternalName: Symbol => InternalName, methodSymToDescriptor: Symbol => String): InlineInfo = { + val selfType = { + // The mixin phase uses typeOfThis for the self parameter in implementation class methods. + val selfSym = classSym.typeOfThis.typeSymbol + if (selfSym != classSym) Some(classSymToInternalName(selfSym)) else None + } + + val isEffectivelyFinal = classSym.isEffectivelyFinal + + 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]]. + val methodInlineInfos = classSym.info.decls.iterator.filter(m => m.isMethod && !scalaPrimitives.isPrimitive(m)).flatMap({ + case methodSym => + 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(ClassSymbolInfoFailureSI9111(classSym.fullName)) + None + } else { + val name = methodSym.javaSimpleName.toString // same as in genDefDef + val signature = name + methodSymToDescriptor(methodSym) + + // Some detours are required here because of changing flags (lateDEFERRED, lateMODULE): + // 1. Why the phase travel? Concrete trait methods obtain the lateDEFERRED flag in Mixin. + // This makes isEffectivelyFinalOrNotOverridden false, which would prevent non-final + // but non-overridden methods of sealed traits from being inlined. + // 2. Why the special case for `classSym.isImplClass`? Impl class symbols obtain the + // lateMODULE flag during Mixin. During the phase travel to exitingPickler, the late + // flag is ignored. The members are therefore not isEffectivelyFinal (their owner + // is not a module). Since we know that all impl class members are static, we can + // just take the shortcut. + val effectivelyFinal = classSym.isImplClass || exitingPickler(methodSym.isEffectivelyFinalOrNotOverridden) + + // Identify trait interface methods that have a static implementation in the implementation + // class. Invocations of these methods can be re-wrired directly to the static implementation + // if they are final or the receiver is known. + // + // Using `erasure.needsImplMethod` is not enough: it keeps field accessors, module getters + // and super accessors. When AddInterfaces creates the impl class, these methods are + // initially added to it. + // + // The mixin phase later on filters out most of these members from the impl class (see + // Mixin.isImplementedStatically). However, accessors for concrete lazy vals remain in the + // impl class after mixin. So the filter in mixin is not exactly what we need here (we + // want to identify concrete trait methods, not any accessors). So we check some symbol + // properties manually. + val traitMethodWithStaticImplementation = { + import symtab.Flags._ + classSym.isTrait && !classSym.isImplClass && + erasure.needsImplMethod(methodSym) && + !methodSym.isModule && + !(methodSym hasFlag (ACCESSOR | SUPERACCESSOR)) + } + + val info = MethodInlineInfo( + effectivelyFinal = effectivelyFinal, + traitMethodWithStaticImplementation = traitMethodWithStaticImplementation, + annotatedInline = methodSym.hasAnnotation(ScalaInlineClass), + annotatedNoInline = methodSym.hasAnnotation(ScalaNoInlineClass) + ) + Some((signature, info)) + } + }).toMap + + InlineInfo(selfType, isEffectivelyFinal, methodInlineInfos, warning) + } } 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 a194fe8fe4..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 enclosingNestedClassesChain: List[ClassBType] = - if (isNestedClass) this :: info.nestedInfo.get.enclosingClass.enclosingNestedClassesChain - else Nil + def isNestedClass: Either[NoClassBTypeInfo, Boolean] = info.map(_.nestedInfo.isDefined) - def innerClassAttributeEntry: Option[InnerClassEntry] = info.nestedInfo map { + 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: 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. @@ -936,4 +1098,60 @@ object BTypes { * But that would create overhead in a Collection[InternalName]. */ type InternalName = String + + /** + * Metadata about a ClassBType, used by the inliner. + * + * More information may be added in the future to enable more elaborate inlinine heuristics. + * + * @param traitImplClassSelfType `Some(tp)` if this InlineInfo describes a trait, and the `self` + * parameter type of the methods in the implementation class is not + * the trait itself. Example: + * trait T { self: U => def f = 1 } + * Generates something like: + * class T$class { static def f(self: U) = 1 } + * + * In order to inline a trat method call, the INVOKEINTERFACE is + * rewritten to an INVOKESTATIC of the impl class, so we need the + * self type (U) to get the right signature. + * + * `None` if the self type is the interface type, or if this + * InlineInfo does not describe a trait. + * + * @param isEffectivelyFinal True if the class cannot have subclasses: final classes, module + * classes, trait impl classes. + * + * @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[ClassInlineInfoWarning]) + + val EmptyInlineInfo = InlineInfo(None, false, Map.empty, None) + + /** + * Metadata about a method, used by the inliner. + * + * @param effectivelyFinal True if the method cannot be overridden (in Scala) + * @param traitMethodWithStaticImplementation True if the method is an interface method method of + * a trait method and has a static counterpart in the + * implementation class. + * @param annotatedInline True if the method is annotated `@inline` + * @param annotatedNoInline True if the method is annotated `@noinline` + */ + final case class MethodInlineInfo(effectivelyFinal: Boolean, + 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 707336e5de..86a69b92ea 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/GenASM.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/GenASM.scala @@ -9,6 +9,7 @@ package backend.jvm import scala.collection.{ mutable, immutable } import scala.reflect.internal.pickling.{ PickleFormat, PickleBuffer } +import scala.tools.nsc.backend.jvm.opt.InlineInfoAttribute import scala.tools.nsc.symtab._ import scala.tools.asm import asm.Label @@ -532,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}") } @@ -1292,6 +1293,9 @@ abstract class GenASM extends SubComponent with BytecodeWriters { self => jclass.visitAttribute(if(ssa.isDefined) pickleMarkerLocal else pickleMarkerForeign) emitAnnotations(jclass, c.symbol.annotations ++ ssa) + if (!settings.YskipInlineInfoAttribute.value) + jclass.visitAttribute(InlineInfoAttribute(buildInlineInfoFromClassSymbol(c.symbol, javaName, javaType(_).getDescriptor))) + // typestate: entering mode with valid call sequences: // ( visitInnerClass | visitField | visitMethod )* visitEnd 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 new file mode 100644 index 0000000000..e7dd5abc57 --- /dev/null +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/InlineInfoAttribute.scala @@ -0,0 +1,148 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2014 LAMP/EPFL + * @author Martin Odersky + */ + +package scala.tools.nsc +package backend.jvm +package opt + +import scala.tools.asm._ +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. + * The compiler does so for every class being compiled. + * + * The reason is that a precise InlineInfo can only be obtained if the symbol for a class is available. + * For example, we need to know if a method is final in Scala's terms, or if it has the @inline annotation. + * Looking up a class symbol for a given class filename is brittle (name-mangling). + * + * The attribute is also helpful for inlining mixin methods. The mixin phase only adds mixin method + * symbols to classes that are being compiled. For all other class symbols, there are no mixin members. + * However, the inliner requires an InlineInfo for inlining mixin members. That problem is solved by + * reading the InlineInfo from this attribute. + * + * In principle we could encode the InlineInfo into a Java annotation (instead of a classfile attribute). + * However, an attribute allows us to save many bits. In particular, note that the strings in an + * InlineInfo are serialized as references to constants in the constant pool, and those strings + * (traitImplClassSelfType, method names, method signatures) would exist in there anyway. So the + * ScalaInlineAttribute remains relatively compact. + */ +case class InlineInfoAttribute(inlineInfo: InlineInfo) extends Attribute(InlineInfoAttribute.attributeName) { + /** + * Not sure what this method is good for, it is not invoked anywhere in the ASM framework. However, + * the example in the ASM manual also overrides it to `false` for custom attributes, so it might be + * a good idea. + */ + override def isUnknown: Boolean = false + + /** + * Serialize the `inlineInfo` into a byte array. Strings are added to the constant pool and serialized + * as references. + */ + override def write(cw: ClassWriter, code: Array[Byte], len: Int, maxStack: Int, maxLocals: Int): ByteVector = { + val result = new ByteVector() + + result.putByte(InlineInfoAttribute.VERSION) + + var hasSelfIsFinal = 0 + if (inlineInfo.isEffectivelyFinal) hasSelfIsFinal |= 1 + if (inlineInfo.traitImplClassSelfType.isDefined) hasSelfIsFinal |= 2 + result.putByte(hasSelfIsFinal) + + for (selfInternalName <- inlineInfo.traitImplClassSelfType) { + result.putShort(cw.newUTF8(selfInternalName)) + } + + // The method count fits in a short (the methods_count in a classfile is also a short) + result.putShort(inlineInfo.methodInfos.size) + + // Sort the methodInfos for stability of classfiles + for ((nameAndType, info) <- inlineInfo.methodInfos.toList.sortBy(_._1)) { + val (name, desc) = nameAndType.span(_ != '(') + // Name and desc are added separately because a NameAndType entry also stores them separately. + // This makes sure that we use the existing constant pool entries for the method. + result.putShort(cw.newUTF8(name)) + result.putShort(cw.newUTF8(desc)) + + var inlineInfo = 0 + if (info.effectivelyFinal) inlineInfo |= 1 + if (info.traitMethodWithStaticImplementation) inlineInfo |= 2 + if (info.annotatedInline) inlineInfo |= 4 + if (info.annotatedNoInline) inlineInfo |= 8 + result.putByte(inlineInfo) + } + + result + } + + /** + * De-serialize the attribute into an InlineInfo. The attribute starts at cr.b(off), but we don't + * need to access that array directly, we can use the `read` methods provided by the ClassReader. + * + * `buf` is a pre-allocated character array that is guaranteed to be long enough to hold any + * string of the constant pool. So we can use it to invoke `cr.readUTF8`. + */ + override def read(cr: ClassReader, off: Int, len: Int, buf: Array[Char], codeOff: Int, labels: Array[Label]): InlineInfoAttribute = { + var next = off + + def nextByte() = { val r = cr.readByte(next) ; next += 1; r } + def nextUTF8() = { val r = cr.readUTF8(next, buf); next += 2; r } + def nextShort() = { val r = cr.readShort(next) ; next += 2; r } + + val version = nextByte() + if (version == 1) { + val hasSelfIsFinal = nextByte() + val isFinal = (hasSelfIsFinal & 1) != 0 + val hasSelf = (hasSelfIsFinal & 2) != 0 + + val self = if (hasSelf) { + val selfName = nextUTF8() + Some(selfName) + } else { + None + } + + val numEntries = nextShort() + val infos = (0 until numEntries).map(_ => { + val name = nextUTF8() + val desc = nextUTF8() + + val inlineInfo = nextByte() + val isFinal = (inlineInfo & 1) != 0 + val traitMethodWithStaticImplementation = (inlineInfo & 2) != 0 + val isInline = (inlineInfo & 4) != 0 + val isNoInline = (inlineInfo & 8) != 0 + (name + desc, MethodInlineInfo(isFinal, traitMethodWithStaticImplementation, isInline, isNoInline)) + }).toMap + + InlineInfoAttribute(InlineInfo(self, isFinal, infos, None)) + } else { + val msg = UnknownScalaInlineInfoVersion(cr.getClassName, version) + InlineInfoAttribute(BTypes.EmptyInlineInfo.copy(warning = Some(msg))) + } + } +} + +object InlineInfoAttribute { + /** + * [u1] version + * [u1] isEffectivelyFinal (<< 0), hasTraitImplClassSelfType (<< 1) + * [u2]? traitImplClassSelfType (reference) + * [u2] numMethodEntries + * [u2] name (reference) + * [u2] descriptor (reference) + * [u1] isFinal (<< 0), traitMethodWithStaticImplementation (<< 1), hasInlineAnnotation (<< 2), hasNoInlineAnnotation (<< 3) + */ + final val VERSION: Byte = 1 + + final val attributeName = "ScalaInlineInfo" +} + +/** + * In order to instruct the ASM framework to de-serialize the ScalaInlineInfo attribute, we need + * to pass a prototype instance when running the class reader. + */ +object InlineInfoAttributePrototype extends InlineInfoAttribute(InlineInfo(null, false, null, null)) 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 a5b722612d..d273995e6e 100644 --- a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala @@ -213,26 +213,29 @@ trait ScalaSettings extends AbsScalaSettings // the current standard is "inline" but we are moving towards "method" val Ydelambdafy = ChoiceSetting ("-Ydelambdafy", "strategy", "Strategy used for translating lambdas into JVM code.", List("inline", "method"), "inline") + val YskipInlineInfoAttribute = BooleanSetting("-Yskip-inline-info-attribute", "Do not add the ScalaInlineInfo attribute to classfiles generated by -Ybackend:GenASM") + 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) } @@ -245,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/AddInterfaces.scala b/src/compiler/scala/tools/nsc/transform/AddInterfaces.scala index f786ffb8f3..3591372bbe 100644 --- a/src/compiler/scala/tools/nsc/transform/AddInterfaces.scala +++ b/src/compiler/scala/tools/nsc/transform/AddInterfaces.scala @@ -55,7 +55,7 @@ abstract class AddInterfaces extends InfoTransform { self: Erasure => ) /** Does symbol need an implementation method? */ - private def needsImplMethod(sym: Symbol) = ( + def needsImplMethod(sym: Symbol) = ( sym.isMethod && isInterfaceMember(sym) && (!sym.hasFlag(DEFERRED | SUPERACCESSOR) || (sym hasFlag lateDEFERRED)) 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 d4756f0af3..598f1f3c93 100644 --- a/src/compiler/scala/tools/nsc/typechecker/Typers.scala +++ b/src/compiler/scala/tools/nsc/typechecker/Typers.scala @@ -151,7 +151,7 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper for(ar <- argResultsBuff) paramTp = paramTp.subst(ar.subst.from, ar.subst.to) - val res = if (paramFailed || (paramTp.isError && {paramFailed = true; true})) SearchFailure else inferImplicit(fun, paramTp, context.reportErrors, isView = false, context) + val res = if (paramFailed || (paramTp.isErroneous && {paramFailed = true; true})) SearchFailure else inferImplicit(fun, paramTp, context.reportErrors, isView = false, context) argResultsBuff += res if (res.isSuccess) { @@ -1039,11 +1039,11 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper // to non-continuation types. if (tree.tpe <:< AnyTpe) pt.dealias match { case TypeRef(_, UnitClass, _) => // (12) - if (settings.warnValueDiscard) + if (!isPastTyper && settings.warnValueDiscard) context.warning(tree.pos, "discarded non-Unit value") return typedPos(tree.pos, mode, pt)(Block(List(tree), Literal(Constant(())))) case TypeRef(_, sym, _) if isNumericValueClass(sym) && isNumericSubType(tree.tpe, pt) => - if (settings.warnNumericWiden) + if (!isPastTyper && settings.warnNumericWiden) context.warning(tree.pos, "implicit numeric widening") return typedPos(tree.pos, mode, pt)(Select(tree, "to" + sym.name)) case _ => @@ -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 _ => @@ -5200,13 +5200,10 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper def suspiciousExpr = InterpolatorCodeRegex findFirstIn s def suspiciousIdents = InterpolatorIdentRegex findAllIn s map (s => suspiciousSym(s drop 1)) - // heuristics - no warning on e.g. a string with only "$asInstanceOf" - if (s contains ' ') ( - if (suspiciousExpr.nonEmpty) - warn("detected an interpolated expression") // "${...}" - else - suspiciousIdents find isPlausible foreach (sym => warn(s"detected interpolated identifier `$$${sym.name}`")) // "$id" - ) + if (suspiciousExpr.nonEmpty) + warn("detected an interpolated expression") // "${...}" + else + suspiciousIdents find isPlausible foreach (sym => warn(s"detected interpolated identifier `$$${sym.name}`")) // "$id" } lit match { case Literal(Constant(s: String)) if !isRecognizablyNotForInterpolation => maybeWarn(s) 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/library/scala/collection/immutable/Stream.scala b/src/library/scala/collection/immutable/Stream.scala index 9ed5162061..f303e79bb3 100644 --- a/src/library/scala/collection/immutable/Stream.scala +++ b/src/library/scala/collection/immutable/Stream.scala @@ -743,16 +743,18 @@ self => b append end return b } - if ((cursor ne scout) && scout.tailDefined) { + if (cursor ne scout) { cursor = scout - scout = scout.tail - // Use 2x 1x iterator trick for cycle detection; slow iterator can add strings - while ((cursor ne scout) && scout.tailDefined) { - b append sep append cursor.head - n += 1 - cursor = cursor.tail + if (scout.tailDefined) { scout = scout.tail - if (scout.tailDefined) scout = scout.tail + // Use 2x 1x iterator trick for cycle detection; slow iterator can add strings + while ((cursor ne scout) && scout.tailDefined) { + b append sep append cursor.head + n += 1 + cursor = cursor.tail + scout = scout.tail + if (scout.tailDefined) scout = scout.tail + } } } if (!scout.tailDefined) { // Not a cycle, scout hit an end @@ -761,6 +763,9 @@ self => n += 1 cursor = cursor.tail } + if (cursor.nonEmpty) { + b append sep append cursor.head + } } else { // Cycle. diff --git a/src/library/scala/concurrent/Promise.scala b/src/library/scala/concurrent/Promise.scala index eb8044ed3b..0f4e98db57 100644 --- a/src/library/scala/concurrent/Promise.scala +++ b/src/library/scala/concurrent/Promise.scala @@ -66,10 +66,7 @@ trait Promise[T] { * * @return This promise */ - final def completeWith(other: Future[T]): this.type = { - other onComplete { this complete _ } - this - } + final def completeWith(other: Future[T]): this.type = tryCompleteWith(other) /** Attempts to complete this promise with the specified future, once that future is completed. * diff --git a/src/partest-extras/scala/tools/partest/ReplTest.scala b/src/partest-extras/scala/tools/partest/ReplTest.scala index a728e8bdef..5b65d6ab9b 100644 --- a/src/partest-extras/scala/tools/partest/ReplTest.scala +++ b/src/partest-extras/scala/tools/partest/ReplTest.scala @@ -8,6 +8,7 @@ package scala.tools.partest import scala.tools.nsc.Settings import scala.tools.nsc.interpreter.ILoop import java.lang.reflect.{ Method => JMethod, Field => JField } +import scala.util.matching.Regex.Match /** A class for testing repl code. * It filters the line of output that mentions a version number. @@ -22,6 +23,9 @@ abstract class ReplTest extends DirectTest { s.Xnojline.value = true transformSettings(s) } + /** True for SessionTest to preserve session text. */ + def inSession: Boolean = false + /** True to preserve welcome text. */ def welcoming: Boolean = false lazy val welcome = "(Welcome to Scala) version .*".r def normalize(s: String) = s match { @@ -36,7 +40,7 @@ abstract class ReplTest extends DirectTest { val s = settings log("eval(): settings = " + s) //ILoop.runForTranscript(code, s).lines drop 1 // not always first line - val lines = ILoop.runForTranscript(code, s).lines + val lines = ILoop.runForTranscript(code, s, inSession = inSession).lines if (welcoming) lines map normalize else lines filter unwelcoming } @@ -57,13 +61,30 @@ abstract class SessionTest extends ReplTest { /** Session transcript, as a triple-quoted, multiline, marginalized string. */ def session: String - /** Expected output, as an iterator. */ - def expected = session.stripMargin.lines + /** Expected output, as an iterator, optionally marginally stripped. */ + def expected = if (stripMargins) session.stripMargin.lines else session.lines + + /** Override with false if we should not strip margins because of leading continuation lines. */ + def stripMargins: Boolean = true + + /** Analogous to stripMargins, don't mangle continuation lines on echo. */ + override def inSession: Boolean = true /** Code is the command list culled from the session (or the expected session output). - * Would be nicer if code were lazy lines. + * Would be nicer if code were lazy lines so you could generate arbitrarily long text. + * Retain user input: prompt lines and continuations, without the prefix; or pasted text plus ctl-D. */ - override final def code = expected filter (_ startsWith prompt) map (_ drop prompt.length) mkString "\n" + import SessionTest._ + override final def code = input findAllMatchIn (expected mkString ("", "\n", "\n")) map { + case input(null, null, prompted) => + def continued(m: Match): Option[String] = m match { + case margin(text) => Some(text) + case _ => None + } + margin.replaceSomeIn(prompted, continued) + case input(cmd, pasted, null) => + cmd + pasted + "\u0004" + } mkString final def prompt = "scala> " @@ -75,3 +96,9 @@ abstract class SessionTest extends ReplTest { if (evaled != wanted) Console print nest.FileManager.compareContents(wanted, evaled, "expected", "actual") } } +object SessionTest { + // \R for line break is Java 8, \v for vertical space might suffice + val input = """(?m)^scala> (:pa.*\u000A)// Entering paste mode.*\u000A\u000A((?:.*\u000A)*)\u000A// Exiting paste mode.*\u000A|^scala> (.*\u000A(?:\s*\| .*\u000A)*)""".r + + val margin = """(?m)^\s*\| (.*)$""".r +} 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/src/reflect/scala/reflect/runtime/JavaMirrors.scala b/src/reflect/scala/reflect/runtime/JavaMirrors.scala index 1c751fb93b..3b497227e7 100644 --- a/src/reflect/scala/reflect/runtime/JavaMirrors.scala +++ b/src/reflect/scala/reflect/runtime/JavaMirrors.scala @@ -428,9 +428,12 @@ private[scala] trait JavaMirrors extends internal.SymbolTable with api.JavaUnive var i = 0 while (i < args1.length) { val arg = args(i) - if (i >= paramCount) args1(i) = arg // don't transform varargs - else if (isByName(i)) args1(i) = () => arg // don't transform by-name value class params - else if (isDerivedValueClass(i)) args1(i) = paramUnboxers(i).invoke(arg) + args1(i) = ( + if (i >= paramCount) arg // don't transform varargs + else if (isByName(i)) () => arg // don't transform by-name value class params + else if (isDerivedValueClass(i)) paramUnboxers(i).invoke(arg) // do get the underlying value + else arg // don't molest anything else + ) i += 1 } jinvoke(args1) diff --git a/src/repl/scala/tools/nsc/interpreter/ILoop.scala b/src/repl/scala/tools/nsc/interpreter/ILoop.scala index 4d71e0e09e..4221126caa 100644 --- a/src/repl/scala/tools/nsc/interpreter/ILoop.scala +++ b/src/repl/scala/tools/nsc/interpreter/ILoop.scala @@ -937,25 +937,30 @@ object ILoop { // Designed primarily for use by test code: take a String with a // bunch of code, and prints out a transcript of what it would look // like if you'd just typed it into the repl. - def runForTranscript(code: String, settings: Settings): String = { + def runForTranscript(code: String, settings: Settings, inSession: Boolean = false): String = { import java.io.{ BufferedReader, StringReader, OutputStreamWriter } stringFromStream { ostream => Console.withOut(ostream) { val output = new JPrintWriter(new OutputStreamWriter(ostream), true) { - override def write(str: String) = { - // completely skip continuation lines - if (str forall (ch => ch.isWhitespace || ch == '|')) () + // skip margin prefix for continuation lines, unless preserving session text for test + override def write(str: String) = + if (!inSession && (str forall (ch => ch.isWhitespace || ch == '|'))) () // repl.paste.ContinueString else super.write(str) - } } val input = new BufferedReader(new StringReader(code.trim + "\n")) { override def readLine(): String = { - val s = super.readLine() - // helping out by printing the line being interpreted. - if (s != null) + mark(1) // default buffer is 8k + val c = read() + if (c == -1 || c == 4) { + null + } else { + reset() + val s = super.readLine() + // helping out by printing the line being interpreted. output.println(s) - s + s + } } } val repl = new ILoop(input, output) diff --git a/test/files/jvm/future-spec/PromiseTests.scala b/test/files/jvm/future-spec/PromiseTests.scala index 12b9168c5d..67c8c542ba 100644 --- a/test/files/jvm/future-spec/PromiseTests.scala +++ b/test/files/jvm/future-spec/PromiseTests.scala @@ -44,20 +44,79 @@ class PromiseTests extends MinimalScalaTest { }.getMessage mustBe ("br0ken") } + "be completable with a completed Promise" in { + { + val p = Promise[String]() + p.tryCompleteWith(Promise[String]().success("foo").future) + Await.result(p.future, defaultTimeout) mustBe ("foo") + } + { + val p = Promise[String]() + p.completeWith(Promise[String]().success("foo").future) + Await.result(p.future, defaultTimeout) mustBe ("foo") + } + { + val p = Promise[String]() + p.tryCompleteWith(Promise[String]().failure(new RuntimeException("br0ken")).future) + intercept[RuntimeException] { + Await.result(p.future, defaultTimeout) + }.getMessage mustBe ("br0ken") + } + { + val p = Promise[String]() + p.tryCompleteWith(Promise[String]().failure(new RuntimeException("br0ken")).future) + intercept[RuntimeException] { + Await.result(p.future, defaultTimeout) + }.getMessage mustBe ("br0ken") + } + } } "A successful Promise" should { - val result = "test value" - val promise = Promise[String]().complete(Success(result)) - promise.isCompleted mustBe (true) - futureWithResult(_(promise.future, result)) + "be completed" in { + val result = "test value" + val promise = Promise[String]().complete(Success(result)) + promise.isCompleted mustBe (true) + futureWithResult(_(promise.future, result)) + } + + "not be completable with a completed Promise" in { + { + val p = Promise.successful("bar") + p.tryCompleteWith(Promise[String]().success("foo").future) + Await.result(p.future, defaultTimeout) mustBe ("bar") + } + { + val p = Promise.successful("bar") + p.completeWith(Promise[String]().success("foo").future) + Await.result(p.future, defaultTimeout) mustBe ("bar") + } + } } "A failed Promise" should { - val message = "Expected Exception" - val promise = Promise[String]().complete(Failure(new RuntimeException(message))) - promise.isCompleted mustBe (true) - futureWithException[RuntimeException](_(promise.future, message)) + "be completed" in { + val message = "Expected Exception" + val promise = Promise[String]().complete(Failure(new RuntimeException(message))) + promise.isCompleted mustBe (true) + futureWithException[RuntimeException](_(promise.future, message)) + } + "not be completable with a completed Promise" in { + { + val p = Promise[String]().failure(new RuntimeException("unbr0ken")) + p.tryCompleteWith(Promise[String].failure(new Exception("br0ken")).future) + intercept[RuntimeException] { + Await.result(p.future, defaultTimeout) + }.getMessage mustBe ("unbr0ken") + } + { + val p = Promise[String]().failure(new RuntimeException("unbr0ken")) + p.completeWith(Promise[String]().failure(new Exception("br0ken")).future) + intercept[RuntimeException] { + Await.result(p.future, defaultTimeout) + }.getMessage mustBe ("unbr0ken") + } + } } "An interrupted Promise" should { diff --git a/test/files/jvm/t8689.check b/test/files/jvm/t8689.check new file mode 100644 index 0000000000..2e9ba477f8 --- /dev/null +++ b/test/files/jvm/t8689.check @@ -0,0 +1 @@ +success diff --git a/test/files/jvm/t8689.scala b/test/files/jvm/t8689.scala new file mode 100644 index 0000000000..ef43a1df63 --- /dev/null +++ b/test/files/jvm/t8689.scala @@ -0,0 +1,13 @@ +object Test { + def main(args: Array[String]): Unit = { + import scala.concurrent._ + import ExecutionContext.Implicits.global + val source1 = Promise[Int]() + val source2 = Promise[Int]() + source2.completeWith(source1.future).future.onComplete { + case _ => print("success") + } + source2.tryFailure(new TimeoutException) + source1.success(123) + } +}
\ No newline at end of file diff --git a/test/files/neg/t7848-interp-warn.check b/test/files/neg/t7848-interp-warn.check index 4cf9d55ffd..637fc8941a 100644 --- a/test/files/neg/t7848-interp-warn.check +++ b/test/files/neg/t7848-interp-warn.check @@ -4,9 +4,12 @@ t7848-interp-warn.scala:8: warning: possible missing interpolator: detected inte t7848-interp-warn.scala:12: warning: possible missing interpolator: detected an interpolated expression "A doubly important ${foo * 2} message!" ^ +t7848-interp-warn.scala:15: warning: possible missing interpolator: detected interpolated identifier `$bar` + def i = s"Try using '${ "$bar" }' instead." // was: no warn on space test + ^ t7848-interp-warn.scala:16: warning: possible missing interpolator: detected interpolated identifier `$bar` def j = s"Try using '${ "something like $bar" }' instead." // warn ^ error: No warnings can be incurred under -Xfatal-warnings. -three warnings found +four warnings found one error found diff --git a/test/files/neg/t7848-interp-warn.scala b/test/files/neg/t7848-interp-warn.scala index 3887aff8de..a76141041d 100644 --- a/test/files/neg/t7848-interp-warn.scala +++ b/test/files/neg/t7848-interp-warn.scala @@ -12,7 +12,7 @@ object Test { "A doubly important ${foo * 2} message!" } def h = s"Try using '$$bar' instead." // no warn - def i = s"Try using '${ "$bar" }' instead." // no warn on space test + def i = s"Try using '${ "$bar" }' instead." // was: no warn on space test def j = s"Try using '${ "something like $bar" }' instead." // warn def k = f"Try using '$bar' instead." // no warn on other std interps } diff --git a/test/files/neg/t9127.check b/test/files/neg/t9127.check new file mode 100644 index 0000000000..2ecf8af464 --- /dev/null +++ b/test/files/neg/t9127.check @@ -0,0 +1,12 @@ +t9127.scala:4: warning: possible missing interpolator: detected interpolated identifier `$s` + val t = "$s" + ^ +t9127.scala:5: warning: possible missing interpolator: detected an interpolated expression + val u = "a${s}b" + ^ +t9127.scala:6: warning: possible missing interpolator: detected interpolated identifier `$s` + val v = "a$s b" + ^ +error: No warnings can be incurred under -Xfatal-warnings. +three warnings found +one error found diff --git a/test/files/neg/t9127.flags b/test/files/neg/t9127.flags new file mode 100644 index 0000000000..b0d7bc25cb --- /dev/null +++ b/test/files/neg/t9127.flags @@ -0,0 +1 @@ +-Xlint:missing-interpolator -Xfatal-warnings diff --git a/test/files/neg/t9127.scala b/test/files/neg/t9127.scala new file mode 100644 index 0000000000..c0746144eb --- /dev/null +++ b/test/files/neg/t9127.scala @@ -0,0 +1,7 @@ + +trait X { + val s = "hello" + val t = "$s" + val u = "a${s}b" + val v = "a$s b" +} diff --git a/test/files/neg/t9231.check b/test/files/neg/t9231.check new file mode 100644 index 0000000000..43c14f53ca --- /dev/null +++ b/test/files/neg/t9231.check @@ -0,0 +1,4 @@ +t9231.scala:8: error: not found: type DoesNotExist + foo[DoesNotExist] + ^ +one error found diff --git a/test/files/neg/t9231.scala b/test/files/neg/t9231.scala new file mode 100644 index 0000000000..05b1d24e9a --- /dev/null +++ b/test/files/neg/t9231.scala @@ -0,0 +1,9 @@ +class M[A] +class C { + implicit def M1: M[Int] = null + implicit def M2: M[String] = null + + def foo[A](implicit M: M[A]) = null + + foo[DoesNotExist] +} diff --git a/test/files/pos/jesper.scala b/test/files/pos/jesper.scala new file mode 100644 index 0000000000..82623e4a24 --- /dev/null +++ b/test/files/pos/jesper.scala @@ -0,0 +1,30 @@ +object Pair { + sealed trait Pair { + type First + type Second <: Pair + } + + case class End() extends Pair { + type First = Nothing + type Second = End + + def ::[T](v : T) : Cons[T, End] = Cons(v, this) + } + + object End extends End() + + final case class Cons[T1, T2 <: Pair](_1 : T1, _2 : T2) extends Pair { + type First = T1 + type Second = T2 + + def ::[T](v : T) : Cons[T, Cons[T1, T2]] = Cons(v, this) + def find[T](implicit finder : Cons[T1, T2] => T) = finder(this) + } + + implicit def findFirst[T1, T2 <: Pair] : Cons[T1, T2] => T1 = (p : Cons[T1, T2]) => p._1 + implicit def findSecond[T, T1, T2 <: Pair](implicit finder : T2 => T) : Cons[T1, T2] => T = (p : Cons[T1, T2]) => finder(p._2) + + val p : Cons[Int, Cons[Boolean, End]] = 10 :: false :: End +// val x : Boolean = p.find[Boolean](findSecond(findFirst)) + val x2 : Boolean = p.find[Boolean] // Doesn't compile +} diff --git a/test/files/pos/t9020.flags b/test/files/pos/t9020.flags new file mode 100644 index 0000000000..efb2dd3e6f --- /dev/null +++ b/test/files/pos/t9020.flags @@ -0,0 +1 @@ +-Ywarn-value-discard -Xfatal-warnings diff --git a/test/files/pos/t9020.scala b/test/files/pos/t9020.scala new file mode 100644 index 0000000000..16e31e2572 --- /dev/null +++ b/test/files/pos/t9020.scala @@ -0,0 +1,10 @@ +trait ValueDiscard[@specialized U] { + def u: U +} +/* Was: +scalac-hash v2.11.5 -Ywarn-value-discard test/files/pos/t9020.scala +test/files/pos/t9020.scala:2: warning: discarded non-Unit value + def u: U + ^ +one warning found +*/ 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/res/t9170.check b/test/files/res/t9170.check new file mode 100644 index 0000000000..6d40b6ba8d --- /dev/null +++ b/test/files/res/t9170.check @@ -0,0 +1,7 @@ + +nsc> t9170/A.scala:3: error: double definition: +def f[A](a: => A): Int at line 2 and +def f[A](a: => Either[Exception,A]): Int at line 3 +have same type after erasure: (a: Function0)Int + def f[A](a: => Either[Exception, A]) = 2 + ^ diff --git a/test/files/res/t9170.res b/test/files/res/t9170.res new file mode 100644 index 0000000000..c2aec2f8dd --- /dev/null +++ b/test/files/res/t9170.res @@ -0,0 +1,2 @@ +t9170/A.scala +t9170/A.scala diff --git a/test/files/res/t9170/A.scala b/test/files/res/t9170/A.scala new file mode 100644 index 0000000000..239df89679 --- /dev/null +++ b/test/files/res/t9170/A.scala @@ -0,0 +1,4 @@ +object Y { + def f[A](a: => A) = 1 + def f[A](a: => Either[Exception, A]) = 2 +} 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/t7974.check b/test/files/run/t7974.check index d8152d3286..4eae5eb152 100644 --- a/test/files/run/t7974.check +++ b/test/files/run/t7974.check @@ -1,19 +1,3 @@ -public class Symbols { - - - - - // access flags 0x12 - private final Lscala/Symbol; someSymbol3 - - // access flags 0xA - private static Lscala/Symbol; symbol$1 - - // access flags 0xA - private static Lscala/Symbol; symbol$2 - - // access flags 0xA - private static Lscala/Symbol; symbol$3 // access flags 0x9 public static <clinit>()V @@ -33,6 +17,7 @@ public class Symbols { MAXSTACK = 2 MAXLOCALS = 0 + // access flags 0x1 public someSymbol1()Lscala/Symbol; GETSTATIC Symbols.symbol$1 : Lscala/Symbol; @@ -40,6 +25,7 @@ public class Symbols { MAXSTACK = 1 MAXLOCALS = 1 + // access flags 0x1 public someSymbol2()Lscala/Symbol; GETSTATIC Symbols.symbol$2 : Lscala/Symbol; @@ -47,6 +33,7 @@ public class Symbols { MAXSTACK = 1 MAXLOCALS = 1 + // access flags 0x1 public sameSymbol1()Lscala/Symbol; GETSTATIC Symbols.symbol$1 : Lscala/Symbol; @@ -54,6 +41,7 @@ public class Symbols { MAXSTACK = 1 MAXLOCALS = 1 + // access flags 0x1 public someSymbol3()Lscala/Symbol; ALOAD 0 @@ -62,6 +50,7 @@ public class Symbols { MAXSTACK = 1 MAXLOCALS = 1 + // access flags 0x1 public <init>()V ALOAD 0 @@ -72,4 +61,4 @@ public class Symbols { RETURN MAXSTACK = 2 MAXLOCALS = 1 -} + diff --git a/test/files/run/t7974/Test.scala b/test/files/run/t7974/Test.scala index 29d2b9cb64..296ec32ee2 100644 --- a/test/files/run/t7974/Test.scala +++ b/test/files/run/t7974/Test.scala @@ -1,20 +1,14 @@ -import java.io.PrintWriter; +import java.io.PrintWriter import scala.tools.partest.BytecodeTest +import scala.tools.nsc.backend.jvm.AsmUtils import scala.tools.asm.util._ import scala.tools.nsc.util.stringFromWriter +import scala.collection.convert.decorateAsScala._ object Test extends BytecodeTest { def show { val classNode = loadClassNode("Symbols", skipDebugInfo = true) - val textifier = new Textifier - classNode.accept(new TraceClassVisitor(null, textifier, null)) - - val classString = stringFromWriter(w => textifier.print(w)) - val result = - classString.split('\n') - .dropWhile(elem => elem != "public class Symbols {") - .filterNot(elem => elem.startsWith(" @Lscala/reflect/ScalaSignature") || elem.startsWith(" ATTRIBUTE ScalaSig")) - result foreach println + classNode.methods.asScala.foreach(m => println(AsmUtils.textify(m))) } } diff --git a/test/files/run/t9102.scala b/test/files/run/t9102.scala new file mode 100644 index 0000000000..c46cf0e4b4 --- /dev/null +++ b/test/files/run/t9102.scala @@ -0,0 +1,81 @@ + +object Test extends App { + import reflect.runtime._, universe._ + + class C { def f(i: Int, j: => Int) = i + j } + + class V(val v: Int) extends AnyVal { def doubled = 2 * v } + class D { def f(i: Int, j: V) = i + j.doubled } + + class E(i: Int, j: V) + + locally { + val ms = typeOf[C].member(TermName("f")).asMethod + val im = currentMirror reflect (new C) + val mm = im reflectMethod ms + assert(mm(2,3) == 5) + } + locally { + val ms = typeOf[D].member(TermName("f")).asMethod + val im = currentMirror reflect (new D) + val mm = im reflectMethod ms + assert(mm(2, new V(3)) == 8) + } + locally { + val ms = typeOf[E].typeSymbol.asClass.primaryConstructor + val cm = currentMirror reflectClass typeOf[E].typeSymbol.asClass + val mm = cm reflectConstructor ms.asMethod + assert(mm(42, new V(7)).isInstanceOf[E]) + } +} + +/* Session tests without special init code should reside in simple script files. + * Also, provide filters such as for `(bound to C@74f7d1d2)`. + +import scala.tools.partest.SessionTest + +object Test extends SessionTest { +//Welcome to Scala version 2.11.6 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_40). + def session = + s"""|Type in expressions to have them evaluated. + |Type :help for more information. + | + |scala> import reflect.runtime._, universe._ + |import reflect.runtime._ + |import universe._ + | + |scala> class C { def f(i: Int, j: => Int) = i + j } + |defined class C + | + |scala> typeOf[C].member(TermName("f")) + |res0: reflect.runtime.universe.Symbol = method f + | + |scala> .asMethod + |res1: reflect.runtime.universe.MethodSymbol = method f + | + |scala> currentMirror reflect (new C) + |res2: reflect.runtime.universe.InstanceMirror = instance mirror for C@74f7d1d2 + | + |scala> res2 reflectMethod res1 + |res3: reflect.runtime.universe.MethodMirror = method mirror for def f(i: scala.Int,j: => scala.Int): scala.Int (bound to C@74f7d1d2) + | + |scala> res3(2,3) + |res4: Any = 5 + | + |scala> :quit""" +} +*/ + +/* was: +scala> res3(2,3) +java.lang.IllegalArgumentException + at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) + at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + at java.lang.reflect.Method.invoke(Method.java:497) + at scala.reflect.runtime.JavaMirrors$JavaMirror$JavaMethodMirror.jinvokeraw(JavaMirrors.scala:335) + at scala.reflect.runtime.JavaMirrors$JavaMirror$JavaMethodMirror.jinvoke(JavaMirrors.scala:339) + at scala.reflect.runtime.JavaMirrors$JavaMirror$JavaTransformingMethodMirror.apply(JavaMirrors.scala:436) + ... 33 elided +*/ + diff --git a/test/files/run/t9170.scala b/test/files/run/t9170.scala new file mode 100644 index 0000000000..25a0e84581 --- /dev/null +++ b/test/files/run/t9170.scala @@ -0,0 +1,58 @@ + +import scala.tools.partest.SessionTest + +object Test extends SessionTest { + + override def stripMargins = false + + def session = +"""Type in expressions to have them evaluated. +Type :help for more information. + +scala> object Y { def f[A](a: => A) = 1 ; def f[A](a: => Either[Exception, A]) = 2 } +<console>:7: error: double definition: +def f[A](a: => A): Int at line 7 and +def f[A](a: => Either[Exception,A]): Int at line 7 +have same type after erasure: (a: Function0)Int + object Y { def f[A](a: => A) = 1 ; def f[A](a: => Either[Exception, A]) = 2 } + ^ + +scala> object Y { def f[A](a: => A) = 1 ; def f[A](a: => Either[Exception, A]) = 2 } +<console>:7: error: double definition: +def f[A](a: => A): Int at line 7 and +def f[A](a: => Either[Exception,A]): Int at line 7 +have same type after erasure: (a: Function0)Int + object Y { def f[A](a: => A) = 1 ; def f[A](a: => Either[Exception, A]) = 2 } + ^ + +scala> object Y { + | def f[A](a: => A) = 1 + | def f[A](a: => Either[Exception, A]) = 2 + | } +<console>:9: error: double definition: +def f[A](a: => A): Int at line 8 and +def f[A](a: => Either[Exception,A]): Int at line 9 +have same type after erasure: (a: Function0)Int + def f[A](a: => Either[Exception, A]) = 2 + ^ + +scala> :pa +// Entering paste mode (ctrl-D to finish) + +object Y { + def f[A](a: => A) = 1 + def f[A](a: => Either[Exception, A]) = 2 +} + +// Exiting paste mode, now interpreting. + +<console>:9: error: double definition: +def f[A](a: => A): Int at line 8 and +def f[A](a: => Either[Exception,A]): Int at line 9 +have same type after erasure: (a: Function0)Int + def f[A](a: => Either[Exception, A]) = 2 + ^ + +scala> :quit""" +} + diff --git a/test/files/run/t9219.check b/test/files/run/t9219.check new file mode 100644 index 0000000000..3509ece003 --- /dev/null +++ b/test/files/run/t9219.check @@ -0,0 +1,3 @@ +Stream(1, 2, ?) +Stream(1, 2, 3, 4, ?) +Stream(1, 2, 3, 4, 5, 6, ?) diff --git a/test/files/run/t9219.scala b/test/files/run/t9219.scala new file mode 100644 index 0000000000..c15f55faac --- /dev/null +++ b/test/files/run/t9219.scala @@ -0,0 +1,11 @@ +object Test extends App { + def check[U](f: Stream[Int] => U) = { + val s = Stream.from(1) + f(s) + println(s) + } + + check(_.tail) + check(_.take(4).force) + check(_(5)) +} 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/ScalaInlineInfoTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/ScalaInlineInfoTest.scala new file mode 100644 index 0000000000..f8e887426b --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/ScalaInlineInfoTest.scala @@ -0,0 +1,85 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import scala.tools.asm.Opcodes._ +import org.junit.Assert._ + +import CodeGenTools._ +import scala.tools.nsc.backend.jvm.BTypes.{MethodInlineInfo, InlineInfo} +import scala.tools.partest.ASMConverters +import ASMConverters._ +import scala.collection.convert.decorateAsScala._ + +object ScalaInlineInfoTest { + var compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:none") + def clear(): Unit = { compiler = null } +} + +@RunWith(classOf[JUnit4]) +class ScalaInlineInfoTest { + val compiler = newCompiler() + + @Test + def traitMembersInlineInfo(): Unit = { + val code = + """trait T { + | def f1 = 1 // concrete method + | private def f2 = 1 // implOnly method (does not end up in the interface) + | def f3 = { + | def nest = 0 // nested method (does not end up in the interface) + | nest + | } + | + | @inline + | def f4 = super.toString // super accessor + | + | object O // module accessor (method is generated) + | def f5 = { + | object L { val x = 0 } // nested module (just flattened out) + | L.x + | } + | + | @noinline + | def f6: Int // abstract method (not in impl class) + | + | // fields + | + | val x1 = 0 + | var y2 = 0 + | var x3: Int + | lazy val x4 = 0 + | + | final val x5 = 0 + |} + """.stripMargin + + val cs @ List(t, tl, to, tCls) = compileClasses(compiler)(code) + val List(info) = t.attrs.asScala.collect({ case a: InlineInfoAttribute => a.inlineInfo }).toList + val expect = InlineInfo( + None, // self type + false, // final class + Map( + ("O()LT$O$;", MethodInlineInfo(true, false,false,false)), + ("T$$super$toString()Ljava/lang/String;",MethodInlineInfo(false,false,false,false)), + ("T$_setter_$x1_$eq(I)V", MethodInlineInfo(false,false,false,false)), + ("f1()I", MethodInlineInfo(false,true, false,false)), + ("f3()I", MethodInlineInfo(false,true, false,false)), + ("f4()Ljava/lang/String;", MethodInlineInfo(false,true, true, false)), + ("f5()I", MethodInlineInfo(false,true, false,false)), + ("f6()I", MethodInlineInfo(false,false,false,true )), + ("x1()I", MethodInlineInfo(false,false,false,false)), + ("x3()I", MethodInlineInfo(false,false,false,false)), + ("x3_$eq(I)V", MethodInlineInfo(false,false,false,false)), + ("x4()I", MethodInlineInfo(false,false,false,false)), + ("x5()I", MethodInlineInfo(true, false,false,false)), + ("y2()I", MethodInlineInfo(false,false,false,false)), + ("y2_$eq(I)V", MethodInlineInfo(false,false,false,false))), + None // warning + ) + assert(info == expect, info) + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala index 4a45dd9138..da9853148b 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala @@ -13,9 +13,34 @@ import scala.tools.testing.AssertUtil._ import CodeGenTools._ import scala.tools.partest.ASMConverters import ASMConverters._ +import scala.tools.testing.ClearAfterClass + +object UnreachableCodeTest extends ClearAfterClass.Clearable { + // jvm-1.6 enables emitting stack map frames, which impacts the code generation wrt dead basic blocks, + // see comment in BCodeBodyBuilder + var methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:method") + var dceCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code") + var noOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:none") + + // jvm-1.5 disables computing stack map frames, and it emits dead code as-is. note that this flag triggers a deprecation warning + var noOptNoFramesCompiler = newCompiler(extraArgs = "-target:jvm-1.5 -Ybackend:GenBCode -Yopt:l:none -deprecation") + + def clear(): Unit = { + methodOptCompiler = null + dceCompiler = null + noOptCompiler = null + noOptNoFramesCompiler = null + } +} @RunWith(classOf[JUnit4]) -class UnreachableCodeTest { +class UnreachableCodeTest extends ClearAfterClass { + ClearAfterClass.stateToClear = UnreachableCodeTest + + val methodOptCompiler = UnreachableCodeTest.methodOptCompiler + val dceCompiler = UnreachableCodeTest.dceCompiler + val noOptCompiler = UnreachableCodeTest.noOptCompiler + val noOptNoFramesCompiler = UnreachableCodeTest.noOptNoFramesCompiler def assertEliminateDead(code: (Instruction, Boolean)*): Unit = { val method = genMethod()(code.map(_._1): _*) @@ -25,15 +50,6 @@ class UnreachableCodeTest { assertSameCode(nonEliminated, expectedLive) } - // jvm-1.6 enables emitting stack map frames, which impacts the code generation wrt dead basic blocks, - // see comment in BCodeBodyBuilder - val methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:method") - val dceCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code") - val noOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:none") - - // jvm-1.5 disables computing stack map frames, and it emits dead code as-is. - val noOptNoFramesCompiler = newCompiler(extraArgs = "-target:jvm-1.5 -Ybackend:GenBCode -Yopt:l:none") - @Test def basicElimination(): Unit = { assertEliminateDead( @@ -138,7 +154,8 @@ class UnreachableCodeTest { assertSameCode(noDce.dropNonOp, List(Op(ICONST_1), Op(IRETURN), Op(ATHROW), Op(ATHROW))) // when NOT computing stack map frames, ASM's ClassWriter does not replace dead code by NOP/ATHROW - val noDceNoFrames = singleMethodInstructions(noOptNoFramesCompiler)(code) + val warn = "target:jvm-1.5 is deprecated" + val noDceNoFrames = singleMethodInstructions(noOptNoFramesCompiler)(code, allowMessage = _.msg contains warn) assertSameCode(noDceNoFrames.dropNonOp, List(Op(ICONST_1), Op(IRETURN), Op(ICONST_2), Op(IRETURN))) } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala index 24a1f9d1c1..769736669b 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala @@ -12,10 +12,18 @@ import scala.collection.JavaConverters._ import CodeGenTools._ import scala.tools.partest.ASMConverters import ASMConverters._ +import scala.tools.testing.ClearAfterClass + +object UnusedLocalVariablesTest extends ClearAfterClass.Clearable { + var dceCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:unreachable-code") + def clear(): Unit = { dceCompiler = null } +} @RunWith(classOf[JUnit4]) -class UnusedLocalVariablesTest { - val dceCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:unreachable-code") +class UnusedLocalVariablesTest extends ClearAfterClass { + ClearAfterClass.stateToClear = UnusedLocalVariablesTest + + val dceCompiler = UnusedLocalVariablesTest.dceCompiler @Test def removeUnusedVar(): Unit = { diff --git a/test/junit/scala/tools/testing/ClearAfterClass.java b/test/junit/scala/tools/testing/ClearAfterClass.java new file mode 100644 index 0000000000..232d459c4e --- /dev/null +++ b/test/junit/scala/tools/testing/ClearAfterClass.java @@ -0,0 +1,20 @@ +package scala.tools.testing; + +import org.junit.AfterClass; + +/** + * Extend this class to use JUnit's @AfterClass. This annotation only works on static methods, + * which cannot be written in Scala. + * + * Example: {@link scala.tools.nsc.backend.jvm.opt.InlinerTest} + */ +public class ClearAfterClass { + public static interface Clearable { + void clear(); + } + + public static Clearable stateToClear; + + @AfterClass + public static void clearState() { stateToClear.clear(); } +} diff --git a/test/junit/scala/tools/testing/TempDir.scala b/test/junit/scala/tools/testing/TempDir.scala new file mode 100644 index 0000000000..475de8c4a2 --- /dev/null +++ b/test/junit/scala/tools/testing/TempDir.scala @@ -0,0 +1,18 @@ +package scala.tools.testing + +import java.io.{IOException, File} + +object TempDir { + final val TEMP_DIR_ATTEMPTS = 10000 + def createTempDir(): File = { + val baseDir = new File(System.getProperty("java.io.tmpdir")) + val baseName = System.currentTimeMillis() + "-" + var c = 0 + while (c < TEMP_DIR_ATTEMPTS) { + val tempDir = new File(baseDir, baseName + c) + if (tempDir.mkdir()) return tempDir + c += 1 + } + throw new IOException(s"Failed to create directory") + } +} diff --git a/versions.properties b/versions.properties index fa08e56346..096108a21f 100644 --- a/versions.properties +++ b/versions.properties @@ -4,7 +4,7 @@ # when adding new properties that influence a release, # also add them to the update.versions mechanism in build.xml, # which is used by scala-release-2.11.x in scala/jenkins-scripts -starr.version=2.11.5 +starr.version=2.11.6 starr.use.released=1 # These are the versions of the modules that go with this release. @@ -14,7 +14,7 @@ starr.use.released=1 scala.binary.version=2.11 # e.g. 2.11.0-RC1, 2.11.0, 2.11.1-RC1, 2.11.1 # this defines the dependency on scala-continuations-plugin in scala-dist's pom -scala.full.version=2.11.5 +scala.full.version=2.11.6 # external modules shipped with distribution, as specified by scala-library-all's pom scala-xml.version.number=1.0.3 |