Merge "Update tests that assume no vertical class merging"
diff --git a/build.gradle b/build.gradle
index 7f41caa..7761e37 100644
--- a/build.gradle
+++ b/build.gradle
@@ -501,6 +501,9 @@
task.relocate('org.apache.commons', 'com.android.tools.r8.org.apache.commons')
task.relocate('org.objectweb.asm', 'com.android.tools.r8.org.objectweb.asm')
task.relocate('it.unimi.dsi.fastutil', 'com.android.tools.r8.it.unimi.dsi.fastutil')
+ task.relocate('kotlin', 'com.android.tools.r8.jetbrains.kotlin')
+ task.relocate('kotlinx', 'com.android.tools.r8.jetbrains.kotlinx')
+ task.relocate('org.jetbrains', 'com.android.tools.r8.org.jetbrains')
}
task repackageDeps(type: ShadowJar) {
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfo.java b/src/main/java/com/android/tools/r8/graph/AppInfo.java
index 6cbad63..69a744f 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfo.java
@@ -481,15 +481,15 @@
return result;
}
- public boolean canTriggerStaticInitializer(DexType type) {
+ public boolean canTriggerStaticInitializer(DexType type, boolean ignoreTypeItself) {
Set<DexType> knownInterfaces = Sets.newIdentityHashSet();
// Process superclass chain.
DexType clazz = type;
while (clazz != null && clazz != dexItemFactory.objectType) {
DexClass definition = definitionFor(clazz);
- if (canTriggerStaticInitializer(definition)) {
- return true; // Assume it *may* trigger if we didn't find the definition.
+ if (canTriggerStaticInitializer(definition) && (!ignoreTypeItself || clazz != type)) {
+ return true;
}
knownInterfaces.addAll(Arrays.asList(definition.interfaces.values));
clazz = definition.superType;
@@ -501,7 +501,7 @@
DexType iface = queue.remove();
DexClass definition = definitionFor(iface);
if (canTriggerStaticInitializer(definition)) {
- return true; // Assume it *may* trigger if we didn't find the definition.
+ return true;
}
if (!definition.isInterface()) {
throw new Unreachable(iface.toSourceString() + " is expected to be an interface");
@@ -517,6 +517,7 @@
}
private static boolean canTriggerStaticInitializer(DexClass clazz) {
+ // Assume it *may* trigger if we didn't find the definition.
return clazz == null || clazz.hasClassInitializer();
}
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index d234702..4281e80 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -590,6 +590,24 @@
}
}
+ public static class TrivialInitializer {
+ private TrivialInitializer() {
+ }
+
+ public static final class InstanceClassTrivialInitializer extends TrivialInitializer {
+ public static final InstanceClassTrivialInitializer INSTANCE =
+ new InstanceClassTrivialInitializer();
+ }
+
+ public static final class ClassTrivialInitializer extends TrivialInitializer {
+ public final DexField field;
+
+ public ClassTrivialInitializer(DexField field) {
+ this.field = field;
+ }
+ }
+ }
+
public static class OptimizationInfo {
private int returnedArgument = -1;
@@ -604,6 +622,7 @@
// Stores information about instance methods and constructors for
// class inliner, null value indicates that the method is not eligible.
private ClassInlinerEligibility classInlinerEligibility = null;
+ private TrivialInitializer trivialInitializerInfo = null;
private OptimizationInfo() {
// Intentionally left empty.
@@ -648,6 +667,14 @@
return this.classInlinerEligibility;
}
+ private void setTrivialInitializer(TrivialInitializer info) {
+ this.trivialInitializerInfo = info;
+ }
+
+ public TrivialInitializer getTrivialInitializerInfo() {
+ return this.trivialInitializerInfo;
+ }
+
public long getReturnedConstant() {
assert returnsConstant();
return returnedConstant;
@@ -750,6 +777,10 @@
ensureMutableOI().setClassInlinerEligibility(eligibility);
}
+ synchronized public void setTrivialInitializer(TrivialInitializer info) {
+ ensureMutableOI().setTrivialInitializer(info);
+ }
+
synchronized public void markForceInline() {
ensureMutableOI().markForceInline();
}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 7d7e5f3..735fffa 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -725,17 +725,20 @@
interfaceMethodRewriter.rewriteMethodReferences(method, code);
assert code.isConsistentSSA();
}
- if (lambdaMerger != null) {
- lambdaMerger.processMethodCode(method, code);
+
+ if (classInliner != null) {
+ // Class inliner should work before lambda merger, so if it inlines the
+ // lambda, it is not get collected by merger.
+ assert options.enableInlining && inliner != null;
+ classInliner.processMethodCode(
+ appInfo.withLiveness(), method, code, isProcessedConcurrently,
+ methodsToInline -> inliner.performForcedInlining(method, code, methodsToInline)
+ );
assert code.isConsistentSSA();
}
- if (classInliner != null) {
- assert options.enableInlining && inliner != null;
- classInliner.processMethodCode(
- appInfo.withSubtyping(), method, code, isProcessedConcurrently,
- methodsToInline -> inliner.performForcedInlining(method, code, methodsToInline)
- );
+ if (lambdaMerger != null) {
+ lambdaMerger.processMethodCode(method, code);
assert code.isConsistentSSA();
}
@@ -760,6 +763,9 @@
// Analysis must be done after method is rewritten by logArgumentTypes()
codeRewriter.identifyClassInlinerEligibility(method, code, feedback);
+ if (method.isInstanceInitializer() || method.isClassInitializer()) {
+ codeRewriter.identifyTrivialInitializer(method, code, feedback);
+ }
printMethod(code, "Optimized IR (SSA)");
finalizeIR(method, code, feedback);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
index 16a77c8..2f17a4c 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
@@ -6,6 +6,7 @@
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
import com.android.tools.r8.ir.optimize.Inliner.Constraint;
public interface OptimizationFeedback {
@@ -17,4 +18,5 @@
void markCheckNullReceiverBeforeAnySideEffect(DexEncodedMethod method, boolean mark);
void markTriggerClassInitBeforeAnySideEffect(DexEncodedMethod method, boolean mark);
void setClassInlinerEligibility(DexEncodedMethod method, ClassInlinerEligibility eligibility);
+ void setTrivialInitializer(DexEncodedMethod method, TrivialInitializer info);
}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
index 7303b6b..1026e9b 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
@@ -6,6 +6,7 @@
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
import com.android.tools.r8.ir.optimize.Inliner.Constraint;
public class OptimizationFeedbackDirect implements OptimizationFeedback {
@@ -50,4 +51,9 @@
DexEncodedMethod method, ClassInlinerEligibility eligibility) {
method.setClassInlinerEligibility(eligibility);
}
+
+ @Override
+ public void setTrivialInitializer(DexEncodedMethod method, TrivialInitializer info) {
+ method.setTrivialInitializer(info);
+ }
}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
index 6915a2f..8fac02f 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
@@ -6,6 +6,7 @@
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
import com.android.tools.r8.ir.optimize.Inliner.Constraint;
public class OptimizationFeedbackIgnore implements OptimizationFeedback {
@@ -35,4 +36,8 @@
public void setClassInlinerEligibility(
DexEncodedMethod method, ClassInlinerEligibility eligibility) {
}
+
+ @Override
+ public void setTrivialInitializer(DexEncodedMethod method, TrivialInitializer info) {
+ }
}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index e610d20..c3f02da 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -13,6 +13,9 @@
import com.android.tools.r8.graph.DexEncodedField;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer.ClassTrivialInitializer;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer.InstanceClassTrivialInitializer;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMethod;
@@ -48,6 +51,7 @@
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.If;
import com.android.tools.r8.ir.code.If.Type;
+import com.android.tools.r8.ir.code.InstancePut;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.InstructionIterator;
import com.android.tools.r8.ir.code.InstructionListIterator;
@@ -812,6 +816,187 @@
method, new ClassInlinerEligibility(receiverUsedAsReturnValue));
}
+ public void identifyTrivialInitializer(
+ DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
+ boolean isInstanceInitializer = method.isInstanceInitializer();
+ boolean isClassInitializer = method.isClassInitializer();
+ assert isInstanceInitializer || isClassInitializer;
+ if (method.accessFlags.isNative()) {
+ return;
+ }
+
+ DexClass clazz = appInfo.definitionFor(method.method.holder);
+ if (clazz == null) {
+ return;
+ }
+
+ feedback.setTrivialInitializer(method,
+ isInstanceInitializer
+ ? computeInstanceInitializerInfo(code, clazz, appInfo::definitionFor)
+ : computeClassInitializerInfo(code, clazz));
+ }
+
+ // This method defines trivial instance initializer as follows:
+ //
+ // ** The initializer may only call the initializer of the base class, which
+ // itself must be trivial.
+ //
+ // ** java.lang.Object.<init>() is considered trivial.
+ //
+ // ** all arguments passed to a super-class initializer must be non-throwing
+ // constants or arguments.
+ //
+ // ** Assigns arguments or non-throwing constants to fields of this class.
+ //
+ // (Note that this initializer does not have to have zero arguments.)
+ private TrivialInitializer computeInstanceInitializerInfo(
+ IRCode code, DexClass clazz, Function<DexType, DexClass> typeToClass) {
+ Value receiver = code.getThis();
+
+ InstructionIterator it = code.instructionIterator();
+ while (it.hasNext()) {
+ Instruction insn = it.next();
+
+ if (insn.isReturn()) {
+ continue;
+ }
+
+ if (insn.isArgument()) {
+ continue;
+ }
+
+ if (insn.isConstInstruction()) {
+ if (insn.instructionInstanceCanThrow()) {
+ return null;
+ } else {
+ continue;
+ }
+ }
+
+ if (insn.isInvokeDirect()) {
+ InvokeDirect invokedDirect = insn.asInvokeDirect();
+ DexMethod invokedMethod = invokedDirect.getInvokedMethod();
+
+ if (invokedMethod.holder != clazz.superType) {
+ return null;
+ }
+
+ // java.lang.Object.<init>() is considered trivial.
+ if (invokedMethod == dexItemFactory.objectMethods.constructor) {
+ continue;
+ }
+
+ DexClass holder = typeToClass.apply(invokedMethod.holder);
+ if (holder == null) {
+ return null;
+ }
+
+ DexEncodedMethod callTarget = holder.lookupDirectMethod(invokedMethod);
+ if (callTarget == null ||
+ !callTarget.isInstanceInitializer() ||
+ callTarget.getOptimizationInfo().getTrivialInitializerInfo() == null ||
+ invokedDirect.getReceiver() != receiver) {
+ return null;
+ }
+
+ for (Value value : invokedDirect.inValues()) {
+ if (value != receiver && !(value.isConstant() || value.isArgument())) {
+ return null;
+ }
+ }
+ continue;
+ }
+
+ if (insn.isInstancePut()) {
+ InstancePut instancePut = insn.asInstancePut();
+ DexEncodedField field = clazz.lookupInstanceField(instancePut.getField());
+ if (field == null ||
+ instancePut.object() != receiver ||
+ (instancePut.value() != receiver && !instancePut.value().isArgument())) {
+ return null;
+ }
+ continue;
+ }
+
+ // Other instructions make the instance initializer not eligible.
+ return null;
+ }
+
+ return InstanceClassTrivialInitializer.INSTANCE;
+ }
+
+ // This method defines trivial class initializer as follows:
+ //
+ // ** The initializer may only instantiate an instance of the same class,
+ // initialize it with a call to a trivial constructor *without* arguments,
+ // and assign this instance to a static final field of the same class.
+ //
+ private synchronized TrivialInitializer computeClassInitializerInfo(IRCode code, DexClass clazz) {
+ InstructionIterator it = code.instructionIterator();
+
+ Value createdSingletonInstance = null;
+ DexField singletonField = null;
+
+ while (it.hasNext()) {
+ Instruction insn = it.next();
+
+ if (insn.isReturn()) {
+ continue;
+ }
+
+ if (insn.isNewInstance()) {
+ NewInstance newInstance = insn.asNewInstance();
+ if (createdSingletonInstance != null ||
+ newInstance.clazz != clazz.type ||
+ insn.outValue() == null) {
+ return null;
+ }
+ createdSingletonInstance = insn.outValue();
+ continue;
+ }
+
+ if (insn.isInvokeDirect()) {
+ InvokeDirect invokedDirect = insn.asInvokeDirect();
+ if (createdSingletonInstance == null ||
+ invokedDirect.getReceiver() != createdSingletonInstance) {
+ return null;
+ }
+
+ DexEncodedMethod callTarget = clazz.lookupDirectMethod(invokedDirect.getInvokedMethod());
+ if (callTarget == null ||
+ !callTarget.isInstanceInitializer() ||
+ !callTarget.method.proto.parameters.isEmpty() ||
+ callTarget.getOptimizationInfo().getTrivialInitializerInfo() == null) {
+ return null;
+ }
+ continue;
+ }
+
+ if (insn.isStaticPut()) {
+ StaticPut staticPut = insn.asStaticPut();
+ if (singletonField != null ||
+ createdSingletonInstance == null ||
+ staticPut.inValue() != createdSingletonInstance) {
+ return null;
+ }
+
+ DexEncodedField field = clazz.lookupStaticField(staticPut.getField());
+ if (field == null ||
+ !field.accessFlags.isStatic() ||
+ !field.accessFlags.isFinal()) {
+ return null;
+ }
+ singletonField = field.field;
+ continue;
+ }
+
+ // Other instructions make the class initializer not eligible.
+ return null;
+ }
+
+ return singletonField == null ? null : new ClassTrivialInitializer(singletonField);
+ }
+
/**
* An enum used to classify instructions according to a particular effect that they produce.
*
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
index d7a57df..e6b61f8 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
@@ -10,6 +10,8 @@
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer.ClassTrivialInitializer;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMethod;
@@ -22,12 +24,13 @@
import com.android.tools.r8.ir.code.InstructionListIterator;
import com.android.tools.r8.ir.code.InvokeDirect;
import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
-import com.android.tools.r8.ir.code.NewInstance;
import com.android.tools.r8.ir.code.Phi;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.code.ValueType;
import com.android.tools.r8.ir.optimize.Inliner.InliningInfo;
import com.android.tools.r8.ir.optimize.Inliner.Reason;
+import com.android.tools.r8.kotlin.KotlinInfo;
+import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
import com.google.common.collect.Streams;
import java.util.ArrayList;
import java.util.IdentityHashMap;
@@ -55,11 +58,12 @@
// Process method code and inline eligible class instantiations, in short:
//
- // - collect all 'new-instance' instructions in the original code. Note that class
- // inlining, if happens, mutates code and may add new 'new-instance' instructions.
- // Processing them as well is possible, but does not seem to bring much value.
+ // - collect all 'new-instance' and 'static-get' instructions (called roots below) in
+ // the original code. Note that class inlining, if happens, mutates code and may add
+ // new root instructions. Processing them as well is possible, but does not seem to
+ // bring much value.
//
- // - for each 'new-instance' we check if it is eligible for inlining, i.e:
+ // - for each 'new-instance' root we check if it is eligible for inlining, i.e:
// -> the class of the new instance is 'eligible' (see computeClassEligible(...))
// -> the instance is initialized with 'eligible' constructor (see comments in
// CodeRewriter::identifyClassInlinerEligibility(...))
@@ -71,14 +75,20 @@
// NOTE: if method receiver is used as a return value, the method call
// should ignore return value
//
- // - inline eligible 'new-instance' instructions, i.e:
+ // - for each 'static-get' root we check if it is eligible for inlining, i.e:
+ // -> the class of the new instance is 'eligible' (see computeClassEligible(...))
+ // *and* has a trivial class constructor (see CoreRewriter::computeClassInitializerInfo
+ // and description in isClassAndUsageEligible(...)) initializing the field root reads
+ // -> has only 'eligible' uses, (see above)
+ //
+ // - inline eligible root instructions, i.e:
// -> force inline methods called on the instance (including the initializer);
// (this may introduce additional instance field reads/writes on the receiver)
// -> replace instance field reads with appropriate values calculated based on
// fields writes
- // -> remove the call to superclass initializer
+ // -> remove the call to superclass initializer (if root is 'new-instance')
// -> remove all field writes
- // -> remove 'new-instance' instructions
+ // -> remove root instructions
//
// For example:
//
@@ -93,12 +103,21 @@
// return x;
// }
// }
+ // static class F {
+ // final static F I = new F();
+ // int getX() {
+ // return 123;
+ // }
+ // }
// static int method1() {
// return new L(1).x;
// }
// static int method2() {
// return new L(1).getX();
// }
+ // static int method3() {
+ // return F.I.getX();
+ // }
// }
//
// Code after class C is 'inlined':
@@ -109,47 +128,59 @@
// static int method2() {
// return 1;
// }
+ // static int method3() {
+ // return "F::getX";
+ // }
// }
//
public final void processMethodCode(
- AppInfoWithSubtyping appInfo,
+ AppInfoWithLiveness appInfo,
DexEncodedMethod method,
IRCode code,
Predicate<DexEncodedMethod> isProcessedConcurrently,
InlinerAction inliner) {
- // Collect all the new-instance instructions in the code before inlining.
- List<NewInstance> newInstances = Streams.stream(code.instructionIterator())
- .filter(Instruction::isNewInstance)
- .map(Instruction::asNewInstance)
+ // Collect all the new-instance and static-get instructions in the code before inlining.
+ List<Instruction> roots = Streams.stream(code.instructionIterator())
+ .filter(insn -> insn.isNewInstance() || insn.isStaticGet())
.collect(Collectors.toList());
- for (NewInstance newInstance : newInstances) {
- Value eligibleInstance = newInstance.outValue();
+ for (Instruction root : roots) {
+ Value eligibleInstance = root.outValue();
if (eligibleInstance == null) {
continue;
}
- DexType eligibleClass = newInstance.clazz;
- if (!isClassEligible(appInfo, eligibleClass)) {
+ DexType eligibleClass = root.isNewInstance()
+ ? root.asNewInstance().clazz : root.asStaticGet().getField().type;
+ DexClass eligibleClassDefinition = appInfo.definitionFor(eligibleClass);
+ if (eligibleClassDefinition == null) {
continue;
}
- Map<InvokeMethodWithReceiver, InliningInfo> methodCalls = checkInstanceUsersEligibility(
- appInfo, method, isProcessedConcurrently, newInstance, eligibleInstance, eligibleClass);
+ // Check the static initializer of the type.
+ if (!isClassAndUsageEligible(root, eligibleClass,
+ eligibleClassDefinition, isProcessedConcurrently, appInfo)) {
+ continue;
+ }
+
+ Map<InvokeMethodWithReceiver, InliningInfo> methodCalls =
+ checkInstanceUsersEligibility(appInfo, method, isProcessedConcurrently,
+ root, eligibleInstance, eligibleClass, eligibleClassDefinition);
if (methodCalls == null) {
continue;
}
- if (getTotalEstimatedMethodSize(methodCalls) >= totalMethodInstructionLimit) {
+ if (!instructionLimitExempt(eligibleClassDefinition, appInfo) &&
+ getTotalEstimatedMethodSize(methodCalls) >= totalMethodInstructionLimit) {
continue;
}
// Inline the class instance.
forceInlineAllMethodInvocations(inliner, methodCalls);
- removeSuperClassInitializerAndFieldReads(code, newInstance, eligibleInstance);
+ removeSuperClassInitializerAndFieldReads(code, root, eligibleInstance);
removeFieldWrites(eligibleInstance, eligibleClass);
- removeInstruction(newInstance);
+ removeInstruction(root);
// Restore normality.
code.removeAllTrivialPhis();
@@ -157,10 +188,96 @@
}
}
+ private boolean isClassAndUsageEligible(Instruction root,
+ DexType eligibleClass, DexClass eligibleClassDefinition,
+ Predicate<DexEncodedMethod> isProcessedConcurrently, AppInfoWithLiveness appInfo) {
+ if (!isClassEligible(appInfo, eligibleClass)) {
+ return false;
+ }
+
+ if (root.isNewInstance()) {
+ // There must be no static initializer on the class itself.
+ return !eligibleClassDefinition.hasClassInitializer();
+ }
+
+ assert root.isStaticGet();
+
+ // Checking if we can safely inline class implemented following singleton-like
+ // pattern, by which we assume a static final field holding on to the reference
+ // initialized in class constructor.
+ //
+ // In general we are targeting cases when the class is defined as:
+ //
+ // class X {
+ // static final X F;
+ // static {
+ // F = new X();
+ // }
+ // }
+ //
+ // and being used as follows:
+ //
+ // void foo() {
+ // f = X.F;
+ // f.bar();
+ // }
+ //
+ // The main difference from the similar case of class inliner with 'new-instance'
+ // instruction is that in this case the instance we inline is not just leaked, but
+ // is actually published via X.F field. There are several risks we need to address
+ // in this case:
+ //
+ // Risk: instance stored in field X.F has changed after it was initialized in
+ // class initializer
+ // Solution: we assume that final field X.F is not modified outside the class
+ // initializer. In rare cases when it is (e.g. via reflections) it should
+ // be marked with keep rules
+ //
+ // Risk: instance stored in field X.F is not initialized yet
+ // Solution: not initialized instance can only be visible if X.<clinit>
+ // triggers other class initialization which references X.F. This
+ // situation should never happen if we:
+ // -- don't allow any superclasses to have static initializer,
+ // -- don't allow any subclasses,
+ // -- guarantee the class has trivial class initializer
+ // (see CodeRewriter::computeClassInitializerInfo), and
+ // -- guarantee the instance is initialized with trivial instance
+ // initializer (see CodeRewriter::computeInstanceInitializerInfo)
+ //
+ // Risk: instance stored in field X.F was mutated
+ // Solution: we require that class X does not have any instance fields, and
+ // if any of its superclasses has instance fields, accessing them will make
+ // this instance not eligible for inlining. I.e. even though the instance is
+ // publicized and its state has been mutated, it will not effect the logic
+ // of class inlining
+ //
+
+ if (eligibleClassDefinition.instanceFields().length > 0 ||
+ !eligibleClassDefinition.accessFlags.isFinal()) {
+ return false;
+ }
+
+ // Singleton instance must be initialized in class constructor.
+ DexEncodedMethod classInitializer = eligibleClassDefinition.getClassInitializer();
+ if (classInitializer == null || isProcessedConcurrently.test(classInitializer)) {
+ return false;
+ }
+
+ TrivialInitializer info =
+ classInitializer.getOptimizationInfo().getTrivialInitializerInfo();
+ assert info == null || info instanceof ClassTrivialInitializer;
+ DexField instanceField = root.asStaticGet().getField();
+ // Singleton instance field must NOT be pinned.
+ return info != null &&
+ ((ClassTrivialInitializer) info).field == instanceField &&
+ !appInfo.isPinned(eligibleClassDefinition.lookupStaticField(instanceField).field);
+ }
+
private Map<InvokeMethodWithReceiver, InliningInfo> checkInstanceUsersEligibility(
AppInfoWithSubtyping appInfo, DexEncodedMethod method,
Predicate<DexEncodedMethod> isProcessedConcurrently,
- NewInstance newInstanceInsn, Value receiver, DexType clazz) {
+ Instruction root, Value receiver,
+ DexType eligibleClass, DexClass eligibleClassDefinition) {
// No Phi users.
if (receiver.numberOfPhiUsers() > 0) {
@@ -169,14 +286,13 @@
Map<InvokeMethodWithReceiver, InliningInfo> methodCalls = new IdentityHashMap<>();
- DexClass definition = appInfo.definitionFor(clazz);
-
for (Instruction user : receiver.uniqueUsers()) {
// Field read/write.
if (user.isInstanceGet() ||
(user.isInstancePut() && user.asInstancePut().value() != receiver)) {
DexField field = user.asFieldInstruction().getField();
- if (field.clazz == newInstanceInsn.clazz && definition.lookupInstanceField(field) != null) {
+ if (field.clazz == eligibleClass &&
+ eligibleClassDefinition.lookupInstanceField(field) != null) {
// Since class inliner currently only supports classes directly extending
// java.lang.Object, we don't need to worry about fields defined in superclasses.
continue;
@@ -184,10 +300,10 @@
return null; // Not eligible.
}
- // Eligible constructor call.
- if (user.isInvokeDirect()) {
+ // Eligible constructor call (for new instance roots only).
+ if (user.isInvokeDirect() && root.isNewInstance()) {
InliningInfo inliningInfo = isEligibleConstructorCall(appInfo, method,
- user.asInvokeDirect(), receiver, clazz, isProcessedConcurrently);
+ user.asInvokeDirect(), receiver, eligibleClass, isProcessedConcurrently);
if (inliningInfo != null) {
methodCalls.put(user.asInvokeDirect(), inliningInfo);
continue;
@@ -199,7 +315,7 @@
if (user.isInvokeVirtual() || user.isInvokeInterface()) {
InliningInfo inliningInfo = isEligibleMethodCall(
appInfo, method, user.asInvokeMethodWithReceiver(),
- receiver, clazz, isProcessedConcurrently);
+ receiver, eligibleClass, isProcessedConcurrently);
if (inliningInfo != null) {
methodCalls.put(user.asInvokeMethodWithReceiver(), inliningInfo);
continue;
@@ -215,11 +331,12 @@
// Remove call to superclass initializer, replace field reads with appropriate
// values, insert phis when needed.
private void removeSuperClassInitializerAndFieldReads(
- IRCode code, NewInstance newInstance, Value eligibleInstance) {
+ IRCode code, Instruction root, Value eligibleInstance) {
Map<DexField, FieldValueHelper> fieldHelpers = new IdentityHashMap<>();
for (Instruction user : eligibleInstance.uniqueUsers()) {
// Remove the call to superclass constructor.
- if (user.isInvokeDirect() &&
+ if (root.isNewInstance() &&
+ user.isInvokeDirect() &&
user.asInvokeDirect().getInvokedMethod() == factory.objectMethods.constructor) {
removeInstruction(user);
continue;
@@ -227,7 +344,7 @@
if (user.isInstanceGet()) {
// Replace a field read with appropriate value.
- replaceFieldRead(code, newInstance, user.asInstanceGet(), fieldHelpers);
+ replaceFieldRead(code, root, user.asInstanceGet(), fieldHelpers);
continue;
}
@@ -261,13 +378,24 @@
return totalSize;
}
- private void replaceFieldRead(IRCode code, NewInstance newInstance,
+ private boolean instructionLimitExempt(
+ DexClass eligibleClassDefinition, AppInfoWithLiveness appInfo) {
+ if (appInfo.isPinned(eligibleClassDefinition.type)) {
+ return false;
+ }
+ KotlinInfo kotlinInfo = eligibleClassDefinition.getKotlinInfo();
+ return kotlinInfo != null &&
+ kotlinInfo.isSyntheticClass() &&
+ kotlinInfo.asSyntheticClass().isLambda();
+ }
+
+ private void replaceFieldRead(IRCode code, Instruction root,
InstanceGet fieldRead, Map<DexField, FieldValueHelper> fieldHelpers) {
Value value = fieldRead.outValue();
if (value != null) {
FieldValueHelper helper = fieldHelpers.computeIfAbsent(
- fieldRead.getField(), field -> new FieldValueHelper(field, code, newInstance));
+ fieldRead.getField(), field -> new FieldValueHelper(field, code, root));
Value newValue = helper.getValueForFieldRead(fieldRead.getBlock(), fieldRead);
value.replaceUsers(newValue);
for (FieldValueHelper fieldValueHelper : fieldHelpers.values()) {
@@ -282,16 +410,16 @@
private static final class FieldValueHelper {
final DexField field;
final IRCode code;
- final NewInstance newInstance;
+ final Instruction root;
private Value defaultValue = null;
private final Map<BasicBlock, Value> ins = new IdentityHashMap<>();
private final Map<BasicBlock, Value> outs = new IdentityHashMap<>();
- private FieldValueHelper(DexField field, IRCode code, NewInstance newInstance) {
+ private FieldValueHelper(DexField field, IRCode code, Instruction root) {
this.field = field;
this.code = code;
- this.newInstance = newInstance;
+ this.root = root;
}
void replaceValue(Value oldValue, Value newValue) {
@@ -370,10 +498,10 @@
Instruction instruction = iterator.previous();
assert instruction != null;
- if (instruction == newInstance ||
+ if (instruction == root ||
(instruction.isInstancePut() &&
instruction.asInstancePut().getField() == field &&
- instruction.asInstancePut().object() == newInstance.outValue())) {
+ instruction.asInstancePut().object() == root.outValue())) {
valueProducingInsn = instruction;
break;
}
@@ -386,14 +514,14 @@
return valueProducingInsn.asInstancePut().value();
}
- assert newInstance == valueProducingInsn;
+ assert root == valueProducingInsn;
if (defaultValue == null) {
// If we met newInstance it means that default value is supposed to be used.
defaultValue = code.createValue(ValueType.fromDexType(field.type));
ConstNumber defaultValueInsn = new ConstNumber(defaultValue, 0);
- defaultValueInsn.setPosition(newInstance.getPosition());
+ defaultValueInsn.setPosition(root.getPosition());
LinkedList<Instruction> instructions = block.getInstructions();
- instructions.add(instructions.indexOf(newInstance) + 1, defaultValueInsn);
+ instructions.add(instructions.indexOf(root) + 1, defaultValueInsn);
defaultValueInsn.setBlock(block);
}
return defaultValue;
@@ -539,7 +667,7 @@
// - is not an abstract class or interface
// - directly extends java.lang.Object
// - does not declare finalizer
- // - does not trigger any static initializers
+ // - does not trigger any static initializers except for its own
private boolean computeClassEligible(AppInfo appInfo, DexType clazz) {
DexClass definition = appInfo.definitionFor(clazz);
if (definition == null || definition.isLibraryClass() ||
@@ -561,6 +689,6 @@
}
// Check for static initializers in this class or any of interfaces it implements.
- return !appInfo.canTriggerStaticInitializer(clazz);
+ return !appInfo.canTriggerStaticInitializer(clazz, true);
}
}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
index 38768cd..51a5262 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
@@ -16,13 +16,17 @@
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.ToolHelper.ProcessResult;
import com.android.tools.r8.VmTestRunner;
+import com.android.tools.r8.code.Instruction;
import com.android.tools.r8.code.NewInstance;
+import com.android.tools.r8.code.Sget;
import com.android.tools.r8.graph.DexCode;
import com.android.tools.r8.ir.optimize.classinliner.builders.BuildersTestClass;
import com.android.tools.r8.ir.optimize.classinliner.builders.ControlFlow;
import com.android.tools.r8.ir.optimize.classinliner.builders.Pair;
import com.android.tools.r8.ir.optimize.classinliner.builders.PairBuilder;
import com.android.tools.r8.ir.optimize.classinliner.builders.Tuple;
+import com.android.tools.r8.ir.optimize.classinliner.code.C;
+import com.android.tools.r8.ir.optimize.classinliner.code.CodeTestClass;
import com.android.tools.r8.ir.optimize.classinliner.trivial.ClassWithFinal;
import com.android.tools.r8.ir.optimize.classinliner.trivial.CycleReferenceAB;
import com.android.tools.r8.ir.optimize.classinliner.trivial.CycleReferenceBA;
@@ -46,6 +50,7 @@
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -67,64 +72,64 @@
ToolHelper.getClassAsBytes(CycleReferenceBA.class),
ToolHelper.getClassAsBytes(ClassWithFinal.class)
};
- String main = TrivialTestClass.class.getCanonicalName();
- ProcessResult javaOutput = runOnJavaRaw(main, classes);
- assertEquals(0, javaOutput.exitCode);
-
AndroidApp app = runR8(buildAndroidApp(classes), TrivialTestClass.class);
+ String javaOutput = runOnJava(TrivialTestClass.class);
+ String artOutput = runOnArt(app, TrivialTestClass.class);
+ assertEquals(javaOutput, artOutput);
+
DexInspector inspector = new DexInspector(app);
ClassSubject clazz = inspector.clazz(TrivialTestClass.class);
assertEquals(
Collections.singleton("java.lang.StringBuilder"),
- collectNewInstanceTypes(clazz, "testInner"));
+ collectTypes(clazz, "testInner", "void"));
assertEquals(
Collections.emptySet(),
- collectNewInstanceTypes(clazz, "testConstructorMapping1"));
+ collectTypes(clazz, "testConstructorMapping1", "void"));
assertEquals(
Collections.emptySet(),
- collectNewInstanceTypes(clazz, "testConstructorMapping2"));
+ collectTypes(clazz, "testConstructorMapping2", "void"));
assertEquals(
Collections.singleton("java.lang.StringBuilder"),
- collectNewInstanceTypes(clazz, "testConstructorMapping3"));
+ collectTypes(clazz, "testConstructorMapping3", "void"));
assertEquals(
Collections.emptySet(),
- collectNewInstanceTypes(clazz, "testEmptyClass"));
+ collectTypes(clazz, "testEmptyClass", "void"));
assertEquals(
Collections.singleton(
"com.android.tools.r8.ir.optimize.classinliner.trivial.EmptyClassWithInitializer"),
- collectNewInstanceTypes(clazz, "testEmptyClassWithInitializer"));
+ collectTypes(clazz, "testEmptyClassWithInitializer", "void"));
assertEquals(
Collections.singleton(
"com.android.tools.r8.ir.optimize.classinliner.trivial.ClassWithFinal"),
- collectNewInstanceTypes(clazz, "testClassWithFinalizer"));
+ collectTypes(clazz, "testClassWithFinalizer", "void"));
assertEquals(
Collections.emptySet(),
- collectNewInstanceTypes(clazz, "testCallOnIface1"));
+ collectTypes(clazz, "testCallOnIface1", "void"));
assertEquals(
Collections.singleton(
"com.android.tools.r8.ir.optimize.classinliner.trivial.Iface2Impl"),
- collectNewInstanceTypes(clazz, "testCallOnIface2"));
+ collectTypes(clazz, "testCallOnIface2", "void"));
assertEquals(
Sets.newHashSet(
"com.android.tools.r8.ir.optimize.classinliner.trivial.CycleReferenceAB",
"java.lang.StringBuilder"),
- collectNewInstanceTypes(clazz, "testCycles"));
+ collectTypes(clazz, "testCycles", "void"));
assertEquals(
Sets.newHashSet("java.lang.StringBuilder",
"com.android.tools.r8.ir.optimize.classinliner.trivial.CycleReferenceAB"),
- collectNewInstanceTypes(inspector.clazz(CycleReferenceAB.class), "foo", "int"));
+ collectTypes(inspector.clazz(CycleReferenceAB.class), "foo", "void", "int"));
assertFalse(inspector.clazz(CycleReferenceBA.class).isPresent());
}
@@ -139,12 +144,12 @@
ToolHelper.getClassAsBytes(PairBuilder.class),
ToolHelper.getClassAsBytes(ControlFlow.class),
};
- String main = BuildersTestClass.class.getCanonicalName();
- ProcessResult javaOutput = runOnJavaRaw(main, classes);
- assertEquals(0, javaOutput.exitCode);
-
AndroidApp app = runR8(buildAndroidApp(classes), BuildersTestClass.class);
+ String javaOutput = runOnJava(BuildersTestClass.class);
+ String artOutput = runOnArt(app, BuildersTestClass.class);
+ assertEquals(javaOutput, artOutput);
+
DexInspector inspector = new DexInspector(app);
ClassSubject clazz = inspector.clazz(BuildersTestClass.class);
@@ -152,7 +157,7 @@
Sets.newHashSet(
"com.android.tools.r8.ir.optimize.classinliner.builders.Pair",
"java.lang.StringBuilder"),
- collectNewInstanceTypes(clazz, "testSimpleBuilder"));
+ collectTypes(clazz, "testSimpleBuilder", "void"));
// Note that Pair created instances were also inlined in the following method since
// we use 'System.out.println(pX.toString())', if we used 'System.out.println(pX)'
@@ -160,25 +165,25 @@
// would make it not eligible for inlining.
assertEquals(
Collections.singleton("java.lang.StringBuilder"),
- collectNewInstanceTypes(clazz, "testSimpleBuilderWithMultipleBuilds"));
+ collectTypes(clazz, "testSimpleBuilderWithMultipleBuilds", "void"));
assertFalse(inspector.clazz(PairBuilder.class).isPresent());
assertEquals(
Collections.singleton("java.lang.StringBuilder"),
- collectNewInstanceTypes(clazz, "testBuilderConstructors"));
+ collectTypes(clazz, "testBuilderConstructors", "void"));
assertFalse(inspector.clazz(Tuple.class).isPresent());
assertEquals(
Collections.singleton("java.lang.StringBuilder"),
- collectNewInstanceTypes(clazz, "testWithControlFlow"));
+ collectTypes(clazz, "testWithControlFlow", "void"));
assertFalse(inspector.clazz(ControlFlow.class).isPresent());
assertEquals(
Collections.emptySet(),
- collectNewInstanceTypes(clazz, "testWithMoreControlFlow"));
+ collectTypes(clazz, "testWithMoreControlFlow", "void"));
assertFalse(inspector.clazz(BuildersTestClass.Pos.class).isPresent());
}
@@ -216,14 +221,65 @@
assertThat(artResult.stderr, containsString("IncompatibleClassChangeError"));
}
- private Set<String> collectNewInstanceTypes(
- ClassSubject clazz, String methodName, String... params) {
+ @Test
+ public void testCodeSample() throws Exception {
+ byte[][] classes = {
+ ToolHelper.getClassAsBytes(C.class),
+ ToolHelper.getClassAsBytes(C.L.class),
+ ToolHelper.getClassAsBytes(C.F.class),
+ ToolHelper.getClassAsBytes(CodeTestClass.class)
+ };
+ AndroidApp app = runR8(buildAndroidApp(classes), CodeTestClass.class);
+
+ String javaOutput = runOnJava(CodeTestClass.class);
+ String artOutput = runOnArt(app, CodeTestClass.class);
+ assertEquals(javaOutput, artOutput);
+
+ DexInspector inspector = new DexInspector(app);
+ ClassSubject clazz = inspector.clazz(C.class);
+
+ assertEquals(
+ Collections.emptySet(),
+ collectTypes(clazz, "method1", "int"));
+
+ assertEquals(
+ Collections.emptySet(),
+ collectTypes(clazz, "method2", "int"));
+
+ assertEquals(
+ Collections.emptySet(),
+ collectTypes(clazz, "method3", "int"));
+
+ assertFalse(inspector.clazz(C.L.class).isPresent());
+ assertFalse(inspector.clazz(C.F.class).isPresent());
+ }
+
+ private Set<String> collectTypes(
+ ClassSubject clazz, String methodName, String retValue, String... params) {
+ return Stream.concat(
+ collectNewInstanceTypesWithRetValue(clazz, methodName, retValue, params),
+ collectStaticGetTypesWithRetValue(clazz, methodName, retValue, params)
+ ).collect(Collectors.toSet());
+ }
+
+ private Stream<String> collectNewInstanceTypesWithRetValue(
+ ClassSubject clazz, String methodName, String retValue, String... params) {
assertNotNull(clazz);
- MethodSignature signature = new MethodSignature(methodName, "void", params);
+ MethodSignature signature = new MethodSignature(methodName, retValue, params);
DexCode code = clazz.method(signature).getMethod().getCode().asDexCode();
return filterInstructionKind(code, NewInstance.class)
- .map(insn -> ((NewInstance) insn).getType().toSourceString())
- .collect(Collectors.toSet());
+ .map(insn -> ((NewInstance) insn).getType().toSourceString());
+ }
+
+ private Stream<String> collectStaticGetTypesWithRetValue(
+ ClassSubject clazz, String methodName, String retValue, String... params) {
+ assertNotNull(clazz);
+ MethodSignature signature = new MethodSignature(methodName, retValue, params);
+ DexCode code = clazz.method(signature).getMethod().getCode().asDexCode();
+ return filterInstructionKind(code, Sget.class)
+ .map(Instruction::getField)
+ .filter(field -> field.clazz == field.type)
+ .map(field -> field.clazz.toSourceString());
}
private AndroidApp runR8(AndroidApp app, Class mainClass) throws Exception {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/code/C.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/code/C.java
new file mode 100644
index 0000000..38e75c4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/code/C.java
@@ -0,0 +1,39 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.classinliner.code;
+
+public class C {
+ public static class L {
+ public final int x;
+
+ public L(int x) {
+ this.x = x;
+ }
+
+ public int getX() {
+ return x;
+ }
+ }
+
+ public final static class F {
+ public final static F I = new F();
+
+ public int getX() {
+ return 123;
+ }
+ }
+
+ public synchronized static int method1() {
+ return new L(1).x;
+ }
+
+ public synchronized static int method2() {
+ return new L(1).getX();
+ }
+
+ public synchronized static int method3() {
+ return F.I.getX();
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/code/CodeTestClass.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/code/CodeTestClass.java
new file mode 100644
index 0000000..eb5dc10
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/code/CodeTestClass.java
@@ -0,0 +1,14 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.classinliner.code;
+
+public class CodeTestClass {
+ public static void main(String[] args) {
+ System.out.println(C.method1());
+ System.out.println(C.method2());
+ System.out.println(C.method3());
+ }
+}
+
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
new file mode 100644
index 0000000..eaae740
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
@@ -0,0 +1,108 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.kotlin;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+
+import com.android.tools.r8.code.NewInstance;
+import com.android.tools.r8.code.SgetObject;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexCode;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.google.common.collect.Sets;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.Test;
+
+public class KotlinClassInlinerTest extends AbstractR8KotlinTestBase {
+ private static boolean isLambda(DexClass clazz) {
+ return !clazz.type.getPackageDescriptor().startsWith("kotlin") &&
+ (isKStyleLambdaOrGroup(clazz) || isJStyleLambdaOrGroup(clazz));
+ }
+
+ private static boolean isKStyleLambdaOrGroup(DexClass clazz) {
+ return clazz.superType.descriptor.toString().equals("Lkotlin/jvm/internal/Lambda;");
+ }
+
+ private static boolean isJStyleLambdaOrGroup(DexClass clazz) {
+ return clazz.superType.descriptor.toString().equals("Ljava/lang/Object;") &&
+ clazz.interfaces.size() == 1;
+ }
+
+ private static Predicate<DexType> createLambdaCheck(DexInspector inspector) {
+ Set<DexType> lambdaClasses = inspector.allClasses().stream()
+ .filter(clazz -> isLambda(clazz.getDexClass()))
+ .map(clazz -> clazz.getDexClass().type)
+ .collect(Collectors.toSet());
+ return lambdaClasses::contains;
+ }
+
+ @Test
+ public void testJStyleLambdas() throws Exception {
+ final String mainClassName = "class_inliner_lambda_j_style.MainKt";
+ runTest("class_inliner_lambda_j_style", mainClassName, (app) -> {
+ DexInspector inspector = new DexInspector(app);
+ Predicate<DexType> lambdaCheck = createLambdaCheck(inspector);
+ ClassSubject clazz = inspector.clazz(mainClassName);
+
+ assertEquals(
+ Sets.newHashSet(),
+ collectAccessedLambdaTypes(lambdaCheck, clazz, "testStateless"));
+
+ assertEquals(
+ Sets.newHashSet(
+ "class_inliner_lambda_j_style.MainKt$testStateful$3"),
+ collectAccessedLambdaTypes(lambdaCheck, clazz, "testStateful"));
+
+ assertFalse(
+ inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful$1").isPresent());
+
+ assertEquals(
+ Sets.newHashSet(
+ "class_inliner_lambda_j_style.MainKt$testStateful2$1"),
+ collectAccessedLambdaTypes(lambdaCheck, clazz, "testStateful2"));
+
+ assertEquals(
+ Sets.newHashSet(
+ "class_inliner_lambda_j_style.MainKt$testStateful3$1"),
+ collectAccessedLambdaTypes(lambdaCheck, clazz, "testStateful3"));
+ });
+ }
+
+ private Set<String> collectAccessedLambdaTypes(
+ Predicate<DexType> isLambdaType, ClassSubject clazz, String methodName, String... params) {
+ assertNotNull(clazz);
+ MethodSignature signature = new MethodSignature(methodName, "void", params);
+ DexCode code = clazz.method(signature).getMethod().getCode().asDexCode();
+ return Stream.concat(
+ filterInstructionKind(code, NewInstance.class)
+ .map(insn -> ((NewInstance) insn).getType()),
+ filterInstructionKind(code, SgetObject.class)
+ .map(insn -> insn.getField().getHolder())
+ )
+ .filter(isLambdaType)
+ .map(DexType::toSourceString)
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ protected void runTest(String folder, String mainClass,
+ AndroidAppInspector inspector) throws Exception {
+ runTest(
+ folder, mainClass, null,
+ options -> {
+ options.enableInlining = true;
+ options.enableClassInlining = true;
+ options.enableLambdaMerging = false;
+ }, inspector);
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
index 6ebb08b..dc9b642 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
@@ -587,7 +587,7 @@
final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
String mainClass = addMainToClasspath(
"properties.ObjectPropertiesKt", "objectProperties_usePrimitiveProp");
- runTest(PACKAGE_NAME, mainClass, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
String propertyName = "primitiveProp";
@@ -614,7 +614,7 @@
final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
String mainClass = addMainToClasspath(
"properties.ObjectPropertiesKt", "objectProperties_usePrivateProp");
- runTest(PACKAGE_NAME, mainClass, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
String propertyName = "privateProp";
@@ -641,7 +641,7 @@
final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
String mainClass = addMainToClasspath(
"properties.ObjectPropertiesKt", "objectProperties_useInternalProp");
- runTest(PACKAGE_NAME, mainClass, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
String propertyName = "internalProp";
@@ -668,7 +668,7 @@
final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
String mainClass = addMainToClasspath(
"properties.ObjectPropertiesKt", "objectProperties_usePublicProp");
- runTest(PACKAGE_NAME, mainClass, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
String propertyName = "publicProp";
@@ -695,7 +695,7 @@
final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
String mainClass = addMainToClasspath(
"properties.ObjectPropertiesKt", "objectProperties_useLateInitPrivateProp");
- runTest(PACKAGE_NAME, mainClass, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
String propertyName = "privateLateInitProp";
@@ -722,7 +722,7 @@
final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
String mainClass = addMainToClasspath(
"properties.ObjectPropertiesKt", "objectProperties_useLateInitInternalProp");
- runTest(PACKAGE_NAME, mainClass, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
String propertyName = "internalLateInitProp";
@@ -745,7 +745,7 @@
final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
String mainClass = addMainToClasspath(
"properties.ObjectPropertiesKt", "objectProperties_useLateInitPublicProp");
- runTest(PACKAGE_NAME, mainClass, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
String propertyName = "publicLateInitProp";
@@ -768,7 +768,7 @@
final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
String mainClass = addMainToClasspath(
"properties.FilePropertiesKt", "fileProperties_usePrimitiveProp");
- runTest(PACKAGE_NAME, mainClass, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
String propertyName = "primitiveProp";
@@ -795,7 +795,7 @@
final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
String mainClass = addMainToClasspath(
"properties.FilePropertiesKt", "fileProperties_usePrivateProp");
- runTest(PACKAGE_NAME, mainClass, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
String propertyName = "privateProp";
@@ -821,7 +821,7 @@
final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
String mainClass = addMainToClasspath(
"properties.FilePropertiesKt", "fileProperties_useInternalProp");
- runTest(PACKAGE_NAME, mainClass, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
String propertyName = "internalProp";
@@ -847,7 +847,7 @@
final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
String mainClass = addMainToClasspath(
"properties.FilePropertiesKt", "fileProperties_usePublicProp");
- runTest(PACKAGE_NAME, mainClass, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
String propertyName = "publicProp";
@@ -874,7 +874,7 @@
final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
String mainClass = addMainToClasspath(
"properties.FilePropertiesKt", "fileProperties_useLateInitPrivateProp");
- runTest(PACKAGE_NAME, mainClass, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject fileClass = checkClassIsKept(dexInspector, testedClass.getClassName());
String propertyName = "privateLateInitProp";
@@ -900,7 +900,7 @@
final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
String mainClass = addMainToClasspath(
"properties.FilePropertiesKt", "fileProperties_useLateInitInternalProp");
- runTest(PACKAGE_NAME, mainClass, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
String propertyName = "internalLateInitProp";
@@ -924,7 +924,7 @@
final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
String mainClass = addMainToClasspath(
"properties.FilePropertiesKt", "fileProperties_useLateInitPublicProp");
- runTest(PACKAGE_NAME, mainClass, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
String propertyName = "publicLateInitProp";
diff --git a/src/test/kotlinR8TestResources/class_inliner_lambda_j_style/SamIface.java b/src/test/kotlinR8TestResources/class_inliner_lambda_j_style/SamIface.java
new file mode 100644
index 0000000..bccef3e
--- /dev/null
+++ b/src/test/kotlinR8TestResources/class_inliner_lambda_j_style/SamIface.java
@@ -0,0 +1,21 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package class_inliner_lambda_j_style;
+
+public interface SamIface {
+ String foo();
+
+ class Consumer {
+ public static void consume(SamIface iface) {
+ System.out.println(iface.foo());
+ }
+
+ public static void consumeBig(SamIface iface) {
+ System.out.println("Bigger than inline limit, class name: " + iface.getClass().getName());
+ System.out.println("Bigger than inline limit, result: '" + iface.foo() + "'");
+ consume(iface);
+ }
+ }
+}
diff --git a/src/test/kotlinR8TestResources/class_inliner_lambda_j_style/main.kt b/src/test/kotlinR8TestResources/class_inliner_lambda_j_style/main.kt
new file mode 100644
index 0000000..ebf26cd
--- /dev/null
+++ b/src/test/kotlinR8TestResources/class_inliner_lambda_j_style/main.kt
@@ -0,0 +1,99 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package class_inliner_lambda_j_style
+
+private var COUNT = 0
+
+fun nextInt() = COUNT++
+fun next() = "${nextInt()}".padStart(3, '0')
+
+fun main(args: Array<String>) {
+ testStateless()
+ testStateful()
+ testStateful2()
+ testStateful3()
+}
+
+@Synchronized
+fun testStateless() {
+ SamIface.Consumer.consume { "123" }
+ SamIface.Consumer.consume { "ABC" }
+ SamIface.Consumer.consume {
+ var x = 0
+ println("A: ${x++}")
+ println("B: ${x++}")
+ println("C: ${x++}")
+ println("D: ${x++}")
+ println("E: ${x++}")
+ "outer + $x"
+ }
+}
+
+@Synchronized
+fun testStateful() {
+ var someVariable = 0
+
+ SamIface.Consumer.consume {
+ println("A: someVariable = $someVariable")
+ someVariable += 1
+ "B: someVariable = $someVariable"
+ }
+ SamIface.Consumer.consume {
+ SamIface.Consumer.consume {
+ println("E: someVariable = $someVariable")
+ someVariable += 1
+ "F: someVariable = $someVariable"
+ }
+ for (i in 1..20) {
+ someVariable += 1
+ if (i % 4 == 0) {
+ println("G: someVariable = $someVariable")
+ }
+ }
+ someVariable += 1
+ "H: someVariable = $someVariable"
+ }
+ SamIface.Consumer.consumeBig {
+ println("I: someVariable = $someVariable")
+ someVariable += 1
+ "J: someVariable = $someVariable"
+ }
+}
+
+@Synchronized
+fun testStateful2() {
+ var someVariable = 0
+ SamIface.Consumer.consumeBig {
+ println("[Z] A: someVariable = $someVariable")
+ someVariable += 1
+ println("[Z] B: someVariable = $someVariable")
+ someVariable += 1
+ println("[Z] C: someVariable = $someVariable")
+ someVariable += 1
+ println("[Z] D: someVariable = $someVariable")
+ someVariable += 1
+ println("[Z] E: someVariable = $someVariable")
+ someVariable += 1
+ "[Z] F: someVariable = $someVariable"
+ }
+}
+
+@Synchronized
+fun testStateful3() {
+ var someVariable = 0
+ SamIface.Consumer.consumeBig {
+ println("[W] A: someVariable = $someVariable")
+ someVariable += 1
+ println("[W] B: someVariable = $someVariable")
+ someVariable += 1
+ println("[W] C: someVariable = $someVariable")
+ someVariable += 1
+ println("[W] D: someVariable = $someVariable")
+ someVariable += 1
+ println("[W] E: someVariable = $someVariable")
+ someVariable += 1
+ "[W] F: someVariable = $someVariable"
+ }
+}