blob: 8e78a50c68a43e24b787b5b683bc1c18faa5a899 [file] [log] [blame]
// Copyright (c) 2019, 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;
import com.android.tools.r8.AssertionsConfiguration;
import com.android.tools.r8.AssertionsConfiguration.AssertionTransformation;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexString;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.InstructionListIterator;
import com.android.tools.r8.ir.code.InvokeMethod;
import com.android.tools.r8.ir.code.StaticGet;
import com.android.tools.r8.ir.code.StaticPut;
import com.android.tools.r8.utils.AssertionConfigurationWithDefault;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.ThrowingCharIterator;
import com.android.tools.r8.utils.Timing;
import java.io.UTFDataFormatException;
import java.util.List;
import java.util.stream.Collectors;
public class AssertionsRewriter {
private static class ConfigurationEntryWithDexString {
private AssertionsConfiguration entry;
private final DexString value;
private ConfigurationEntryWithDexString(
AssertionsConfiguration configuration, DexItemFactory dexItemFactory) {
this.entry = configuration;
switch (configuration.getScope()) {
case PACKAGE:
if (configuration.getValue().length() == 0) {
value = dexItemFactory.createString("");
} else {
value =
dexItemFactory.createString(
"L"
+ configuration
.getValue()
.replace(
DescriptorUtils.JAVA_PACKAGE_SEPARATOR,
DescriptorUtils.DESCRIPTOR_PACKAGE_SEPARATOR)
+ "/");
}
break;
case CLASS:
value =
dexItemFactory.createString(
"L"
+ configuration
.getValue()
.replace(
DescriptorUtils.JAVA_PACKAGE_SEPARATOR,
DescriptorUtils.DESCRIPTOR_PACKAGE_SEPARATOR)
+ ";");
break;
case ALL:
value = null;
break;
default:
throw new Unreachable();
}
}
}
private final AppView<?> appView;
private final DexItemFactory dexItemFactory;
private final AssertionTransformation defaultTransformation;
private final List<ConfigurationEntryWithDexString> configuration;
private final AssertionsConfiguration.AssertionTransformation kotlinTransformation;
private final boolean enabled;
public AssertionsRewriter(AppView<?> appView) {
this.appView = appView;
this.dexItemFactory = appView.dexItemFactory();
this.enabled = isEnabled(appView.options());
if (!enabled) {
defaultTransformation = null;
configuration = null;
kotlinTransformation = null;
return;
}
// Convert the assertion transformation to the representation used for this rewriter.
this.defaultTransformation = appView.options().assertionsConfiguration.defautlTransformation;
this.configuration =
appView.options().assertionsConfiguration.assertionsConfigurations.stream()
.map(entry -> new ConfigurationEntryWithDexString(entry, appView.dexItemFactory()))
.collect(Collectors.toList());
kotlinTransformation =
getTransformationForType(appView.dexItemFactory().kotlin.assertions.type);
}
// Static method used by other analyses to see if additional analysis is required to support
// this rewriting.
public static boolean isEnabled(InternalOptions options) {
AssertionConfigurationWithDefault configuration = options.assertionsConfiguration;
return configuration != null && !configuration.isPassthroughAll();
}
private AssertionTransformation getTransformationForMethod(DexEncodedMethod method) {
return getTransformationForType(method.getHolderType());
}
private AssertionTransformation getTransformationForType(DexType type) {
AssertionTransformation transformation = defaultTransformation;
for (ConfigurationEntryWithDexString entry : configuration) {
switch (entry.entry.getScope()) {
case ALL:
transformation = entry.entry.getTransformation();
break;
case PACKAGE:
if (entry.value.size == 0) {
if (!type.descriptor.contains(dexItemFactory.descriptorSeparator)) {
transformation = entry.entry.getTransformation();
}
} else if (type.descriptor.startsWith(entry.value)) {
transformation = entry.entry.getTransformation();
}
break;
case CLASS:
if (type.descriptor.equals(entry.value)) {
transformation = entry.entry.getTransformation();
}
if (isDescriptorForClassOrInnerClass(entry.value, type.descriptor)) {
transformation = entry.entry.getTransformation();
}
break;
default:
throw new Unreachable();
}
}
assert transformation != null;
return transformation;
}
private boolean isDescriptorForClassOrInnerClass(
DexString classDescriptor, DexString classOrInnerClassDescriptor) {
// Same string same class.
if (classOrInnerClassDescriptor == classDescriptor) {
return true;
}
// Check for inner class name by checking if the prefix is the class descriptor,
// where ';' is replaced whit '$' and no '/' after that.
if (classOrInnerClassDescriptor.size < classDescriptor.size) {
return false;
}
ThrowingCharIterator<UTFDataFormatException> i1 = classDescriptor.iterator();
ThrowingCharIterator<UTFDataFormatException> i2 = classOrInnerClassDescriptor.iterator();
try {
while (i1.hasNext()) {
char c1 = i1.nextChar();
char c2 = i2.nextChar();
// The Java VM behaviour is including all inner classes as well when a class is specified.
if (c1 == ';' && c2 == DescriptorUtils.INNER_CLASS_SEPARATOR) {
// If there is a '/' after the '$' this is not an inner class after all.
while (i2.hasNext()) {
if (i2.nextChar() == DescriptorUtils.DESCRIPTOR_PACKAGE_SEPARATOR) {
return false;
}
}
return true;
}
if (c1 != c2) {
return false;
}
}
assert i2.hasNext();
return false;
} catch (UTFDataFormatException e) {
return false;
}
}
/**
* For supporting assert javac adds the static field $assertionsDisabled to all classes which have
* methods with assertions. This is used to support the Java VM -ea flag.
*
* <p>The class:
*
* <pre>
* class A {
* void m() {
* assert xxx;
* }
* }
* </pre>
*
* Is compiled into:
*
* <pre>
* class A {
* static boolean $assertionsDisabled;
* static {
* $assertionsDisabled = A.class.desiredAssertionStatus();
* }
*
* // method with "assert xxx";
* void m() {
* if (!$assertionsDisabled) {
* if (!xxx) {
* throw new AssertionError(...);
* }
* }
* }
* }
* </pre>
*
* With the rewriting below and AssertionTransformation.DISABLE (and other rewritings) the
* resulting code is:
*
* <pre>
* class A {
* void m() {
* }
* }
* </pre>
*
* With AssertionTransformation.ENABLE (and other rewritings) the resulting code is:
*
* <pre>
* class A {
* static boolean $assertionsDisabled;
* void m() {
* if (!xxx) {
* throw new AssertionError(...);
* }
* }
* }
* </pre>
* For Kotlin the Class instance method desiredAssertionStatus() is only called for the class
* kotlin._Assertions, where kotlin._Assertions.class.desiredAssertionStatus() is read into the
* static field kotlin._Assertions.ENABLED.
*
* <pre>
* class _Assertions {
* public static boolean ENABLED = _Assertions.class.desiredAssertionStatus();
* }
* </pre>
*
* <p>(actual code
* https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/jvm/src/kotlin/util/AssertionsJVM.kt)
*
* <p>The class:
*
* <pre>
* class A {
* void m() {
* assert(xxx)
* }
* }
* </pre>
*
* Is compiled into:
*
* <pre>
* class A {
* void m() {
* if (!xxx) {
* if (kotlin._Assertions.ENABLED) {
* throw new AssertionError("Assertion failed")
* }
* }
* }
* }
* </pre>
*
* With the rewriting below and AssertionTransformation.DISABLE (and other rewritings) the resulting code is:
*
* <pre>
* class A {
* void m() {
* if (!xxx) {}
* }
* }
* </pre>
*
* With AssertionTransformation.ENABLE (and other rewritings) the resulting code is:
*
* <pre>
* class A {
* void m() {
* if (!xxx) {
* throw new AssertionError("Assertion failed")
* }
* }
* }
* </pre>
* NOTE: that in Kotlin the assertion condition is always calculated. So it is still present in
* the code and even for AssertionTransformation.DISABLE.
*/
public void run(DexEncodedMethod method, IRCode code, Timing timing) {
if (enabled) {
timing.begin("Rewrite assertions");
runInternal(method, code);
timing.end();
}
}
private void runInternal(DexEncodedMethod method, IRCode code) {
AssertionTransformation transformation = getTransformationForMethod(method);
if (transformation == AssertionTransformation.PASSTHROUGH) {
return;
}
DexEncodedMethod clinit;
// If the <clinit> of this class did not have have code to turn on assertions don't try to
// remove assertion code from the method (including <clinit> itself.
if (method.isClassInitializer()) {
clinit = method;
} else {
DexClass clazz = appView.definitionFor(method.getHolderType());
if (clazz == null) {
return;
}
clinit = clazz.getClassInitializer();
}
// For javac generated code it is assumed that the code in <clinit> will tell if the code
// in other methods of the class can have assertion checks.
boolean isInitializerEnablingJavaVmAssertions =
clinit != null && clinit.getOptimizationInfo().isInitializerEnablingJavaVmAssertions();
// This code will process the assertion code in all methods including <clinit>.
InstructionListIterator iterator = code.instructionListIterator();
while (iterator.hasNext()) {
Instruction current = iterator.next();
if (current.isInvokeMethod()) {
InvokeMethod invoke = current.asInvokeMethod();
if (invoke.getInvokedMethod() == dexItemFactory.classMethods.desiredAssertionStatus) {
if (method.getHolderType() == dexItemFactory.kotlin.assertions.type) {
rewriteKotlinAssertionEnable(code, transformation, iterator, invoke);
} else {
iterator.replaceCurrentInstruction(code.createIntConstant(0, current.getLocalInfo()));
}
}
} else if (current.isStaticPut()) {
StaticPut staticPut = current.asStaticPut();
if (isInitializerEnablingJavaVmAssertions
&& staticPut.getField().name == dexItemFactory.assertionsDisabled) {
iterator.remove();
}
} else if (current.isStaticGet()) {
StaticGet staticGet = current.asStaticGet();
// Rewrite $assertionsDisabled getter (only if the initializer enabled assertions).
if (isInitializerEnablingJavaVmAssertions
&& staticGet.getField().name == dexItemFactory.assertionsDisabled) {
iterator.replaceCurrentInstruction(
code.createIntConstant(
transformation == AssertionTransformation.DISABLE ? 1 : 0,
current.getLocalInfo()));
}
// Rewrite kotlin._Assertions.ENABLED getter.
if (staticGet.getField() == dexItemFactory.kotlin.assertions.enabledField) {
iterator.replaceCurrentInstruction(
code.createIntConstant(
kotlinTransformation == AssertionTransformation.DISABLE ? 0 : 1,
current.getLocalInfo()));
}
}
}
}
private void rewriteKotlinAssertionEnable(
IRCode code,
AssertionTransformation transformation,
InstructionListIterator iterator,
InvokeMethod invoke) {
if (iterator.hasNext() && transformation == AssertionTransformation.DISABLE) {
// Check if the invocation of Class.desiredAssertionStatus() is followed by a static
// put to kotlin._Assertions.ENABLED, and if so remove both instructions.
// See comment in ClassInitializerAssertionEnablingAnalysis for the expected instruction
// sequence.
Instruction nextInstruction = iterator.next();
if (nextInstruction.isStaticPut()
&& nextInstruction.asStaticPut().getField().holder
== dexItemFactory.kotlin.assertions.type
&& nextInstruction.asStaticPut().getField().name == dexItemFactory.enabledFieldName
&& invoke.outValue().numberOfUsers() == 1
&& invoke.outValue().numberOfPhiUsers() == 0
&& invoke.outValue().singleUniqueUser() == nextInstruction) {
iterator.removeOrReplaceByDebugLocalRead();
Instruction prevInstruction = iterator.previous();
assert prevInstruction == invoke;
iterator.removeOrReplaceByDebugLocalRead();
} else {
Instruction instruction = iterator.previous();
assert instruction == nextInstruction;
instruction = iterator.previous();
assert instruction == invoke;
instruction = iterator.next();
assert instruction == invoke;
iterator.replaceCurrentInstruction(code.createIntConstant(0));
}
} else {
iterator.replaceCurrentInstruction(
code.createIntConstant(transformation == AssertionTransformation.ENABLE ? 1 : 0));
}
}
}