Merge "Only fail with discard checks when tree-shaking"
diff --git a/.gitignore b/.gitignore
index 2ece82e3..86f3139 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,36 +22,24 @@
third_party/android_jar/lib.tar.gz
third_party/android_jar/lib-v[0-9][0-9]
third_party/android_jar/lib-v[0-9][0-9].tar.gz
-third_party/gmscore/v4
-third_party/gmscore/v4.tar.gz
-third_party/gmscore/v5
-third_party/gmscore/v5.tar.gz
-third_party/gmscore/v6
-third_party/gmscore/v6.tar.gz
-third_party/gmscore/v7
-third_party/gmscore/v7.tar.gz
-third_party/gmscore/v8
-third_party/gmscore/v8.tar.gz
-third_party/gmscore/gmscore_v9
-third_party/gmscore/gmscore_v9.tar.gz
-third_party/gmscore/gmscore_v10
-third_party/gmscore/gmscore_v10.tar.gz
+third_party/gmail/*
+!third_party/gmail/*.sha1
+third_party/gmscore/*
+!third_party/gmscore/*.sha1
third_party/gradle/gradle.tar.gz
third_party/gradle/gradle
third_party/jasmin.tar.gz
third_party/jasmin
third_party/jdwp-tests.tar.gz
third_party/jdwp-tests
-third_party/proguard/proguard5.2.1.tar.gz
-third_party/proguard/proguard5.2.1
+third_party/photos/*
+!third_party/photos/*.sha1
+third_party/proguard/*
+!third_party/proguard/*.sha1
third_party/proguardsettings.tar.gz
third_party/proguardsettings/
-third_party/youtube/youtube.android_11.47
-third_party/youtube/youtube.android_11.47.tar.gz
-third_party/youtube/youtube.android_12.10
-third_party/youtube/youtube.android_12.10.tar.gz
-third_party/youtube/youtube.android_12.17/
-third_party/youtube/youtube.android_12.17.tar.gz
+third_party/youtube/*
+!third_party/youtube/*sha1
third_party/jctf
third_party/jctf.tar.gz
third_party/android_cts_baseline
@@ -63,3 +51,4 @@
#*#
*~
.#*
+
diff --git a/README.md b/README.md
index 9d93bc2..1bd795c 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,83 @@
-The R8 repo contains two tools.
+# D8 dexer and R8 shrinker
-1. D8 is a dexer that converts java byte code to dex code.
+The R8 repo contains two tools:
-2. R8 is a java program shrinking and minification tool.
+- D8 is a dexer that converts java byte code to dex code.
+- R8 is a java program shrinking and minification tool that converts java byte
+ code to optimized dex code.
+
+D8 is a replacement for the DX dexer and R8 is a replacement for
+the [Proguard](https://www.guardsquare.com/en/proguard) shrinking and
+minification tool.
+
+## Downloading and building
+
+The R8 project uses [`depot_tools`](https://www.chromium.org/developers/how-tos/install-depot-tools)
+from the chromium project to manage dependencies. Install `depot_tools` and add it to
+your path before proceeding.
+
+The R8 project uses Java 8 language features and requires a Java 8 compiler
+and runtime system.
+
+Typical steps to download and build:
+
+
+ $ git clone https://r8.googlesource.com/r8
+ $ cd r8
+ $ tools/gradle.py d8 r8
+
+The `tools/gradle.py` script will bootstrap using depot_tools to download
+a version of gradle to use for building on the first run. This will produce
+two jar files: `build/libs/d8.jar` and `build/libs/r8.jar`.
+
+## Running D8
+
+The D8 dexer has a simple command-line interface with only a few options.
+
+The most important option is whether to build in debug or release mode. Debug
+is the default mode and includes debugging information in the resulting dex
+files. Debugging information contains information about local variables used
+when debugging dex code. This information is not useful when shipping final
+Android apps to users and therefore, final builds should use the `--release`
+flag to remove this debugging information to produce smaller dex files.
+
+Typical invocations of D8 to produce dex file(s) in the out directoy:
+
+Debug mode build:
+
+ $ java -jar build/libs/d8.jar --output out input.jar
+
+Release mode build:
+
+ $ java -jar build/libs/d8.jar --release --output out input.jar
+
+The full set of D8 options can be obtained by running the command line tool with
+the `--help` option.
+
+## Running R8
+
+R8 is a [Proguard](https://www.guardsquare.com/en/proguard) replacement for
+whole-program optimization, shrinking and minification. R8 uses the Proguard
+keep rule format for specifying the entry points for an application.
+
+Typical invocations of R8 to produce optimized dex file(s) in the out directory:
+
+ $ java -jar build/libs/r8.jar --release --output out --pg-conf proguard.cfg input.jar
+
+The full set of R8 options can be obtained by running the command line tool with
+the `--help` option.
+
+## Testing
+
+Typical steps to run tests:
+
+ $ tools/test.py --no_internal
+
+The `tools/test.py` script will use depot_tools to download a lot of tests
+and test dependencies on the first run. This includes prebuilt version of the
+art runtime on which to validate the produced dex code.
+
+## Getting help
+
+For questions, bug reports and other issues reach out to us at
+r8-dev@googlegroups.com.
diff --git a/build.gradle b/build.gradle
index eb09333..d8402bc 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,7 +1,8 @@
// Copyright (c) 2016, 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.
-import org.gradle.internal.os.OperatingSystem;
+import org.gradle.internal.os.OperatingSystem
+import utils.Utils;
apply plugin: 'java'
apply plugin: 'idea'
@@ -98,7 +99,8 @@
examplesAndroidOCompile group: 'org.ow2.asm', name: 'asm', version: '5.1'
}
-def osString = OperatingSystem.current().isLinux() ? "linux" : "mac"
+def osString = OperatingSystem.current().isLinux() ? "linux" :
+ OperatingSystem.current().isMacOsX() ? "mac" : "windows"
def cloudDependencies = [
"tests" : [
@@ -107,6 +109,7 @@
"third_party": [
"android_jar/lib-v14.tar.gz",
"android_jar/lib-v19.tar.gz",
+ "android_jar/lib-v21.tar.gz",
"android_jar/lib-v24.tar.gz",
"android_jar/lib-v25.tar.gz",
"android_jar/lib-v26.tar.gz",
@@ -136,14 +139,21 @@
def sha1File = "${gzFile}.sha1"
inputs.file sha1File
outputs.file gzFile
- executable "bash"
- args "-c", "download_from_google_storage -n -b r8-deps -u -s ${sha1File}"
+ List<String> dlFromStorageArgs = ["-n", "-b", "r8-deps", "-u", "-s", "${sha1File}"]
+ if (OperatingSystem.current().isWindows()) {
+ executable "download_from_google_storage.bat"
+ args dlFromStorageArgs
+ } else {
+ executable "bash"
+ args "-c", "download_from_google_storage " + String.join(" ", dlFromStorageArgs)
+ }
}
}
}
def x20Dependencies = [
"third_party": [
+ "gmail/gmail_android_170604.16.tar.gz",
"gmscore/v4.tar.gz",
"gmscore/v5.tar.gz",
"gmscore/v6.tar.gz",
@@ -151,11 +161,13 @@
"gmscore/v8.tar.gz",
"gmscore/gmscore_v9.tar.gz",
"gmscore/gmscore_v10.tar.gz",
+ "gmscore/latest.tar.gz",
"photos/2017-06-06.tar.gz",
- "youtube/youtube.android_11.47.tar.gz",
"youtube/youtube.android_12.10.tar.gz",
"youtube/youtube.android_12.17.tar.gz",
+ "youtube/youtube.android_12.22.tar.gz",
"proguardsettings.tar.gz",
+ "proguard/proguard_internal_159423826.tar.gz",
],
]
@@ -370,7 +382,11 @@
}
task "dex_debuginfo_examples"(type: Exec,
dependsOn: ["jar_debuginfo_examples", "downloadDeps"]) {
- executable file("tools/linux/dx/bin/dx");
+ if (OperatingSystem.current().isWindows()) {
+ executable file("tools/windows/dx/bin/dx.bat")
+ } else {
+ executable file("tools/linux/dx/bin/dx");
+ }
args "--dex"
args "--output=build/test/${hostDexJar}"
args "build/test/${hostJar}"
@@ -421,7 +437,12 @@
task buildExampleJars {
dependsOn downloadProguard
def examplesDir = file("src/test/examples")
- def proguardScript = "third_party/proguard/proguard5.2.1/bin/proguard.sh"
+ def proguardScript
+ if (OperatingSystem.current().isWindows()) {
+ proguardScript = "third_party/proguard/proguard5.2.1/bin/proguard.bat"
+ } else {
+ proguardScript = "third_party/proguard/proguard5.2.1/bin/proguard.sh"
+ }
task "compile_examples"(type: JavaCompile) {
source = fileTree(dir: examplesDir, include: '**/*.java')
destinationDir = file("build/test/examples/classes")
@@ -453,11 +474,17 @@
// Enable these to get stdout and stderr redirected to files...
// standardOutput = new FileOutputStream('proguard.stdout')
// errorOutput = new FileOutputStream('proguard.stderr')
- executable "bash"
- args "-c", "${proguardScript} '-verbose -dontwarn java.** -injars ${jarPath}" +
+ def proguardArguments = "-verbose -dontwarn java.** -injars ${jarPath}" +
" -outjars ${proguardJarPath}" +
" -include ${proguardConfigPath}" +
- " -printmapping ${proguardMapPath}'"
+ " -printmapping ${proguardMapPath}"
+ if (OperatingSystem.current().isWindows()) {
+ executable "${proguardScript}"
+ args "${proguardArguments}"
+ } else {
+ executable "bash"
+ args "-c", "${proguardScript} '${proguardArguments}'"
+ }
outputs.file proguardJarPath
}
} else {
@@ -560,11 +587,12 @@
}
task buildExamples {
- if (OperatingSystem.current().isMacOsX()) {
- logger.lifecycle("WARNING: Testing (including building examples) is only partially supported on Mac OS.")
+ if (OperatingSystem.current().isMacOsX() || OperatingSystem.current().isWindows()) {
+ logger.lifecycle("WARNING: Testing (including building examples) is only partially supported on your " +
+ "platform (" + OperatingSystem.current().getName() + ").")
} else if (!OperatingSystem.current().isLinux()) {
logger.lifecycle("WARNING: Testing (including building examples) is not supported on your platform. " +
- "It is fully supported on Linux and partially supported on Mac OS")
+ "It is fully supported on Linux and partially supported on Mac OS and Windows")
return;
}
dependsOn buildDebugTestResourcesJars
@@ -659,6 +687,9 @@
forkEvery = 0
// Use the Concurrent Mark Sweep GC (CMS) to keep memory usage at a resonable level.
jvmArgs = ["-XX:+UseConcMarkSweepGC"]
+ if (project.hasProperty('disable_assertions')) {
+ enableAssertions = false
+ }
}
task buildPreNJdwpTestsJar(type: Jar) {
@@ -725,12 +756,18 @@
systemProperty 'jctf_compile_only', '1'
}
- if (OperatingSystem.current().isLinux() || OperatingSystem.current().isMacOsX()) {
+ if (OperatingSystem.current().isLinux()
+ || OperatingSystem.current().isMacOsX()
+ || OperatingSystem.current().isWindows()) {
if (OperatingSystem.current().isMacOsX()) {
logger.lifecycle("WARNING: Testing in only partially supported on Mac OS. " +
"Art only runs on Linux and tests requiring Art runs in a Docker container, which must be present. " +
"See tools/docker/README.md for details.")
}
+ if (OperatingSystem.current().isWindows()) {
+ logger.lifecycle("WARNING: Testing in only partially supported on Windows. " +
+ "Art only runs on Linux and tests requiring Art will be skipped")
+ }
dependsOn downloadDeps
dependsOn buildExamples
dependsOn buildSmali
@@ -740,7 +777,7 @@
dependsOn buildPreNJdwpTestsJar
} else {
logger.lifecycle("WARNING: Testing in not supported on your platform. Testing is only fully supported on " +
- "Linux and partially supported on Mac OS. Art does not run on other platforms.")
+ "Linux and partially supported on Mac OS and Windows. Art does not run on other platforms.")
}
}
@@ -953,7 +990,7 @@
def artTestDir = file("${androidCheckoutDir}/art/test")
def artRunTestScript = file("${artTestDir}/run-test")
def dxExecutable = new File("tools/linux/dx/bin/dx");
- def dexMergerExecutable = new File("tools/linux/dx/bin/dexmerger");
+ def dexMergerExecutable = Utils.dexMergerExecutable()
def dexToolName = dexTool == DexTool.DX ? "dx" : "jack"
def name = dir.getName();
diff --git a/buildSrc/src/main/java/dx/DexMerger.java b/buildSrc/src/main/java/dx/DexMerger.java
index 39a4bfb..d3d8427 100644
--- a/buildSrc/src/main/java/dx/DexMerger.java
+++ b/buildSrc/src/main/java/dx/DexMerger.java
@@ -53,7 +53,7 @@
public void execute(ExecSpec execSpec) {
try {
if (dexMergerExecutable == null) {
- dexMergerExecutable = new File("tools/" + Utils.toolsDir() + "/dx/bin/dexmerger");
+ dexMergerExecutable = Utils.dexMergerExecutable();
}
execSpec.setExecutable(dexMergerExecutable);
execSpec.args(destination.getCanonicalPath());
diff --git a/buildSrc/src/main/java/dx/Dx.java b/buildSrc/src/main/java/dx/Dx.java
index e4b89c4..86d1bce 100644
--- a/buildSrc/src/main/java/dx/Dx.java
+++ b/buildSrc/src/main/java/dx/Dx.java
@@ -64,7 +64,8 @@
public void execute(ExecSpec execSpec) {
try {
if (dxExecutable == null) {
- dxExecutable = new File("tools/" + Utils.toolsDir() + "/dx/bin/dx");
+ String dxExecutableName = Utils.toolsDir().equals("windows") ? "dx.bat" : "dx";
+ dxExecutable = new File("tools/" + Utils.toolsDir() + "/dx/bin/" + dxExecutableName);
}
execSpec.setExecutable(dxExecutable);
execSpec.args("--dex");
diff --git a/buildSrc/src/main/java/utils/Utils.java b/buildSrc/src/main/java/utils/Utils.java
index 5e5e9d7..7158e80 100644
--- a/buildSrc/src/main/java/utils/Utils.java
+++ b/buildSrc/src/main/java/utils/Utils.java
@@ -3,8 +3,22 @@
// BSD-style license that can be found in the LICENSE file.
package utils;
+import java.io.File;
+
public class Utils {
public static String toolsDir() {
- return System.getProperty("os.name").equals("Mac OS X") ? "mac" : "linux";
+ String osName = System.getProperty("os.name");
+ if (osName.equals("Mac OS X")) {
+ return "mac";
+ } else if (osName.contains("Windows")) {
+ return "windows";
+ } else {
+ return "linux";
+ }
}
-}
+
+ public static File dexMergerExecutable() {
+ String executableName = Utils.toolsDir().equals("windows") ? "dexmerger.bat" : "dexmerger";
+ return new File("tools/" + Utils.toolsDir() + "/dx/bin/" + executableName);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/android/tools/r8/BaseCommand.java b/src/main/java/com/android/tools/r8/BaseCommand.java
index 3497778..85c430a 100644
--- a/src/main/java/com/android/tools/r8/BaseCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCommand.java
@@ -5,13 +5,12 @@
import com.android.tools.r8.dex.Constants;
import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.OutputMode;
import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.OutputMode;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collection;
-import java.util.List;
abstract class BaseCommand {
@@ -147,7 +146,7 @@
}
/** Add library file resources. */
- public B addLibraryFiles(List<Path> files) throws IOException {
+ public B addLibraryFiles(Collection<Path> files) throws IOException {
app.addLibraryFiles(files);
return self();
}
diff --git a/src/main/java/com/android/tools/r8/BaseOutput.java b/src/main/java/com/android/tools/r8/BaseOutput.java
index cb4d473..a1fbb8e 100644
--- a/src/main/java/com/android/tools/r8/BaseOutput.java
+++ b/src/main/java/com/android/tools/r8/BaseOutput.java
@@ -49,6 +49,7 @@
* Write the output resources to a zip-archive or directory.
*
* @param output Path to existing directory or non-existing zip-archive.
+ * @param overwrite true to allow overwriting existing files with outputs.
*/
- abstract public void write(Path output) throws IOException;
+ abstract public void write(Path output, boolean overwrite) throws IOException;
}
diff --git a/src/main/java/com/android/tools/r8/ClassFileResourceProvider.java b/src/main/java/com/android/tools/r8/ClassFileResourceProvider.java
new file mode 100644
index 0000000..7dbb420
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ClassFileResourceProvider.java
@@ -0,0 +1,27 @@
+// Copyright (c) 2017, 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;
+
+import java.util.Set;
+
+/**
+ * Represents a provider for application resources of class file kind.
+ *
+ * Note that the classes will only be created for resources provided by
+ * resource providers on-demand when they are needed by the tool. If
+ * never needed, the resource will never be loaded.
+ */
+public interface ClassFileResourceProvider {
+ /** Returns all class descriptors. */
+ Set<String> getClassDescriptors();
+
+ /**
+ * Get the class resource associated with the descriptor, or null if
+ * this provider does not have one.
+ *
+ * Method may be called several times for the same resource, and should
+ * support concurrent calls from different threads.
+ */
+ Resource getResource(String descriptor);
+}
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 0a0c976..522f6f0 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -7,6 +7,7 @@
import com.android.tools.r8.dex.ApplicationReader;
import com.android.tools.r8.dex.ApplicationWriter;
+import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.graph.AppInfo;
import com.android.tools.r8.graph.DexApplication;
import com.android.tools.r8.ir.conversion.IRConverter;
@@ -59,11 +60,12 @@
* @return the compilation result.
*/
public static D8Output run(D8Command command) throws IOException {
- CompilationResult result = runForTesting(command.getInputApp(), command.getInternalOptions());
+ InternalOptions options = command.getInternalOptions();
+ CompilationResult result = runForTesting(command.getInputApp(), options);
assert result != null;
D8Output output = new D8Output(result.androidApp, command.getOutputMode());
if (command.getOutputPath() != null) {
- output.write(command.getOutputPath());
+ output.write(command.getOutputPath(), options.overwriteOutputs);
}
return output;
}
@@ -79,12 +81,13 @@
* @return the compilation result.
*/
public static D8Output run(D8Command command, ExecutorService executor) throws IOException {
+ InternalOptions options = command.getInternalOptions();
CompilationResult result = runForTesting(
- command.getInputApp(), command.getInternalOptions(), executor);
+ command.getInputApp(), options, executor);
assert result != null;
D8Output output = new D8Output(result.androidApp, command.getOutputMode());
if (command.getOutputPath() != null) {
- output.write(command.getOutputPath());
+ output.write(command.getOutputPath(), options.overwriteOutputs);
}
return output;
}
@@ -162,7 +165,7 @@
app = optimize(app, appInfo, options);
// If a method filter is present don't produce output since the application is likely partial.
- if (!options.methodsFilter.isEmpty()) {
+ if (options.hasMethodsFilter()) {
System.out.println("Finished compilation with method filter: ");
options.methodsFilter.forEach((m) -> System.out.println(" - " + m));
return null;
@@ -178,13 +181,17 @@
options.printWarnings();
return output;
} catch (ExecutionException e) {
- throw new RuntimeException(e.getMessage(), e.getCause());
+ if (e.getCause() instanceof CompilationError) {
+ throw (CompilationError) e.getCause();
+ } else {
+ throw new RuntimeException(e.getMessage(), e.getCause());
+ }
}
}
private static DexApplication optimize(
DexApplication application, AppInfo appInfo, InternalOptions options)
- throws IOException, ExecutionException {
+ throws IOException {
final CfgPrinter printer = options.printCfg ? new CfgPrinter() : null;
IRConverter converter = new IRConverter(application, appInfo, options, printer);
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 0ad793c..62cb5a5 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -3,15 +3,20 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8;
+import static com.android.tools.r8.utils.FileUtils.isArchive;
+
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.OffOrAuto;
import com.android.tools.r8.utils.OutputMode;
+import com.android.tools.r8.utils.PreloadedClassFileProvider;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collection;
/**
* Immutable command structure for an invocation of the {@code D8} compiler.
@@ -39,12 +44,46 @@
super(app, CompilationMode.DEBUG);
}
- public Builder addClasspathResourceProvider(ResourceProvider provider) {
+ /** Add classpath file resources. */
+ public Builder addClasspathFiles(Path... files) throws IOException {
+ return addClasspathFiles(Arrays.asList(files));
+ }
+
+ /** Add classpath file resources. */
+ public Builder addClasspathFiles(Collection<Path> files) throws IOException {
+ for (Path file : files) {
+ if (isArchive(file)) {
+ addClasspathResourceProvider(PreloadedClassFileProvider.fromArchive(file));
+ } else {
+ super.addClasspathFiles(file);
+ }
+ }
+ return this;
+ }
+
+ /** Add library file resources. */
+ public Builder addLibraryFiles(Path... files) throws IOException {
+ return addLibraryFiles(Arrays.asList(files));
+ }
+
+ /** Add library file resources. */
+ public Builder addLibraryFiles(Collection<Path> files) throws IOException {
+ for (Path file : files) {
+ if (isArchive(file)) {
+ addLibraryResourceProvider(PreloadedClassFileProvider.fromArchive(file));
+ } else {
+ super.addLibraryFiles(file);
+ }
+ }
+ return this;
+ }
+
+ public Builder addClasspathResourceProvider(ClassFileResourceProvider provider) {
getAppBuilder().addClasspathResourceProvider(provider);
return this;
}
- public Builder addLibraryResourceProvider(ResourceProvider provider) {
+ public Builder addLibraryResourceProvider(ClassFileResourceProvider provider) {
getAppBuilder().addLibraryResourceProvider(provider);
return this;
}
@@ -155,7 +194,7 @@
assert !internal.debug;
internal.debug = getMode() == CompilationMode.DEBUG;
internal.minApiLevel = getMinApiLevel();
- internal.fillDexFiles = true;
+ internal.overwriteOutputs = true;
// Assert and fixup defaults.
assert !internal.skipMinification;
internal.skipMinification = true;
@@ -171,8 +210,6 @@
internal.removeSwitchMaps = false;
assert internal.outline.enabled;
internal.outline.enabled = false;
- internal.lazyClasspathLoading = true;
- internal.lazyLibraryLoading = true;
internal.outputMode = getOutputMode();
return internal;
}
diff --git a/src/main/java/com/android/tools/r8/D8Output.java b/src/main/java/com/android/tools/r8/D8Output.java
index 1910e9b..4ba7067 100644
--- a/src/main/java/com/android/tools/r8/D8Output.java
+++ b/src/main/java/com/android/tools/r8/D8Output.java
@@ -16,7 +16,7 @@
}
@Override
- public void write(Path output) throws IOException {
- getAndroidApp().write(output, getOutputMode());
+ public void write(Path output, boolean overwrite) throws IOException {
+ getAndroidApp().write(output, getOutputMode(), overwrite);
}
}
diff --git a/src/main/java/com/android/tools/r8/Disassemble.java b/src/main/java/com/android/tools/r8/Disassemble.java
index 71daa5c..b183ecc 100644
--- a/src/main/java/com/android/tools/r8/Disassemble.java
+++ b/src/main/java/com/android/tools/r8/Disassemble.java
@@ -4,19 +4,145 @@
package com.android.tools.r8;
import com.android.tools.r8.shaking.ProguardRuleParserException;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.OutputMode;
+import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.util.Arrays;
-import java.util.List;
import java.util.concurrent.ExecutionException;
-import java.util.stream.Collectors;
public class Disassemble {
+ public static class DisassembleCommand extends BaseCommand {
+
+ public static class Builder
+ extends BaseCommand.Builder<DisassembleCommand, DisassembleCommand.Builder> {
+ private boolean useSmali = false;
+
+ private Builder() {
+ super(CompilationMode.RELEASE);
+ }
+
+ @Override
+ DisassembleCommand.Builder self() {
+ return this;
+ }
+
+ public DisassembleCommand.Builder setProguardMapFile(Path path) {
+ getAppBuilder().setProguardMapFile(path);
+ return this;
+ }
+
+ public DisassembleCommand.Builder setUseSmali(boolean useSmali) {
+ this.useSmali = useSmali;
+ return this;
+ }
+
+ @Override
+ public DisassembleCommand build() throws CompilationException, IOException {
+ // If printing versions ignore everything else.
+ if (isPrintHelp() || isPrintVersion()) {
+ return new DisassembleCommand(isPrintHelp(), isPrintVersion());
+ }
+
+ return new DisassembleCommand(
+ getAppBuilder().build(),
+ getOutputPath(),
+ getOutputMode(),
+ getMode(),
+ getMinApiLevel(),
+ useSmali);
+ }
+ }
+
+ static final String USAGE_MESSAGE = String.join("\n", ImmutableList.of(
+ "Usage: disasm [options] <input-files>",
+ " where <input-files> are dex files",
+ " and options are:",
+ " --smali # Disassemble using smali syntax.",
+ " --pg-map <file> # Proguard map <file> for mapping names.",
+ " --version # Print the version of r8.",
+ " --help # Print this message."));
+
+
+ private final boolean useSmali;
+
+ public static DisassembleCommand.Builder builder() {
+ return new DisassembleCommand.Builder();
+ }
+
+ public static DisassembleCommand.Builder parse(String[] args)
+ throws CompilationException, IOException {
+ DisassembleCommand.Builder builder = builder();
+ parse(args, builder);
+ return builder;
+ }
+
+ private static void parse(String[] args, DisassembleCommand.Builder builder)
+ throws CompilationException, IOException {
+ for (int i = 0; i < args.length; i++) {
+ String arg = args[i].trim();
+ if (arg.length() == 0) {
+ continue;
+ } else if (arg.equals("--help")) {
+ builder.setPrintHelp(true);
+ } else if (arg.equals("--version")) {
+ builder.setPrintVersion(true);
+ } else if (arg.equals("--smali")) {
+ builder.setUseSmali(true);
+ } else if (arg.equals("--pg-map")) {
+ builder.setProguardMapFile(Paths.get(args[++i]));
+ } else {
+ if (arg.startsWith("--")) {
+ throw new CompilationException("Unknown option: " + arg);
+ }
+ builder.addProgramFiles(Paths.get(arg));
+ }
+ }
+ }
+
+ private DisassembleCommand(
+ AndroidApp inputApp,
+ Path outputPath,
+ OutputMode outputMode,
+ CompilationMode mode,
+ int minApiLevel,
+ boolean useSmali) {
+ super(inputApp, outputPath, outputMode, mode, minApiLevel);
+ //assert getOutputMode() == OutputMode.Indexed : "Only regular mode is supported in R8";
+ this.useSmali = useSmali;
+ }
+
+ private DisassembleCommand(boolean printHelp, boolean printVersion) {
+ super(printHelp, printVersion);
+ this.useSmali = false;
+ }
+
+ public boolean useSmali() {
+ return useSmali;
+ }
+
+ @Override
+ InternalOptions getInternalOptions() {
+ InternalOptions internal = new InternalOptions();
+ internal.useSmaliSyntax = useSmali;
+ return internal;
+ }
+ }
+
public static void main(String[] args)
throws IOException, ProguardRuleParserException, CompilationException, ExecutionException {
- List<Path> files = Arrays.stream(args).map(s -> Paths.get(s)).collect(Collectors.toList());
- R8Command command = R8Command.builder().addProgramFiles(files).build();
+ DisassembleCommand.Builder builder = DisassembleCommand.parse(args);
+ DisassembleCommand command = builder.build();
+ if (command.isPrintHelp()) {
+ System.out.println(DisassembleCommand.USAGE_MESSAGE);
+ return;
+ }
+ if (command.isPrintVersion()) {
+ System.out.println("R8 v0.0.1");
+ return;
+ }
R8.disassemble(command);
}
}
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 390e752..11cc9bc 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -39,14 +39,12 @@
import com.android.tools.r8.utils.CfgPrinter;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.InternalOptions.AttributeRemovalOptions;
-import com.android.tools.r8.utils.OutputMode;
import com.android.tools.r8.utils.PackageDistribution;
import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.Timing;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closer;
import java.io.ByteArrayOutputStream;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
@@ -55,6 +53,7 @@
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutionException;
@@ -145,7 +144,8 @@
return result;
}
- public static void disassemble(R8Command command) throws IOException, ExecutionException {
+ public static void disassemble(Disassemble.DisassembleCommand command)
+ throws IOException, ExecutionException {
Path output = command.getOutputPath();
AndroidApp app = command.getInputApp();
InternalOptions options = command.getInternalOptions();
@@ -172,7 +172,7 @@
}
static CompilationResult runForTesting(AndroidApp app, InternalOptions options)
- throws ProguardRuleParserException, ExecutionException, IOException {
+ throws ProguardRuleParserException, IOException {
ExecutorService executor = ThreadUtils.getExecutorService(options);
try {
return runForTesting(app, options, executor);
@@ -185,12 +185,12 @@
AndroidApp app,
InternalOptions options,
ExecutorService executor)
- throws ProguardRuleParserException, ExecutionException, IOException {
+ throws ProguardRuleParserException, IOException {
return new R8(options).run(app, executor);
}
private CompilationResult run(AndroidApp inputApp, ExecutorService executorService)
- throws IOException, ExecutionException, ProguardRuleParserException {
+ throws IOException, ProguardRuleParserException {
if (options.quiet) {
System.setOut(new PrintStream(ByteStreams.nullOutputStream()));
}
@@ -207,7 +207,6 @@
RootSet rootSet;
byte[] proguardSeedsData = null;
timing.begin("Strip unused code");
- Set<DexType> mainDexBaseClasses = null;
try {
Set<DexType> missingClasses = appInfo.getMissingClasses();
missingClasses = filterMissingClasses(missingClasses, options.dontWarnPatterns);
@@ -281,7 +280,7 @@
// Lets find classes which may have code executed before secondary dex files installation.
RootSet mainDexRootSet =
new RootSetBuilder(application, appInfo, options.mainDexKeepRules).run(executorService);
- mainDexBaseClasses = enqueuer.traceMainDex(mainDexRootSet, timing);
+ Set<DexType> mainDexBaseClasses = enqueuer.traceMainDex(mainDexRootSet, timing);
// Calculate the automatic main dex list according to legacy multidex constraints.
// Add those classes to an eventual manual list of classes.
@@ -324,7 +323,7 @@
timing.end();
// If a method filter is present don't produce output since the application is likely partial.
- if (!options.methodsFilter.isEmpty()) {
+ if (options.hasMethodsFilter()) {
System.out.println("Finished compilation with method filter: ");
options.methodsFilter.forEach((m) -> System.out.println(" - " + m));
return null;
@@ -348,21 +347,14 @@
packageDistribution,
options);
- if (options.printMapping && androidApp.hasProguardMap() && !options.skipMinification) {
- Path mapOutput = options.printMappingFile;
- try (Closer closer = Closer.create()) {
- OutputStream mapOut;
- if (mapOutput == null) {
- mapOut = System.out;
- } else {
- mapOut = new FileOutputStream(mapOutput.toFile());
- closer.register(mapOut);
- }
- androidApp.writeProguardMap(closer, mapOut);
- }
- }
options.printWarnings();
return new CompilationResult(androidApp, application, appInfo);
+ } catch (ExecutionException e) {
+ if (e.getCause() instanceof CompilationError) {
+ throw (CompilationError) e.getCause();
+ } else {
+ throw new RuntimeException(e.getMessage(), e.getCause());
+ }
} finally {
// Dump timings.
if (options.printTimes) {
@@ -381,13 +373,66 @@
* @return the compilation result.
*/
public static AndroidApp run(R8Command command)
- throws IOException, CompilationException, ExecutionException, ProguardRuleParserException {
- AndroidApp outputApp =
- runForTesting(command.getInputApp(), command.getInternalOptions()).androidApp;
- if (command.getOutputPath() != null) {
- outputApp.write(command.getOutputPath(), OutputMode.Indexed);
+ throws IOException, CompilationException, ProguardRuleParserException {
+ ExecutorService executorService = ThreadUtils.getExecutorService(command.getInternalOptions());
+ try {
+ return run(command, executorService);
+ } finally {
+ executorService.shutdown();
}
- return outputApp;
+ }
+
+ static void writeOutputs(R8Command command, InternalOptions options, AndroidApp outputApp)
+ throws IOException {
+ if (command.getOutputPath() != null) {
+ outputApp.write(command.getOutputPath(), options.outputMode, options.overwriteOutputs);
+ }
+
+ if (options.printMapping && !options.skipMinification) {
+ assert outputApp.hasProguardMap();
+ try (Closer closer = Closer.create()) {
+ OutputStream mapOut = openPathWithDefault(
+ closer,
+ options.printMappingFile,
+ options.overwriteOutputs,
+ System.out);
+ outputApp.writeProguardMap(closer, mapOut);
+ }
+ }
+ if (options.printSeeds) {
+ assert outputApp.hasProguardSeeds();
+ try (Closer closer = Closer.create()) {
+ OutputStream seedsOut =
+ openPathWithDefault(closer, options.seedsFile, options.overwriteOutputs, System.out);
+ outputApp.writeProguardSeeds(closer, seedsOut);
+ }
+ }
+ if (options.printMainDexList && outputApp.hasMainDexList()) {
+ try (Closer closer = Closer.create()) {
+ OutputStream mainDexOut =
+ openPathWithDefault(closer, options.printMainDexListFile, true, System.out);
+ outputApp.writeMainDexList(closer, mainDexOut);
+ }
+ }
+ }
+
+ private static OutputStream openPathWithDefault(Closer closer,
+ Path file,
+ boolean allowOverwrite,
+ PrintStream defaultOutput) throws IOException {
+ OutputStream mapOut;
+ if (file == null) {
+ mapOut = defaultOutput;
+ } else {
+ if (!allowOverwrite) {
+ mapOut = Files.newOutputStream(file, StandardOpenOption.CREATE_NEW);
+ } else {
+ mapOut = Files.newOutputStream(file,
+ StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ }
+ closer.register(mapOut);
+ }
+ return mapOut;
}
/**
@@ -401,17 +446,16 @@
* @return the compilation result.
*/
public static AndroidApp run(R8Command command, ExecutorService executor)
- throws IOException, CompilationException, ExecutionException, ProguardRuleParserException {
+ throws IOException, CompilationException, ProguardRuleParserException {
+ InternalOptions options = command.getInternalOptions();
AndroidApp outputApp =
- runForTesting(command.getInputApp(), command.getInternalOptions(), executor).androidApp;
- if (command.getOutputPath() != null) {
- outputApp.write(command.getOutputPath(), OutputMode.Indexed);
- }
+ runForTesting(command.getInputApp(), options, executor).androidApp;
+ writeOutputs(command, options, outputApp);
return outputApp;
}
private static void run(String[] args)
- throws ExecutionException, IOException, ProguardRuleParserException, CompilationException {
+ throws IOException, ProguardRuleParserException, CompilationException {
R8Command.Builder builder = R8Command.parse(args);
if (builder.getOutputPath() == null) {
builder.setOutputPath(Paths.get("."));
@@ -442,7 +486,7 @@
} catch (ProguardRuleParserException e) {
System.err.println("Failed parsing proguard keep rules: " + e.getMessage());
System.exit(1);
- } catch (RuntimeException | ExecutionException e) {
+ } catch (RuntimeException e) {
System.err.println("Compilation failed with an internal error.");
Throwable cause = e.getCause() == null ? e : e.getCause();
cause.printStackTrace();
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 9b44546..06ebe60 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -26,6 +26,7 @@
public static class Builder extends BaseCommand.Builder<R8Command, Builder> {
private final List<Path> mainDexRules = new ArrayList<>();
+ private boolean minimalMainDex = false;
private final List<Path> proguardConfigFiles = new ArrayList<>();
private Optional<Boolean> treeShaking = Optional.empty();
private Optional<Boolean> minification = Optional.empty();
@@ -77,6 +78,16 @@
}
/**
+ * Request minimal main dex generated when main dex rules are used.
+ *
+ * The main purpose of this is to verify that the main dex rules are sufficient
+ * for running on a platform without native multi dex support.
+ */
+ public Builder setMinimalMainDex(boolean value) {
+ minimalMainDex = value;
+ return this;
+ }
+ /**
* Add proguard configuration file resources.
*/
public Builder addProguardConfigurationFiles(Path... paths) {
@@ -161,6 +172,7 @@
getOutputPath(),
getOutputMode(),
mainDexKeepRules,
+ minimalMainDex,
configuration,
getMode(),
getMinApiLevel(),
@@ -196,6 +208,7 @@
" --help # Print this message."));
private final ImmutableList<ProguardConfigurationRule> mainDexKeepRules;
+ private final boolean minimalMainDex;
private final ProguardConfiguration proguardConfiguration;
private final boolean useTreeShaking;
private final boolean useMinification;
@@ -259,6 +272,8 @@
builder.setMinification(false);
} else if (arg.equals("--multidex-rules")) {
builder.addMainDexRules(Paths.get(args[++i]));
+ } else if (arg.equals("--minimal-maindex")) {
+ builder.setMinimalMainDex(true);
} else if (arg.equals("--pg-conf")) {
builder.addProguardConfigurationFiles(Paths.get(args[++i]));
} else if (arg.equals("--pg-map")) {
@@ -300,6 +315,7 @@
Path outputPath,
OutputMode outputMode,
ImmutableList<ProguardConfigurationRule> mainDexKeepRules,
+ boolean minimalMainDex,
ProguardConfiguration proguardConfiguration,
CompilationMode mode,
int minApiLevel,
@@ -311,6 +327,7 @@
assert mainDexKeepRules != null;
assert getOutputMode() == OutputMode.Indexed : "Only regular mode is supported in R8";
this.mainDexKeepRules = mainDexKeepRules;
+ this.minimalMainDex = minimalMainDex;
this.proguardConfiguration = proguardConfiguration;
this.useTreeShaking = useTreeShaking;
this.useMinification = useMinification;
@@ -320,6 +337,7 @@
private R8Command(boolean printHelp, boolean printVersion) {
super(printHelp, printVersion);
mainDexKeepRules = ImmutableList.of();
+ minimalMainDex = false;
proguardConfiguration = null;
useTreeShaking = false;
useMinification = false;
@@ -346,6 +364,7 @@
internal.useTreeShaking = useTreeShaking();
assert !internal.ignoreMissingClasses;
internal.ignoreMissingClasses = ignoreMissingClasses;
+ internal.overwriteOutputs = true;
// TODO(zerny): Consider which other proguard options should be given flags.
assert internal.packagePrefix.length() == 0;
@@ -375,6 +394,7 @@
internal.classObfuscationDictionary = proguardConfiguration.getClassObfuscationDictionary();
internal.obfuscationDictionary = proguardConfiguration.getObfuscationDictionary();
internal.mainDexKeepRules = mainDexKeepRules;
+ internal.minimalMainDex = minimalMainDex;
internal.keepRules = proguardConfiguration.getRules();
internal.dontWarnPatterns = proguardConfiguration.getDontWarnPatterns();
internal.outputMode = getOutputMode();
diff --git a/src/main/java/com/android/tools/r8/ReadMainDexList.java b/src/main/java/com/android/tools/r8/ReadMainDexList.java
new file mode 100644
index 0000000..3a76c1f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ReadMainDexList.java
@@ -0,0 +1,58 @@
+// Copyright (c) 2017, 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;
+
+import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.naming.ProguardMapReader;
+import com.android.tools.r8.utils.FileUtils;
+import java.nio.file.Paths;
+import java.util.stream.Collectors;
+
+/**
+ * Utility for applying proguard map and sorting the main dex list.
+ */
+public class ReadMainDexList {
+
+ private String DOT_CLASS = ".class";
+
+ private String stripDotClass(String name) {
+ return name.endsWith(DOT_CLASS) ? name.substring(0, name.length() - DOT_CLASS.length()) : name;
+ }
+
+ private String addDotClass(String name) {
+ return name + DOT_CLASS;
+ }
+
+ private String deobfuscateClassName(String name, ClassNameMapper mapper) {
+ if (mapper == null) {
+ return name;
+ }
+ return mapper.deobfuscateClassName(name);
+ }
+
+ private void run(String[] args) throws Exception {
+ if (args.length != 1 && args.length != 2) {
+ System.out.println("Usage: command <main_dex_list> [<proguard_map>]");
+ System.exit(0);
+ }
+
+ final ClassNameMapper mapper =
+ args.length == 2 ? ProguardMapReader.mapperFromFile(Paths.get(args[1])) : null;
+
+ FileUtils.readTextFile(Paths.get(args[0]))
+ .stream()
+ .map(this::stripDotClass)
+ .map(name -> name.replace('/', '.'))
+ .map(name -> deobfuscateClassName(name, mapper))
+ .map(name -> name.replace('.', '/'))
+ .map(this::addDotClass)
+ .sorted()
+ .collect(Collectors.toList())
+ .forEach(System.out::println);
+ }
+
+ public static void main(String[] args) throws Exception {
+ new ReadMainDexList().run(args);
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/ResourceProvider.java b/src/main/java/com/android/tools/r8/ResourceProvider.java
deleted file mode 100644
index 2a5b0dc..0000000
--- a/src/main/java/com/android/tools/r8/ResourceProvider.java
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (c) 2017, 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;
-
-/**
- * Represents a provider for application resources. All resources returned
- * via this provider should be class file resources and will be loaded on-demand
- * or not loaded at all.
- *
- * NOTE: currently only resources representing Java class files can be loaded
- * with lazy resource providers.
- */
-public interface ResourceProvider {
- /** Get the class resource associated with the descriptor, or null. */
- Resource getResource(String descriptor);
-}
diff --git a/src/main/java/com/android/tools/r8/bisect/BisectState.java b/src/main/java/com/android/tools/r8/bisect/BisectState.java
index 945578a..148c167 100644
--- a/src/main/java/com/android/tools/r8/bisect/BisectState.java
+++ b/src/main/java/com/android/tools/r8/bisect/BisectState.java
@@ -8,8 +8,6 @@
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.DexApplication;
import com.android.tools.r8.graph.DexApplication.Builder;
-import com.android.tools.r8.graph.DexClassPromise;
-import com.android.tools.r8.graph.DexLibraryClass;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.naming.NamingLens;
@@ -24,7 +22,6 @@
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -261,23 +258,20 @@
System.out.println("Next bisection range: " + nextRange);
int goodClasses = 0;
int badClasses = 0;
- Map<DexType, DexClassPromise> classes = new HashMap<>();
- for (DexLibraryClass clazz : badApp.libraryClasses()) {
- classes.put(clazz.type, clazz);
- }
+ List<DexProgramClass> programClasses = new ArrayList<>();
for (DexProgramClass clazz : badApp.classes()) {
DexProgramClass goodClass = getGoodClass(clazz);
if (goodClass != null) {
- classes.put(goodClass.type, goodClass);
+ programClasses.add(goodClass);
++goodClasses;
} else {
- classes.put(clazz.type, clazz);
+ programClasses.add(clazz);
assert !nextRange.isEmpty();
++badClasses;
}
}
System.out.println("Class split is good: " + goodClasses + ", bad: " + badClasses);
- return new Builder(badApp, classes).build();
+ return new Builder(badApp).replaceProgramClasses(programClasses).build();
}
private DexProgramClass getGoodClass(DexProgramClass clazz) {
diff --git a/src/main/java/com/android/tools/r8/compatdx/CompatDx.java b/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
index c174a31..4a499ec 100644
--- a/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
+++ b/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
@@ -484,7 +484,7 @@
}
writeZipWithClasses(inputs, result, output);
} else {
- result.write(output);
+ result.write(output, true);
}
}
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
index af6bebe..c7a66c2 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -8,22 +8,29 @@
import static com.android.tools.r8.dex.Constants.ANDROID_O_API;
import static com.android.tools.r8.dex.Constants.ANDROID_O_DEX_VERSION;
import static com.android.tools.r8.dex.Constants.DEFAULT_ANDROID_API;
+import static com.android.tools.r8.graph.ClassKind.CLASSPATH;
+import static com.android.tools.r8.graph.ClassKind.LIBRARY;
+import static com.android.tools.r8.graph.ClassKind.PROGRAM;
import static com.android.tools.r8.utils.FileUtils.DEFAULT_DEX_FILENAME;
+import com.android.tools.r8.ClassFileResourceProvider;
import com.android.tools.r8.Resource;
-import com.android.tools.r8.ResourceProvider;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.graph.ClassKind;
import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClasspathClass;
import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexLibraryClass;
+import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.JarApplicationReader;
import com.android.tools.r8.graph.JarClassFileReader;
-import com.android.tools.r8.graph.LazyClassFileLoader;
import com.android.tools.r8.naming.ProguardMapReader;
import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.ClassProvider;
+import com.android.tools.r8.utils.ClasspathClassCollection;
import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.LazyClassCollection;
+import com.android.tools.r8.utils.LibraryClassCollection;
import com.android.tools.r8.utils.MainDexList;
import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.Timing;
@@ -32,7 +39,8 @@
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
-import java.util.Set;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -67,107 +75,25 @@
final DexApplication.Builder builder = new DexApplication.Builder(itemFactory, timing);
try (Closer closer = Closer.create()) {
List<Future<?>> futures = new ArrayList<>();
+ // Still preload some of the classes, primarily for two reasons:
+ // (a) class lazy loading is not supported for DEX files
+ // now and current implementation of parallel DEX file
+ // loading will be lost with on-demand class loading.
+ // (b) some of the class file resources don't provide information
+ // about class descriptor.
+ // TODO: try and preload less classes.
readProguardMap(builder, executorService, futures, closer);
readMainDexList(builder, executorService, futures, closer);
- readDexSources(builder, executorService, futures, closer);
- readClassSources(builder, closer);
- initializeLazyClassCollection(builder);
+ ClassReader classReader = new ClassReader(executorService, futures, closer);
+ classReader.readSources();
ThreadUtils.awaitFutures(futures);
- } catch (ExecutionException e) {
- // If the reading failed with a valid compilation error, rethrow the unwrapped exception.
- Throwable cause = e.getCause();
- if (cause != null && cause instanceof CompilationError) {
- throw (CompilationError) cause;
- }
- throw e;
+ classReader.initializeLazyClassCollection(builder);
} finally {
timing.end();
}
return builder.build();
}
- private void readClassSources(DexApplication.Builder builder, Closer closer)
- throws IOException, ExecutionException {
- JarApplicationReader application = new JarApplicationReader(options);
- JarClassFileReader reader = new JarClassFileReader(
- application, builder::addClassIgnoringLibraryDuplicates);
- for (Resource input : inputApp.getClassProgramResources()) {
- reader.read(DEFAULT_DEX_FILENAME, ClassKind.PROGRAM, input.getStream(closer));
- }
- for (Resource input : inputApp.getClassClasspathResources()) {
- if (options.lazyClasspathLoading && getResourceClassDescriptorOrNull(input) != null) {
- addLazyLoader(application, ClassKind.CLASSPATH, builder, input);
- } else {
- reader.read(DEFAULT_DEX_FILENAME, ClassKind.CLASSPATH, input.getStream(closer));
- }
- }
- for (Resource input : inputApp.getClassLibraryResources()) {
- if (options.lazyLibraryLoading && getResourceClassDescriptorOrNull(input) != null) {
- addLazyLoader(application, ClassKind.LIBRARY, builder, input);
- } else {
- reader.read(DEFAULT_DEX_FILENAME, ClassKind.LIBRARY, input.getStream(closer));
- }
- }
- }
-
- private void initializeLazyClassCollection(DexApplication.Builder builder) {
- List<ResourceProvider> classpathProviders = inputApp.getClasspathResourceProviders();
- List<ResourceProvider> libraryProviders = inputApp.getLibraryResourceProviders();
- if (!classpathProviders.isEmpty() || !libraryProviders.isEmpty()) {
- builder.setLazyClassCollection(new LazyClassCollection(
- new JarApplicationReader(options), classpathProviders, libraryProviders));
- }
- }
-
- private void addLazyLoader(JarApplicationReader application,
- ClassKind classKind, DexApplication.Builder builder, Resource resource) {
- // Generate expected DEX type.
- String classDescriptor = getResourceClassDescriptorOrNull(resource);
- assert classDescriptor != null;
- DexType type = options.itemFactory.createType(classDescriptor);
- LazyClassFileLoader newLoader = new LazyClassFileLoader(type, resource, classKind, application);
- builder.addClassPromise(newLoader, true);
- }
-
- private void readDexSources(DexApplication.Builder builder, ExecutorService executorService,
- List<Future<?>> futures, Closer closer)
- throws IOException, ExecutionException {
- List<Resource> dexProgramSources = inputApp.getDexProgramResources();
- List<Resource> dexClasspathSources = inputApp.getDexClasspathResources();
- List<Resource> dexLibrarySources = inputApp.getDexLibraryResources();
- int numberOfFiles = dexProgramSources.size()
- + dexLibrarySources.size() + dexClasspathSources.size();
- if (numberOfFiles > 0) {
- List<DexFileReader> fileReaders = new ArrayList<>(numberOfFiles);
- int computedMinApiLevel = options.minApiLevel;
- for (Resource input : dexProgramSources) {
- DexFile file = new DexFile(input.getStream(closer));
- computedMinApiLevel = verifyOrComputeMinApiLevel(computedMinApiLevel, file);
- fileReaders.add(new DexFileReader(file, ClassKind.PROGRAM, itemFactory));
- }
- for (Resource input : dexClasspathSources) {
- DexFile file = new DexFile(input.getStream(closer));
- fileReaders.add(new DexFileReader(file, ClassKind.CLASSPATH, itemFactory));
- }
- for (Resource input : dexLibrarySources) {
- DexFile file = new DexFile(input.getStream(closer));
- computedMinApiLevel = verifyOrComputeMinApiLevel(computedMinApiLevel, file);
- fileReaders.add(new DexFileReader(file, ClassKind.LIBRARY, itemFactory));
- }
- options.minApiLevel = computedMinApiLevel;
- for (DexFileReader reader : fileReaders) {
- DexFileReader.populateIndexTables(reader);
- }
- // Read the DexCode items and DexProgramClass items in parallel.
- for (DexFileReader reader : fileReaders) {
- futures.add(executorService.submit(() -> {
- reader.addCodeItemsTo(); // Depends on Everything for parsing.
- reader.addClassDefsTo(builder::addClassPromise); // Depends on Methods, Code items etc.
- }));
- }
- }
- }
-
private int verifyOrComputeMinApiLevel(int computedMinApiLevel, DexFile file) {
int version = file.getDexVersion();
if (options.minApiLevel == DEFAULT_ANDROID_API) {
@@ -232,9 +158,110 @@
}
}
- private static String getResourceClassDescriptorOrNull(Resource resource) {
- Set<String> descriptors = resource.getClassDescriptors();
- return (descriptors == null) || (descriptors.size() != 1)
- ? null : descriptors.iterator().next();
+ private final class ClassReader {
+ private final ExecutorService executorService;
+ private final List<Future<?>> futures;
+ private final Closer closer;
+
+ // We use concurrent queues to collect classes
+ // since the classes can be collected concurrently.
+ private final Queue<DexProgramClass> programClasses = new ConcurrentLinkedQueue<>();
+ private final Queue<DexClasspathClass> classpathClasses = new ConcurrentLinkedQueue<>();
+ private final Queue<DexLibraryClass> libraryClasses = new ConcurrentLinkedQueue<>();
+ // Jar application reader to share across all class readers.
+ private final JarApplicationReader application = new JarApplicationReader(options);
+
+ ClassReader(ExecutorService executorService, List<Future<?>> futures, Closer closer) {
+ this.executorService = executorService;
+ this.futures = futures;
+ this.closer = closer;
+ }
+
+ private <T extends DexClass> void readDexSources(List<Resource> dexSources,
+ ClassKind classKind, Queue<T> classes) throws IOException, ExecutionException {
+ if (dexSources.size() > 0) {
+ List<DexFileReader> fileReaders = new ArrayList<>(dexSources.size());
+ int computedMinApiLevel = options.minApiLevel;
+ for (Resource input : dexSources) {
+ DexFile file = new DexFile(input.getStream(closer));
+ computedMinApiLevel = verifyOrComputeMinApiLevel(computedMinApiLevel, file);
+ fileReaders.add(new DexFileReader(file, classKind, itemFactory));
+ }
+ options.minApiLevel = computedMinApiLevel;
+ for (DexFileReader reader : fileReaders) {
+ DexFileReader.populateIndexTables(reader);
+ }
+ // Read the DexCode items and DexProgramClass items in parallel.
+ for (DexFileReader reader : fileReaders) {
+ futures.add(executorService.submit(() -> {
+ reader.addCodeItemsTo(); // Depends on Everything for parsing.
+ reader.addClassDefsTo(
+ classKind.bridgeConsumer(classes::add)); // Depends on Methods, Code items etc.
+ }));
+ }
+ }
+ }
+
+ private <T extends DexClass> void readClassSources(List<Resource> classSources,
+ ClassKind classKind, Queue<T> classes) throws IOException, ExecutionException {
+ JarClassFileReader reader = new JarClassFileReader(
+ application, classKind.bridgeConsumer(classes::add));
+ for (Resource input : classSources) {
+ reader.read(DEFAULT_DEX_FILENAME, classKind, input.getStream(closer));
+ }
+ }
+
+ void readSources() throws IOException, ExecutionException {
+ readDexSources(inputApp.getDexProgramResources(), PROGRAM, programClasses);
+ readDexSources(inputApp.getDexClasspathResources(), CLASSPATH, classpathClasses);
+ readDexSources(inputApp.getDexLibraryResources(), LIBRARY, libraryClasses);
+ readClassSources(inputApp.getClassProgramResources(), PROGRAM, programClasses);
+ readClassSources(inputApp.getClassClasspathResources(), CLASSPATH, classpathClasses);
+ readClassSources(inputApp.getClassLibraryResources(), LIBRARY, libraryClasses);
+ }
+
+ private <T extends DexClass> ClassProvider<T> buildClassProvider(ClassKind classKind,
+ Queue<T> preloadedClasses, List<ClassFileResourceProvider> resourceProviders,
+ JarApplicationReader reader) {
+ List<ClassProvider<T>> providers = new ArrayList<>();
+
+ // Preloaded classes.
+ if (!preloadedClasses.isEmpty()) {
+ providers.add(ClassProvider.forPreloadedClasses(classKind, preloadedClasses));
+ }
+
+ // Class file resource providers.
+ for (ClassFileResourceProvider provider : resourceProviders) {
+ providers.add(ClassProvider.forClassFileResources(classKind, provider, reader));
+ }
+
+ // Combine if needed.
+ if (providers.isEmpty()) {
+ return null;
+ }
+ return providers.size() == 1 ? providers.get(0)
+ : ClassProvider.combine(classKind, providers);
+ }
+
+ void initializeLazyClassCollection(DexApplication.Builder builder) {
+ // Add all program classes to the builder.
+ for (DexProgramClass clazz : programClasses) {
+ builder.addProgramClass(clazz.asProgramClass());
+ }
+
+ // Create classpath class collection if needed.
+ ClassProvider<DexClasspathClass> classpathClassProvider = buildClassProvider(CLASSPATH,
+ classpathClasses, inputApp.getClasspathResourceProviders(), application);
+ if (classpathClassProvider != null) {
+ builder.setClasspathClassCollection(new ClasspathClassCollection(classpathClassProvider));
+ }
+
+ // Create library class collection if needed.
+ ClassProvider<DexLibraryClass> libraryClassProvider = buildClassProvider(LIBRARY,
+ libraryClasses, inputApp.getLibraryResourceProviders(), application);
+ if (libraryClassProvider != null) {
+ builder.setLibraryClassCollection(new LibraryClassCollection(libraryClassProvider));
+ }
+ }
}
}
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index db264ca..1636c56 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -3,6 +3,10 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.dex;
+import com.android.tools.r8.dex.VirtualFile.FilePerClassDistributor;
+import com.android.tools.r8.dex.VirtualFile.FillFilesDistributor;
+import com.android.tools.r8.dex.VirtualFile.PackageMapDistributor;
+import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.graph.AppInfo;
import com.android.tools.r8.graph.DexAnnotation;
import com.android.tools.r8.graph.DexAnnotationSet;
@@ -12,12 +16,15 @@
import com.android.tools.r8.graph.DexDebugInfo;
import com.android.tools.r8.graph.DexEncodedArray;
import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.DexTypeList;
import com.android.tools.r8.graph.DexValue;
import com.android.tools.r8.naming.MinifiedNameMapPrinter;
import com.android.tools.r8.naming.NamingLens;
import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.OutputMode;
import com.android.tools.r8.utils.PackageDistribution;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -111,13 +118,38 @@
application.dexItemFactory.sort(namingLens);
SortAnnotations sortAnnotations = new SortAnnotations();
application.classes().forEach((clazz) -> clazz.addDependencies(sortAnnotations));
- Map<Integer, VirtualFile> newFiles =
- VirtualFile.fileSetFrom(this, packageDistribution, executorService);
- // Write the dex files and the Proguard mapping file in parallel.
+ // Distribute classes into dex files.
+ VirtualFile.Distributor distributor = null;
+ if (options.outputMode == OutputMode.FilePerClass) {
+ assert packageDistribution == null :
+ "Cannot combine package distribution definition with file-per-class option.";
+ distributor = new FilePerClassDistributor(this);
+ } else if (options.minApiLevel < Constants.ANDROID_L_API
+ && options.mainDexKeepRules.isEmpty()
+ && application.mainDexList.isEmpty()) {
+ if (packageDistribution != null) {
+ throw new CompilationError("Cannot apply package distribution. Multidex is not"
+ + " supported with API level " + options.minApiLevel +"."
+ + " For API level < " + Constants.ANDROID_L_API + ", main dex classes list or"
+ + " rules must be specified.");
+ }
+ distributor = new VirtualFile.MonoDexDistributor(this);
+ } else if (packageDistribution != null) {
+ assert !options.minimalMainDex :
+ "Cannot combine package distribution definition with minimal-main-dex option.";
+ distributor = new PackageMapDistributor(this, packageDistribution, executorService);
+ } else {
+ distributor = new FillFilesDistributor(this, options.minimalMainDex);
+ }
+ Map<Integer, VirtualFile> newFiles = distributor.run();
+
+ // Write the dex files and the Proguard mapping file in parallel. Use a linked hash map
+ // as the order matters when addDexProgramData is called below.
LinkedHashMap<VirtualFile, Future<byte[]>> dexDataFutures = new LinkedHashMap<>();
- for (Integer index : newFiles.keySet()) {
- VirtualFile newFile = newFiles.get(index);
+ for (int i = 0; i < newFiles.size(); i++) {
+ VirtualFile newFile = newFiles.get(i);
+ assert newFile.getId() == i;
if (!newFile.isEmpty()) {
dexDataFutures.put(newFile, executorService.submit(() -> writeDexFile(newFile)));
}
@@ -141,6 +173,10 @@
if (proguardSeedsData != null) {
builder.setProguardSeedsData(proguardSeedsData);
}
+ byte[] mainDexList = writeMainDexList();
+ if (mainDexList != null) {
+ builder.setMainDexListData(mainDexList);
+ }
return builder.build();
} finally {
application.timing.end();
@@ -177,4 +213,22 @@
}
return null;
}
+
+ private String mapMainDexListName(DexType type) {
+ return DescriptorUtils.descriptorToJavaType(namingLens.lookupDescriptor(type).toString())
+ .replace('.', '/') + ".class";
+ }
+
+ private byte[] writeMainDexList() throws IOException {
+ if (application.mainDexList.isEmpty()) {
+ return null;
+ }
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ PrintWriter writer = new PrintWriter(bytes);
+ application.mainDexList.forEach(
+ type -> writer.println(mapMainDexListName(type))
+ );
+ writer.flush();
+ return bytes.toByteArray();
+ }
}
diff --git a/src/main/java/com/android/tools/r8/dex/Constants.java b/src/main/java/com/android/tools/r8/dex/Constants.java
index 201fc82..3ea63bb 100644
--- a/src/main/java/com/android/tools/r8/dex/Constants.java
+++ b/src/main/java/com/android/tools/r8/dex/Constants.java
@@ -10,6 +10,7 @@
public static final int ANDROID_O_API = 26;
public static final int ANDROID_N_API = 24;
+ public static final int ANDROID_L_API = 21;
public static final int ANDROID_K_API = 19;
public static final int DEFAULT_ANDROID_API = 1;
diff --git a/src/main/java/com/android/tools/r8/dex/VirtualFile.java b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
index 07fce3a..0e89d4a 100644
--- a/src/main/java/com/android/tools/r8/dex/VirtualFile.java
+++ b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
@@ -23,11 +23,10 @@
import com.android.tools.r8.naming.NamingLens;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.FileUtils;
-import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.OutputMode;
import com.android.tools.r8.utils.PackageDistribution;
import com.android.tools.r8.utils.ThreadUtils;
import com.google.common.collect.Iterators;
+import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.io.IOException;
import java.io.OutputStreamWriter;
@@ -55,6 +54,19 @@
public class VirtualFile {
+ // The fill strategy determine how to distribute classes into dex files.
+ enum FillStrategy {
+ // Only put classes matches by the main dex rules into the first dex file. Distribute remaining
+ // classes in additional dex files filling each dex file as much as possible.
+ MINIMAL_MAIN_DEX,
+ // Distribute classes in as few dex files as possible filling each dex file as much as possible.
+ FILL_MAX,
+ // Distribute classes keeping some space for future growth. This is mainly useful together with
+ // the package map distribution.
+ LEAVE_SPACE_FOR_GROWTH,
+ // TODO(sgjesse): Does "minimal main dex" combined with "leave space for growth" make sense?
+ }
+
private static final int MAX_ENTRIES = (Short.MAX_VALUE << 1) + 1;
/**
* When distributing classes across files we aim to leave some space. The amount of space left is
@@ -72,6 +84,10 @@
this.transaction = new IndexedItemTransaction(indexedItems, namingLens);
}
+ public int getId() {
+ return id;
+ }
+
public Set<String> getClassDescriptors() {
Set<String> classDescriptors = new HashSet<>();
for (DexProgramClass clazz : indexedItems.classes) {
@@ -81,119 +97,6 @@
return classDescriptors;
}
- public static Map<Integer, VirtualFile> fileSetFrom(
- ApplicationWriter writer,
- PackageDistribution packageDistribution,
- ExecutorService executorService)
- throws ExecutionException, IOException {
- // Strategy for distributing classes for write out:
- // 1. Place all files in the package distribution file in the proposed files (if any).
- // 2. Place the remaining files based on their packages in sorted order.
- DexApplication application = writer.application;
- InternalOptions options = writer.options;
- Map<Integer, VirtualFile> nameToFileMap = new LinkedHashMap<>();
-
- if (options.outputMode == OutputMode.FilePerClass) {
- assert packageDistribution == null :
- "Cannot combine package distribution definition with file-per-class option.";
- // Assign dedicated virtual files for all program classes.
- for (DexProgramClass clazz : application.classes()) {
- VirtualFile file = new VirtualFile(nameToFileMap.size(), writer.namingLens);
- nameToFileMap.put(nameToFileMap.size(), file);
- file.addClass(clazz);
- file.commitTransaction();
- }
- return nameToFileMap;
- }
-
- if (packageDistribution != null) {
- int maxReferencedIndex = packageDistribution.maxReferencedIndex();
- for (int index = 0; index <= maxReferencedIndex; index++) {
- VirtualFile file = new VirtualFile(index, writer.namingLens);
- nameToFileMap.put(index, file);
- }
- } else {
- // If we had no map we default to 1 file, the package populator will add more if needed.
- nameToFileMap.put(0, new VirtualFile(0, writer.namingLens));
- }
- Set<DexProgramClass> classes = Sets.newHashSet(application.classes());
-
- // Compute the original names.
- Map<DexProgramClass, String> originalNames = computeOriginalNameMapping(classes,
- application.getProguardMap());
-
- if (application.mainDexList != null) {
- VirtualFile mainDexFile = nameToFileMap.get(0);
- for (DexType type : application.mainDexList) {
- DexClass clazz = application.definitionFor(type);
- if (clazz != null && clazz.isProgramClass()) {
- DexProgramClass programClass = (DexProgramClass) clazz;
- mainDexFile.addClass(programClass);
- if (mainDexFile.isFull()) {
- throw new CompilationError("Cannot fit requested classes in main-dex file.");
- }
- classes.remove(programClass);
- } else {
- System.out.println(
- "WARNING: Application does not contain `"
- + type.toDescriptorString()
- + "` as referenced in main-dex-list.");
- }
- mainDexFile.commitTransaction();
- }
- }
-
- // Sort the classes based on the original names.
- // This with make classes from the same package be adjacent.
- classes = sortClassesByPackage(classes, originalNames);
-
- Set<String> usedPrefixes = null;
- if (packageDistribution != null) {
- ArrayList<Future<List<DexProgramClass>>> futures = new ArrayList<>(nameToFileMap.size());
- usedPrefixes = packageDistribution.getFiles();
- for (VirtualFile file : nameToFileMap.values()) {
- PackageMapPopulator populator =
- new PackageMapPopulator(file, classes, packageDistribution, originalNames);
- futures.add(executorService.submit(populator));
- }
- ThreadUtils.awaitFutures(futures).forEach(classes::removeAll);
- }
-
- // TODO(zerny): Add the package map to AndroidApp and refactor its generation.
- Path newPackageMap = Paths.get("package.map");
- Map<String, Integer> newAssignments;
- if (classes.isEmpty()) {
- newAssignments = Collections.emptyMap();
- } else {
- newAssignments =
- new PackageSplitPopulator(
- nameToFileMap, classes, originalNames, usedPrefixes, application.dexItemFactory,
- options, writer.namingLens)
- .call();
- if (!newAssignments.isEmpty() && nameToFileMap.size() > 1) {
- if (packageDistribution == null) {
- System.out.println(" * Consider using a package map to improve patch sizes.");
- } else {
- System.err.println(" * The used package map is missing entries. The following default "
- + "mappings have been used:");
- Writer output = new OutputStreamWriter(System.err);
- for (Entry<String, Integer> entry : newAssignments.entrySet()) {
- output.write(" ");
- PackageDistribution.formatEntry(entry, output);
- output.write("\n");
- }
- output.flush();
- System.err.println(" * Consider updating the map.");
- }
- }
- }
- if (packageDistribution != null || nameToFileMap.size() > 1) {
- System.out.println(" - " + newPackageMap.toString());
- PackageDistribution.writePackageToFileMap(newPackageMap, newAssignments, packageDistribution);
- }
- return nameToFileMap;
- }
-
public static String deriveCommonPrefixAndSanityCheck(List<String> fileNames) {
Iterator<String> nameIterator = fileNames.iterator();
String first = nameIterator.next();
@@ -228,38 +131,6 @@
return originalNames;
}
- private static TreeSet<DexProgramClass> sortClassesByPackage(Set<DexProgramClass> classes,
- Map<DexProgramClass, String> originalNames) {
- TreeSet<DexProgramClass> sortedClasses = new TreeSet<>(
- (DexProgramClass a, DexProgramClass b) -> {
- String originalA = originalNames.get(a);
- String originalB = originalNames.get(b);
- int indexA = originalA.lastIndexOf('.');
- int indexB = originalB.lastIndexOf('.');
- if (indexA == -1 && indexB == -1) {
- // Empty package, compare the class names.
- return originalA.compareTo(originalB);
- }
- if (indexA == -1) {
- // Empty package name comes first.
- return -1;
- }
- if (indexB == -1) {
- // Empty package name comes first.
- return 1;
- }
- String prefixA = originalA.substring(0, indexA);
- String prefixB = originalB.substring(0, indexB);
- int result = prefixA.compareTo(prefixB);
- if (result != 0) {
- return result;
- }
- return originalA.compareTo(originalB);
- });
- sortedClasses.addAll(classes);
- return sortedClasses;
- }
-
private static String extractPrefixToken(int prefixLength, String className, boolean addStar) {
int index = 0;
int lastIndex = 0;
@@ -303,11 +174,11 @@
return isFull(transaction.getNumberOfMethods(), transaction.getNumberOfFields(), MAX_ENTRIES);
}
- private boolean isFilledEnough(InternalOptions options) {
+ private boolean isFilledEnough(FillStrategy fillStrategy) {
return isFull(
transaction.getNumberOfMethods(),
transaction.getNumberOfFields(),
- options.fillDexFiles ? MAX_ENTRIES : MAX_PREFILL_ENTRIES);
+ fillStrategy == FillStrategy.FILL_MAX ? MAX_ENTRIES : MAX_PREFILL_ENTRIES);
}
public void abortTransaction() {
@@ -326,6 +197,240 @@
return indexedItems.classes;
}
+ public abstract static class Distributor {
+ protected final DexApplication application;
+ protected final ApplicationWriter writer;
+ protected final Map<Integer, VirtualFile> nameToFileMap = new HashMap<>();
+
+ public Distributor(ApplicationWriter writer) {
+ this.application = writer.application;
+ this.writer = writer;
+ }
+
+ public abstract Map<Integer, VirtualFile> run() throws ExecutionException, IOException;
+ }
+
+ public static class FilePerClassDistributor extends Distributor {
+
+ public FilePerClassDistributor(ApplicationWriter writer) {
+ super(writer);
+ }
+
+ public Map<Integer, VirtualFile> run() throws ExecutionException, IOException {
+ // Assign dedicated virtual files for all program classes.
+ for (DexProgramClass clazz : application.classes()) {
+ VirtualFile file = new VirtualFile(nameToFileMap.size(), writer.namingLens);
+ nameToFileMap.put(nameToFileMap.size(), file);
+ file.addClass(clazz);
+ file.commitTransaction();
+ }
+ return nameToFileMap;
+ }
+ }
+
+ public abstract static class DistributorBase extends Distributor {
+ protected Set<DexProgramClass> classes;
+ protected Map<DexProgramClass, String> originalNames;
+
+ public DistributorBase(ApplicationWriter writer) {
+ super(writer);
+
+ classes = Sets.newHashSet(application.classes());
+ originalNames = computeOriginalNameMapping(classes, application.getProguardMap());
+ }
+
+ protected void fillForMainDexList(Set<DexProgramClass> classes) {
+ if (application.mainDexList != null) {
+ VirtualFile mainDexFile = nameToFileMap.get(0);
+ for (DexType type : application.mainDexList) {
+ DexClass clazz = application.definitionFor(type);
+ if (clazz != null && clazz.isProgramClass()) {
+ DexProgramClass programClass = (DexProgramClass) clazz;
+ mainDexFile.addClass(programClass);
+ if (mainDexFile.isFull()) {
+ throw new CompilationError("Cannot fit requested classes in main-dex file.");
+ }
+ classes.remove(programClass);
+ } else {
+ System.out.println(
+ "WARNING: Application does not contain `"
+ + type.toSourceString()
+ + "` as referenced in main-dex-list.");
+ }
+ mainDexFile.commitTransaction();
+ }
+ }
+ }
+
+ TreeSet<DexProgramClass> sortClassesByPackage(Set<DexProgramClass> classes,
+ Map<DexProgramClass, String> originalNames) {
+ TreeSet<DexProgramClass> sortedClasses = new TreeSet<>(
+ (DexProgramClass a, DexProgramClass b) -> {
+ String originalA = originalNames.get(a);
+ String originalB = originalNames.get(b);
+ int indexA = originalA.lastIndexOf('.');
+ int indexB = originalB.lastIndexOf('.');
+ if (indexA == -1 && indexB == -1) {
+ // Empty package, compare the class names.
+ return originalA.compareTo(originalB);
+ }
+ if (indexA == -1) {
+ // Empty package name comes first.
+ return -1;
+ }
+ if (indexB == -1) {
+ // Empty package name comes first.
+ return 1;
+ }
+ String prefixA = originalA.substring(0, indexA);
+ String prefixB = originalB.substring(0, indexB);
+ int result = prefixA.compareTo(prefixB);
+ if (result != 0) {
+ return result;
+ }
+ return originalA.compareTo(originalB);
+ });
+ sortedClasses.addAll(classes);
+ return sortedClasses;
+ }
+ }
+
+ public static class FillFilesDistributor extends DistributorBase {
+ private final FillStrategy fillStrategy;
+
+ public FillFilesDistributor(ApplicationWriter writer, boolean minimalMainDex) {
+ super(writer);
+ this.fillStrategy = minimalMainDex ? FillStrategy.MINIMAL_MAIN_DEX : FillStrategy.FILL_MAX;
+ }
+
+ public Map<Integer, VirtualFile> run() throws ExecutionException, IOException {
+ // Strategy for distributing classes for write out:
+ // 1. Place the remaining files based on their packages in sorted order.
+
+ // Start with 1 file. The package populator will add more if needed.
+ nameToFileMap.put(0, new VirtualFile(0, writer.namingLens));
+
+ // First fill required classes into the main dex file.
+ fillForMainDexList(classes);
+
+ // Sort the remaining classes based on the original names.
+ // This with make classes from the same package be adjacent.
+ classes = sortClassesByPackage(classes, originalNames);
+
+ new PackageSplitPopulator(
+ nameToFileMap, classes, originalNames, null, application.dexItemFactory,
+ fillStrategy, writer.namingLens)
+ .call();
+ return nameToFileMap;
+ }
+ }
+
+ public static class MonoDexDistributor extends DistributorBase {
+ public MonoDexDistributor(ApplicationWriter writer) {
+ super(writer);
+ }
+
+ @Override
+ public Map<Integer, VirtualFile> run() throws ExecutionException, IOException {
+ VirtualFile mainDexFile = new VirtualFile(0, writer.namingLens);
+ nameToFileMap.put(0, mainDexFile);
+
+ for (DexProgramClass programClass : classes) {
+ mainDexFile.addClass(programClass);
+ if (mainDexFile.isFull()) {
+ throw new CompilationError("Cannot fit all classes in a single dex file.");
+ }
+ }
+ mainDexFile.commitTransaction();
+ return nameToFileMap;
+ }
+ }
+
+ public static class PackageMapDistributor extends DistributorBase {
+ private final PackageDistribution packageDistribution;
+ private final ExecutorService executorService;
+
+ public PackageMapDistributor(
+ ApplicationWriter writer,
+ PackageDistribution packageDistribution,
+ ExecutorService executorService) {
+ super(writer);
+ this.packageDistribution = packageDistribution;
+ this.executorService = executorService;
+ }
+
+ public Map<Integer, VirtualFile> run() throws ExecutionException, IOException {
+ // Strategy for distributing classes for write out:
+ // 1. Place all files in the package distribution file in the proposed files (if any).
+ // 2. Place the remaining files based on their packages in sorted order.
+
+ int maxReferencedIndex = packageDistribution.maxReferencedIndex();
+ for (int index = 0; index <= maxReferencedIndex; index++) {
+ VirtualFile file = new VirtualFile(index, writer.namingLens);
+ nameToFileMap.put(index, file);
+ }
+
+ // First fill required classes into the main dex file.
+ fillForMainDexList(classes);
+
+ // Sort the remaining classes based on the original names.
+ // This with make classes from the same package be adjacent.
+ classes = sortClassesByPackage(classes, originalNames);
+
+ Set<String> usedPrefixes = fillForDistribution(classes, originalNames);
+
+ // TODO(zerny): Add the package map to AndroidApp and refactor its generation.
+ Map<String, Integer> newAssignments;
+ if (classes.isEmpty()) {
+ newAssignments = Collections.emptyMap();
+ } else {
+ newAssignments =
+ new PackageSplitPopulator(
+ nameToFileMap, classes, originalNames, usedPrefixes, application.dexItemFactory,
+ FillStrategy.LEAVE_SPACE_FOR_GROWTH, writer.namingLens)
+ .call();
+ if (!newAssignments.isEmpty() && nameToFileMap.size() > 1) {
+ System.err.println(" * The used package map is missing entries. The following default "
+ + "mappings have been used:");
+ writeAssignments(newAssignments, new OutputStreamWriter(System.err));
+ System.err.println(" * Consider updating the map.");
+ }
+ }
+
+ Path newPackageMap = Paths.get("package.map");
+ System.out.println(" - " + newPackageMap.toString());
+ PackageDistribution.writePackageToFileMap(newPackageMap, newAssignments, packageDistribution);
+
+ return nameToFileMap;
+ }
+
+ private Set<String> fillForDistribution(Set<DexProgramClass> classes,
+ Map<DexProgramClass, String> originalNames) throws ExecutionException {
+ Set<String> usedPrefixes = null;
+ if (packageDistribution != null) {
+ ArrayList<Future<List<DexProgramClass>>> futures = new ArrayList<>(nameToFileMap.size());
+ usedPrefixes = packageDistribution.getFiles();
+ for (VirtualFile file : nameToFileMap.values()) {
+ PackageMapPopulator populator =
+ new PackageMapPopulator(file, classes, packageDistribution, originalNames);
+ futures.add(executorService.submit(populator));
+ }
+ ThreadUtils.awaitFutures(futures).forEach(classes::removeAll);
+ }
+ return usedPrefixes;
+ }
+
+ private void writeAssignments(Map<String, Integer> assignments, Writer output)
+ throws IOException{
+ for (Entry<String, Integer> entry : assignments.entrySet()) {
+ output.write(" ");
+ PackageDistribution.formatEntry(entry, output);
+ output.write("\n");
+ }
+ output.flush();
+ }
+ }
+
private static class VirtualFileIndexedItemCollection implements IndexedItemCollection {
final int id;
@@ -575,6 +680,7 @@
this.originalNames = originalNames;
}
+ @Override
public List<DexProgramClass> call() {
String currentPrefix = null;
int currentFileId = -1;
@@ -598,9 +704,9 @@
inserted.add(clazz);
}
if (file.isFull()) {
- throw new RuntimeException(
+ throw new CompilationError(
"Cannot fit package " + currentPrefix
- + " in requested dex file, consider to remove mapping.");
+ + " in requested dex file, consider removing mapping.");
}
}
file.commitTransaction();
@@ -643,6 +749,74 @@
}
/**
+ * Helper class to cycle through the set of virtual files.
+ *
+ * Iteration starts at the first file and iterates through all files.
+ *
+ * When {@link VirtualFileCycler#restart()} is called iteration of all files is restarted at the
+ * current file.
+ *
+ * If the fill strategy indicate that the main dex file should be minimal, then the main dex file
+ * will not be part of the iteration.
+ */
+ private static class VirtualFileCycler {
+ private Map<Integer, VirtualFile> files;
+ private final NamingLens namingLens;
+ private final FillStrategy fillStrategy;
+
+ private int nextFileId;
+ private Iterator<VirtualFile> allFilesCyclic;
+ private Iterator<VirtualFile> activeFiles;
+
+ VirtualFileCycler(Map<Integer, VirtualFile> files, NamingLens namingLens,
+ FillStrategy fillStrategy) {
+ this.files = files;
+ this.namingLens = namingLens;
+ this.fillStrategy = fillStrategy;
+
+ nextFileId = files.size();
+ if (fillStrategy == FillStrategy.MINIMAL_MAIN_DEX) {
+ // The main dex file is filtered out, so ensure at least one file for the remaining
+ // classes
+ files.put(nextFileId, new VirtualFile(nextFileId, namingLens));
+ this.files = Maps.filterKeys(files, key -> key != 0);
+ nextFileId++;
+ }
+
+ reset();
+ }
+
+ private void reset() {
+ allFilesCyclic = Iterators.cycle(files.values());
+ restart();
+ }
+
+ boolean hasNext() {
+ return activeFiles.hasNext();
+ }
+
+ VirtualFile next() {
+ VirtualFile next = activeFiles.next();
+ assert fillStrategy != FillStrategy.MINIMAL_MAIN_DEX || next.getId() != 0;
+ return next;
+ }
+
+ // Start a new iteration over all files, starting at the current one.
+ void restart() {
+ activeFiles = Iterators.limit(allFilesCyclic, files.size());
+ }
+
+ VirtualFile addFile() {
+ VirtualFile newFile = new VirtualFile(nextFileId, namingLens);
+ files.put(nextFileId, newFile);
+ nextFileId++;
+
+ reset();
+ return newFile;
+ }
+ }
+
+ /**
* Distributes the given classes over the files in package order.
*
* <p>The populator avoids package splits. Big packages are split into subpackages if their size
@@ -666,13 +840,12 @@
*/
private static final int MIN_FILL_FACTOR = 5;
- private final Map<Integer, VirtualFile> files;
private final List<DexProgramClass> classes;
private final Map<DexProgramClass, String> originalNames;
private final Set<String> previousPrefixes;
private final DexItemFactory dexItemFactory;
- private final InternalOptions options;
- private final NamingLens namingLens;
+ private final FillStrategy fillStrategy;
+ private final VirtualFileCycler cycler;
PackageSplitPopulator(
Map<Integer, VirtualFile> files,
@@ -680,30 +853,28 @@
Map<DexProgramClass, String> originalNames,
Set<String> previousPrefixes,
DexItemFactory dexItemFactory,
- InternalOptions options,
+ FillStrategy fillStrategy,
NamingLens namingLens) {
- this.files = files;
this.classes = new ArrayList<>(classes);
this.originalNames = originalNames;
this.previousPrefixes = previousPrefixes;
this.dexItemFactory = dexItemFactory;
- this.options = options;
- this.namingLens = namingLens;
+ this.fillStrategy = fillStrategy;
+ this.cycler = new VirtualFileCycler(files, namingLens, fillStrategy);
}
private String getOriginalName(DexProgramClass clazz) {
return originalNames != null ? originalNames.get(clazz) : clazz.toString();
}
+ @Override
public Map<String, Integer> call() throws IOException {
- Iterator<VirtualFile> allFilesCyclic = Iterators.cycle(files.values());
- Iterator<VirtualFile> activeFiles = Iterators.limit(allFilesCyclic, files.size());
int prefixLength = MINIMUM_PREFIX_LENGTH;
int transactionStartIndex = 0;
int fileStartIndex = 0;
String currentPrefix = null;
Map<String, Integer> newPackageAssignments = new LinkedHashMap<>();
- VirtualFile current = activeFiles.next();
+ VirtualFile current = cycler.next();
List<DexProgramClass> nonPackageClasses = new ArrayList<>();
for (int classIndex = 0; classIndex < classes.size(); classIndex++) {
DexProgramClass clazz = classes.get(classIndex);
@@ -711,8 +882,8 @@
if (!PackageMapPopulator.coveredByPrefix(originalName, currentPrefix)) {
if (currentPrefix != null) {
current.commitTransaction();
- // Reset the iterator to again iterate over all files, starting with the current one.
- activeFiles = Iterators.limit(allFilesCyclic, files.size());
+ // Reset the cycler to again iterate over all files, starting with the current one.
+ cycler.restart();
assert !newPackageAssignments.containsKey(currentPrefix);
newPackageAssignments.put(currentPrefix, current.id);
// Try to reduce the prefix length if possible. Only do this on a successful commit.
@@ -750,7 +921,7 @@
nonPackageClasses.add(clazz);
continue;
}
- if (current.isFilledEnough(options) || current.isFull()) {
+ if (current.isFilledEnough(fillStrategy) || current.isFull()) {
current.abortTransaction();
// We allow for a final rollback that has at most 20% of classes in it.
// This is a somewhat random number that was empirically chosen.
@@ -762,7 +933,7 @@
// The idea is that we do not increase the number of files, so it has to fit
// somewhere.
fileStartIndex = transactionStartIndex;
- if (!activeFiles.hasNext()) {
+ if (!cycler.hasNext()) {
// Special case where we simply will never be able to fit the current package into
// one dex file. This is currently the case for Strings in jumbo tests, see:
// b/33227518
@@ -773,11 +944,9 @@
transactionStartIndex = classIndex + 1;
}
// All files are filled up to the 20% mark.
- files.put(files.size(), new VirtualFile(files.size(), namingLens));
- allFilesCyclic = Iterators.cycle(files.values());
- activeFiles = Iterators.limit(allFilesCyclic, files.size());
+ cycler.addFile();
}
- current = activeFiles.next();
+ current = cycler.next();
}
currentPrefix = null;
// Go back to previous start index.
@@ -791,24 +960,25 @@
newPackageAssignments.put(currentPrefix, current.id);
}
if (nonPackageClasses.size() > 0) {
- addNonPackageClasses(Iterators.limit(allFilesCyclic, files.size()), nonPackageClasses);
+ addNonPackageClasses(cycler, nonPackageClasses);
}
return newPackageAssignments;
}
- private void addNonPackageClasses(Iterator<VirtualFile> activeFiles,
- List<DexProgramClass> nonPackageClasses) throws IOException {
+ private void addNonPackageClasses(
+ VirtualFileCycler cycler, List<DexProgramClass> nonPackageClasses) {
+ cycler.restart();
VirtualFile current;
- current = activeFiles.next();
+ current = cycler.next();
for (DexProgramClass clazz : nonPackageClasses) {
- if (current.isFilledEnough(options)) {
- current = getVirtualFile(activeFiles);
+ if (current.isFilledEnough(fillStrategy)) {
+ current = getVirtualFile(cycler);
}
current.addClass(clazz);
while (current.isFull()) {
// This only happens if we have a huge class, that takes up more than 20% of a dex file.
current.abortTransaction();
- current = getVirtualFile(activeFiles);
+ current = getVirtualFile(cycler);
boolean wasEmpty = current.isEmpty();
current.addClass(clazz);
if (wasEmpty && current.isFull()) {
@@ -820,12 +990,12 @@
}
}
- private VirtualFile getVirtualFile(Iterator<VirtualFile> activeFiles) throws IOException {
+ private VirtualFile getVirtualFile(VirtualFileCycler cycler) {
VirtualFile current = null;
- while (activeFiles.hasNext() && (current = activeFiles.next()).isFilledEnough(options)) {}
- if (current == null || current.isFilledEnough(options)) {
- current = new VirtualFile(files.size(), namingLens);
- files.put(files.size(), current);
+ while (cycler.hasNext()
+ && (current = cycler.next()).isFilledEnough(fillStrategy)) {}
+ if (current == null || current.isFilledEnough(fillStrategy)) {
+ current = cycler.addFile();
}
return current;
}
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 a558b3e..a5c8ff8 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfo.java
@@ -40,9 +40,8 @@
private Map<Descriptor, KeyedDexItem> computeDefinitions(DexType type) {
Builder<Descriptor, KeyedDexItem> builder = ImmutableMap.builder();
- DexClassPromise promise = app.definitionFor(type);
- if (promise != null) {
- DexClass clazz = promise.get();
+ DexClass clazz = app.definitionFor(type);
+ if (clazz != null) {
registerDefinitions(builder, clazz.directMethods());
registerDefinitions(builder, clazz.virtualMethods());
registerDefinitions(builder, clazz.instanceFields());
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java b/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
index 9330fdd..9e3e118 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
@@ -21,7 +21,7 @@
public AppInfoWithSubtyping(DexApplication application) {
super(application);
- populateSubtypeMap(application.getClassMap(), application.dexItemFactory);
+ populateSubtypeMap(application.getFullClassMap(), application.dexItemFactory);
}
protected AppInfoWithSubtyping(AppInfoWithSubtyping previous) {
@@ -33,7 +33,7 @@
protected AppInfoWithSubtyping(AppInfoWithSubtyping previous, GraphLense lense) {
super(previous, lense);
// Recompute subtype map if we have modified the graph.
- populateSubtypeMap(previous.app.getClassMap(), dexItemFactory);
+ populateSubtypeMap(previous.app.getFullClassMap(), dexItemFactory);
}
public Set<DexType> getMissingClasses() {
@@ -84,22 +84,17 @@
}
}
- private void populateSubtypeMap(
- Map<DexType, DexClassPromise> classes, DexItemFactory dexItemFactory) {
+ private void populateSubtypeMap(Map<DexType, DexClass> classes, DexItemFactory dexItemFactory) {
dexItemFactory.clearSubtypeInformation();
dexItemFactory.objectType.tagAsSubtypeRoot();
Hashtable<DexType, Set<DexType>> map = new Hashtable<>();
- Function<DexType, DexClass> typeToClass = type -> {
- DexClassPromise promise = classes.get(type);
- return promise == null ? null : promise.get();
- };
- for (Map.Entry<DexType, DexClassPromise> entry : classes.entrySet()) {
- populateAllSuperTypes(map, entry.getKey(), entry.getValue().get(), typeToClass);
+ for (Map.Entry<DexType, DexClass> entry : classes.entrySet()) {
+ populateAllSuperTypes(map, entry.getKey(), entry.getValue(), classes::get);
}
for (Map.Entry<DexType, Set<DexType>> entry : map.entrySet()) {
subtypeMap.put(entry.getKey(), ImmutableSet.copyOf(entry.getValue()));
}
- assert DexType.validateLevelsAreCorrect(typeToClass, dexItemFactory);
+ assert DexType.validateLevelsAreCorrect(classes::get, dexItemFactory);
}
// For mapping invoke virtual instruction to target methods.
diff --git a/src/main/java/com/android/tools/r8/graph/ClassKind.java b/src/main/java/com/android/tools/r8/graph/ClassKind.java
index 6f53a40..9298ba5 100644
--- a/src/main/java/com/android/tools/r8/graph/ClassKind.java
+++ b/src/main/java/com/android/tools/r8/graph/ClassKind.java
@@ -1,12 +1,14 @@
package com.android.tools.r8.graph;
import com.android.tools.r8.Resource;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
/** Kind of the application class. Can be program, classpath or library. */
public enum ClassKind {
- PROGRAM(DexProgramClass::new),
- CLASSPATH(DexClasspathClass::new),
- LIBRARY(DexLibraryClass::new);
+ PROGRAM(DexProgramClass::new, DexClass::isProgramClass),
+ CLASSPATH(DexClasspathClass::new, DexClass::isClasspathClass),
+ LIBRARY(DexLibraryClass::new, DexClass::isLibraryClass);
private interface Factory {
DexClass create(DexType type, Resource.Kind origin, DexAccessFlags accessFlags,
@@ -17,9 +19,11 @@
}
private final Factory factory;
+ private final Predicate<DexClass> check;
- ClassKind(Factory factory) {
+ ClassKind(Factory factory, Predicate<DexClass> check) {
this.factory = factory;
+ this.check = check;
}
public DexClass create(
@@ -30,4 +34,16 @@
return factory.create(type, origin, accessFlags, superType, interfaces, sourceFile,
annotations, staticFields, instanceFields, directMethods, virtualMethods);
}
+
+ public boolean isOfKind(DexClass clazz) {
+ return check.test(clazz);
+ }
+
+ public <T extends DexClass> Consumer<DexClass> bridgeConsumer(Consumer<T> consumer) {
+ return clazz -> {
+ assert isOfKind(clazz);
+ @SuppressWarnings("unchecked") T specialized = (T) clazz;
+ consumer.accept(specialized);
+ };
+ }
}
diff --git a/src/main/java/com/android/tools/r8/graph/Code.java b/src/main/java/com/android/tools/r8/graph/Code.java
index 04c01be..dca5e02 100644
--- a/src/main/java/com/android/tools/r8/graph/Code.java
+++ b/src/main/java/com/android/tools/r8/graph/Code.java
@@ -19,7 +19,7 @@
public abstract String toString();
- public abstract String toString(ClassNameMapper naming);
+ public abstract String toString(DexEncodedMethod method, ClassNameMapper naming);
public boolean isDexCode() {
return false;
diff --git a/src/main/java/com/android/tools/r8/graph/DexApplication.java b/src/main/java/com/android/tools/r8/graph/DexApplication.java
index acbc205..eea0169 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplication.java
@@ -6,16 +6,13 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.graph;
-import com.android.tools.r8.Resource;
-import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.ir.desugar.LambdaRewriter;
-import com.android.tools.r8.logging.Log;
import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.utils.ClasspathClassCollection;
import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.LazyClassCollection;
+import com.android.tools.r8.utils.LibraryClassCollection;
+import com.android.tools.r8.utils.ProgramClassCollection;
import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.Timing;
-import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import java.io.ByteArrayOutputStream;
@@ -26,25 +23,20 @@
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.Comparator;
import java.util.Hashtable;
+import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class DexApplication {
- // Maps type into class promise, may be used concurrently.
- private final ImmutableMap<DexType, DexClassPromise> classMap;
-
- // Lazily loaded classes.
- //
- // Note that this collection is autonomous and may be used in several
- // different applications. Particularly, it is the case when one
- // application is being build based on another one. Among others,
- // it will have an important side-effect: class conflict resolution,
- // generated errors in particular, may be different in lazy scenario.
- private final LazyClassCollection lazyClassCollection;
+ // Maps type into class, may be used concurrently.
+ private ProgramClassCollection programClasses;
+ private ClasspathClassCollection classpathClasses;
+ private LibraryClassCollection libraryClasses;
public final ImmutableSet<DexType> mainDexList;
@@ -60,67 +52,97 @@
/** Constructor should only be invoked by the DexApplication.Builder. */
private DexApplication(
ClassNameMapper proguardMap,
- ImmutableMap<DexType, DexClassPromise> classMap,
- LazyClassCollection lazyClassCollection,
+ ProgramClassCollection programClasses,
+ ClasspathClassCollection classpathClasses,
+ LibraryClassCollection libraryClasses,
ImmutableSet<DexType> mainDexList,
DexItemFactory dexItemFactory,
DexString highestSortingString,
Timing timing) {
+ assert programClasses != null;
this.proguardMap = proguardMap;
- this.lazyClassCollection = lazyClassCollection;
+ this.programClasses = programClasses;
+ this.classpathClasses = classpathClasses;
+ this.libraryClasses = libraryClasses;
this.mainDexList = mainDexList;
- this.classMap = classMap;
this.dexItemFactory = dexItemFactory;
this.highestSortingString = highestSortingString;
this.timing = timing;
}
- ImmutableMap<DexType, DexClassPromise> getClassMap() {
- assert lazyClassCollection == null : "Only allowed in non-lazy scenarios.";
- return classMap;
+ /** Force load all classes and return type -> class map containing all the classes */
+ public Map<DexType, DexClass> getFullClassMap() {
+ return forceLoadAllClasses();
}
- public Iterable<DexProgramClass> classes() {
- List<DexProgramClass> result = new ArrayList<>();
- // Note: we ignore lazy class collection because it
- // is never supposed to be used for program classes.
- for (DexClassPromise promise : classMap.values()) {
- if (promise.isProgramClass()) {
- result.add(promise.get().asProgramClass());
- }
- }
- return result;
+ // Reorder classes randomly. Note that the order of classes in program or library
+ // class collections should not matter for compilation of valid code and when running
+ // with assertions enabled we reorder the classes randomly to catch possible issues.
+ // Also note that the order may add to non-determinism in reporting errors for invalid
+ // code, but this non-determinism exists even with the same order of classes since we
+ // may process classes concurrently and fail-fast on the first error.
+ private <T> boolean reorderClasses(List<T> classes) {
+ Collections.shuffle(classes);
+ return true;
}
- public Iterable<DexLibraryClass> libraryClasses() {
- assert lazyClassCollection == null : "Only allowed in non-lazy scenarios.";
- List<DexLibraryClass> result = new ArrayList<>();
- for (DexClassPromise promise : classMap.values()) {
- if (promise.isLibraryClass()) {
- result.add(promise.get().asLibraryClass());
+ public List<DexProgramClass> classes() {
+ List<DexProgramClass> classes = programClasses.collectLoadedClasses();
+ assert reorderClasses(classes);
+ return classes;
+ }
+
+ public List<DexLibraryClass> libraryClasses() {
+ assert classpathClasses == null : "Operation is not supported.";
+ Map<DexType, DexClass> classMap = forceLoadAllClasses();
+ List<DexLibraryClass> classes = new ArrayList<>();
+ for (DexClass clazz : classMap.values()) {
+ if (clazz.isLibraryClass()) {
+ classes.add(clazz.asLibraryClass());
}
}
- return result;
+ assert reorderClasses(classes);
+ return classes;
+ }
+
+ private Map<DexType, DexClass> forceLoadAllClasses() {
+ Map<DexType, DexClass> loaded = new IdentityHashMap<>();
+
+ // program classes are supposed to be loaded, but force-loading them is no-op.
+ programClasses.forceLoad(type -> true);
+ programClasses.collectLoadedClasses().forEach(clazz -> loaded.put(clazz.type, clazz));
+
+ if (classpathClasses != null) {
+ classpathClasses.forceLoad(type -> !loaded.containsKey(type));
+ classpathClasses.collectLoadedClasses().forEach(clazz -> loaded.put(clazz.type, clazz));
+ }
+
+ if (libraryClasses != null) {
+ libraryClasses.forceLoad(type -> !loaded.containsKey(type));
+ libraryClasses.collectLoadedClasses().forEach(clazz -> loaded.put(clazz.type, clazz));
+ }
+
+ return loaded;
}
public DexClass definitionFor(DexType type) {
- DexClassPromise promise = classMap.get(type);
- // In presence of lazy class collection we also reach out to it
- // as well unless the class found is already a program class.
- if (lazyClassCollection != null && (promise == null || !promise.isProgramClass())) {
- promise = lazyClassCollection.get(type, promise);
+ DexClass clazz = programClasses.get(type);
+ if (clazz == null && classpathClasses != null) {
+ clazz = classpathClasses.get(type);
}
- return promise == null ? null : promise.get();
+ if (clazz == null && libraryClasses != null) {
+ clazz = libraryClasses.get(type);
+ }
+ return clazz;
}
public DexProgramClass programDefinitionFor(DexType type) {
- DexClassPromise promise = classMap.get(type);
- // Don't bother about lazy class collection, it should never load program classes.
- return (promise == null || !promise.isProgramClass()) ? null : promise.get().asProgramClass();
+ DexClass clazz = programClasses.get(type);
+ return clazz == null ? null : clazz.asProgramClass();
}
public String toString() {
- return "Application (classes #" + classMap.size() + ")";
+ return "Application (" + programClasses + "; " + classpathClasses + "; " + libraryClasses + ")";
}
public ClassNameMapper getProguardMap() {
@@ -152,7 +174,7 @@
ps.println("Bytecode for");
ps.println("Class: '" + clazzName + "'");
ps.println("Method: '" + methodName + "':");
- ps.println(method.getCode().toString(naming));
+ ps.println(method.getCode().toString(method, naming));
} catch (IOException e) {
e.printStackTrace();
} finally {
@@ -170,7 +192,7 @@
* <p>If no directory is provided everything is written to System.out.
*/
public void disassemble(Path outputDir, InternalOptions options) {
- for (DexClass clazz : classes()) {
+ for (DexProgramClass clazz : programClasses.collectLoadedClasses()) {
for (DexEncodedMethod method : clazz.virtualMethods()) {
if (options.methodMatchesFilter(method)) {
disassemble(method, getProguardMap(), outputDir);
@@ -202,21 +224,48 @@
if (clazz.type != dexItemFactory.objectType) {
builder.append(".super ");
builder.append(clazz.superType.toSmaliString());
- // TODO(sgjesse): Add implemented interfaces
builder.append("\n");
+ for (DexType iface : clazz.interfaces.values) {
+ builder.append(".implements ");
+ builder.append(iface.toSmaliString());
+ builder.append("\n");
+ }
}
ps.append(builder.toString());
}
- /** Write smali source for the application code on the provided PrintStream. */
+ private void writeClassFooter(DexClass clazz, PrintStream ps) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("# End of class ");
+ builder.append(clazz.type.toSmaliString());
+ builder.append("\n");
+ ps.append(builder.toString());
+ }
+
+ /**
+ * Write smali source for the application code on the provided PrintStream.
+ */
public void smali(InternalOptions options, PrintStream ps) {
- List<DexProgramClass> classes = (List<DexProgramClass>) classes();
+ List<DexProgramClass> classes = programClasses.collectLoadedClasses();
classes.sort(Comparator.comparing(DexProgramClass::toSourceString));
+ boolean firstClass = true;
for (DexClass clazz : classes) {
boolean classHeaderWritten = false;
+ if (!options.hasMethodsFilter()) {
+ if (!firstClass) {
+ ps.append("\n");
+ firstClass = false;
+ }
+ writeClassHeader(clazz, ps);
+ classHeaderWritten = true;
+ }
for (DexEncodedMethod method : clazz.virtualMethods()) {
if (options.methodMatchesFilter(method)) {
if (!classHeaderWritten) {
+ if (!firstClass) {
+ ps.append("\n");
+ firstClass = false;
+ }
writeClassHeader(clazz, ps);
classHeaderWritten = true;
}
@@ -227,6 +276,10 @@
for (DexEncodedMethod method : clazz.directMethods()) {
if (options.methodMatchesFilter(method)) {
if (!classHeaderWritten) {
+ if (!firstClass) {
+ ps.append("\n");
+ firstClass = false;
+ }
writeClassHeader(clazz, ps);
classHeaderWritten = true;
}
@@ -234,13 +287,24 @@
ps.append(method.toSmaliString(getProguardMap()));
}
}
+ if (classHeaderWritten) {
+ ps.append("\n");
+ writeClassFooter(clazz, ps);
+ }
}
}
public static class Builder {
+ // We handle program class collection separately from classpath
+ // and library class collections. Since while we assume program
+ // class collection should always be fully loaded and thus fully
+ // represented by the map (making it easy, for example, adding
+ // new or removing existing classes), classpath and library
+ // collections will be considered monolithic collections.
- private final Hashtable<DexType, DexClassPromise> classMap = new Hashtable<>();
- private LazyClassCollection lazyClassCollection;
+ private final List<DexProgramClass> programClasses;
+ private ClasspathClassCollection classpathClasses;
+ private LibraryClassCollection libraryClasses;
public final Hashtable<DexCode, DexCode> codeItems = new Hashtable<>();
@@ -252,18 +316,17 @@
private final Set<DexType> mainDexList = Sets.newIdentityHashSet();
public Builder(DexItemFactory dexItemFactory, Timing timing) {
+ this.programClasses = new ArrayList<>();
this.dexItemFactory = dexItemFactory;
this.timing = timing;
- this.lazyClassCollection = null;
+ this.classpathClasses = null;
+ this.libraryClasses = null;
}
public Builder(DexApplication application) {
- this(application, application.classMap);
- }
-
- public Builder(DexApplication application, Map<DexType, DexClassPromise> classMap) {
- this.classMap.putAll(classMap);
- this.lazyClassCollection = application.lazyClassCollection;
+ programClasses = application.programClasses.collectLoadedClasses();
+ classpathClasses = application.classpathClasses;
+ libraryClasses = application.libraryClasses;
proguardMap = application.proguardMap;
timing = application.timing;
highestSortingString = application.highestSortingString;
@@ -277,62 +340,45 @@
return this;
}
+ public synchronized Builder replaceProgramClasses(List<DexProgramClass> newProgramClasses) {
+ assert newProgramClasses != null;
+ this.programClasses.clear();
+ this.programClasses.addAll(newProgramClasses);
+ return this;
+ }
+
public synchronized Builder setHighestSortingString(DexString value) {
highestSortingString = value;
return this;
}
- public Builder addClassPromise(DexClassPromise promise) {
- addClassPromise(promise, false);
+ public synchronized Builder addProgramClass(DexProgramClass clazz) {
+ programClasses.add(clazz);
return this;
}
- public Builder setLazyClassCollection(LazyClassCollection lazyClassMap) {
- this.lazyClassCollection = lazyClassMap;
+ public Builder setClasspathClassCollection(ClasspathClassCollection classes) {
+ this.classpathClasses = classes;
return this;
}
- public Builder addClassIgnoringLibraryDuplicates(DexClass clazz) {
- addClass(clazz, true);
+ public Builder setLibraryClassCollection(LibraryClassCollection classes) {
+ this.libraryClasses = classes;
return this;
}
- public Builder addSynthesizedClass(DexProgramClass synthesizedClass, boolean addToMainDexList) {
+ public synchronized Builder addSynthesizedClass(
+ DexProgramClass synthesizedClass, boolean addToMainDexList) {
assert synthesizedClass.isProgramClass() : "All synthesized classes must be program classes";
- addClassPromise(synthesizedClass);
+ addProgramClass(synthesizedClass);
if (addToMainDexList && !mainDexList.isEmpty()) {
mainDexList.add(synthesizedClass.type);
}
return this;
}
- public List<DexProgramClass> getProgramClasses() {
- List<DexProgramClass> result = new ArrayList<>();
- // Note: we ignore lazy class collection because it
- // is never supposed to be used for program classes.
- for (DexClassPromise promise : classMap.values()) {
- if (promise.isProgramClass()) {
- result.add(promise.get().asProgramClass());
- }
- }
- return result;
- }
-
- // Callback from FileReader when parsing a DexProgramClass (multi-threaded).
- private void addClass(DexClass clazz, boolean skipLibDups) {
- addClassPromise(clazz, skipLibDups);
- }
-
- public synchronized void addClassPromise(DexClassPromise promise, boolean skipLibDups) {
- assert promise != null;
- DexType type = promise.getType();
- DexClassPromise oldPromise = classMap.get(type);
- if (oldPromise != null) {
- promise = chooseClass(promise, oldPromise, skipLibDups);
- }
- if (oldPromise != promise) {
- classMap.put(type, promise);
- }
+ public Collection<DexProgramClass> getProgramClasses() {
+ return programClasses;
}
public Builder addToMainDexList(Collection<DexType> mainDexList) {
@@ -343,90 +389,13 @@
public DexApplication build() {
return new DexApplication(
proguardMap,
- ImmutableMap.copyOf(classMap),
- lazyClassCollection,
+ ProgramClassCollection.create(programClasses),
+ classpathClasses,
+ libraryClasses,
ImmutableSet.copyOf(mainDexList),
dexItemFactory,
highestSortingString,
timing);
}
}
-
- public static DexClassPromise chooseClass(
- DexClassPromise a, DexClassPromise b, boolean skipLibDups) {
- // NOTE: We assume that there should not be any conflicting names in user defined
- // classes and/or linked jars. If we ever want to allow 'keep first'-like policy
- // to resolve this kind of conflict between program and/or classpath classes, we'll
- // need to make sure we choose the class we keep deterministically.
- if (a.isProgramClass() && b.isProgramClass()) {
- if (allowProgramClassConflict(a, b)) {
- return a;
- }
- throw new CompilationError(
- "Program type already present: " + a.getType().toSourceString());
- }
- if (a.isProgramClass()) {
- return chooseBetweenProgramAndOtherClass(a, b);
- }
- if (b.isProgramClass()) {
- return chooseBetweenProgramAndOtherClass(b, a);
- }
-
- if (a.isClasspathClass() && b.isClasspathClass()) {
- throw new CompilationError(
- "Classpath type already present: " + a.getType().toSourceString());
- }
- if (a.isClasspathClass()) {
- return chooseBetweenClasspathAndLibraryClass(a, b);
- }
- if (b.isClasspathClass()) {
- return chooseBetweenClasspathAndLibraryClass(b, a);
- }
-
- return chooseBetweenLibraryClasses(b, a, skipLibDups);
- }
-
- private static boolean allowProgramClassConflict(DexClassPromise a, DexClassPromise b) {
- // Currently only allow collapsing synthetic lambda classes.
- return a.getOrigin() == Resource.Kind.DEX
- && b.getOrigin() == Resource.Kind.DEX
- && a.get().accessFlags.isSynthetic()
- && b.get().accessFlags.isSynthetic()
- && LambdaRewriter.hasLambdaClassPrefix(a.getType())
- && LambdaRewriter.hasLambdaClassPrefix(b.getType());
- }
-
- private static DexClassPromise chooseBetweenProgramAndOtherClass(
- DexClassPromise selected, DexClassPromise ignored) {
- assert selected.isProgramClass() && !ignored.isProgramClass();
- if (ignored.isLibraryClass()) {
- logIgnoredClass(ignored, "Class `%s` was specified as library and program type.");
- }
- // We don't log program/classpath class conflict since it is expected case.
- return selected;
- }
-
- private static DexClassPromise chooseBetweenClasspathAndLibraryClass(
- DexClassPromise selected, DexClassPromise ignored) {
- assert selected.isClasspathClass() && ignored.isLibraryClass();
- logIgnoredClass(ignored, "Class `%s` was specified as library and classpath type.");
- return selected;
- }
-
- private static DexClassPromise chooseBetweenLibraryClasses(
- DexClassPromise selected, DexClassPromise ignored, boolean skipDups) {
- assert selected.isLibraryClass() && ignored.isLibraryClass();
- if (!skipDups) {
- throw new CompilationError(
- "Library type already present: " + selected.getType().toSourceString());
- }
- logIgnoredClass(ignored, "Class `%s` was specified twice as a library type.");
- return selected;
- }
-
- private static void logIgnoredClass(DexClassPromise ignored, String message) {
- if (Log.ENABLED) {
- Log.warn(DexApplication.class, message, ignored.getType().toSourceString());
- }
- }
}
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index f684309..8560de3 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -9,7 +9,7 @@
import com.android.tools.r8.errors.Unreachable;
import com.google.common.base.MoreObjects;
-public abstract class DexClass extends DexItem implements DexClassPromise {
+public abstract class DexClass extends DexItem {
private static final DexEncodedMethod[] NO_METHODS = {};
private static final DexEncodedField[] NO_FIELDS = {};
@@ -50,6 +50,12 @@
throw new CompilationError("Interface " + type.toString() + " cannot implement itself");
}
}
+ if (!type.descriptor.isValidClassDescriptor()) {
+ throw new CompilationError(
+ "Class descriptor '"
+ + type.descriptor.toString()
+ + "' cannot be represented in dex format.");
+ }
}
@Override
@@ -118,7 +124,6 @@
public abstract void addDependencies(MixedSectionCollection collector);
- @Override
public boolean isProgramClass() {
return false;
}
@@ -127,7 +132,6 @@
return null;
}
- @Override
public boolean isClasspathClass() {
return false;
}
@@ -136,7 +140,6 @@
return null;
}
- @Override
public boolean isLibraryClass() {
return false;
}
@@ -154,21 +157,10 @@
return null;
}
- @Override
public Resource.Kind getOrigin() {
return this.origin;
}
- @Override
- public DexClass get() {
- return this;
- }
-
- @Override
- public DexType getType() {
- return type;
- }
-
public boolean hasClassInitializer() {
return getClassInitializer() != null;
}
diff --git a/src/main/java/com/android/tools/r8/graph/DexClassPromise.java b/src/main/java/com/android/tools/r8/graph/DexClassPromise.java
deleted file mode 100644
index c0bdf20..0000000
--- a/src/main/java/com/android/tools/r8/graph/DexClassPromise.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (c) 2017, 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.graph;
-
-import com.android.tools.r8.Resource;
-
-/**
- * Provides a way for delayed DexClass discovery.
- *
- * Provides minimal class details of the promised class and
- * provides the class when asked by calling method get().
- *
- * Note that DexClass also implements this interface, since it
- * represents a 'materialized' promise for a class.
- */
-public interface DexClassPromise {
- DexType getType();
-
- Resource.Kind getOrigin();
-
- boolean isProgramClass();
-
- boolean isClasspathClass();
-
- boolean isLibraryClass();
-
- DexClass get();
-}
diff --git a/src/main/java/com/android/tools/r8/graph/DexCode.java b/src/main/java/com/android/tools/r8/graph/DexCode.java
index ebfd388..f48e7db 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -15,9 +15,11 @@
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.StringUtils;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
+import java.util.Iterator;
import java.util.Map;
import java.util.Set;
@@ -157,10 +159,10 @@
}
public String toString() {
- return toString(null);
+ return toString(null, null);
}
- public String toString(ClassNameMapper naming) {
+ public String toString(DexEncodedMethod method, ClassNameMapper naming) {
StringBuilder builder = new StringBuilder();
builder.append("registers: ").append(registerSize);
builder.append(", inputs: ").append(incomingRegisterSize);
@@ -168,12 +170,22 @@
builder.append("------------------------------------------------------------\n");
builder.append("inst# offset instruction arguments\n");
builder.append("------------------------------------------------------------\n");
+ DexDebugEntry debugInfo = null;
+ Iterator<DexDebugEntry> debugInfoIterator = Collections.emptyIterator();
+ if (getDebugInfo() != null && method != null) {
+ debugInfoIterator = new DexDebugEntryBuilder(method, new DexItemFactory()).build().iterator();
+ debugInfo = debugInfoIterator.hasNext() ? debugInfoIterator.next() : null;
+ }
int instructionNumber = 0;
for (Instruction insn : instructions) {
StringUtils.appendLeftPadded(builder, Integer.toString(instructionNumber++), 5);
builder.append(": ")
.append(insn.toString(naming))
.append('\n');
+ if (debugInfo != null && debugInfo.address == insn.getOffset()) {
+ builder.append(" ").append(debugInfo).append("\n");
+ debugInfo = debugInfoIterator.hasNext() ? debugInfoIterator.next() : null;
+ }
}
if (tries.length > 0) {
builder.append("Tries (numbers are offsets)\n");
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugEntryBuilder.java b/src/main/java/com/android/tools/r8/graph/DexDebugEntryBuilder.java
index f6d8fb4..4410f2a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugEntryBuilder.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugEntryBuilder.java
@@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.graph;
+import com.android.tools.r8.ir.code.MoveType;
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.HashMap;
@@ -60,19 +61,19 @@
DexCode code = method.getCode().asDexCode();
DexDebugInfo info = code.getDebugInfo();
int argumentRegister = code.registerSize - code.incomingRegisterSize;
- int argumentCount = code.incomingRegisterSize;
if (!method.accessFlags.isStatic()) {
- --argumentCount;
DexString name = factory.thisName;
DexType type = method.method.getHolder();
- startLocal(argumentRegister++, name, type, null);
+ startLocal(argumentRegister, name, type, null);
+ argumentRegister += MoveType.fromDexType(type).requiredRegisters();
}
DexType[] types = method.method.proto.parameters.values;
DexString[] names = info.parameters;
- for (int i = 0; i < argumentCount; i++) {
+ for (int i = 0; i < types.length; i++) {
// If null, the parameter has a parameterized type and the local is introduced in the stream.
if (names[i] != null) {
- startLocal(argumentRegister++, names[i], types[i], null);
+ startLocal(argumentRegister, names[i], types[i], null);
+ argumentRegister += MoveType.fromDexType(types[i]).requiredRegisters();
}
}
currentLine = info.startLine;
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 188440d..3df2fab 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -22,7 +22,7 @@
import com.android.tools.r8.ir.code.MoveType;
import com.android.tools.r8.ir.code.ValueNumberGenerator;
import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
import com.android.tools.r8.ir.regalloc.RegisterAllocator;
import com.android.tools.r8.ir.synthetic.ForwardMethodSourceCode;
import com.android.tools.r8.ir.synthetic.SynthesizedCode;
@@ -73,6 +73,11 @@
return compilationState != CompilationState.NOT_PROCESSED;
}
+ public boolean cannotInline() {
+ return compilationState == CompilationState.NOT_PROCESSED
+ || compilationState == CompilationState.PROCESSED_NOT_INLINING_CANDIDATE;
+ }
+
public boolean isInliningCandidate(DexEncodedMethod container, boolean alwaysInline) {
if (container.accessFlags.isStatic() && container.accessFlags.isConstructor()) {
// This will probably never happen but never inline a class initializer.
@@ -102,7 +107,8 @@
return compilationState == PROCESSED_INLINING_CANDIDATE_PUBLIC;
}
- public void markProcessed(InliningConstraint state) {
+ public boolean markProcessed(Constraint state) {
+ CompilationState prevCompilationState = compilationState;
switch (state) {
case ALWAYS:
compilationState = PROCESSED_INLINING_CANDIDATE_PUBLIC;
@@ -117,6 +123,7 @@
compilationState = PROCESSED_NOT_INLINING_CANDIDATE;
break;
}
+ return prevCompilationState != compilationState;
}
public void markNotProcessed() {
@@ -341,7 +348,7 @@
}
public String codeToString() {
- return code == null ? "<no code>" : code.toString();
+ return code == null ? "<no code>" : code.toString(this, null);
}
@Override
diff --git a/src/main/java/com/android/tools/r8/graph/DexField.java b/src/main/java/com/android/tools/r8/graph/DexField.java
index f97c147..5616967 100644
--- a/src/main/java/com/android/tools/r8/graph/DexField.java
+++ b/src/main/java/com/android/tools/r8/graph/DexField.java
@@ -4,6 +4,7 @@
package com.android.tools.r8.graph;
import com.android.tools.r8.dex.IndexedItemCollection;
+import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.naming.NamingLens;
public class DexField extends Descriptor<DexEncodedField, DexField> implements
@@ -17,6 +18,10 @@
this.clazz = clazz;
this.type = type;
this.name = name;
+ if (!name.isValidFieldName()) {
+ throw new CompilationError(
+ "Field name '" + name.toString() + "' cannot be represented in dex format.");
+ }
}
@Override
diff --git a/src/main/java/com/android/tools/r8/graph/DexMethod.java b/src/main/java/com/android/tools/r8/graph/DexMethod.java
index de536f6..a066d24 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethod.java
@@ -4,6 +4,7 @@
package com.android.tools.r8.graph;
import com.android.tools.r8.dex.IndexedItemCollection;
+import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.naming.NamingLens;
public class DexMethod extends Descriptor<DexEncodedMethod, DexMethod>
@@ -20,6 +21,10 @@
this.holder = holder;
this.proto = proto;
this.name = name;
+ if (!name.isValidMethodName()) {
+ throw new CompilationError(
+ "Method name '" + name.toString() + "' cannot be represented in dex format.");
+ }
}
@Override
diff --git a/src/main/java/com/android/tools/r8/graph/DexString.java b/src/main/java/com/android/tools/r8/graph/DexString.java
index e9b687e..e70e8f0 100644
--- a/src/main/java/com/android/tools/r8/graph/DexString.java
+++ b/src/main/java/com/android/tools/r8/graph/DexString.java
@@ -185,6 +185,127 @@
return slowCompareTo(other);
}
+ private boolean isSimpleNameChar(char ch) {
+ if (ch >= 'A' && ch <= 'Z') {
+ return true;
+ }
+ if (ch >= 'a' && ch <= 'z') {
+ return true;
+ }
+ if (ch >= '0' && ch <= '9') {
+ return true;
+ }
+ if (ch == '$' || ch == '-' || ch == '_') {
+ return true;
+ }
+ if (ch >= 0x00a1 && ch <= 0x1fff) {
+ return true;
+ }
+ if (ch >= 0x2010 && ch <= 0x2027) {
+ return true;
+ }
+ if (ch >= 0x2030 && ch <= 0xd7ff) {
+ return true;
+ }
+ if (ch >= 0xe000 && ch <= 0xffef) {
+ return true;
+ }
+ if (ch >= 0x10000 && ch <= 0x10ffff) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isValidClassDescriptor(String string) {
+ if (string.length() < 3
+ || string.charAt(0) != 'L'
+ || string.charAt(string.length() - 1) != ';') {
+ return false;
+ }
+ if (string.charAt(1) == '/' || string.charAt(string.length() - 2) == '/') {
+ return false;
+ }
+ for (int i = 1; i < string.length() - 1; i++) {
+ char ch = string.charAt(i);
+ if (ch == '/') {
+ continue;
+ }
+ if (isSimpleNameChar(ch)) {
+ continue;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ private boolean isValidMethodName(String string) {
+ if (string.isEmpty()) {
+ return false;
+ }
+ // According to https://source.android.com/devices/tech/dalvik/dex-format#membername
+ // '<' SimpleName '>' should be valid. However, the art verifier only allows <init>
+ // and <clinit> which is reasonable.
+ if ((string.charAt(0) == '<') &&
+ (string.equals("<init>") || string.equals("<clinit>"))) {
+ return true;
+ }
+ for (int i = 0; i < string.length(); i++) {
+ char ch = string.charAt(i);
+ if (isSimpleNameChar(ch)) {
+ continue;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ private boolean isValidFieldName(String string) {
+ if (string.isEmpty()) {
+ return false;
+ }
+ int start = 0;
+ int end = string.length();
+ if (string.charAt(0) == '<') {
+ if (string.charAt(string.length() - 1) == '>') {
+ start = 1;
+ end = string.length() - 1;
+ } else {
+ return false;
+ }
+ }
+ for (int i = start; i < end; i++) {
+ if (isSimpleNameChar(string.charAt(i))) {
+ continue;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ public boolean isValidMethodName() {
+ try {
+ return isValidMethodName(decode());
+ } catch (UTFDataFormatException e) {
+ return false;
+ }
+ }
+
+ public boolean isValidFieldName() {
+ try {
+ return isValidFieldName(decode());
+ } catch (UTFDataFormatException e) {
+ return false;
+ }
+ }
+
+ public boolean isValidClassDescriptor() {
+ try {
+ return isValidClassDescriptor(decode());
+ } catch (UTFDataFormatException e) {
+ return false;
+ }
+ }
+
public String dump() {
StringBuilder builder = new StringBuilder();
builder.append(toString());
diff --git a/src/main/java/com/android/tools/r8/graph/GraphLense.java b/src/main/java/com/android/tools/r8/graph/GraphLense.java
index e6c2d00..b9173dd 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLense.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLense.java
@@ -6,6 +6,19 @@
import java.util.IdentityHashMap;
import java.util.Map;
+/**
+ * A GraphLense implements a virtual view on top of the graph, used to delay global rewrites until
+ * later IR processing stages.
+ * <p>
+ * Valid remappings are limited to the following operations:
+ * <ul>
+ * <li>Mapping a classes type to one of the super/subtypes.</li>
+ * <li>Renaming private methods/fields.</li>
+ * <li>Moving methods/fields to a super/subclass.</li>
+ * <li>Replacing method/field references by the same method/field on a super/subtype</li>
+ * </ul>
+ * Note that the latter two have to take visibility into account.
+ */
public abstract class GraphLense {
public static class Builder {
diff --git a/src/main/java/com/android/tools/r8/graph/JarCode.java b/src/main/java/com/android/tools/r8/graph/JarCode.java
index 2766684..9a367ff 100644
--- a/src/main/java/com/android/tools/r8/graph/JarCode.java
+++ b/src/main/java/com/android/tools/r8/graph/JarCode.java
@@ -112,7 +112,7 @@
}
@Override
- public String toString(ClassNameMapper naming) {
+ public String toString(DexEncodedMethod method, ClassNameMapper naming) {
return toString();
}
diff --git a/src/main/java/com/android/tools/r8/graph/LazyClassFileLoader.java b/src/main/java/com/android/tools/r8/graph/LazyClassFileLoader.java
deleted file mode 100644
index e5792cd..0000000
--- a/src/main/java/com/android/tools/r8/graph/LazyClassFileLoader.java
+++ /dev/null
@@ -1,104 +0,0 @@
-// Copyright (c) 2017, 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.graph;
-
-import static com.android.tools.r8.utils.FileUtils.DEFAULT_DEX_FILENAME;
-
-import com.android.tools.r8.Resource;
-import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.errors.Unreachable;
-import com.google.common.io.Closer;
-import java.io.IOException;
-
-// Lazily loads a class file represented by resource.
-public final class LazyClassFileLoader implements DexClassPromise {
- // Resource representing file definition.
- private final Resource resource;
- // Class kind to be created.
- private final ClassKind classKind;
-
- // Application reader to be used. Note that the reader may be reused in
- // many loaders and may be used concurrently, it is considered to be
- // thread-safe since its only state is internal options which we
- // consider immutable after they are initialized (barring dex factory
- // which is thread-safe).
- private final JarApplicationReader reader;
-
- // Dex type of the class to be created.
- private final DexType type;
-
- // Cached loaded class if get(...) method has already been called, this
- // field is only accessed in context synchronized on `this`.
- private DexClass loadedClass = null;
-
- public LazyClassFileLoader(DexType type,
- Resource resource, ClassKind classKind, JarApplicationReader reader) {
- this.resource = resource;
- this.reader = reader;
- this.type = type;
- this.classKind = classKind;
- assert classKind != ClassKind.PROGRAM;
- }
-
- // Callback method for JarClassFileReader, is always called in synchronized context.
- private void addClass(DexClass clazz) {
- assert clazz != null;
- assert loadedClass == null;
- loadedClass = clazz;
- }
-
- @Override
- public DexType getType() {
- return type;
- }
-
- @Override
- public Resource.Kind getOrigin() {
- return Resource.Kind.CLASSFILE;
- }
-
- @Override
- public boolean isProgramClass() {
- return false;
- }
-
- @Override
- public boolean isClasspathClass() {
- return classKind == ClassKind.CLASSPATH;
- }
-
- @Override
- public boolean isLibraryClass() {
- return classKind == ClassKind.LIBRARY;
- }
-
- // Loads the class from the resource. Synchronized on `this` to avoid
- // unnecessary complications, thus all threads trying to load a class with
- // this loader will wait for the first load to finish.
- @Override
- public synchronized DexClass get() {
- if (loadedClass != null) {
- return loadedClass;
- }
-
- try (Closer closer = Closer.create()) {
- JarClassFileReader reader = new JarClassFileReader(this.reader, this::addClass);
- reader.read(DEFAULT_DEX_FILENAME, classKind, resource.getStream(closer));
- } catch (IOException e) {
- throw new CompilationError("Failed to load class: " + type.toSourceString(), e);
- }
-
- if (loadedClass == null) {
- throw new Unreachable("Class is supposed to be loaded: " + type.toSourceString());
- }
-
- if (loadedClass.type != type) {
- throw new CompilationError("Class content provided for type descriptor "
- + type.toSourceString() + " actually defines class " + loadedClass.type.toSourceString());
- }
-
- return loadedClass;
- }
-}
diff --git a/src/main/java/com/android/tools/r8/graph/ObjectToOffsetMapping.java b/src/main/java/com/android/tools/r8/graph/ObjectToOffsetMapping.java
index 1d5f38c..7dab70a 100644
--- a/src/main/java/com/android/tools/r8/graph/ObjectToOffsetMapping.java
+++ b/src/main/java/com/android/tools/r8/graph/ObjectToOffsetMapping.java
@@ -79,6 +79,7 @@
private static DexProgramClass[] sortClasses(
DexApplication application, DexProgramClass[] classes) {
+ Arrays.sort(classes, (o1, o2) -> o1.type.descriptor.slowCompareTo(o2.type.descriptor));
SortingProgramClassVisitor classVisitor = new SortingProgramClassVisitor(application, classes);
classVisitor.run(classes);
return classVisitor.getSortedClasses();
diff --git a/src/main/java/com/android/tools/r8/ir/code/Argument.java b/src/main/java/com/android/tools/r8/ir/code/Argument.java
index 94c415e..94f6507 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Argument.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Argument.java
@@ -7,7 +7,7 @@
import com.android.tools.r8.graph.AppInfo;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
import com.android.tools.r8.utils.InternalOptions;
/**
@@ -67,7 +67,7 @@
}
@Override
- public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
- return InliningConstraint.ALWAYS;
+ public Constraint inliningConstraint(AppInfo info, DexType holder) {
+ return Constraint.ALWAYS;
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
index 8bb31d2..ab2056d 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.ir.code;
+import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.ir.conversion.DexBuilder;
import com.android.tools.r8.ir.conversion.IRBuilder;
@@ -119,7 +120,7 @@
public List<BasicBlock> getNormalPredecessors() {
ImmutableList.Builder<BasicBlock> normals = ImmutableList.builder();
for (BasicBlock predecessor : predecessors) {
- if (!predecessor.isCatchSuccessor(this)) {
+ if (!predecessor.hasCatchSuccessor(this)) {
normals.add(predecessor);
}
}
@@ -647,10 +648,10 @@
public EdgeType getEdgeType(BasicBlock successor) {
assert successors.indexOf(successor) >= 0;
- return isCatchSuccessor(successor) ? EdgeType.EXCEPTIONAL : EdgeType.NORMAL;
+ return hasCatchSuccessor(successor) ? EdgeType.EXCEPTIONAL : EdgeType.NORMAL;
}
- public boolean isCatchSuccessor(BasicBlock block) {
+ public boolean hasCatchSuccessor(BasicBlock block) {
if (!hasCatchHandlers()) {
return false;
}
@@ -658,7 +659,7 @@
}
public int guardsForCatchSuccessor(BasicBlock block) {
- assert isCatchSuccessor(block);
+ assert hasCatchSuccessor(block);
int index = successors.indexOf(block);
int count = 0;
for (int handler : catchHandlers.getAllTargets()) {
@@ -694,7 +695,7 @@
StringBuilder builder, List<BasicBlock> list, Function<BasicBlock, String> postfix) {
if (list.size() > 0) {
for (BasicBlock block : list) {
- builder.append(block.getNumber());
+ builder.append(block.number >= 0 ? block.number : "<unknown>");
builder.append(postfix.apply(block));
builder.append(" ");
}
@@ -713,7 +714,7 @@
}
private String predecessorPostfix(BasicBlock block) {
- if (isCatchSuccessor(block)) {
+ if (hasCatchSuccessor(block)) {
return new String(new char[guardsForCatchSuccessor(block)]).replace("\0", "*");
}
return "";
@@ -1053,7 +1054,7 @@
// one new phi to merge the two exception values, and all other phis don't need
// to be changed.
for (BasicBlock catchSuccessor : catchSuccessors) {
- catchSuccessor.splitCriticalExceptioEdges(
+ catchSuccessor.splitCriticalExceptionEdges(
code.valueNumberGenerator,
newBlock -> {
newBlock.setNumber(code.blocks.size());
@@ -1062,15 +1063,6 @@
}
}
- private boolean allPredecessorsHaveCatchEdges() {
- for (BasicBlock predecessor : getPredecessors()) {
- if (!predecessor.isCatchSuccessor(this)) {
- assert false;
- }
- }
- return true;
- }
-
/**
* Assumes that `this` block is a catch handler target (note that it does not have to
* start with MoveException instruction, since the instruction can be removed by
@@ -1086,10 +1078,8 @@
*
* NOTE: onNewBlock must assign block number to the newly created block.
*/
- public void splitCriticalExceptioEdges(
+ public void splitCriticalExceptionEdges(
ValueNumberGenerator valueNumberGenerator, Consumer<BasicBlock> onNewBlock) {
- assert allPredecessorsHaveCatchEdges();
-
List<BasicBlock> predecessors = this.getPredecessors();
boolean hasMoveException = entry().isMoveException();
MoveException move = null;
@@ -1103,6 +1093,10 @@
List<BasicBlock> newPredecessors = new ArrayList<>();
List<Value> values = new ArrayList<>(predecessors.size());
for (BasicBlock predecessor : predecessors) {
+ if (!predecessor.hasCatchSuccessor(this)) {
+ throw new CompilationError(
+ "Invalid block structure: catch block reachable via non-exceptional flow.");
+ }
BasicBlock newBlock = new BasicBlock();
newPredecessors.add(newBlock);
if (hasMoveException) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/Binop.java b/src/main/java/com/android/tools/r8/ir/code/Binop.java
index 83240bb..899516a 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Binop.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Binop.java
@@ -10,7 +10,7 @@
import com.android.tools.r8.graph.AppInfo;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
public abstract class Binop extends Instruction {
@@ -112,7 +112,7 @@
}
@Override
- public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
- return InliningConstraint.ALWAYS;
+ public Constraint inliningConstraint(AppInfo info, DexType holder) {
+ return Constraint.ALWAYS;
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java b/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
index 451832f..4cca9dd 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
@@ -15,7 +15,7 @@
import com.android.tools.r8.graph.AppInfo;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
import com.android.tools.r8.utils.NumberUtils;
public class ConstNumber extends ConstInstruction {
@@ -188,7 +188,7 @@
}
@Override
- public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
- return InliningConstraint.ALWAYS;
+ public Constraint inliningConstraint(AppInfo info, DexType holder) {
+ return Constraint.ALWAYS;
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/Div.java b/src/main/java/com/android/tools/r8/ir/code/Div.java
index c8c1c32..8fd9bf0 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Div.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Div.java
@@ -14,9 +14,6 @@
import com.android.tools.r8.code.DivIntLit8;
import com.android.tools.r8.code.DivLong;
import com.android.tools.r8.code.DivLong2Addr;
-import com.android.tools.r8.graph.AppInfo;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
public class Div extends ArithmeticBinop {
diff --git a/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java b/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java
index ba1e0e9..0b8d876 100644
--- a/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java
@@ -9,8 +9,7 @@
import com.android.tools.r8.graph.DexEncodedField;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.ir.optimize.Inliner;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
import java.util.List;
abstract class FieldInstruction extends Instruction {
@@ -43,7 +42,7 @@
}
@Override
- public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
+ public Constraint inliningConstraint(AppInfo info, DexType holder) {
// Resolve the field if possible and decide whether the instruction can inlined.
DexType fieldHolder = field.getHolder();
DexEncodedField target = info.lookupInstanceTarget(fieldHolder, field);
@@ -51,15 +50,15 @@
if ((target != null) && (fieldClass != null) && !fieldClass.isLibraryClass()) {
DexAccessFlags flags = target.accessFlags;
if (flags.isPublic()) {
- return Inliner.InliningConstraint.ALWAYS;
+ return Constraint.ALWAYS;
}
if (flags.isPrivate() && (fieldHolder == holder)) {
- return Inliner.InliningConstraint.PRIVATE;
+ return Constraint.PRIVATE;
}
if (flags.isProtected() && (fieldHolder.isSamePackage(holder))) {
- return Inliner.InliningConstraint.PACKAGE;
+ return Constraint.PACKAGE;
}
}
- return Inliner.InliningConstraint.NEVER;
+ return Constraint.NEVER;
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstanceOf.java b/src/main/java/com/android/tools/r8/ir/code/InstanceOf.java
index 3fe498d..1126299 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstanceOf.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstanceOf.java
@@ -9,7 +9,7 @@
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
public class InstanceOf extends Instruction {
@@ -75,14 +75,14 @@
}
@Override
- public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
+ public Constraint inliningConstraint(AppInfo info, DexType holder) {
DexClass targetClass = info.definitionFor(type());
if (targetClass == null) {
- return InliningConstraint.NEVER;
+ return Constraint.NEVER;
}
if (targetClass.accessFlags.isPublic()) {
- return InliningConstraint.ALWAYS;
+ return Constraint.ALWAYS;
}
- return InliningConstraint.NEVER;
+ return Constraint.NEVER;
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index 1b18ccc..2e821cf 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -9,7 +9,7 @@
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.ir.code.Value.DebugInfo;
import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
import com.android.tools.r8.ir.regalloc.RegisterAllocator;
import com.android.tools.r8.utils.CfgPrinter;
import com.android.tools.r8.utils.InternalOptions;
@@ -797,7 +797,7 @@
}
// Returns the inlining constraint for this instruction.
- public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
- return InliningConstraint.NEVER;
+ public Constraint inliningConstraint(AppInfo info, DexType holder) {
+ return Constraint.NEVER;
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/Invoke.java b/src/main/java/com/android/tools/r8/ir/code/Invoke.java
index 5b516b7..14104d7 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Invoke.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Invoke.java
@@ -91,8 +91,14 @@
}
protected int argumentRegisterValue(int i, DexBuilder builder) {
+ assert needsRangedInvoke(builder);
if (i < arguments().size()) {
- return builder.allocatedRegister(arguments().get(i), getNumber());
+ // If argument values flow into ranged invokes, all the ranged invoke arguments
+ // are arguments to this method in order. Therefore, we use the incoming registers
+ // for the ranged invoke arguments. We know that arguments are always available there.
+ // If argument reuse is allowed there is no splitting and if argument reuse is disallowed
+ // the argument registers are never overwritten.
+ return builder.argumentOrAllocateRegister(arguments().get(i), getNumber());
}
return 0;
}
@@ -120,10 +126,10 @@
protected boolean argumentsConsecutive(DexBuilder builder) {
Value value = arguments().get(0);
- int next = builder.allocatedRegister(value, getNumber()) + value.requiredRegisters();
+ int next = builder.argumentOrAllocateRegister(value, getNumber()) + value.requiredRegisters();
for (int i = 1; i < arguments().size(); i++) {
value = arguments().get(i);
- assert next == builder.allocatedRegister(value, getNumber());
+ assert next == builder.argumentOrAllocateRegister(value, getNumber());
next += value.requiredRegisters();
}
return true;
@@ -162,9 +168,45 @@
if (requiredArgumentRegisters() > 5) {
return Constants.U16BIT_MAX;
}
+ if (argumentsAreConsecutiveInputArguments()) {
+ return Constants.U16BIT_MAX;
+ }
return Constants.U4BIT_MAX;
}
+ private boolean argumentsAreConsecutiveInputArguments() {
+ if (arguments().size() == 0) {
+ return false;
+ }
+ Value current = arguments().get(0);
+ if (!current.isArgument()) {
+ return false;
+ }
+ for (int i = 1; i < arguments().size(); i++) {
+ Value next = arguments().get(i);
+ if (current.getNextConsecutive() != next) {
+ return false;
+ }
+ current = next;
+ }
+ return true;
+ }
+
+ private boolean argumentsAreConsecutiveInputArgumentsWithHighRegisters(
+ DexBuilder builder) {
+ if (!argumentsAreConsecutiveInputArguments()) {
+ return false;
+ }
+ Value lastArgument = arguments().get(arguments().size() - 1);
+ return builder.argumentOrAllocateRegister(lastArgument, getNumber()) > Constants.U4BIT_MAX;
+ }
+
+ protected boolean needsRangedInvoke(DexBuilder builder) {
+ return requiredArgumentRegisters() > 5
+ || hasHighArgumentRegister(builder)
+ || argumentsAreConsecutiveInputArgumentsWithHighRegisters(builder);
+ }
+
@Override
public int maxOutValueRegister() {
return Constants.U8BIT_MAX;
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeCustom.java b/src/main/java/com/android/tools/r8/ir/code/InvokeCustom.java
index 2c7ea4b..d0d0739 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeCustom.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeCustom.java
@@ -50,7 +50,7 @@
com.android.tools.r8.code.Instruction instruction;
int argumentRegisters = requiredArgumentRegisters();
builder.requestOutgoingRegisters(argumentRegisters);
- if (argumentRegisters > 5 || hasHighArgumentRegister(builder)) {
+ if (needsRangedInvoke(builder)) {
assert argumentsConsecutive(builder);
int firstRegister = argumentRegisterValue(0, builder);
instruction = new InvokeCustomRange(firstRegister, argumentRegisters, getCallSite());
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java b/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
index 95776f4..f11d62f 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
@@ -39,7 +39,7 @@
com.android.tools.r8.code.Instruction instruction;
int argumentRegisters = requiredArgumentRegisters();
builder.requestOutgoingRegisters(argumentRegisters);
- if (argumentRegisters > 5 || hasHighArgumentRegister(builder)) {
+ if (needsRangedInvoke(builder)) {
assert argumentsConsecutive(builder);
int firstRegister = argumentRegisterValue(0, builder);
instruction = new InvokeDirectRange(firstRegister, argumentRegisters, getInvokedMethod());
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeInterface.java b/src/main/java/com/android/tools/r8/ir/code/InvokeInterface.java
index f1c1d65..9d0c01f 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeInterface.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeInterface.java
@@ -38,7 +38,7 @@
com.android.tools.r8.code.Instruction instruction;
int argumentRegisters = requiredArgumentRegisters();
builder.requestOutgoingRegisters(argumentRegisters);
- if (argumentRegisters > 5 || hasHighArgumentRegister(builder)) {
+ if (needsRangedInvoke(builder)) {
assert argumentsConsecutive(builder);
int firstRegister = argumentRegisterValue(0, builder);
instruction = new InvokeInterfaceRange(firstRegister, argumentRegisters, getInvokedMethod());
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeNewArray.java b/src/main/java/com/android/tools/r8/ir/code/InvokeNewArray.java
index 0725fed..2912c1a 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeNewArray.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeNewArray.java
@@ -49,7 +49,7 @@
com.android.tools.r8.code.Instruction instruction;
int argumentRegisters = requiredArgumentRegisters();
builder.requestOutgoingRegisters(argumentRegisters);
- if (argumentRegisters > 5 || hasHighArgumentRegister(builder)) {
+ if (needsRangedInvoke(builder)) {
assert argumentsConsecutive(builder);
int firstRegister = argumentRegisterValue(0, builder);
instruction = new FilledNewArrayRange(firstRegister, argumentRegisters, type);
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java b/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
index 9c87144..efc12db 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
@@ -47,7 +47,7 @@
com.android.tools.r8.code.Instruction instruction;
int argumentRegisters = requiredArgumentRegisters();
builder.requestOutgoingRegisters(argumentRegisters);
- if (argumentRegisters > 5 || hasHighArgumentRegister(builder)) {
+ if (needsRangedInvoke(builder)) {
assert argumentsConsecutive(builder);
int firstRegister = argumentRegisterValue(0, builder);
instruction = new InvokePolymorphicRange(
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java b/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java
index 0c4fe14..dc22e2e 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java
@@ -38,7 +38,7 @@
com.android.tools.r8.code.Instruction instruction;
int argumentRegisters = requiredArgumentRegisters();
builder.requestOutgoingRegisters(argumentRegisters);
- if (argumentRegisters > 5 || hasHighArgumentRegister(builder)) {
+ if (needsRangedInvoke(builder)) {
assert argumentsConsecutive(builder);
int firstRegister = argumentRegisterValue(0, builder);
instruction = new InvokeStaticRange(firstRegister, argumentRegisters, getInvokedMethod());
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeSuper.java b/src/main/java/com/android/tools/r8/ir/code/InvokeSuper.java
index 2e1e3e7..fc79c11 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeSuper.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeSuper.java
@@ -38,7 +38,7 @@
com.android.tools.r8.code.Instruction instruction;
int argumentRegisters = requiredArgumentRegisters();
builder.requestOutgoingRegisters(argumentRegisters);
- if (argumentRegisters > 5 || hasHighArgumentRegister(builder)) {
+ if (needsRangedInvoke(builder)) {
assert argumentsConsecutive(builder);
int firstRegister = argumentRegisterValue(0, builder);
instruction = new InvokeSuperRange(firstRegister, argumentRegisters, getInvokedMethod());
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java b/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java
index 339ae71..3988980 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java
@@ -38,7 +38,7 @@
com.android.tools.r8.code.Instruction instruction;
int argumentRegisters = requiredArgumentRegisters();
builder.requestOutgoingRegisters(argumentRegisters);
- if (argumentRegisters > 5 || hasHighArgumentRegister(builder)) {
+ if (needsRangedInvoke(builder)) {
assert argumentsConsecutive(builder);
int firstRegister = argumentRegisterValue(0, builder);
instruction = new InvokeVirtualRange(firstRegister, argumentRegisters, getInvokedMethod());
diff --git a/src/main/java/com/android/tools/r8/ir/code/JumpInstruction.java b/src/main/java/com/android/tools/r8/ir/code/JumpInstruction.java
index 2a02218..7b1c007 100644
--- a/src/main/java/com/android/tools/r8/ir/code/JumpInstruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/JumpInstruction.java
@@ -5,7 +5,7 @@
import com.android.tools.r8.graph.AppInfo;
import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
import com.android.tools.r8.utils.InternalOptions;
import java.util.List;
@@ -47,7 +47,7 @@
}
@Override
- public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
- return InliningConstraint.ALWAYS;
+ public Constraint inliningConstraint(AppInfo info, DexType holder) {
+ return Constraint.ALWAYS;
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/Monitor.java b/src/main/java/com/android/tools/r8/ir/code/Monitor.java
index 869d1d0..b3e155b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Monitor.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Monitor.java
@@ -29,7 +29,15 @@
@Override
public void buildDex(DexBuilder builder) {
- int object = builder.allocatedRegister(object(), getNumber());
+ // If the monitor object is an argument, we use the argument register for all the monitor
+ // enters and exits in order to not confuse the Art verifier lock verification code.
+ // This is best effort. If the argument happens to be in a very high register we cannot
+ // do it and the lock verification can hit a case where it gets confused. Not much we
+ // can do about that, but this should avoid it in the most common cases.
+ int object = builder.argumentOrAllocateRegister(object(), getNumber());
+ if (object > maxInValueRegister()) {
+ object = builder.allocatedRegister(object(), getNumber());
+ }
if (type == Type.ENTER) {
builder.add(this, new MonitorEnter(object));
} else {
diff --git a/src/main/java/com/android/tools/r8/ir/code/Move.java b/src/main/java/com/android/tools/r8/ir/code/Move.java
index 6f70c57..91e19f0 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Move.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Move.java
@@ -8,7 +8,7 @@
import com.android.tools.r8.graph.AppInfo;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
public class Move extends Instruction {
@@ -81,7 +81,7 @@
}
@Override
- public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
- return InliningConstraint.ALWAYS;
+ public Constraint inliningConstraint(AppInfo info, DexType holder) {
+ return Constraint.ALWAYS;
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/Phi.java b/src/main/java/com/android/tools/r8/ir/code/Phi.java
index 5912f0f..de44e83 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Phi.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Phi.java
@@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.ir.code;
+import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.graph.DebugLocalInfo;
import com.android.tools.r8.ir.code.BasicBlock.EdgeType;
import com.android.tools.r8.ir.conversion.IRBuilder;
@@ -60,6 +61,9 @@
// exactly once by adding the operands.
assert operands.isEmpty();
boolean canBeNull = false;
+ if (block.getPredecessors().size() == 0) {
+ throwUndefinedValueError();
+ }
for (BasicBlock pred : block.getPredecessors()) {
EdgeType edgeType = pred.getEdgeType(block);
// Since this read has been delayed we must provide the local info for the value.
@@ -79,6 +83,9 @@
// exactly once by adding the operands.
assert this.operands.isEmpty();
boolean canBeNull = false;
+ if (operands.size() == 0) {
+ throwUndefinedValueError();
+ }
for (Value operand : operands) {
canBeNull |= operand.canBeNull();
appendOperand(operand);
@@ -89,6 +96,13 @@
removeTrivialPhi();
}
+ private void throwUndefinedValueError() {
+ throw new CompilationError(
+ "Undefined value encountered during compilation. "
+ + "This is typically caused by invalid dex input that uses a register "
+ + "that is not define on all control-flow paths leading to the use.");
+ }
+
private void appendOperand(Value operand) {
operands.add(operand);
operand.addPhiUser(this);
@@ -174,9 +188,6 @@
same = op;
}
assert isTrivialPhi();
- if (same == null) {
- same = Value.UNDEFINED;
- }
// Removing this phi, so get rid of it as a phi user from all of the operands to avoid
// recursively getting back here with the same phi. If the phi has itself as an operand
// that also removes the self-reference.
diff --git a/src/main/java/com/android/tools/r8/ir/code/Return.java b/src/main/java/com/android/tools/r8/ir/code/Return.java
index 2819c58..142206c 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Return.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Return.java
@@ -11,7 +11,7 @@
import com.android.tools.r8.graph.AppInfo;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
public class Return extends JumpInstruction {
@@ -110,7 +110,7 @@
}
@Override
- public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
- return InliningConstraint.ALWAYS;
+ public Constraint inliningConstraint(AppInfo info, DexType holder) {
+ return Constraint.ALWAYS;
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/Throw.java b/src/main/java/com/android/tools/r8/ir/code/Throw.java
index e0ba866..9bd6a81 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Throw.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Throw.java
@@ -7,7 +7,7 @@
import com.android.tools.r8.graph.AppInfo;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
public class Throw extends JumpInstruction {
@@ -63,7 +63,7 @@
}
@Override
- public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
- return InliningConstraint.ALWAYS;
+ public Constraint inliningConstraint(AppInfo info, DexType holder) {
+ return Constraint.ALWAYS;
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/Unop.java b/src/main/java/com/android/tools/r8/ir/code/Unop.java
index ff1744a..174b7b6 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Unop.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Unop.java
@@ -6,7 +6,7 @@
import com.android.tools.r8.dex.Constants;
import com.android.tools.r8.graph.AppInfo;
import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
abstract public class Unop extends Instruction {
@@ -43,7 +43,7 @@
}
@Override
- public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
- return InliningConstraint.ALWAYS;
+ public Constraint inliningConstraint(AppInfo info, DexType holder) {
+ return Constraint.ALWAYS;
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
index bd48134..c506867 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
@@ -19,6 +19,8 @@
import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
import com.google.common.collect.Sets;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -134,10 +136,14 @@
private final List<DexEncodedMethod> leaves;
private final boolean brokeCycles;
+ private final Map<DexEncodedMethod, Set<DexEncodedMethod>> cycleBreakingCalls;
- private Leaves(List<DexEncodedMethod> leaves, boolean brokeCycles) {
+ private Leaves(List<DexEncodedMethod> leaves, boolean brokeCycles,
+ Map<DexEncodedMethod, Set<DexEncodedMethod>> cycleBreakingCalls) {
this.leaves = leaves;
this.brokeCycles = brokeCycles;
+ this.cycleBreakingCalls = cycleBreakingCalls;
+ assert brokeCycles == (cycleBreakingCalls.size() != 0);
}
public int size() {
@@ -152,31 +158,38 @@
return brokeCycles;
}
+ /**
+ * Calls that were broken to produce the leaves.
+ *
+ * If {@link Leaves#breakCycles()} return <code>true</code> this provides the calls for each
+ * leaf that were broken.
+ *
+ * NOTE: The broken calls are not confined to the set of leaves.
+ */
+ public Map<DexEncodedMethod, Set<DexEncodedMethod>> getCycleBreakingCalls() {
+ return cycleBreakingCalls;
+ }
+
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Leaves: ");
builder.append(leaves.size());
builder.append("\n");
- builder.append(brokeCycles ? "Cycles broken" : "No cycles broken");
+ builder.append(brokeCycles ? "Call cycles broken" : "No call cycles broken");
return builder.toString();
}
}
- private final GraphLense graphLense;
private final Map<DexEncodedMethod, Node> nodes = new LinkedHashMap<>();
private List<Node> leaves = null;
private Set<DexEncodedMethod> singleCallSite = Sets.newIdentityHashSet();
private Set<DexEncodedMethod> doubleCallSite = Sets.newIdentityHashSet();
- private CallGraph(GraphLense graphLense) {
- this.graphLense = graphLense;
- }
-
public static CallGraph build(DexApplication application, AppInfoWithSubtyping appInfo,
GraphLense graphLense) {
- CallGraph graph = new CallGraph(graphLense);
+ CallGraph graph = new CallGraph();
for (DexClass clazz : application.classes()) {
for (DexEncodedMethod method : clazz.directMethods()) {
@@ -279,18 +292,19 @@
* <p>
*
* @return object with the leaves as a List of {@link DexEncodedMethod} and <code>boolean</code>
- * indication of whether cycels where broken to produce leaves. <code>null</code> if the graph is
+ * indication of whether cycles were broken to produce leaves. <code>null</code> if the graph is
* empty.
*/
public Leaves pickLeaves() {
boolean cyclesBroken = false;
+ Map<DexEncodedMethod, Set<DexEncodedMethod>> brokenCalls = Collections.emptyMap();
if (isEmpty()) {
return null;
}
List<DexEncodedMethod> leaves = removeLeaves();
if (leaves.size() == 0) {
// The graph had no more leaves, so break cycles to construct leaves.
- breakCycles();
+ brokenCalls = breakCycles();
cyclesBroken = true;
leaves = removeLeaves();
}
@@ -298,7 +312,7 @@
for (DexEncodedMethod leaf : leaves) {
assert !leaf.isProcessed();
}
- return new Leaves(leaves, cyclesBroken);
+ return new Leaves(leaves, cyclesBroken, brokenCalls);
}
/**
@@ -309,35 +323,41 @@
* (outgoing) degree.
* <p>
* It will avoid removing edges from bridge-methods if possible.
+ * <p>
+ * Returns the calls that were broken.
*/
- private void breakCycles() {
+ private Map<DexEncodedMethod, Set<DexEncodedMethod>> breakCycles() {
+ Map<DexEncodedMethod, Set<DexEncodedMethod>> brokenCalls = new IdentityHashMap<>();
// Break non bridges with degree 1.
int minDegree = nodes.size();
for (Node node : nodes.values()) {
// Break cycles and add all leaves created in the process.
if (!node.isBridge() && node.callDegree() <= 1) {
assert node.callDegree() == 1;
- removeAllCalls(node);
+ Set<DexEncodedMethod> calls = removeAllCalls(node);
leaves.add(node);
+ brokenCalls.put(node.method, calls);
} else {
minDegree = Integer.min(minDegree, node.callDegree());
}
}
- // Return if new leaves where created.
+ // Return if new leaves were created.
if (leaves.size() > 0) {
- return;
+ return brokenCalls;
}
// Break methods with the found minimum degree and add all leaves created in the process.
for (Node node : nodes.values()) {
if (node.callDegree() <= minDegree) {
assert node.callDegree() == minDegree;
- removeAllCalls(node);
+ Set<DexEncodedMethod> calls = removeAllCalls(node);
leaves.add(node);
+ brokenCalls.put(node.method, calls);
}
}
assert leaves.size() > 0;
+ return brokenCalls;
}
synchronized private Node ensureMethodNode(DexEncodedMethod method) {
@@ -356,11 +376,14 @@
callee.invokeCount++;
}
- private void removeAllCalls(Node node) {
+ private Set<DexEncodedMethod> removeAllCalls(Node node) {
+ Set<DexEncodedMethod> calls = Sets.newIdentityHashSet();
for (Node call : node.calls) {
+ calls.add(call.method);
call.callees.remove(node);
}
node.calls.clear();
+ return calls;
}
private void remove(Node node, List<Node> leaves) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
index 4484aec..50afe5f 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
@@ -318,6 +318,12 @@
return registerAllocator.getRegisterForValue(value, instructionNumber);
}
+ // Get the argument register for a value if it is an argument, otherwise returns the
+ // allocated register at the instruction number.
+ public int argumentOrAllocateRegister(Value value, int instructionNumber) {
+ return registerAllocator.getArgumentOrAllocateRegisterForValue(value, instructionNumber);
+ }
+
public boolean argumentValueUsesHighRegister(Value value, int instructionNumber) {
return registerAllocator.argumentValueUsesHighRegister(value, instructionNumber);
}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
index 7e47ed2..118d0eb 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
@@ -268,7 +268,7 @@
}
@Override
- public boolean traceInstruction(int index, IRBuilder builder) {
+ public int traceInstruction(int index, IRBuilder builder) {
Instruction dex = code.instructions[index];
int offset = dex.getOffset();
assert !dex.isPayload();
@@ -277,9 +277,9 @@
// Check that we don't ever have instructions that can throw and have targets.
assert !dex.canThrow();
for (int relativeOffset : targets) {
- builder.ensureSuccessorBlock(offset + relativeOffset);
+ builder.ensureNormalSuccessorBlock(offset, offset + relativeOffset);
}
- return true;
+ return index;
}
if (dex.canThrow()) {
// If the instruction can throw and is in a try block, add edges to its catch successors.
@@ -297,7 +297,7 @@
builder.ensureBlockWithoutEnqueuing(tryRangeStartAddress);
// Edge to exceptional successors.
for (Integer handlerOffset : getUniqueTryHandlerOffsets(tryRange)) {
- builder.ensureSuccessorBlock(handlerOffset);
+ builder.ensureExceptionalSuccessorBlock(offset, handlerOffset);
}
// If the following instruction is a move-result include it in this (the invokes) block.
if (index + 1 < code.instructions.length && isMoveResult(code.instructions[index + 1])) {
@@ -307,29 +307,29 @@
}
// Edge to normal successor if any (fallthrough).
if (!(dex instanceof Throw)) {
- builder.ensureSuccessorBlock(dex.getOffset() + dex.getSize());
+ builder.ensureNormalSuccessorBlock(offset, dex.getOffset() + dex.getSize());
}
- return true;
+ return index;
}
// Close the block if the instruction is a throw, otherwise the block remains open.
- return dex instanceof Throw;
+ return dex instanceof Throw ? index : -1;
}
if (dex.isSwitch()) {
// TODO(zerny): Remove this from block computation.
switchPayloadResolver.addPayloadUser(dex);
for (int target : switchPayloadResolver.absoluteTargets(dex)) {
- builder.ensureSuccessorBlock(target);
+ builder.ensureNormalSuccessorBlock(offset, target);
}
- builder.ensureSuccessorBlock(offset + dex.getSize());
- return true;
+ builder.ensureNormalSuccessorBlock(offset, offset + dex.getSize());
+ return index;
}
// TODO(zerny): Remove this from block computation.
if (dex.hasPayload()) {
arrayFilledDataPayloadResolver.addPayloadUser((FillArrayData) dex);
}
// This instruction does not close the block.
- return false;
+ return -1;
}
private boolean inTryRange(Try tryItem, int offset) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
index bd8a469..8cd9a2a 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -81,9 +81,14 @@
import com.android.tools.r8.ir.code.ValueNumberGenerator;
import com.android.tools.r8.ir.code.Xor;
import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.StringUtils.BraceType;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
+import it.unimi.dsi.fastutil.ints.IntArraySet;
+import it.unimi.dsi.fastutil.ints.IntIterator;
+import it.unimi.dsi.fastutil.ints.IntSet;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
@@ -101,6 +106,8 @@
*/
public class IRBuilder {
+ public static final int INITIAL_BLOCK_OFFSET = -1;
+
// SSA construction uses a worklist of basic blocks reachable from the entry and their
// instruction offsets.
private static class WorklistItem {
@@ -158,8 +165,64 @@
}
}
+ public static class BlockInfo {
+ BasicBlock block = new BasicBlock();
+ IntSet normalPredecessors = new IntArraySet();
+ IntSet normalSuccessors = new IntArraySet();
+ IntSet exceptionalPredecessors = new IntArraySet();
+ IntSet exceptionalSuccessors = new IntArraySet();
+
+ void addNormalPredecessor(int offset) {
+ normalPredecessors.add(offset);
+ }
+
+ void addNormalSuccessor(int offset) {
+ normalSuccessors.add(offset);
+ }
+
+ void replaceNormalPredecessor(int existing, int replacement) {
+ normalPredecessors.remove(existing);
+ normalPredecessors.add(replacement);
+ }
+
+ void addExceptionalPredecessor(int offset) {
+ exceptionalPredecessors.add(offset);
+ }
+
+ void addExceptionalSuccessor(int offset) {
+ exceptionalSuccessors.add(offset);
+ }
+
+ int predecessorCount() {
+ return normalPredecessors.size() + exceptionalPredecessors.size();
+ }
+
+ BlockInfo split(
+ int blockStartOffset, int fallthroughOffset, Int2ReferenceMap<BlockInfo> targets) {
+ BlockInfo fallthroughInfo = new BlockInfo();
+ fallthroughInfo.normalPredecessors = new IntArraySet(Collections.singleton(blockStartOffset));
+ fallthroughInfo.block.incrementUnfilledPredecessorCount();
+ // Move all normal successors to the fallthrough block.
+ IntIterator normalSuccessorIterator = normalSuccessors.iterator();
+ while (normalSuccessorIterator.hasNext()) {
+ BlockInfo normalSuccessor = targets.get(normalSuccessorIterator.nextInt());
+ normalSuccessor.replaceNormalPredecessor(blockStartOffset, fallthroughOffset);
+ }
+ fallthroughInfo.normalSuccessors = normalSuccessors;
+ normalSuccessors = new IntArraySet(Collections.singleton(fallthroughOffset));
+ // Copy all exceptional successors to the fallthrough block.
+ IntIterator exceptionalSuccessorIterator = fallthroughInfo.exceptionalSuccessors.iterator();
+ while (exceptionalSuccessorIterator.hasNext()) {
+ BlockInfo exceptionalSuccessor = targets.get(exceptionalSuccessorIterator.nextInt());
+ exceptionalSuccessor.addExceptionalPredecessor(fallthroughOffset);
+ }
+ fallthroughInfo.exceptionalSuccessors = new IntArraySet(this.exceptionalSuccessors);
+ return fallthroughInfo;
+ }
+ }
+
// Mapping from instruction offsets to basic-block targets.
- private final Map<Integer, BasicBlock> targets = new HashMap<>();
+ private final Int2ReferenceSortedMap<BlockInfo> targets = new Int2ReferenceAVLTreeMap<>();
// Worklist of reachable blocks.
private final Queue<Integer> traceBlocksWorklist = new LinkedList<>();
@@ -219,6 +282,10 @@
this.options = options;
}
+ public Int2ReferenceSortedMap<BlockInfo> getCFG() {
+ return targets;
+ }
+
private void addToWorklist(BasicBlock block, int firstInstructionIndex) {
// TODO(ager): Filter out the ones that are already in the worklist, mark bit in block?
if (!block.isFilled()) {
@@ -239,13 +306,8 @@
assert source != null;
source.setUp();
- // Create entry block.
- setCurrentBlock(new BasicBlock());
-
- // If the method needs a prelude, the entry block must never be the target of other blocks.
- if (!source.needsPrelude()) {
- targets.put(0, currentBlock);
- }
+ // Create entry block (at a non-targetable address).
+ targets.put(INITIAL_BLOCK_OFFSET, new BlockInfo());
// Process reachable code paths starting from instruction 0.
processedInstructions = new boolean[source.instructionCount()];
@@ -260,15 +322,18 @@
// Process each instruction until the block is closed.
for (int index = startOfBlockIndex; index < source.instructionCount(); ++index) {
markIndexProcessed(index);
- boolean closed = source.traceInstruction(index, this);
- if (closed) {
+ int closedAt = source.traceInstruction(index, this);
+ if (closedAt != -1) {
+ if (closedAt + 1 < source.instructionCount()) {
+ ensureBlockWithoutEnqueuing(source.instructionOffset(closedAt + 1));
+ }
break;
}
// If the next instruction starts a block, fall through to it.
if (index + 1 < source.instructionCount()) {
int nextOffset = source.instructionOffset(index + 1);
- if (getTarget(nextOffset) != null) {
- ensureSuccessorBlock(nextOffset);
+ if (targets.get(nextOffset) != null) {
+ ensureNormalSuccessorBlock(startOfBlockOffset, nextOffset);
break;
}
}
@@ -276,6 +341,7 @@
}
processedInstructions = null;
+ setCurrentBlock(targets.get(INITIAL_BLOCK_OFFSET).block);
source.buildPrelude(this);
// Process normal blocks reachable from the entry block using a worklist of reachable
@@ -311,9 +377,6 @@
// necessary.
splitCriticalEdges();
- // Consistency check.
- assert phiOperandsAreConsistent();
-
// Package up the IR code.
IRCode ir = new IRCode(method, blocks, normalExitBlock, valueNumberGenerator);
@@ -340,11 +403,36 @@
private boolean verifyFilledPredecessors() {
for (BasicBlock block : blocks) {
- assert block.verifyFilledPredecessors();
+ assert verifyFilledPredecessors(block);
}
return true;
}
+ private boolean verifyFilledPredecessors(BasicBlock block) {
+ assert block.verifyFilledPredecessors();
+ // TODO(zerny): Consider moving the validation of the initial control-flow graph to after its
+ // construction and prior to building the IR.
+ for (BlockInfo info : targets.values()) {
+ if (info != null && info.block == block) {
+ assert info.predecessorCount() == block.getPredecessors().size();
+ assert info.normalSuccessors.size() == block.getNormalSucessors().size();
+ if (block.hasCatchHandlers()) {
+ assert info.exceptionalSuccessors.size()
+ == block.getCatchHandlers().getUniqueTargets().size();
+ } else {
+ assert !block.canThrow()
+ || info.exceptionalSuccessors.isEmpty()
+ || (info.exceptionalSuccessors.size() == 1
+ && info.exceptionalSuccessors.iterator().nextInt() < 0);
+ }
+ return true;
+ }
+ }
+ // There are places where we add in new blocks that we do not represent in the initial CFG.
+ // TODO(zerny): Should we maintain the initial CFG after instruction building?
+ return true;
+ }
+
private int processWorklist(int blockNumber) {
for (WorklistItem item = ssaWorklist.poll(); item != null; item = ssaWorklist.poll()) {
if (item.block.isFilled()) {
@@ -359,11 +447,11 @@
source.closedCurrentBlock();
break;
}
- BasicBlock block = getTarget(source.instructionOffset(i));
- if (block != null && block != currentBlock) {
- closeCurrentBlockWithFallThrough(block);
+ BlockInfo info = targets.get(source.instructionOffset(i));
+ if (info != null && info.block != currentBlock) {
+ closeCurrentBlockWithFallThrough(info.block);
source.closedCurrentBlockWithFallthrough(i);
- addToWorklist(block, i);
+ addToWorklist(info.block, i);
break;
}
source.buildInstruction(this, i);
@@ -722,7 +810,7 @@
public void addGoto(int targetOffset) {
addInstruction(new Goto());
BasicBlock targetBlock = getTarget(targetOffset);
- if (currentBlock.isCatchSuccessor(targetBlock)) {
+ if (currentBlock.hasCatchSuccessor(targetBlock)) {
needGotoToCatchBlocks.add(new BasicBlock.Pair(currentBlock, targetBlock));
} else {
currentBlock.link(targetBlock);
@@ -998,9 +1086,15 @@
public void addMoveException(int dest) {
Value out = writeRegister(dest, MoveType.OBJECT, ThrowingInfo.NO_THROW);
+ assert out.getDebugInfo() == null;
MoveException instruction = new MoveException(out);
assert !instruction.instructionTypeCanThrow();
- assert currentBlock.getInstructions().isEmpty();
+ if (!currentBlock.getInstructions().isEmpty()) {
+ throw new CompilationError("Invalid MoveException instruction encountered. "
+ + "The MoveException instruction is not the first instruction in the block in "
+ + method.qualifiedName()
+ + ".");
+ }
addInstruction(instruction);
}
@@ -1151,7 +1245,8 @@
// If this was a packed switch with only fallthrough cases we can make it a goto.
// Oddly, this does happen.
if (numberOfFallthroughs == numberOfTargets) {
- targets.get(fallthroughOffset).decrementUnfilledPredecessorCount(numberOfFallthroughs);
+ BlockInfo info = targets.get(fallthroughOffset);
+ info.block.decrementUnfilledPredecessorCount(numberOfFallthroughs);
addGoto(fallthroughOffset);
return;
}
@@ -1161,7 +1256,8 @@
int bytesSaved = packedSwitchPayloadSize - sparseSwitchPayloadSize;
// Perform the rewrite if we can reduce the payload size by more than 20%.
if (bytesSaved > (packedSwitchPayloadSize / 5)) {
- targets.get(fallthroughOffset).decrementUnfilledPredecessorCount(numberOfFallthroughs);
+ BlockInfo info = targets.get(fallthroughOffset);
+ info.block.decrementUnfilledPredecessorCount(numberOfFallthroughs);
int nextCaseIndex = 0;
int currentKey = keys[0];
keys = new int[numberOfSparseTargets];
@@ -1458,10 +1554,16 @@
BasicBlock block = new BasicBlock();
blocks.add(block);
block.incrementUnfilledPredecessorCount();
- for (Integer offset : source.getCurrentCatchHandlers().getUniqueTargets()) {
- BasicBlock target = getTarget(offset);
- assert !target.isSealed();
- target.incrementUnfilledPredecessorCount();
+ int freshOffset = INITIAL_BLOCK_OFFSET - 1;
+ while (targets.containsKey(freshOffset)) {
+ freshOffset--;
+ }
+ targets.put(freshOffset, null);
+ for (int offset : source.getCurrentCatchHandlers().getUniqueTargets()) {
+ BlockInfo target = targets.get(offset);
+ assert !target.block.isSealed();
+ target.block.incrementUnfilledPredecessorCount();
+ target.addExceptionalPredecessor(freshOffset);
}
addInstruction(new Goto());
currentBlock.link(block);
@@ -1499,21 +1601,32 @@
// Package (ie, SourceCode accessed) helpers.
// Ensure there is a block starting at offset.
- BasicBlock ensureBlockWithoutEnqueuing(int offset) {
- BasicBlock block = targets.get(offset);
- if (block == null) {
- block = new BasicBlock();
- targets.put(offset, block);
+ BlockInfo ensureBlockWithoutEnqueuing(int offset) {
+ assert offset != INITIAL_BLOCK_OFFSET;
+ BlockInfo info = targets.get(offset);
+ if (info == null) {
// If this is a processed instruction, the block split and it has a fall-through predecessor.
if (offset >= 0 && isOffsetProcessed(offset)) {
- block.incrementUnfilledPredecessorCount();
+ int blockStartOffset = getBlockStartOffset(offset);
+ BlockInfo existing = targets.get(blockStartOffset);
+ info = existing.split(blockStartOffset, offset, targets);
+ } else {
+ info = new BlockInfo();
}
+ targets.put(offset, info);
}
- return block;
+ return info;
+ }
+
+ private int getBlockStartOffset(int offset) {
+ if (targets.containsKey(offset)) {
+ return offset;
+ }
+ return targets.headMap(offset).lastIntKey();
}
// Ensure there is a block starting at offset and add it to the work-list if it needs processing.
- private BasicBlock ensureBlock(int offset) {
+ private BlockInfo ensureBlock(int offset) {
// We don't enqueue negative targets (these are special blocks, eg, an argument prelude).
if (offset >= 0 && !isOffsetProcessed(offset)) {
traceBlocksWorklist.add(offset);
@@ -1550,16 +1663,32 @@
}
// Ensure there is a block at offset and add a predecessor to it.
- BasicBlock ensureSuccessorBlock(int offset) {
- BasicBlock block = ensureBlock(offset);
- block.incrementUnfilledPredecessorCount();
- return block;
+ private void ensureSuccessorBlock(int sourceOffset, int targetOffset, boolean normal) {
+ BlockInfo targetInfo = ensureBlock(targetOffset);
+ int sourceStartOffset = getBlockStartOffset(sourceOffset);
+ BlockInfo sourceInfo = targets.get(sourceStartOffset);
+ if (normal) {
+ sourceInfo.addNormalSuccessor(targetOffset);
+ targetInfo.addNormalPredecessor(sourceStartOffset);
+ } else {
+ sourceInfo.addExceptionalSuccessor(targetOffset);
+ targetInfo.addExceptionalPredecessor(sourceStartOffset);
+ }
+ targetInfo.block.incrementUnfilledPredecessorCount();
+ }
+
+ void ensureNormalSuccessorBlock(int sourceOffset, int targetOffset) {
+ ensureSuccessorBlock(sourceOffset, targetOffset, true);
+ }
+
+ void ensureExceptionalSuccessorBlock(int sourceOffset, int targetOffset) {
+ ensureSuccessorBlock(sourceOffset, targetOffset, false);
}
// Private block helpers.
private BasicBlock getTarget(int offset) {
- return targets.get(offset);
+ return targets.get(offset).block;
}
private void closeCurrentBlock() {
@@ -1577,7 +1706,7 @@
assert currentBlock != null;
flushCurrentDebugPosition();
currentBlock.add(new Goto());
- if (currentBlock.isCatchSuccessor(nextBlock)) {
+ if (currentBlock.hasCatchSuccessor(nextBlock)) {
needGotoToCatchBlocks.add(new BasicBlock.Pair(currentBlock, nextBlock));
} else {
currentBlock.link(nextBlock);
@@ -1657,8 +1786,8 @@
target.getPredecessors().add(newBlock);
// Check that the successor indexes are correct.
- assert source.isCatchSuccessor(newBlock);
- assert !source.isCatchSuccessor(target);
+ assert source.hasCatchSuccessor(newBlock);
+ assert !source.hasCatchSuccessor(target);
// Mark the filled predecessors to the blocks.
if (source.isFilled()) {
@@ -1669,22 +1798,6 @@
return blockNumber;
}
- private boolean phiOperandsAreConsistent() {
- for (BasicBlock block : blocks) {
- if (block.hasIncompletePhis()) {
- StringBuilder builder = new StringBuilder("Incomplete phis in ");
- builder.append(method);
- builder.append(". The following registers appear to be uninitialized: ");
- StringUtils.append(builder, block.getIncompletePhiRegisters(), ", ", BraceType.NONE);
- throw new CompilationError(builder.toString());
- }
- for (Phi phi : block.getPhis()) {
- assert phi.getOperands().size() == block.getPredecessors().size();
- }
- }
- return true;
- }
-
/**
* Change to control-flow graph to avoid repeated phi operands when all the same values
* flow in from multiple predecessors.
@@ -1716,6 +1829,15 @@
public void joinPredecessorsWithIdenticalPhis() {
List<BasicBlock> blocksToAdd = new ArrayList<>();
for (BasicBlock block : blocks) {
+ // Consistency check. At this point there should be no incomplete phis.
+ // If there are, the input is typically dex code that uses a register
+ // that is not defined on all control-flow paths.
+ if (block.hasIncompletePhis()) {
+ throw new CompilationError(
+ "Undefined value encountered during compilation. "
+ + "This is typically caused by invalid dex input that uses a register "
+ + "that is not define on all control-flow paths leading to the use.");
+ }
if (block.entry() instanceof MoveException) {
// TODO: Should we support joining in the presence of move-exception instructions?
continue;
@@ -1768,7 +1890,7 @@
// If any of the edges to the block are critical, we need to insert new blocks on each
// containing the move-exception instruction which must remain the first instruction.
if (block.entry() instanceof MoveException) {
- block.splitCriticalExceptioEdges(valueNumberGenerator,
+ block.splitCriticalExceptionEdges(valueNumberGenerator,
newBlock -> {
newBlock.setNumber(blocks.size() + newBlocks.size());
newBlocks.add(newBlock);
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 8370eb8..a787025 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
@@ -25,7 +25,7 @@
import com.android.tools.r8.ir.optimize.CodeRewriter;
import com.android.tools.r8.ir.optimize.DeadCodeRemover;
import com.android.tools.r8.ir.optimize.Inliner;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
import com.android.tools.r8.ir.optimize.MemberValuePropagation;
import com.android.tools.r8.ir.optimize.Outliner;
import com.android.tools.r8.ir.optimize.PeepholeOptimizer;
@@ -37,11 +37,10 @@
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.Timing;
+import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.List;
-import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -66,6 +65,7 @@
private final LensCodeRewriter lensCodeRewriter;
private final Inliner inliner;
private CallGraph callGraph;
+ private OptimizationFeedback ignoreOptimizationFeedback = new OptimizationFeedbackIgnore();
private DexString highestSortingString;
@@ -204,7 +204,7 @@
boolean matchesMethodFilter = options.methodMatchesFilter(method);
if (matchesMethodFilter) {
if (method.getCode().isJarCode()) {
- rewriteCode(method, Outliner::noProcessing);
+ rewriteCode(method, ignoreOptimizationFeedback, Outliner::noProcessing);
}
updateHighestSortingStrings(method);
}
@@ -236,34 +236,41 @@
// Process the application identifying outlining candidates.
timing.begin("IR conversion phase 1");
+ int count = 0;
+ OptimizationFeedback directFeedback = new OptimizationFeedbackDirect();
+ OptimizationFeedbackDelayed delayedFeedback = new OptimizationFeedbackDelayed();
while (!callGraph.isEmpty()) {
+ count++;
CallGraph.Leaves leaves = callGraph.pickLeaves();
- List<DexEncodedMethod> leafMethods = leaves.getLeaves();
- assert leafMethods.size() > 0;
- // If cycles where broken to produce leaves, don't do parallel processing to keep
- // deterministic output.
- // TODO(37133161): Most likely the failing:
- // java.com.android.tools.r8.internal.R8GMSCoreDeterministicTest
- // is caused by processing in multiple threads.
- if (true || leaves.brokeCycles()) {
- for (DexEncodedMethod method : leafMethods) {
- processMethod(method,
- outliner == null ? Outliner::noProcessing : outliner::identifyCandidates);
- }
- } else {
- List<Future<?>> futures = new ArrayList<>();
- // For testing we have the option to randomize the processing order for the
- // deterministic tests.
- if (options.testing.randomizeCallGraphLeaves) {
- Collections.shuffle(leafMethods, new Random(System.nanoTime()));
- }
- for (DexEncodedMethod method : leafMethods) {
+ List<DexEncodedMethod> methods = leaves.getLeaves();
+ assert methods.size() > 0;
+ List<Future<?>> futures = new ArrayList<>();
+
+ // For testing we have the option to determine the processing order of the methods.
+ if (options.testing.irOrdering != null) {
+ methods = options.testing.irOrdering.apply(methods, leaves);
+ }
+
+ while (methods.size() > 0) {
+ // Process the methods on multiple threads. If cycles where broken collect the
+ // optimization feedback to reprocess methods affected by it. This is required to get
+ // deterministic behaviour, as the processing order within each set of leaves is
+ // non-deterministic.
+ for (DexEncodedMethod method : methods) {
futures.add(executorService.submit(() -> {
processMethod(method,
+ leaves.brokeCycles() ? delayedFeedback : directFeedback,
outliner == null ? Outliner::noProcessing : outliner::identifyCandidates);
}));
}
ThreadUtils.awaitFutures(futures);
+ if (leaves.brokeCycles()) {
+ // If cycles in the call graph were broken, then re-process all methods which are
+ // affected by the optimization feedback of other methods in this group.
+ methods = delayedFeedback.applyAndClear(methods, leaves);
+ } else {
+ methods = ImmutableList.of();
+ }
}
}
timing.end();
@@ -276,8 +283,7 @@
if ((inliner != null) && (inliner.doubleInlineCallers.size() > 0)) {
inliner.applyDoubleInlining = true;
for (DexEncodedMethod method : inliner.doubleInlineCallers) {
- method.markNotProcessed();
- processMethod(method, Outliner::noProcessing);
+ processMethod(method, ignoreOptimizationFeedback, Outliner::noProcessing);
assert method.isProcessed();
}
}
@@ -295,8 +301,7 @@
for (DexEncodedMethod method : outliner.getMethodsSelectedForOutlining()) {
// This is the second time we compile this method first mark it not processed.
assert !method.getCode().isOutlineCode();
- method.markNotProcessed();
- processMethod(method, outliner::applyOutliningCandidate);
+ processMethod(method, ignoreOptimizationFeedback, outliner::applyOutliningCandidate);
assert method.isProcessed();
}
builder.addSynthesizedClass(outlineClass, true);
@@ -377,28 +382,28 @@
public void optimizeSynthesizedMethod(DexEncodedMethod method) {
// Process the generated method, but don't apply any outlining.
- processMethod(method, Outliner::noProcessing);
+ processMethod(method, ignoreOptimizationFeedback, Outliner::noProcessing);
}
private String logCode(InternalOptions options, DexEncodedMethod method) {
return options.useSmaliSyntax ? method.toSmaliString(null) : method.codeToString();
}
- private void processMethod(
- DexEncodedMethod method, BiConsumer<IRCode, DexEncodedMethod> outlineHandler) {
+ private void processMethod(DexEncodedMethod method,
+ OptimizationFeedback feedback,
+ BiConsumer<IRCode, DexEncodedMethod> outlineHandler) {
Code code = method.getCode();
boolean matchesMethodFilter = options.methodMatchesFilter(method);
if (code != null && matchesMethodFilter) {
- assert !method.isProcessed();
- InliningConstraint state = rewriteCode(method, outlineHandler);
- method.markProcessed(state);
+ rewriteCode(method, feedback, outlineHandler);
} else {
// Mark abstract methods as processed as well.
- method.markProcessed(InliningConstraint.NEVER);
+ method.markProcessed(Constraint.NEVER);
}
}
- private InliningConstraint rewriteCode(DexEncodedMethod method,
+ private void rewriteCode(DexEncodedMethod method,
+ OptimizationFeedback feedback,
BiConsumer<IRCode, DexEncodedMethod> outlineHandler) {
if (options.verbose) {
System.out.println("Processing: " + method.toSourceString());
@@ -409,7 +414,8 @@
}
IRCode code = method.buildIR(options);
if (code == null) {
- return InliningConstraint.NEVER;
+ feedback.markProcessed(method, Constraint.NEVER);
+ return;
}
if (Log.ENABLED) {
Log.debug(getClass(), "Initial (SSA) flow graph for %s:\n%s", method.toSourceString(), code);
@@ -470,7 +476,7 @@
}
codeRewriter.shortenLiveRanges(code);
- codeRewriter.identifyReturnsArgument(method, code);
+ codeRewriter.identifyReturnsArgument(method, code, feedback);
// Insert code to log arguments if requested.
if (options.methodMatchesLogArgumentsFilter(method)) {
@@ -489,10 +495,13 @@
printMethod(code, "Final IR (non-SSA)");
// After all the optimizations have take place, we compute whether method should be inlined.
+ Constraint state;
if (!options.inlineAccessors || inliner == null) {
- return InliningConstraint.NEVER;
+ state = Constraint.NEVER;
+ } else {
+ state = inliner.identifySimpleMethods(code, method);
}
- return inliner.identifySimpleMethods(code, method);
+ feedback.markProcessed(method, state);
}
private void updateHighestSortingStrings(DexEncodedMethod method) {
@@ -547,7 +556,7 @@
// Always perform dead code elimination before register allocation. The register allocator
// does not allow dead code (to make sure that we do not waste registers for unneeded values).
DeadCodeRemover.removeDeadCode(code, codeRewriter, options);
- LinearScanRegisterAllocator registerAllocator = new LinearScanRegisterAllocator(code);
+ LinearScanRegisterAllocator registerAllocator = new LinearScanRegisterAllocator(code, options);
registerAllocator.allocateRegisters(options.debug);
printMethod(code, "After register allocation (non-SSA)");
printLiveRanges(registerAllocator, "Final live ranges.");
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
index 5647878..12be2c2 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
@@ -28,17 +28,21 @@
import com.android.tools.r8.ir.code.MoveType;
import com.android.tools.r8.ir.code.NumericType;
import com.android.tools.r8.ir.code.Switch;
+import com.android.tools.r8.ir.conversion.IRBuilder.BlockInfo;
import com.android.tools.r8.ir.conversion.JarState.Local;
import com.android.tools.r8.ir.conversion.JarState.Slot;
import com.android.tools.r8.logging.Log;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.Queue;
import java.util.Set;
import java.util.function.BiConsumer;
import org.objectweb.asm.Handle;
@@ -66,7 +70,6 @@
import org.objectweb.asm.util.Textifier;
import org.objectweb.asm.util.TraceMethodVisitor;
-
public class JarSourceCode implements SourceCode {
// Try-catch block wrapper containing resolved offsets.
@@ -110,6 +113,16 @@
}
}
+ private static class JarStateWorklistItem {
+ BlockInfo blockInfo;
+ int instructionIndex;
+
+ public JarStateWorklistItem(BlockInfo blockInfo, int instructionIndex) {
+ this.blockInfo = blockInfo;
+ this.instructionIndex = instructionIndex;
+ }
+ }
+
// Various descriptors.
private static final String INT_ARRAY_DESC = "[I";
private static final String REFLECT_ARRAY_DESC = "Ljava/lang/reflect/Array;";
@@ -144,7 +157,7 @@
// thus that the monitor-entry prelude (part of the argument block which must not have a try-catch
// successor) is not joined with the first instruction block (which likely will have a try-catch
// successor).
- private static final int EXCEPTIONAL_SYNC_EXIT_OFFSET = -1;
+ private static final int EXCEPTIONAL_SYNC_EXIT_OFFSET = -2;
private static final TryCatchBlock EXCEPTIONAL_SYNC_EXIT =
new TryCatchBlock(EXCEPTIONAL_SYNC_EXIT_OFFSET, 0, Integer.MAX_VALUE, null);
@@ -295,6 +308,70 @@
builder.addDebugUninitialized(localRegister, constType(localType));
}
}
+ computeBlockEntryJarStates(builder);
+ }
+
+ private void computeBlockEntryJarStates(IRBuilder builder) {
+ Int2ReferenceSortedMap<BlockInfo> CFG = builder.getCFG();
+ Queue<JarStateWorklistItem> worklist = new LinkedList<>();
+ BlockInfo entry = CFG.get(IRBuilder.INITIAL_BLOCK_OFFSET);
+ if (CFG.get(0) != null) {
+ entry = CFG.get(0);
+ }
+ worklist.add(new JarStateWorklistItem(entry, 0));
+ state.recordStateForTarget(0, this);
+ for (JarStateWorklistItem item = worklist.poll(); item != null; item = worklist.poll()) {
+ state.restoreState(item.instructionIndex);
+ // If the block being restored is a try-catch handler push the exception on the stack.
+ for (int i = 0; i < node.tryCatchBlocks.size(); i++) {
+ TryCatchBlockNode tryCatchBlockNode = (TryCatchBlockNode) node.tryCatchBlocks.get(i);
+ if (tryCatchBlockNode.handler == getInstruction(item.instructionIndex)) {
+ state.push(THROWABLE_TYPE);
+ break;
+ }
+ }
+ // Iterate each of the instructions in the block to compute the outgoing JarState.
+ for (int i = item.instructionIndex; i <= instructionCount(); ++i) {
+ // If we are at the end of the instruction stream or if we have reached the start
+ // of a new block, propagate the state to all successors and add the ones
+ // that changed to the worklist.
+ if (i == instructionCount() || (i != item.instructionIndex && CFG.containsKey(i))) {
+ item.blockInfo.normalSuccessors.iterator().forEachRemaining(offset -> {
+ if (state.recordStateForTarget(offset, this)) {
+ if (offset >= 0) {
+ worklist.add(new JarStateWorklistItem(CFG.get(offset.intValue()), offset));
+ }
+ }
+ });
+ item.blockInfo.exceptionalSuccessors.iterator().forEachRemaining(offset -> {
+ if (state.recordStateForExceptionalTarget(offset, this)) {
+ if (offset >= 0) {
+ worklist.add(new JarStateWorklistItem(CFG.get(offset.intValue()), offset));
+ }
+ }
+ });
+ break;
+ }
+
+ AbstractInsnNode insn = getInstruction(i);
+ updateState(insn);
+
+ if (!isControlFlowInstruction(insn)) {
+ updateStateForLocalVariableEnd(insn);
+ }
+ }
+ }
+ }
+
+ private void updateStateForLocalVariableEnd(AbstractInsnNode insn) {
+ assert !isControlFlowInstruction(insn);
+ if (!(insn.getNext() instanceof LabelNode)) {
+ return;
+ }
+ // If the label is the end of any local-variable scopes end the locals.
+ LabelNode label = (LabelNode) insn.getNext();
+ List<Local> locals = state.getLocalsToClose(label);
+ state.closeLocals(locals);
}
@Override
@@ -308,7 +385,6 @@
private void buildExceptionalPostlude(IRBuilder builder) {
assert isSynchronized();
- assert state.isInvalid();
generatingMethodSynchronization = true;
int exceptionRegister = 0; // We are exiting the method so we just overwrite register 0.
builder.addMoveException(exceptionRegister);
@@ -324,14 +400,10 @@
@Override
public void closedCurrentBlockWithFallthrough(int fallthroughInstructionIndex) {
- assert !state.isInvalid();
- state.recordStateForTarget(fallthroughInstructionIndex, this);
- state.invalidateState();
}
@Override
public void closedCurrentBlock() {
- assert state.isInvalid();
}
@Override
@@ -344,8 +416,8 @@
currentInstruction = insn;
assert verifyExceptionEdgesAreRecorded(insn);
- // Restore the state if invalid.
- if (state.isInvalid()) {
+ // If a new block is starting here, we restore the computed JarState.
+ if (builder.getCFG().containsKey(instructionIndex) || instructionIndex == 0) {
state.restoreState(instructionIndex);
// If the block being restored is a try-catch handler push the exception on the stack.
for (int i = 0; i < node.tryCatchBlocks.size(); i++) {
@@ -406,7 +478,7 @@
@Override
public DebugLocalInfo getCurrentLocal(int register) {
- return state.getLocalInfoForRegister(register);
+ return generatingMethodSynchronization ? null : state.getLocalInfoForRegister(register);
}
@Override
@@ -519,24 +591,24 @@
}
@Override
- public boolean traceInstruction(int index, IRBuilder builder) {
+ public int traceInstruction(int index, IRBuilder builder) {
AbstractInsnNode insn = getInstruction(index);
// Exit early on no-op instructions.
if (insn instanceof LabelNode || insn instanceof LineNumberNode) {
- return false;
+ return -1;
}
// If this instruction exits, close this block.
if (isReturn(insn)) {
- return true;
+ return index;
}
// For each target ensure a basic block and close this block.
int[] targets = getTargets(insn);
if (targets != NO_TARGETS) {
assert !canThrow(insn);
for (int target : targets) {
- builder.ensureSuccessorBlock(target);
+ builder.ensureNormalSuccessorBlock(index, target);
}
- return true;
+ return index;
}
if (canThrow(insn)) {
List<TryCatchBlock> tryCatchBlocks = getTryHandlers(insn);
@@ -549,20 +621,20 @@
int handler = tryCatchBlock.getHandler();
if (!seenHandlerOffsets.contains(handler)) {
seenHandlerOffsets.add(handler);
- builder.ensureSuccessorBlock(handler);
+ builder.ensureExceptionalSuccessorBlock(index, handler);
}
}
// Edge to normal successor if any (fallthrough).
if (!isThrow(insn)) {
- builder.ensureSuccessorBlock(getOffset(insn.getNext()));
+ builder.ensureNormalSuccessorBlock(index, getOffset(insn.getNext()));
}
- return true;
+ return index;
}
// If the throwable instruction is "throw" it closes the block.
- return isThrow(insn);
+ return isThrow(insn) ? index : -1;
}
// This instruction does not close the block.
- return false;
+ return -1;
}
private List<TryCatchBlock> getPotentialTryHandlers(AbstractInsnNode insn) {
@@ -988,6 +1060,674 @@
}
}
+ // State updating procedures.
+
+ private void updateState(AbstractInsnNode insn) {
+ switch (insn.getType()) {
+ case AbstractInsnNode.INSN:
+ updateState((InsnNode) insn);
+ break;
+ case AbstractInsnNode.INT_INSN:
+ updateState((IntInsnNode) insn);
+ break;
+ case AbstractInsnNode.VAR_INSN:
+ updateState((VarInsnNode) insn);
+ break;
+ case AbstractInsnNode.TYPE_INSN:
+ updateState((TypeInsnNode) insn);
+ break;
+ case AbstractInsnNode.FIELD_INSN:
+ updateState((FieldInsnNode) insn);
+ break;
+ case AbstractInsnNode.METHOD_INSN:
+ updateState((MethodInsnNode) insn);
+ break;
+ case AbstractInsnNode.INVOKE_DYNAMIC_INSN:
+ updateState((InvokeDynamicInsnNode) insn);
+ break;
+ case AbstractInsnNode.JUMP_INSN:
+ updateState((JumpInsnNode) insn);
+ break;
+ case AbstractInsnNode.LABEL:
+ updateState((LabelNode) insn);
+ break;
+ case AbstractInsnNode.LDC_INSN:
+ updateState((LdcInsnNode) insn);
+ break;
+ case AbstractInsnNode.IINC_INSN:
+ updateState((IincInsnNode) insn);
+ break;
+ case AbstractInsnNode.TABLESWITCH_INSN:
+ updateState((TableSwitchInsnNode) insn);
+ break;
+ case AbstractInsnNode.LOOKUPSWITCH_INSN:
+ updateState((LookupSwitchInsnNode) insn);
+ break;
+ case AbstractInsnNode.MULTIANEWARRAY_INSN:
+ updateState((MultiANewArrayInsnNode) insn);
+ break;
+ case AbstractInsnNode.LINE:
+ updateState((LineNumberNode) insn);
+ break;
+ default:
+ throw new Unreachable("Unexpected instruction " + insn);
+ }
+ }
+
+ private void updateState(InsnNode insn) {
+ int opcode = insn.getOpcode();
+ switch (opcode) {
+ case Opcodes.NOP:
+ // Intentionally left empty.
+ break;
+ case Opcodes.ACONST_NULL:
+ state.push(JarState.NULL_TYPE);
+ break;
+ case Opcodes.ICONST_M1:
+ case Opcodes.ICONST_0:
+ case Opcodes.ICONST_1:
+ case Opcodes.ICONST_2:
+ case Opcodes.ICONST_3:
+ case Opcodes.ICONST_4:
+ case Opcodes.ICONST_5:
+ state.push(Type.INT_TYPE);
+ break;
+ case Opcodes.LCONST_0:
+ case Opcodes.LCONST_1:
+ state.push(Type.LONG_TYPE);
+ break;
+ case Opcodes.FCONST_0:
+ case Opcodes.FCONST_1:
+ case Opcodes.FCONST_2:
+ state.push(Type.FLOAT_TYPE);
+ break;
+ case Opcodes.DCONST_0:
+ case Opcodes.DCONST_1:
+ state.push(Type.DOUBLE_TYPE);
+ break;
+ case Opcodes.IALOAD:
+ case Opcodes.LALOAD:
+ case Opcodes.FALOAD:
+ case Opcodes.DALOAD:
+ case Opcodes.AALOAD:
+ case Opcodes.BALOAD:
+ case Opcodes.CALOAD:
+ case Opcodes.SALOAD: {
+ state.pop();
+ Slot array = state.pop(JarState.ARRAY_TYPE);
+ Type elementType = getArrayElementType(array.type);
+ if (elementType == null) {
+ // We propagate the null type, which will then get resolved to an
+ // actual type if we have a non-null type on another flow edge.
+ elementType = JarState.NULL_TYPE;
+ }
+ state.push(elementType);
+ break;
+ }
+ case Opcodes.IASTORE:
+ case Opcodes.LASTORE:
+ case Opcodes.FASTORE:
+ case Opcodes.DASTORE:
+ case Opcodes.AASTORE:
+ case Opcodes.BASTORE:
+ case Opcodes.CASTORE:
+ case Opcodes.SASTORE: {
+ state.pop();
+ state.pop();
+ state.pop();
+ break;
+ }
+ case Opcodes.POP: {
+ Slot value = state.pop();
+ assert value.isCategory1();
+ break;
+ }
+ case Opcodes.POP2: {
+ Slot value = state.pop();
+ if (value.isCategory1()) {
+ Slot value2 = state.pop();
+ assert value2.isCategory1();
+ }
+ break;
+ }
+ case Opcodes.DUP: {
+ Slot value = state.peek();
+ assert value.isCategory1();
+ state.push(value.type);
+ break;
+ }
+ case Opcodes.DUP_X1: {
+ // Stack transformation: ..., v2, v1 -> ..., v1, v2, v1
+ Slot value1 = state.pop();
+ Slot value2 = state.pop();
+ assert value1.isCategory1() && value2.isCategory1();
+ int stack2 = state.push(value1.type);
+ int stack1 = state.push(value2.type);
+ state.push(value1.type);
+ assert value2.register == stack2;
+ assert value1.register == stack1;
+ break;
+ }
+ case Opcodes.DUP_X2: {
+ Slot value1 = state.pop();
+ Slot value2 = state.pop();
+ assert value1.isCategory1();
+ if (value2.isCategory1()) {
+ Slot value3 = state.pop();
+ assert value3.isCategory1();
+ // Stack transformation: ..., v3, v2, v1 -> ..., v1, v3, v2, v1
+ updateStateForDupOneBelowTwo(value3, value2, value1);
+ } else {
+ // Stack transformation: ..., w2, v1 -> ..., v1, w2, v1
+ updateStateForDupOneBelowOne(value2, value1);
+ }
+ break;
+ }
+ case Opcodes.DUP2: {
+ Slot value1 = state.pop();
+ if (value1.isCategory1()) {
+ Slot value2 = state.pop();
+ // Stack transformation: ..., v2, v1 -> ..., v2, v1, v2, v1
+ assert value2.isCategory1();
+ state.push(value2.type);
+ state.push(value1.type);
+ state.push(value2.type);
+ state.push(value1.type);
+ } else {
+ // Stack transformation: ..., w1 -> ..., w1, w1
+ state.push(value1.type);
+ state.push(value1.type);
+ }
+ break;
+ }
+ case Opcodes.DUP2_X1: {
+ Slot value1 = state.pop();
+ Slot value2 = state.pop();
+ assert value2.isCategory1();
+ if (value1.isCategory1()) {
+ // Stack transformation: ..., v3, v2, v1 -> v2, v1, v3, v2, v1
+ Slot value3 = state.pop();
+ assert value3.isCategory1();
+ updateStateForDupTwoBelowOne(value3, value2, value1);
+ } else {
+ // Stack transformation: ..., v2, w1 -> ..., w1, v2, w1
+ updateStateForDupOneBelowOne(value2, value1);
+ }
+ break;
+ }
+ case Opcodes.DUP2_X2: {
+ Slot value1 = state.pop();
+ Slot value2 = state.pop();
+ if (!value1.isCategory1() && !value2.isCategory1()) {
+ // State transformation: ..., w2, w1 -> w1, w2, w1
+ updateStateForDupOneBelowOne(value2, value1);
+ } else {
+ Slot value3 = state.pop();
+ if (!value1.isCategory1()) {
+ assert value2.isCategory1();
+ assert value3.isCategory1();
+ // State transformation: ..., v3, v2, w1 -> w1, v3, v2, w1
+ updateStateForDupOneBelowTwo(value3, value2, value1);
+ } else if (!value3.isCategory1()) {
+ assert value1.isCategory1();
+ assert value2.isCategory1();
+ // State transformation: ..., w3, v2, v1 -> v2, v1, w3, v2, v1
+ updateStateForDupTwoBelowOne(value3, value2, value1);
+ } else {
+ Slot value4 = state.pop();
+ assert value1.isCategory1();
+ assert value2.isCategory1();
+ assert value3.isCategory1();
+ assert value4.isCategory1();
+ // State transformation: ..., v4, v3, v2, v1 -> v2, v1, v4, v3, v2, v1
+ updateStateForDupTwoBelowTwo(value4, value3, value2, value1);
+ }
+ }
+ break;
+ }
+ case Opcodes.SWAP: {
+ Slot value1 = state.pop();
+ Slot value2 = state.pop();
+ assert value1.isCategory1() && value2.isCategory1();
+ state.push(value1.type);
+ state.push(value2.type);
+ break;
+ }
+ case Opcodes.IADD:
+ case Opcodes.LADD:
+ case Opcodes.FADD:
+ case Opcodes.DADD:
+ case Opcodes.ISUB:
+ case Opcodes.LSUB:
+ case Opcodes.FSUB:
+ case Opcodes.DSUB:
+ case Opcodes.IMUL:
+ case Opcodes.LMUL:
+ case Opcodes.FMUL:
+ case Opcodes.DMUL:
+ case Opcodes.IDIV:
+ case Opcodes.LDIV:
+ case Opcodes.FDIV:
+ case Opcodes.DDIV:
+ case Opcodes.IREM:
+ case Opcodes.LREM:
+ case Opcodes.FREM:
+ case Opcodes.DREM: {
+ Type type = opType(opcode);
+ state.pop();
+ state.pop();
+ state.push(type);
+ break;
+ }
+ case Opcodes.INEG:
+ case Opcodes.LNEG:
+ case Opcodes.FNEG:
+ case Opcodes.DNEG: {
+ Type type = opType(opcode);
+ state.pop();
+ state.push(type);
+ break;
+ }
+ case Opcodes.ISHL:
+ case Opcodes.LSHL:
+ case Opcodes.ISHR:
+ case Opcodes.LSHR:
+ case Opcodes.IUSHR:
+ case Opcodes.LUSHR: {
+ Type type = opType(opcode);
+ state.pop();
+ state.pop();
+ state.push(type);
+ break;
+ }
+ case Opcodes.IAND:
+ case Opcodes.LAND: {
+ Type type = opcode == Opcodes.IAND ? Type.INT_TYPE : Type.LONG_TYPE;
+ state.pop();
+ state.pop();
+ state.push(type);
+ break;
+ }
+ case Opcodes.IOR:
+ case Opcodes.LOR: {
+ Type type = opcode == Opcodes.IOR ? Type.INT_TYPE : Type.LONG_TYPE;
+ state.pop();
+ state.pop();
+ state.push(type);
+ break;
+ }
+ case Opcodes.IXOR:
+ case Opcodes.LXOR: {
+ Type type = opcode == Opcodes.IXOR ? Type.INT_TYPE : Type.LONG_TYPE;
+ state.pop();
+ state.pop();
+ state.push(type);
+ break;
+ }
+ case Opcodes.I2L:
+ updateStateForConversion(Type.INT_TYPE, Type.LONG_TYPE);
+ break;
+ case Opcodes.I2F:
+ updateStateForConversion(Type.INT_TYPE, Type.FLOAT_TYPE);
+ break;
+ case Opcodes.I2D:
+ updateStateForConversion(Type.INT_TYPE, Type.DOUBLE_TYPE);
+ break;
+ case Opcodes.L2I:
+ updateStateForConversion(Type.LONG_TYPE, Type.INT_TYPE);
+ break;
+ case Opcodes.L2F:
+ updateStateForConversion(Type.LONG_TYPE, Type.FLOAT_TYPE);
+ break;
+ case Opcodes.L2D:
+ updateStateForConversion(Type.LONG_TYPE, Type.DOUBLE_TYPE);
+ break;
+ case Opcodes.F2I:
+ updateStateForConversion(Type.FLOAT_TYPE, Type.INT_TYPE);
+ break;
+ case Opcodes.F2L:
+ updateStateForConversion(Type.FLOAT_TYPE, Type.LONG_TYPE);
+ break;
+ case Opcodes.F2D:
+ updateStateForConversion(Type.FLOAT_TYPE, Type.DOUBLE_TYPE);
+ break;
+ case Opcodes.D2I:
+ updateStateForConversion(Type.DOUBLE_TYPE, Type.INT_TYPE);
+ break;
+ case Opcodes.D2L:
+ updateStateForConversion(Type.DOUBLE_TYPE, Type.LONG_TYPE);
+ break;
+ case Opcodes.D2F:
+ updateStateForConversion(Type.DOUBLE_TYPE, Type.FLOAT_TYPE);
+ break;
+ case Opcodes.I2B:
+ updateStateForConversion(Type.INT_TYPE, Type.BYTE_TYPE);
+ break;
+ case Opcodes.I2C:
+ updateStateForConversion(Type.INT_TYPE, Type.CHAR_TYPE);
+ break;
+ case Opcodes.I2S:
+ updateStateForConversion(Type.INT_TYPE, Type.SHORT_TYPE);
+ break;
+ case Opcodes.LCMP: {
+ state.pop();
+ state.pop();
+ state.push(Type.INT_TYPE);
+ break;
+ }
+ case Opcodes.FCMPL:
+ case Opcodes.FCMPG: {
+ state.pop();
+ state.pop();
+ state.push(Type.INT_TYPE);
+ break;
+ }
+ case Opcodes.DCMPL:
+ case Opcodes.DCMPG: {
+ state.pop();
+ state.pop();
+ state.push(Type.INT_TYPE);
+ break;
+ }
+ case Opcodes.IRETURN: {
+ state.pop();
+ break;
+ }
+ case Opcodes.LRETURN: {
+ state.pop();
+ break;
+ }
+ case Opcodes.FRETURN: {
+ state.pop();
+ break;
+ }
+ case Opcodes.DRETURN: {
+ state.pop();
+ break;
+ }
+ case Opcodes.ARETURN: {
+ state.pop(JarState.REFERENCE_TYPE);
+ break;
+ }
+ case Opcodes.RETURN: {
+ break;
+ }
+ case Opcodes.ARRAYLENGTH: {
+ state.pop(JarState.ARRAY_TYPE);
+ state.push(Type.INT_TYPE);
+ break;
+ }
+ case Opcodes.ATHROW: {
+ state.pop(JarState.OBJECT_TYPE);
+ break;
+ }
+ case Opcodes.MONITORENTER: {
+ state.pop(JarState.REFERENCE_TYPE);
+ break;
+ }
+ case Opcodes.MONITOREXIT: {
+ state.pop(JarState.REFERENCE_TYPE);
+ break;
+ }
+ default:
+ throw new Unreachable("Unexpected Insn opcode: " + insn.getOpcode());
+ }
+ }
+
+ private void updateStateForDupOneBelowTwo(Slot value3, Slot value2, Slot value1) {
+ state.push(value1.type);
+ state.push(value3.type);
+ state.push(value2.type);
+ state.push(value1.type);
+ }
+
+ private void updateStateForDupOneBelowOne(Slot value2, Slot value1) {
+ state.push(value1.type);
+ state.push(value2.type);
+ state.push(value1.type);
+ }
+
+ private void updateStateForDupTwoBelowOne(Slot value3, Slot value2, Slot value1) {
+ state.push(value2.type);
+ state.push(value1.type);
+ state.push(value3.type);
+ state.push(value2.type);
+ state.push(value1.type);
+ }
+
+ private void updateStateForDupTwoBelowTwo(Slot value4, Slot value3, Slot value2, Slot value1) {
+ state.push(value2.type);
+ state.push(value1.type);
+ state.push(value4.type);
+ state.push(value3.type);
+ state.push(value2.type);
+ state.push(value1.type);
+ }
+
+ private void updateState(IntInsnNode insn) {
+ switch (insn.getOpcode()) {
+ case Opcodes.BIPUSH:
+ case Opcodes.SIPUSH: {
+ state.push(Type.INT_TYPE);
+ break;
+ }
+ case Opcodes.NEWARRAY: {
+ String desc = arrayTypeDesc(insn.operand);
+ Type type = Type.getType(desc);
+ state.pop();
+ state.push(type);
+ break;
+ }
+ default:
+ throw new Unreachable("Unexpected IntInsn opcode: " + insn.getOpcode());
+ }
+ }
+
+ private void updateState(VarInsnNode insn) {
+ int opcode = insn.getOpcode();
+ Type expectedType;
+ switch (opcode) {
+ case Opcodes.ILOAD:
+ case Opcodes.ISTORE:
+ expectedType = Type.INT_TYPE;
+ break;
+ case Opcodes.FLOAD:
+ case Opcodes.FSTORE:
+ expectedType = Type.FLOAT_TYPE;
+ break;
+ case Opcodes.LLOAD:
+ case Opcodes.LSTORE:
+ expectedType = Type.LONG_TYPE;
+ break;
+ case Opcodes.DLOAD:
+ case Opcodes.DSTORE:
+ expectedType = Type.DOUBLE_TYPE;
+ break;
+ case Opcodes.ALOAD:
+ case Opcodes.ASTORE:
+ expectedType = JarState.REFERENCE_TYPE;
+ break;
+ case Opcodes.RET: {
+ throw new Unreachable("RET should be handled by the ASM jsr inliner");
+ }
+ default:
+ throw new Unreachable("Unexpected VarInsn opcode: " + insn.getOpcode());
+ }
+ if (Opcodes.ILOAD <= opcode && opcode <= Opcodes.ALOAD) {
+ Slot src = state.readLocal(insn.var, expectedType);
+ state.push(src.type);
+ } else {
+ assert Opcodes.ISTORE <= opcode && opcode <= Opcodes.ASTORE;
+ Slot slot = state.pop();
+ if (slot.type == JarState.NULL_TYPE && expectedType != JarState.REFERENCE_TYPE) {
+ state.writeLocal(insn.var, expectedType);
+ } else {
+ state.writeLocal(insn.var, slot.type);
+ }
+ }
+ }
+
+ private void updateState(TypeInsnNode insn) {
+ Type type = Type.getObjectType(insn.desc);
+ switch (insn.getOpcode()) {
+ case Opcodes.NEW: {
+ state.push(type);
+ break;
+ }
+ case Opcodes.ANEWARRAY: {
+ Type arrayType = makeArrayType(type);
+ state.pop();
+ state.push(arrayType);
+ break;
+ }
+ case Opcodes.CHECKCAST: {
+ // Pop the top value and push it back on with the checked type.
+ state.pop(type);
+ state.push(type);
+ break;
+ }
+ case Opcodes.INSTANCEOF: {
+ state.pop(JarState.REFERENCE_TYPE);
+ state.push(Type.INT_TYPE);
+ break;
+ }
+ default:
+ throw new Unreachable("Unexpected TypeInsn opcode: " + insn.getOpcode());
+ }
+
+ }
+
+ private void updateState(FieldInsnNode insn) {
+ Type type = Type.getType(insn.desc);
+ switch (insn.getOpcode()) {
+ case Opcodes.GETSTATIC:
+ state.push(type);
+ break;
+ case Opcodes.PUTSTATIC:
+ state.pop();
+ break;
+ case Opcodes.GETFIELD: {
+ state.pop(JarState.OBJECT_TYPE);
+ state.push(type);
+ break;
+ }
+ case Opcodes.PUTFIELD: {
+ state.pop();
+ state.pop(JarState.OBJECT_TYPE);
+ break;
+ }
+ default:
+ throw new Unreachable("Unexpected FieldInsn opcode: " + insn.getOpcode());
+ }
+ }
+
+ private void updateState(MethodInsnNode insn) {
+ updateStateForInvoke(insn.desc, insn.getOpcode() != Opcodes.INVOKESTATIC);
+ }
+
+ private void updateState(InvokeDynamicInsnNode insn) {
+ updateStateForInvoke(insn.desc, false /* receiver passed explicitly */);
+ }
+
+ private void updateStateForInvoke(String desc, boolean implicitReceiver) {
+ // Pop arguments.
+ Type[] parameterTypes = Type.getArgumentTypes(desc);
+ state.popReverse(parameterTypes.length);
+ // Pop implicit receiver if needed.
+ if (implicitReceiver) {
+ state.pop();
+ }
+ // Push return value if needed.
+ Type returnType = Type.getReturnType(desc);
+ if (returnType != Type.VOID_TYPE) {
+ state.push(returnType);
+ }
+ }
+
+ private void updateState(JumpInsnNode insn) {
+ int[] targets = getTargets(insn);
+ int opcode = insn.getOpcode();
+ if (Opcodes.IFEQ <= opcode && opcode <= Opcodes.IF_ACMPNE) {
+ assert targets.length == 2;
+ if (opcode <= Opcodes.IFLE) {
+ state.pop();
+ } else {
+ state.pop();
+ state.pop();
+ }
+ } else {
+ switch (opcode) {
+ case Opcodes.GOTO: {
+ assert targets.length == 1;
+ break;
+ }
+ case Opcodes.IFNULL:
+ case Opcodes.IFNONNULL: {
+ state.pop();
+ break;
+ }
+ case Opcodes.JSR: {
+ throw new Unreachable("JSR should be handled by the ASM jsr inliner");
+ }
+ default:
+ throw new Unreachable("Unexpected JumpInsn opcode: " + insn.getOpcode());
+ }
+ }
+ }
+
+ private void updateState(LabelNode insn) {
+ // Open the scope of locals starting at this point.
+ if (insn != initialLabel) {
+ state.openLocals(insn);
+ }
+ }
+
+ private void updateState(LdcInsnNode insn) {
+ if (insn.cst instanceof Type) {
+ Type type = (Type) insn.cst;
+ state.push(type);
+ } else if (insn.cst instanceof String) {
+ state.push(STRING_TYPE);
+ } else if (insn.cst instanceof Long) {
+ state.push(Type.LONG_TYPE);
+ } else if (insn.cst instanceof Double) {
+ state.push(Type.DOUBLE_TYPE);
+ } else if (insn.cst instanceof Integer) {
+ state.push(Type.INT_TYPE);
+ } else {
+ assert insn.cst instanceof Float;
+ state.push(Type.FLOAT_TYPE);
+ }
+ }
+
+ private void updateState(IincInsnNode insn) {
+ state.readLocal(insn.var, Type.INT_TYPE);
+ }
+
+ private void updateState(TableSwitchInsnNode insn) {
+ state.pop();
+ }
+
+ private void updateState(LookupSwitchInsnNode insn) {
+ state.pop();
+ }
+
+ private void updateState(MultiANewArrayInsnNode insn) {
+ // Type of the full array.
+ Type arrayType = Type.getObjectType(insn.desc);
+ state.popReverse(insn.dims, Type.INT_TYPE);
+ state.push(arrayType);
+ }
+
+ private void updateState(LineNumberNode insn) {
+ // Intentionally empty.
+ }
+
+ private void updateStateForConversion(Type from, Type to) {
+ state.pop();
+ state.push(to);
+ }
+
// IR instruction building procedures.
private void build(AbstractInsnNode insn, IRBuilder builder) {
@@ -1510,7 +2250,6 @@
processLocalVariablesAtControlEdge(insn, builder);
}
builder.addThrow(register);
- state.invalidateState();
}
private void addReturn(InsnNode insn, MoveType type, int register, IRBuilder builder) {
@@ -1521,7 +2260,6 @@
} else {
builder.addReturn(type, register);
}
- state.invalidateState();
}
private void dupOneBelowTwo(Slot value3, Slot value2, Slot value1, IRBuilder builder) {
@@ -1914,10 +2652,6 @@
throw new Unreachable("Unexpected JumpInsn opcode: " + insn.getOpcode());
}
}
- for (int target : targets) {
- state.recordStateForTarget(target, this);
- }
- state.invalidateState();
}
private void build(LabelNode insn, IRBuilder builder) {
@@ -1927,11 +2661,6 @@
builder.addDebugLocalStart(local.slot.register, local.info);
}
}
- // Processing local-variable ends is done before closing potential control-flow edges.
- // Record the state for all the try-catch handlers that are active at this label.
- for (TryCatchBlock tryCatchBlock : getTryHandlers(insn)) {
- state.recordStateForExceptionalTarget(tryCatchBlock.getHandler(), this);
- }
}
private void build(LdcInsnNode insn, IRBuilder builder) {
@@ -1969,6 +2698,7 @@
}
private void build(LookupSwitchInsnNode insn, IRBuilder builder) {
+ processLocalVariablesAtControlEdge(insn, builder);
int[] keys = new int[insn.keys.size()];
for (int i = 0; i < insn.keys.size(); i++) {
keys[i] = (int) insn.keys.get(i);
@@ -1980,15 +2710,12 @@
IRBuilder builder) {
int index = state.pop(Type.INT_TYPE).register;
int fallthroughOffset = getOffset(dflt);
- state.recordStateForTarget(fallthroughOffset, this);
int[] labelOffsets = new int[labels.size()];
for (int i = 0; i < labels.size(); i++) {
int offset = getOffset((LabelNode) labels.get(i));
labelOffsets[i] = offset;
- state.recordStateForTarget(offset, this);
}
builder.addSwitch(type, index, keys, fallthroughOffset, labelOffsets);
- state.invalidateState();
}
private void build(MultiANewArrayInsnNode insn, IRBuilder builder) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/JarState.java b/src/main/java/com/android/tools/r8/ir/conversion/JarState.java
index e45fcb2..51a8117 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/JarState.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/JarState.java
@@ -40,13 +40,6 @@
// Type representative for an address type (used by JSR/RET).
public static final Type ADDR_TYPE = Type.getObjectType("<address>");
- private static final Slot INVALID_SLOT = new Slot(-1, null);
- private static final Local INVALID_LOCAL = new Local(INVALID_SLOT, null);
-
- public boolean isInvalid() {
- return stack.peek() == INVALID_SLOT;
- }
-
// Typed mapping from a local slot or stack slot to a virtual register.
public static class Slot {
public final int register;
@@ -54,7 +47,7 @@
@Override
public String toString() {
- return this == INVALID_SLOT ? "<invalid slot>" : "r" + register + ":" + type;
+ return "r" + register + ":" + type;
}
public Slot(int register, Type type) {
@@ -356,15 +349,6 @@
// State procedures.
- public void invalidateState() {
- for (int i = 0; i < locals.length; i++) {
- locals[i] = INVALID_LOCAL;
- }
- stack.clear();
- stack.push(INVALID_SLOT);
- topOfStack = -1;
- }
-
public boolean hasState(int offset) {
return targetStates.get(offset) != null;
}
@@ -374,49 +358,100 @@
assert snapshot != null;
assert locals.length == snapshot.locals.length;
for (int i = 0; i < locals.length; i++) {
- assert locals[i] == INVALID_LOCAL;
locals[i] = snapshot.locals[i];
}
- assert stack.peek() == INVALID_SLOT;
- assert topOfStack == -1;
stack.clear();
stack.addAll(snapshot.stack);
topOfStack = startOfStack + 2 * stack.size();
}
- public void recordStateForTarget(int target, JarSourceCode source) {
- recordStateForTarget(target, locals.clone(), ImmutableList.copyOf(stack), source);
+ public boolean recordStateForTarget(int target, JarSourceCode source) {
+ return recordStateForTarget(target, locals.clone(), ImmutableList.copyOf(stack), source);
}
- public void recordStateForExceptionalTarget(int target, JarSourceCode source) {
- recordStateForTarget(target, locals.clone(), ImmutableList.of(), source);
+ public boolean recordStateForExceptionalTarget(int target, JarSourceCode source) {
+ return recordStateForTarget(target, locals.clone(), ImmutableList.of(), source);
}
- private void recordStateForTarget(int target, Local[] locals, ImmutableList<Slot> stack,
+ private boolean recordStateForTarget(int target, Local[] locals, ImmutableList<Slot> stack,
JarSourceCode source) {
- Snapshot snapshot = targetStates.get(target);
- if (snapshot == null) {
- if (!localVariables.isEmpty()) {
- for (int i = 0; i < locals.length; i++) {
- if (locals[i] != null) {
- locals[i] = new Local(locals[i].slot, null);
- }
- }
- // TODO(zerny): Precompute and sort the local ranges.
- for (LocalVariableNode node : localVariables.keySet()) {
- int startOffset = source.getOffset(node.start);
- int endOffset = source.getOffset(node.end);
- if (startOffset <= target && target < endOffset) {
- int register = getLocalRegister(node.index, Type.getType(node.desc));
- Local local = locals[register];
- locals[register] = new Local(local.slot, localVariables.get(node));
- }
+ if (!localVariables.isEmpty()) {
+ for (int i = 0; i < locals.length; i++) {
+ if (locals[i] != null) {
+ locals[i] = new Local(locals[i].slot, null);
}
}
- targetStates.put(target, new Snapshot(locals, stack));
- return;
+ // TODO(zerny): Precompute and sort the local ranges.
+ for (LocalVariableNode node : localVariables.keySet()) {
+ int startOffset = source.getOffset(node.start);
+ int endOffset = source.getOffset(node.end);
+ if (startOffset <= target && target < endOffset) {
+ int register = getLocalRegister(node.index, Type.getType(node.desc));
+ Local local = locals[register];
+ locals[register] = new Local(local.slot, localVariables.get(node));
+ }
+ }
}
- assert verifyStack(stack, snapshot.stack);
+ Snapshot snapshot = targetStates.get(target);
+ if (snapshot != null) {
+ Local[] newLocals = mergeLocals(snapshot.locals, locals);
+ ImmutableList<Slot> newStack = mergeStacks(snapshot.stack, stack);
+ if (newLocals != snapshot.locals || newStack != snapshot.stack) {
+ targetStates.put(target, new Snapshot(newLocals, newStack));
+ return true;
+ }
+ // The snapshot is up to date - no new type information recoded.
+ return false;
+ }
+ targetStates.put(target, new Snapshot(locals, stack));
+ return true;
+ }
+
+ private ImmutableList<Slot> mergeStacks(
+ ImmutableList<Slot> currentStack, ImmutableList<Slot> newStack) {
+ assert currentStack.size() == newStack.size();
+ List<Slot> mergedStack = null;
+ for (int i = 0; i < currentStack.size(); i++) {
+ if (currentStack.get(i).type == JarState.NULL_TYPE &&
+ newStack.get(i).type != JarState.NULL_TYPE) {
+ if (mergedStack == null) {
+ mergedStack = new ArrayList<>();
+ mergedStack.addAll(currentStack.subList(0, i));
+ }
+ mergedStack.add(newStack.get(i));
+ } else if (mergedStack != null) {
+ assert currentStack.get(i).isCompatibleWith(newStack.get(i).type);
+ mergedStack.add(currentStack.get(i));
+ }
+ }
+ return mergedStack != null ? ImmutableList.copyOf(mergedStack) : currentStack;
+ }
+
+ private Local[] mergeLocals(Local[] currentLocals, Local[] newLocals) {
+ assert currentLocals.length == newLocals.length;
+ Local[] mergedLocals = null;
+ for (int i = 0; i < currentLocals.length; i++) {
+ Local currentLocal = currentLocals[i];
+ Local newLocal = newLocals[i];
+ if (currentLocal == null || newLocal == null) {
+ continue;
+ }
+ // If this assert triggers we can get different debug information for the same local
+ // on different control-flow paths and we will have to merge them.
+ assert currentLocal.info == newLocal.info;
+ if (currentLocal.slot.type == JarState.NULL_TYPE &&
+ newLocal.slot.type != JarState.NULL_TYPE) {
+ if (mergedLocals == null) {
+ mergedLocals = new Local[currentLocals.length];
+ System.arraycopy(currentLocals, 0, mergedLocals, 0, i);
+ }
+ Slot newSlot = new Slot(newLocal.slot.register, newLocal.slot.type);
+ mergedLocals[i] = new Local(newSlot, newLocal.info);
+ } else if (mergedLocals != null) {
+ mergedLocals[i] = currentLocals[i];
+ }
+ }
+ return mergedLocals != null ? mergedLocals : currentLocals;
}
private static boolean verifyStack(List<Slot> stack, List<Slot> other) {
@@ -446,9 +481,6 @@
public static String stackToString(Collection<Slot> stack) {
List<String> strings = new ArrayList<>(stack.size());
for (Slot slot : stack) {
- if (slot == INVALID_SLOT) {
- return "<stack invalidated>";
- }
strings.add(slot.type.toString());
}
StringBuilder builder = new StringBuilder("{ ");
@@ -466,9 +498,6 @@
StringBuilder builder = new StringBuilder("{ ");
boolean first = true;
for (Local local : locals) {
- if (local == INVALID_LOCAL) {
- return "<locals invalidated>";
- }
if (!first) {
builder.append(", ");
} else {
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
new file mode 100644
index 0000000..be5718e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
@@ -0,0 +1,15 @@
+// Copyright (c) 2017, 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.conversion;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+
+public interface OptimizationFeedback {
+ void methodReturnsArgument(DexEncodedMethod method, int argument);
+ void methodReturnsConstant(DexEncodedMethod method, long value);
+ void methodNeverReturnsNull(DexEncodedMethod method);
+ void markProcessed(DexEncodedMethod method, Constraint state);
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDelayed.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDelayed.java
new file mode 100644
index 0000000..25d3fe5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDelayed.java
@@ -0,0 +1,117 @@
+// Copyright (c) 2017, 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.conversion;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Reference2LongMap;
+import it.unimi.dsi.fastutil.objects.Reference2LongOpenHashMap;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class OptimizationFeedbackDelayed implements OptimizationFeedback {
+
+ private Reference2IntMap<DexEncodedMethod> returnsArgument = new Reference2IntOpenHashMap<>();
+ private Reference2LongMap<DexEncodedMethod> returnsConstant = new Reference2LongOpenHashMap<>();
+ private Set<DexEncodedMethod> neverReturnsNull = Sets.newIdentityHashSet();
+ private Map<DexEncodedMethod, Constraint> inliningConstraints = Maps.newIdentityHashMap();
+
+ @Override
+ synchronized public void methodReturnsArgument(DexEncodedMethod method, int argument) {
+ if (method.getOptimizationInfo().returnsArgument()) {
+ assert method.getOptimizationInfo().getReturnedArgument() == argument;
+ return;
+ }
+ assert !returnsArgument.containsKey(method);
+ returnsArgument.put(method, argument);
+ }
+
+ @Override
+ synchronized public void methodReturnsConstant(DexEncodedMethod method, long value) {
+ if (method.getOptimizationInfo().returnsConstant()) {
+ assert method.getOptimizationInfo().getReturnedConstant() == value;
+ return;
+ }
+ assert !returnsConstant.containsKey(method);
+ returnsConstant.put(method, value);
+ }
+
+ @Override
+ synchronized public void methodNeverReturnsNull(DexEncodedMethod method) {
+ if (method.getOptimizationInfo().neverReturnsNull()) {
+ return;
+ }
+ assert !neverReturnsNull.contains(method);
+ neverReturnsNull.add(method);
+ }
+
+ @Override
+ public void markProcessed(DexEncodedMethod method, Constraint state) {
+ if (state == Constraint.NEVER) {
+ assert method.cannotInline();
+ method.markProcessed(state);
+ } else {
+ assert method.cannotInline();
+ inliningConstraints.put(method, state);
+ }
+ }
+
+ private <T> boolean setsOverlap(Set<T> set1, Set<T> set2) {
+ for (T element : set1) {
+ if (set2.contains(element)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Apply the optimization feedback.
+ *
+ * Returns the methods from the passed in list that could be affected by applying the
+ * optimization feedback.
+ */
+ public List<DexEncodedMethod> applyAndClear(
+ List<DexEncodedMethod> processed, CallGraph.Leaves leaves) {
+ returnsArgument.forEach(DexEncodedMethod::markReturnsArgument);
+ returnsConstant.forEach(DexEncodedMethod::markReturnsConstant);
+ neverReturnsNull.forEach(DexEncodedMethod::markNeverReturnsNull);
+
+ // Collect all methods affected by the optimization feedback applied.
+ Set<DexEncodedMethod> all = Sets.newIdentityHashSet();
+ all.addAll(returnsArgument.keySet());
+ all.addAll(returnsConstant.keySet());
+ all.addAll(neverReturnsNull);
+ inliningConstraints.forEach((method, constraint) -> {
+ boolean changed = method.markProcessed(constraint);
+ if (changed) {
+ all.add(method);
+ }
+ });
+
+ // Collect the processed methods which could be affected by the applied optimization feedback.
+ List<DexEncodedMethod> result = new ArrayList<>();
+ for (DexEncodedMethod method : processed) {
+ Set<DexEncodedMethod> calls = leaves.getCycleBreakingCalls().get(method);
+ if (setsOverlap(calls, all)) {
+ result.add(method);
+ }
+ }
+
+ // Clear the collected optimization feedback.
+ returnsArgument.clear();
+ returnsConstant.clear();
+ neverReturnsNull.clear();
+ inliningConstraints.clear();
+
+ return result;
+ }
+}
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
new file mode 100644
index 0000000..dc0a809
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
@@ -0,0 +1,31 @@
+// Copyright (c) 2017, 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.conversion;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+
+public class OptimizationFeedbackDirect implements OptimizationFeedback {
+
+ @Override
+ public void methodReturnsArgument(DexEncodedMethod method, int argument) {
+ method.markReturnsArgument(argument);
+ }
+
+ @Override
+ public void methodReturnsConstant(DexEncodedMethod method, long value) {
+ method.markReturnsConstant(value);
+ }
+
+ @Override
+ public void methodNeverReturnsNull(DexEncodedMethod method) {
+ method.markNeverReturnsNull();
+ }
+
+ @Override
+ public void markProcessed(DexEncodedMethod method, Constraint state) {
+ method.markProcessed(state);
+ }
+}
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
new file mode 100644
index 0000000..3c8057f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
@@ -0,0 +1,23 @@
+// Copyright (c) 2017, 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.conversion;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+
+public class OptimizationFeedbackIgnore implements OptimizationFeedback {
+
+ @Override
+ public void methodReturnsArgument(DexEncodedMethod method, int argument) {}
+
+ @Override
+ public void methodReturnsConstant(DexEncodedMethod method, long value) {}
+
+ @Override
+ public void methodNeverReturnsNull(DexEncodedMethod method) {}
+
+ @Override
+ public void markProcessed(DexEncodedMethod method, Constraint state) {}
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/SourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/SourceCode.java
index ba03f82..a36091d 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/SourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/SourceCode.java
@@ -33,9 +33,10 @@
* <p>The instruction at {@code index} is traced and its target blocks are marked by using
* {@code IRBuilder.ensureSuccessorBlock} (and {@code ensureBlockWithoutEnqueuing}).
*
- * @return True if the instruction closes the current block, false otherwise.
+ * @return If the instruction closes the block, the last index of the block,
+ * otherwise -1.
*/
- boolean traceInstruction(int instructionIndex, IRBuilder builder);
+ int traceInstruction(int instructionIndex, IRBuilder builder);
void closedCurrentBlockWithFallthrough(int fallthroughInstructionIndex);
void closedCurrentBlock();
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
index 01be50d..c3f2d08 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
@@ -166,7 +166,7 @@
private boolean isInMainDexList(DexType iface) {
ImmutableSet<DexType> list = converter.application.mainDexList;
- return list != null && list.contains(iface);
+ return list.contains(iface);
}
// Represent a static interface method as a method of companion class.
@@ -226,7 +226,7 @@
InterfaceProcessor processor = new InterfaceProcessor(this);
for (DexProgramClass clazz : builder.getProgramClasses()) {
if (shouldProcess(clazz, flavour, true)) {
- processor.process(clazz.get().asProgramClass());
+ processor.process(clazz.asProgramClass());
}
}
return processor.companionClasses;
@@ -236,7 +236,7 @@
ClassProcessor processor = new ClassProcessor(this);
for (DexProgramClass clazz : builder.getProgramClasses()) {
if (shouldProcess(clazz, flavour, false)) {
- processor.process(clazz.get());
+ processor.process(clazz);
}
}
return processor.getForwardMethods();
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
index 37bfdaa..cb518a3 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
@@ -11,7 +11,6 @@
import com.android.tools.r8.graph.DexAnnotationSet;
import com.android.tools.r8.graph.DexAnnotationSetRefList;
import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexClassPromise;
import com.android.tools.r8.graph.DexCode;
import com.android.tools.r8.graph.DexEncodedField;
import com.android.tools.r8.graph.DexEncodedMethod;
@@ -388,6 +387,10 @@
DexClass definitionFor(DexType type) {
return rewriter.converter.appInfo.app.definitionFor(type);
}
+
+ DexProgramClass programDefinitionFor(DexType type) {
+ return rewriter.converter.appInfo.app.programDefinitionFor(type);
+ }
}
// Used for targeting methods referenced directly without creating accessors.
@@ -415,9 +418,7 @@
// We already found the static method to be called, just relax its accessibility.
assert descriptor.getAccessibility() != null;
descriptor.getAccessibility().unsetPrivate();
- DexClassPromise promise = definitionFor(descriptor.implHandle.asMethod().holder);
- assert promise != null;
- DexClass implMethodHolder = promise.get();
+ DexClass implMethodHolder = definitionFor(descriptor.implHandle.asMethod().holder);
if (implMethodHolder.isInterface()) {
descriptor.getAccessibility().setPublic();
}
@@ -438,9 +439,7 @@
// For all instantiation points for which compiler creates lambda$
// methods, it creates these methods in the same class/interface.
DexMethod implMethod = descriptor.implHandle.asMethod();
- DexClassPromise promise = definitionFor(implMethod.holder);
- assert promise != null;
- DexClass implMethodHolder = promise.get();
+ DexClass implMethodHolder = definitionFor(implMethod.holder);
DexEncodedMethod[] directMethods = implMethodHolder.directMethods;
for (int i = 0; i < directMethods.length; i++) {
@@ -481,9 +480,8 @@
@Override
boolean ensureAccessibility() {
// Create a static accessor with proper accessibility.
- DexClassPromise promise = definitionFor(callTarget.holder);
- assert promise != null && promise.isProgramClass();
- DexClass accessorClass = promise.get();
+ DexProgramClass accessorClass = programDefinitionFor(callTarget.holder);
+ assert accessorClass != null;
DexAccessFlags accessorFlags = new DexAccessFlags(
Constants.ACC_SYNTHETIC | Constants.ACC_STATIC |
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 e5b8306..c280a23 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
@@ -48,6 +48,7 @@
import com.android.tools.r8.ir.code.Switch;
import com.android.tools.r8.ir.code.Switch.Type;
import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.OptimizationFeedback;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.LongInterval;
import com.google.common.base.Equivalence;
@@ -78,7 +79,7 @@
private static final int UNKNOWN_CAN_THROW = 0;
private static final int CAN_THROW = 1;
private static final int CANNOT_THROW = 2;
- private static final int MAX_FILL_ARRAY_SIZE = 4 * Constants.KILOBYTE;
+ private static final int MAX_FILL_ARRAY_SIZE = 8 * Constants.KILOBYTE;
private final AppInfo appInfo;
private final DexItemFactory dexItemFactory;
@@ -607,7 +608,7 @@
}
public void identifyReturnsArgument(
- DexEncodedMethod method, IRCode code) {
+ DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
if (code.getNormalExitBlock() != null) {
Return ret = code.getNormalExitBlock().exit().asReturn();
if (!ret.isReturnVoid()) {
@@ -616,14 +617,14 @@
// Find the argument number.
int index = code.collectArguments().indexOf(returnValue);
assert index != -1;
- method.markReturnsArgument(index);
+ feedback.methodReturnsArgument(method, index);
}
if (returnValue.isConstant() && returnValue.definition.isConstNumber()) {
long value = returnValue.definition.asConstNumber().getRawValue();
- method.markReturnsConstant(value);
+ feedback.methodReturnsConstant(method, value);
}
if (returnValue.isNeverNull()) {
- method.markNeverReturnsNull();
+ feedback.methodNeverReturnsNull(method);
}
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index f677637..f57c022 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -50,23 +50,23 @@
this.options = options;
}
- private InliningConstraint instructionAllowedForInlining(
+ private Constraint instructionAllowedForInlining(
DexEncodedMethod method, Instruction instruction) {
- InliningConstraint result = instruction.inliningConstraint(appInfo, method.method.holder);
- if ((result == InliningConstraint.NEVER) && instruction.isDebugInstruction()) {
- return InliningConstraint.ALWAYS;
+ Constraint result = instruction.inliningConstraint(appInfo, method.method.holder);
+ if ((result == Constraint.NEVER) && instruction.isDebugInstruction()) {
+ return Constraint.ALWAYS;
}
return result;
}
- public InliningConstraint identifySimpleMethods(IRCode code, DexEncodedMethod method) {
+ public Constraint identifySimpleMethods(IRCode code, DexEncodedMethod method) {
DexCode dex = method.getCode().asDexCode();
// We have generated code for a method and we want to figure out whether the method is a
// candidate for inlining. The code is the final IR after optimizations.
if (dex.instructions.length > INLINING_INSTRUCTION_LIMIT) {
- return InliningConstraint.NEVER;
+ return Constraint.NEVER;
}
- InliningConstraint result = InliningConstraint.ALWAYS;
+ Constraint result = Constraint.ALWAYS;
ListIterator<BasicBlock> iterator = code.listIterator();
assert iterator.hasNext();
BasicBlock block = iterator.next();
@@ -76,9 +76,9 @@
InstructionListIterator it = block.listIterator();
while (it.hasNext()) {
Instruction instruction = it.next();
- InliningConstraint state = instructionAllowedForInlining(method, instruction);
- if (state == InliningConstraint.NEVER) {
- return InliningConstraint.NEVER;
+ Constraint state = instructionAllowedForInlining(method, instruction);
+ if (state == Constraint.NEVER) {
+ return Constraint.NEVER;
}
if (state.ordinal() < result.ordinal()) {
result = state;
@@ -105,7 +105,7 @@
return methodHolder.isSamePackage(targetHolder);
}
- public enum InliningConstraint {
+ public enum Constraint {
// The ordinal values are important so please do not reorder.
NEVER, // Never inline this.
PRIVATE, // Only inline this into methods with same holder.
@@ -113,23 +113,27 @@
ALWAYS, // No restrictions for inlining this.
}
+ public enum Reason {
+ FORCE, // Inlinee is marked for forced inlining (bridge method or renamed constructor).
+ SINGLE_CALLER, // Inlinee has precisely one caller.
+ DUAL_CALLER, // Inlinee has precisely two callers.
+ SIMPLE, // Inlinee has simple code suitable for inlining.
+ }
+
static public class InlineAction {
public final DexEncodedMethod target;
public final Invoke invoke;
- public final boolean forceInline;
+ public final Reason reason;
- public InlineAction(
- DexEncodedMethod target, Invoke invoke, boolean forceInline) {
+ public InlineAction(DexEncodedMethod target, Invoke invoke, Reason reason) {
this.target = target;
this.invoke = invoke;
- this.forceInline = forceInline;
+ this.reason = reason;
}
- public InlineAction(DexEncodedMethod target, Invoke invoke) {
- this.target = target;
- this.invoke = invoke;
- this.forceInline = target.getOptimizationInfo().forceInline();
+ public boolean forceInline() {
+ return reason != Reason.SIMPLE;
}
public IRCode buildIR(ValueNumberGenerator generator, AppInfoWithSubtyping appInfo,
@@ -275,7 +279,7 @@
// Back up before the invoke instruction.
iterator.previous();
instruction_allowance -= numberOfInstructions(inlinee);
- if (instruction_allowance >= 0 || result.forceInline) {
+ if (instruction_allowance >= 0 || result.forceInline()) {
iterator.inlineInvoke(code, inlinee, blockIterator, blocksToRemove, downcast);
}
// If we inlined the invoke from a bridge method, it is no longer a bridge method.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
index 954f11c..7d83150 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
@@ -17,6 +17,7 @@
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.conversion.CallGraph;
import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
+import com.android.tools.r8.ir.optimize.Inliner.Reason;
import com.android.tools.r8.logging.Log;
/**
@@ -103,6 +104,19 @@
return candidate;
}
+ private Reason computeInliningReason(DexEncodedMethod target) {
+ if (target.getOptimizationInfo().forceInline()) {
+ return Reason.FORCE;
+ }
+ if (callGraph.hasSingleCallSite(target)) {
+ return Reason.SINGLE_CALLER;
+ }
+ if (isDoubleInliningTarget(target)) {
+ return Reason.DUAL_CALLER;
+ }
+ return Reason.SIMPLE;
+ }
+
public InlineAction computeForInvokeWithReceiver(InvokeMethodWithReceiver invoke) {
boolean receiverIsNeverNull = invoke.receiverIsNeverNull();
if (!receiverIsNeverNull) {
@@ -120,11 +134,9 @@
return null;
}
- boolean fromBridgeMethod = target.getOptimizationInfo().forceInline();
-
if (target == method) {
// Bridge methods should never have recursive calls.
- assert !fromBridgeMethod;
+ assert !target.getOptimizationInfo().forceInline();
return null;
}
@@ -154,11 +166,9 @@
return null;
}
- boolean doubleInlineTarget = isDoubleInliningTarget(target);
+ Reason reason = computeInliningReason(target);
// Determine if this should be inlined no matter how big it is.
- boolean forceInline =
- fromBridgeMethod | callGraph.hasSingleCallSite(target) | doubleInlineTarget;
- if (!target.isInliningCandidate(method, forceInline)) {
+ if (!target.isInliningCandidate(method, reason != Reason.SIMPLE)) {
// Abort inlining attempt if the single target is not an inlining candidate.
if (info != null) {
info.exclude(invoke, "target is not identified for inlining");
@@ -175,7 +185,7 @@
}
// Attempt to inline a candidate that is only called twice.
- if (doubleInlineTarget && (doubleInlining(target) == null)) {
+ if ((reason == Reason.DUAL_CALLER) && (doubleInlining(target) == null)) {
if (info != null) {
info.exclude(invoke, "target is not ready for double inlining");
}
@@ -185,7 +195,7 @@
if (info != null) {
info.include(invoke.getType(), target);
}
- return new InlineAction(target, invoke, forceInline);
+ return new InlineAction(target, invoke, reason);
}
public InlineAction computeForInvokeVirtual(InvokeVirtual invoke) {
@@ -224,12 +234,9 @@
if (candidate == null) {
return null;
}
- boolean fromBridgeMethod = candidate.getOptimizationInfo().forceInline();
- boolean doubleInlineTarget = isDoubleInliningTarget(candidate);
+ Reason reason = computeInliningReason(candidate);
// Determine if this should be inlined no matter how big it is.
- boolean forceInline =
- fromBridgeMethod | callGraph.hasSingleCallSite(candidate) | doubleInlineTarget;
- if (!candidate.isInliningCandidate(method, forceInline)) {
+ if (!candidate.isInliningCandidate(method, reason != Reason.SIMPLE)) {
// Abort inlining attempt if the single target is not an inlining candidate.
if (info != null) {
info.exclude(invoke, "target is not identified for inlining");
@@ -246,7 +253,7 @@
}
// Attempt to inline a candidate that is only called twice.
- if (doubleInlineTarget && (doubleInlining(candidate) == null)) {
+ if ((reason == Reason.DUAL_CALLER) && (doubleInlining(candidate) == null)) {
if (info != null) {
info.exclude(invoke, "target is not ready for double inlining");
}
@@ -256,7 +263,7 @@
if (info != null) {
info.include(invoke.getType(), candidate);
}
- return new InlineAction(candidate, invoke);
+ return new InlineAction(candidate, invoke, reason);
}
public InlineAction computeForInvokeSuper(InvokeSuper invoke) {
@@ -270,7 +277,7 @@
if (info != null) {
info.include(invoke.getType(), candidate);
}
- return new InlineAction(candidate, invoke);
+ return new InlineAction(candidate, invoke, Reason.SIMPLE);
}
public InlineAction computeForInvokePolymorpic(InvokePolymorphic invoke) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
index b122d23..df5cb27 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
@@ -236,8 +236,7 @@
private boolean isFieldRead(DexEncodedField field, boolean isStatic) {
// Without live set information we cannot tell and assume true.
if (liveSet == null
- || isStatic && liveSet.staticFieldsRead.contains(field.field)
- || !isStatic && liveSet.instanceFieldsRead.contains(field.field)
+ || liveSet.fieldsRead.contains(field.field)
|| liveSet.pinnedItems.contains(field)) {
return true;
}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
index 42b5220..0ea5761 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
@@ -812,9 +812,9 @@
}
@Override
- public boolean traceInstruction(int instructionIndex, IRBuilder builder) {
+ public int traceInstruction(int instructionIndex, IRBuilder builder) {
// There is just one block, and after the last instruction it is closed.
- return instructionIndex == outline.templateInstructions.size();
+ return instructionIndex == outline.templateInstructions.size() ? instructionIndex : -1;
}
@Override
@@ -991,7 +991,7 @@
}
@Override
- public String toString(ClassNameMapper naming) {
+ public String toString(DexEncodedMethod method, ClassNameMapper naming) {
return null;
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
index 3a96ac2..9276f45 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
@@ -25,6 +25,7 @@
import com.android.tools.r8.ir.code.Xor;
import com.android.tools.r8.logging.Log;
import com.android.tools.r8.utils.CfgPrinter;
+import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.ListUtils;
import com.android.tools.r8.utils.StringUtils;
import com.google.common.collect.HashMultiset;
@@ -62,6 +63,8 @@
*/
public class LinearScanRegisterAllocator implements RegisterAllocator {
public static final int NO_REGISTER = Integer.MIN_VALUE;
+ public static final int REGISTER_CANDIDATE_NOT_FOUND = -1;
+ public static final int MIN_CONSTANT_FREE_FOR_POSITIONS = 5;
private enum ArgumentReuseMode {
ALLOW_ARGUMENT_REUSE,
@@ -107,6 +110,8 @@
private final IRCode code;
// Number of registers used for arguments.
private final int numberOfArgumentRegisters;
+ // Compiler options.
+ private final InternalOptions options;
// Mapping from basic blocks to the set of values live at entry to that basic block.
private Map<BasicBlock, Set<Value>> liveAtEntrySets = new IdentityHashMap<>();
@@ -142,8 +147,9 @@
// register.
private boolean hasDedicatedMoveExceptionRegister = false;
- public LinearScanRegisterAllocator(IRCode code) {
+ public LinearScanRegisterAllocator(IRCode code, InternalOptions options) {
this.code = code;
+ this.options = options;
int argumentRegisters = 0;
for (Instruction instruction : code.blocks.getFirst().getInstructions()) {
if (instruction.isArgument()) {
@@ -420,6 +426,14 @@
return getRegisterForIntervals(intervals);
}
+ @Override
+ public int getArgumentOrAllocateRegisterForValue(Value value, int instructionNumber) {
+ if (value.isArgument()) {
+ return getRegisterForIntervals(value.getLiveIntervals());
+ }
+ return getRegisterForValue(value, instructionNumber);
+ }
+
private BasicBlock[] computeLivenessInformation() {
BasicBlock[] blocks = code.numberInstructions();
computeLiveAtEntrySets();
@@ -557,18 +571,18 @@
active.add(argumentInterval);
} else {
// Treat the argument interval as spilled which will require a load to a different
- // register for all usages.
+ // register for all register-constrained usages.
if (argumentInterval.getUses().size() > 1) {
LiveIntervalsUse use = argumentInterval.firstUseWithConstraint();
if (use != null) {
LiveIntervals split;
- if (argumentInterval.getUses().size() == 2) {
- // If there is only one real use (definition plus one real use), split before
+ if (argumentInterval.numberOfUsesWithConstraint() == 1) {
+ // If there is only one register-constrained use, split before
// that one use.
split = argumentInterval.splitBefore(use.getPosition());
} else {
- // If there are multiple real users, split right after the definition to make it
- // more likely that arguments get in usable registers from the start.
+ // If there are multiple register-constrained users, split right after the definition
+ // to make it more likely that arguments get in usable registers from the start.
split = argumentInterval
.splitBefore(argumentInterval.getValue().definition.getNumber() + 1);
}
@@ -908,17 +922,24 @@
if (mode == ArgumentReuseMode.ALLOW_ARGUMENT_REUSE) {
// The sentinel registers cannot be used and we block them.
- freePositions.set(0, 0);
+ freePositions.set(0, 0, false);
+ if (options.debug && !code.method.accessFlags.isStatic()) {
+ // If we are generating debug information, we pin the this value register since the
+ // debugger expects to always be able to find it in the input register.
+ assert numberOfArgumentRegisters > 0;
+ assert preArgumentSentinelValue.getNextConsecutive().requiredRegisters() == 1;
+ freePositions.set(1, 0, false);
+ }
int lastSentinelRegister = numberOfArgumentRegisters + NUMBER_OF_SENTINEL_REGISTERS - 1;
if (lastSentinelRegister <= registerConstraint) {
- freePositions.set(lastSentinelRegister, 0);
+ freePositions.set(lastSentinelRegister, 0, false);
}
} else {
// Argument reuse is not allowed and we block all the argument registers so that
// arguments are never free.
for (int i = 0; i < numberOfArgumentRegisters + NUMBER_OF_SENTINEL_REGISTERS; i++) {
if (i <= registerConstraint) {
- freePositions.set(i, 0);
+ freePositions.set(i, 0, false);
}
}
}
@@ -930,7 +951,7 @@
if (hasDedicatedMoveExceptionRegister) {
int moveExceptionRegister = numberOfArgumentRegisters + NUMBER_OF_SENTINEL_REGISTERS;
if (moveExceptionRegister <= registerConstraint) {
- freePositions.set(moveExceptionRegister, 0);
+ freePositions.set(moveExceptionRegister, 0, false);
}
}
@@ -940,7 +961,7 @@
if (activeRegister <= registerConstraint) {
for (int i = 0; i < intervals.requiredRegisters(); i++) {
if (activeRegister + i <= registerConstraint) {
- freePositions.set(activeRegister + i, 0);
+ freePositions.set(activeRegister + i, 0, intervals.isConstantNumberInterval());
}
}
}
@@ -953,17 +974,18 @@
if (inactiveRegister <= registerConstraint && unhandledInterval.overlaps(intervals)) {
int nextOverlap = unhandledInterval.nextOverlap(intervals);
for (int i = 0; i < intervals.requiredRegisters(); i++) {
- if (inactiveRegister + i <= registerConstraint) {
+ int register = inactiveRegister + i;
+ if (register <= registerConstraint) {
int unhandledStart = toInstructionPosition(unhandledInterval.getStart());
if (nextOverlap == unhandledStart) {
// Don't use the register for an inactive interval that is only free until the next
// instruction. We can get into this situation when unhandledInterval starts at a
// gap position.
- freePositions.set(inactiveRegister + i, 0);
+ freePositions.set(register, 0, freePositions.holdsConstant(register));
} else {
- freePositions.set(
- inactiveRegister + i,
- Math.min(freePositions.get(inactiveRegister + i), nextOverlap));
+ if (nextOverlap < freePositions.get(register)) {
+ freePositions.set(register, nextOverlap, intervals.isConstantNumberInterval());
+ }
}
}
}
@@ -978,7 +1000,8 @@
// Get the register (pair) that is free the longest. That is the register with the largest
// free position.
int candidate = getLargestValidCandidate(
- unhandledInterval, registerConstraint, needsRegisterPair, freePositions);
+ unhandledInterval, registerConstraint, needsRegisterPair, freePositions, false);
+ assert candidate != REGISTER_CANDIDATE_NOT_FOUND;
int largestFreePosition = freePositions.get(candidate);
if (needsRegisterPair) {
largestFreePosition = Math.min(largestFreePosition, freePositions.get(candidate + 1));
@@ -992,7 +1015,24 @@
// argument reuse disallowed.
return false;
}
- allocateBlockedRegister(unhandledInterval);
+ // If the first use for these intervals is unconstrained, just spill this interval instead
+ // of finding another candidate to spill via allocateBlockedRegister.
+ if (!unhandledInterval.getUses().first().hasConstraint()) {
+ int nextConstrainedPosition = unhandledInterval.firstUseWithConstraint().getPosition();
+ int register;
+ // Arguments are always in the argument registers, so for arguments just use that register
+ // for the unconstrained prefix. For everything else, get a spill register.
+ if (unhandledInterval.isArgumentInterval()) {
+ register = unhandledInterval.getSplitParent().getRegister();
+ } else {
+ register = getSpillRegister(unhandledInterval);
+ }
+ LiveIntervals split = unhandledInterval.splitBefore(nextConstrainedPosition);
+ assignRegisterToUnhandledInterval(unhandledInterval, needsRegisterPair, register);
+ unhandled.add(split);
+ } else {
+ allocateBlockedRegister(unhandledInterval);
+ }
} else if (largestFreePosition >= unhandledInterval.getEnd()) {
// Free for the entire interval. Allocate the register.
assignRegisterToUnhandledInterval(unhandledInterval, needsRegisterPair, candidate);
@@ -1004,8 +1044,7 @@
}
// The candidate is free for the beginning of an interval. We split the interval
// and use the register for as long as we can.
- int splitPosition = largestFreePosition;
- LiveIntervals split = unhandledInterval.splitBefore(splitPosition);
+ LiveIntervals split = unhandledInterval.splitBefore(largestFreePosition);
assert split != unhandledInterval;
assignRegisterToUnhandledInterval(unhandledInterval, needsRegisterPair, candidate);
unhandled.add(split);
@@ -1130,14 +1169,15 @@
active.add(unhandledInterval);
}
- private int getLargestCandidate(
- int registerConstraint, RegisterPositions freePositions, boolean needsRegisterPair) {
- int candidate = 0;
- int largest = freePositions.get(0);
- if (needsRegisterPair) {
- largest = Math.min(largest, freePositions.get(1));
- }
- for (int i = 1; i <= registerConstraint; i++) {
+ private int getLargestCandidate(int registerConstraint, RegisterPositions freePositions,
+ boolean needsRegisterPair, boolean restrictToConstant) {
+ int candidate = REGISTER_CANDIDATE_NOT_FOUND;
+ int largest = -1;
+
+ for (int i = 0; i <= registerConstraint; i++) {
+ if (restrictToConstant && !freePositions.holdsConstant(i)) {
+ continue;
+ }
int freePosition = freePositions.get(i);
if (needsRegisterPair) {
if (i >= registerConstraint) {
@@ -1153,17 +1193,23 @@
}
}
}
+
return candidate;
}
private int getLargestValidCandidate(LiveIntervals unhandledInterval, int registerConstraint,
- boolean needsRegisterPair, RegisterPositions freePositions) {
- int candidate = getLargestCandidate(registerConstraint, freePositions, needsRegisterPair);
+ boolean needsRegisterPair, RegisterPositions freePositions, boolean restrictToConstant) {
+ int candidate = getLargestCandidate(registerConstraint, freePositions, needsRegisterPair,
+ restrictToConstant);
+ if (candidate == REGISTER_CANDIDATE_NOT_FOUND) {
+ return candidate;
+ }
if (needsOverlappingLongRegisterWorkaround(unhandledInterval)) {
while (hasOverlappingLongRegisters(unhandledInterval, candidate)) {
// Make the overlapping register unavailable for allocation and try again.
- freePositions.set(candidate, 0);
- candidate = getLargestCandidate(registerConstraint, freePositions, needsRegisterPair);
+ freePositions.set(candidate, 0, freePositions.holdsConstant(candidate));
+ candidate = getLargestCandidate(registerConstraint, freePositions, needsRegisterPair,
+ restrictToConstant);
}
}
return candidate;
@@ -1190,7 +1236,8 @@
for (int i = 0; i < intervals.requiredRegisters(); i++) {
if (activeRegister + i <= registerConstraint) {
int unhandledStart = unhandledInterval.getStart();
- usePositions.set(activeRegister + i, intervals.firstUseAfter(unhandledStart));
+ usePositions.set(activeRegister + i, intervals.firstUseAfter(unhandledStart),
+ intervals.isConstantNumberInterval());
}
}
}
@@ -1202,9 +1249,11 @@
if (inactiveRegister <= registerConstraint && intervals.overlaps(unhandledInterval)) {
for (int i = 0; i < intervals.requiredRegisters(); i++) {
if (inactiveRegister + i <= registerConstraint) {
- int unhandledStart = unhandledInterval.getStart();
- usePositions.set(inactiveRegister + i, Math.min(
- usePositions.get(inactiveRegister + i), intervals.firstUseAfter(unhandledStart)));
+ int firstUse = intervals.firstUseAfter(unhandledInterval.getStart());
+ if (firstUse < usePositions.get(inactiveRegister + i)) {
+ usePositions.set(inactiveRegister + i, firstUse,
+ intervals.isConstantNumberInterval());
+ }
}
}
}
@@ -1213,38 +1262,46 @@
// Disallow the reuse of argument registers by always treating them as being used
// at instruction number 0.
for (int i = 0; i < numberOfArgumentRegisters + NUMBER_OF_SENTINEL_REGISTERS; i++) {
- usePositions.set(i, 0);
+ usePositions.set(i, 0, false);
}
// Disallow reuse of the move exception register if we have reserved one.
if (hasDedicatedMoveExceptionRegister) {
- usePositions.set(numberOfArgumentRegisters + NUMBER_OF_SENTINEL_REGISTERS, 0);
+ usePositions.set(numberOfArgumentRegisters + NUMBER_OF_SENTINEL_REGISTERS, 0, false);
}
// Treat active linked argument intervals as pinned. They cannot be given another register
// at their uses.
- blockLinkedRegisters(
- active, unhandledInterval, registerConstraint, usePositions, blockedPositions);
+ blockLinkedRegisters(active, unhandledInterval, registerConstraint, usePositions,
+ blockedPositions);
// Treat inactive linked argument intervals as pinned. They cannot be given another register
// at their uses.
- blockLinkedRegisters(
- inactive, unhandledInterval, registerConstraint, usePositions, blockedPositions);
+ blockLinkedRegisters(inactive, unhandledInterval, registerConstraint, usePositions,
+ blockedPositions);
// Get the register (pair) that has the highest use position.
boolean needsRegisterPair = unhandledInterval.requiredRegisters() == 2;
- int candidate = getLargestValidCandidate(
- unhandledInterval, registerConstraint, needsRegisterPair, usePositions);
- int largestUsePosition = usePositions.get(candidate);
- int blockedPosition = blockedPositions.get(candidate);
- if (needsRegisterPair) {
- blockedPosition = Math.min(blockedPosition, blockedPositions.get(candidate + 1));
- largestUsePosition = Math.min(largestUsePosition, usePositions.get(candidate + 1));
+
+ int candidate = getLargestValidCandidate(unhandledInterval, registerConstraint,
+ needsRegisterPair, usePositions, true);
+ if (candidate != Integer.MAX_VALUE) {
+ int nonConstCandidate = getLargestValidCandidate(unhandledInterval, registerConstraint,
+ needsRegisterPair, usePositions, false);
+ if (nonConstCandidate == Integer.MAX_VALUE || candidate == REGISTER_CANDIDATE_NOT_FOUND) {
+ candidate = nonConstCandidate;
+ } else {
+ int largestConstUsePosition = getLargestPosition(usePositions, candidate, needsRegisterPair);
+ if (largestConstUsePosition - MIN_CONSTANT_FREE_FOR_POSITIONS < unhandledInterval
+ .getStart()) {
+ // The candidate that can be rematerialized has a live range too short to use it.
+ candidate = nonConstCandidate;
+ }
+ }
}
- // TODO(ager): When selecting a spill candidate also take rematerialization into account.
- // Prefer spilling a constant that can be rematerialized instead of spilling something that
- // cannot be rematerialized.
+ int largestUsePosition = getLargestPosition(usePositions, candidate, needsRegisterPair);
+ int blockedPosition = getBlockedPosition(blockedPositions, candidate, needsRegisterPair);
if (largestUsePosition < unhandledInterval.getFirstUse()) {
// All active and inactive intervals are used before current. Therefore, it is best to spill
@@ -1271,6 +1328,28 @@
}
}
+ private int getLargestPosition(RegisterPositions usePositions, int register,
+ boolean needsRegisterPair) {
+ int largestUsePosition = usePositions.get(register);
+
+ if (needsRegisterPair) {
+ largestUsePosition = Math.min(largestUsePosition, usePositions.get(register + 1));
+ }
+
+ return largestUsePosition;
+ }
+
+ private int getBlockedPosition(RegisterPositions blockedPositions, int register,
+ boolean needsRegisterPair) {
+ int blockedPosition = blockedPositions.get(register);
+
+ if (needsRegisterPair) {
+ blockedPosition = Math.min(blockedPosition, blockedPositions.get(register + 1));
+ }
+
+ return blockedPosition;
+ }
+
private void assignRegisterAndSpill(
LiveIntervals unhandledInterval, boolean needsRegisterPair, int candidate) {
assignRegister(unhandledInterval, candidate);
@@ -1401,7 +1480,8 @@
assert spilled.isSpilled();
assert spilled.getValue().isConstant();
assert !spilled.isLinked() || spilled.isArgumentInterval();
- int maxGapSize = 50 * INSTRUCTION_NUMBER_DELTA;
+ // Do not split range if constant is reused by one of the eleven following instruction.
+ int maxGapSize = 11 * INSTRUCTION_NUMBER_DELTA;
if (!spilled.getUses().isEmpty()) {
// Split at first use after the spill position and add to unhandled to get a register
// assigned for rematerialization.
@@ -1447,15 +1527,12 @@
if (register <= registerConstraint && other.overlaps(interval)) {
for (int i = 0; i < other.requiredRegisters(); i++) {
if (register + i <= registerConstraint) {
- for (LiveIntervalsUse use : other.getUses()) {
- if (use.getPosition() > interval.getStart()) {
- blockedPositions.set(
- register + i,
- Math.min(blockedPositions.get(register + i), use.getPosition()));
- usePositions.set(
- register + i,
- Math.min(usePositions.get(register + i), use.getPosition()));
- }
+ int firstUse = other.firstUseAfter(interval.getStart());
+ if (firstUse < blockedPositions.get(register + i)) {
+ blockedPositions.set(register + i, firstUse, other.isConstantNumberInterval());
+ // If we start blocking registers other than linked arguments, we might need to
+ // explicitly update the use positions as well as blocked positions.
+ assert usePositions.get(register + i) <= blockedPositions.get(register + i);
}
}
}
@@ -1507,7 +1584,7 @@
// If we are processing an exception edge, we need to use the throwing instruction
// as the instruction we are coming from.
int fromInstruction = block.exit().getNumber();
- boolean isCatch = block.isCatchSuccessor(successor);
+ boolean isCatch = block.hasCatchSuccessor(successor);
if (isCatch) {
for (Instruction instruction : block.getInstructions()) {
if (instruction.instructionTypeCanThrow()) {
@@ -1748,7 +1825,7 @@
private void generateArgumentMoves(Invoke invoke, InstructionListIterator insertAt) {
// If the invoke instruction require more than 5 registers we link the inputs because they
// need to be in consecutive registers.
- if (invoke.requiredArgumentRegisters() > 5) {
+ if (invoke.requiredArgumentRegisters() > 5 && !argumentsAreAlreadyLinked(invoke)) {
List<Value> arguments = invoke.arguments();
Value previous = null;
for (int i = 0; i < arguments.size(); i++) {
@@ -1766,6 +1843,19 @@
}
}
+ private boolean argumentsAreAlreadyLinked(Invoke invoke) {
+ Iterator<Value> it = invoke.arguments().iterator();
+ Value current = it.next();
+ while (it.hasNext()) {
+ Value next = it.next();
+ if (!current.isLinked() || current.getNextConsecutive() != next) {
+ return false;
+ }
+ current = next;
+ }
+ return true;
+ }
+
private Value createSentinelRegisterValue() {
return createValue(MoveType.OBJECT, null);
}
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervals.java b/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervals.java
index c966813..f01ed3a 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervals.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervals.java
@@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.ir.regalloc;
+import static com.android.tools.r8.dex.Constants.U16BIT_MAX;
import static com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator.NO_REGISTER;
import com.android.tools.r8.dex.Constants;
@@ -29,7 +30,7 @@
private boolean spilled = false;
// Only registers up to and including the registerLimit are allowed for this interval.
- private int registerLimit = Constants.U16BIT_MAX;
+ private int registerLimit = U16BIT_MAX;
// Max register used for any of the non-spilled splits for these live intervals or for any of the
// live intervals that this live interval is connected to by phi moves. This is used to
@@ -331,7 +332,7 @@
public LiveIntervalsUse firstUseWithConstraint() {
for (LiveIntervalsUse use : uses) {
- if (use.getLimit() < Constants.U16BIT_MAX) {
+ if (use.hasConstraint()) {
return use;
}
}
@@ -386,13 +387,14 @@
}
private void recomputeLimit() {
- registerLimit = Constants.U16BIT_MAX;
+ registerLimit = U16BIT_MAX;
for (LiveIntervalsUse use : uses) {
updateRegisterConstraint(use.getLimit());
}
}
public LiveIntervals getSplitCovering(int instructionNumber) {
+ assert getSplitParent() == this;
// Check if this interval itself is covering the instruction.
if (getStart() <= instructionNumber && getEnd() > instructionNumber) {
return this;
@@ -417,6 +419,20 @@
return null;
}
+ public boolean isConstantNumberInterval() {
+ return value.definition != null && value.definition.isConstNumber();
+ }
+
+ public int numberOfUsesWithConstraint() {
+ int count = 0;
+ for (LiveIntervalsUse use : getUses()) {
+ if (use.hasConstraint()) {
+ count++;
+ }
+ }
+ return count;
+ }
+
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
@@ -468,4 +484,4 @@
splitChild.print(printer, number + delta, number);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervalsUse.java b/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervalsUse.java
index fd37af8..a602134 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervalsUse.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervalsUse.java
@@ -3,6 +3,8 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.ir.regalloc;
+import static com.android.tools.r8.dex.Constants.U16BIT_MAX;
+
public class LiveIntervalsUse implements Comparable<LiveIntervalsUse> {
private int position;
private int limit;
@@ -41,4 +43,8 @@
}
return limit - o.limit;
}
+
+ public boolean hasConstraint() {
+ return limit < U16BIT_MAX;
+ }
}
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterAllocator.java b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterAllocator.java
index 9dba6d0..79ba8db 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterAllocator.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterAllocator.java
@@ -10,4 +10,5 @@
int registersUsed();
int getRegisterForValue(Value value, int instructionNumber);
boolean argumentValueUsesHighRegister(Value value, int instructionNumber);
+ int getArgumentOrAllocateRegisterForValue(Value value, int instructionNumber);
}
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterPositions.java b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterPositions.java
index 87eedca..35b87fc 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterPositions.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterPositions.java
@@ -4,6 +4,7 @@
package com.android.tools.r8.ir.regalloc;
import java.util.Arrays;
+import java.util.BitSet;
/**
* Simple mapping from a register to an int value.
@@ -16,6 +17,7 @@
private static final int INITIAL_SIZE = 16;
private int limit;
private int[] backing;
+ private BitSet registerHoldsConstant;
public RegisterPositions(int limit) {
this.limit = limit;
@@ -23,13 +25,19 @@
for (int i = 0; i < INITIAL_SIZE; i++) {
backing[i] = Integer.MAX_VALUE;
}
+ registerHoldsConstant = new BitSet(limit);
}
- public void set(int index, int value) {
+ public boolean holdsConstant(int index) {
+ return registerHoldsConstant.get(index);
+ }
+
+ public void set(int index, int value, boolean holdsConstant) {
if (index >= backing.length) {
grow(index + 1);
}
backing[index] = value;
+ registerHoldsConstant.set(index, holdsConstant);
}
public int get(int index) {
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/SingleBlockSourceCode.java b/src/main/java/com/android/tools/r8/ir/synthetic/SingleBlockSourceCode.java
index 8c97f8f..22cf6d5 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/SingleBlockSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/SingleBlockSourceCode.java
@@ -124,8 +124,8 @@
}
@Override
- public final boolean traceInstruction(int instructionIndex, IRBuilder builder) {
- return instructionIndex == constructors.size() - 1;
+ public final int traceInstruction(int instructionIndex, IRBuilder builder) {
+ return (instructionIndex == constructors.size() - 1) ? instructionIndex : -1;
}
@Override
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/SynthesizedCode.java b/src/main/java/com/android/tools/r8/ir/synthetic/SynthesizedCode.java
index 7716663..3d3f9f1 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/SynthesizedCode.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/SynthesizedCode.java
@@ -37,7 +37,7 @@
@Override
public final String toString() {
- return toString(null);
+ return toString(null, null);
}
private static void registerReachableDefinitionsDefault(UseRegistry registry) {
@@ -61,7 +61,7 @@
}
@Override
- public final String toString(ClassNameMapper naming) {
+ public final String toString(DexEncodedMethod method, ClassNameMapper naming) {
return "SynthesizedCode: " + sourceCode.toString();
}
}
diff --git a/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java b/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
index 9e9a1ad..504cc4a 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
@@ -59,10 +59,15 @@
return canonicalizeSignature(new FieldSignature(field.name.toString(), type));
}
+ /**
+ * Deobfuscate a class name.
+ *
+ * Returns the deobfuscated name if a mapping was found. Otherwise it returns the passed in name.
+ */
public String deobfuscateClassName(String name) {
ClassNaming classNaming = classNameMappings.get(name);
if (classNaming == null) {
- return null;
+ return name;
}
return classNaming.originalName;
}
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
index ee0614f..17da59b 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
@@ -4,7 +4,6 @@
package com.android.tools.r8.optimize;
import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.graph.Descriptor;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedField;
import com.android.tools.r8.graph.DexEncodedMethod;
@@ -98,11 +97,6 @@
return searchClass.type;
}
- private boolean isNotFromLibrary(Descriptor item) {
- DexClass clazz = appInfo.definitionFor(item.getHolder());
- return clazz != null && !clazz.isLibraryClass();
- }
-
private DexEncodedMethod virtualLookup(DexMethod method) {
return appInfo.lookupVirtualDefinition(method.getHolder(), method);
}
@@ -162,12 +156,12 @@
}
DexProgramClass newHolder = null;
// Recurse through supertype chain.
- if (originalClass.superType.isSubtypeOf(targetClass.getType(), appInfo)) {
+ if (originalClass.superType.isSubtypeOf(targetClass.type, appInfo)) {
DexClass superClass = appInfo.definitionFor(originalClass.superType);
newHolder = findBridgeMethodHolder(superClass, targetClass, packageDescriptor);
} else {
for (DexType iface : originalClass.interfaces.values) {
- if (iface.isSubtypeOf(targetClass.getType(), appInfo)) {
+ if (iface.isSubtypeOf(targetClass.type, appInfo)) {
DexClass interfaceClass = appInfo.definitionFor(iface);
newHolder = findBridgeMethodHolder(interfaceClass, targetClass, packageDescriptor);
}
@@ -190,13 +184,23 @@
for (DexField field : fields) {
field = lense.lookupField(field, null);
DexEncodedField target = lookup.apply(field.getHolder(), field);
- // Rebind to the lowest library class or program class.
- if (target != null && target.field != field) {
+ // Rebind to the lowest library class or program class. Do not rebind accesses to fields that
+ // are not public, as this might lead to access violation errors.
+ if (target != null && target.field != field && isVisibleFromOtherClasses(target)) {
builder.map(field, validTargetFor(target.field, field, lookupTargetOnClass));
}
}
}
+ private boolean isVisibleFromOtherClasses(DexEncodedField field) {
+ // If the field is not public, the visibility on the class can not be a further constraint.
+ if (!field.accessFlags.isPublic()) {
+ return true;
+ }
+ // If the field is public, then a non-public holder class will further constrain visibility.
+ return appInfo.definitionFor(field.field.getHolder()).accessFlags.isPublic();
+ }
+
private static void privateMethodsCheck(DexProgramClass clazz, DexEncodedMethod method) {
throw new Unreachable("Direct invokes should not require forwarding.");
}
@@ -211,9 +215,9 @@
computeMethodRebinding(appInfo.staticInvokes, appInfo::lookupStaticTarget,
DexClass::findDirectTarget, DexProgramClass::addStaticMethod);
- computeFieldRebinding(Sets.union(appInfo.staticFieldsRead, appInfo.staticFieldsWritten),
+ computeFieldRebinding(Sets.union(appInfo.staticFieldReads, appInfo.staticFieldWrites),
appInfo::lookupStaticTarget, DexClass::findStaticTarget);
- computeFieldRebinding(Sets.union(appInfo.instanceFieldsRead, appInfo.instanceFieldsWritten),
+ computeFieldRebinding(Sets.union(appInfo.instanceFieldReads, appInfo.instanceFieldWrites),
appInfo::lookupInstanceTarget, DexClass::findInstanceTarget);
return builder.build(lense, appInfo.dexItemFactory);
}
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 2dab755..abdda65 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -37,6 +37,7 @@
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import java.util.ArrayDeque;
+import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
@@ -47,6 +48,7 @@
import java.util.Queue;
import java.util.Set;
import java.util.function.BiFunction;
+import java.util.function.Function;
import java.util.stream.Collectors;
/**
@@ -856,9 +858,8 @@
}
private Set<DexField> collectFields(Map<DexType, Set<DexField>> map) {
- Set<DexField> set = Sets.newIdentityHashSet();
- map.values().forEach(set::addAll);
- return set;
+ return map.values().stream().flatMap(Collection::stream)
+ .collect(Collectors.toCollection(Sets::newIdentityHashSet));
}
Set<DexField> collectInstanceFieldsRead() {
@@ -877,6 +878,32 @@
return Collections.unmodifiableSet(collectFields(staticFieldsWritten));
}
+ private Set<DexField> collectReachedFields(Map<DexType, Set<DexField>> map,
+ Function<DexField, DexField> lookup) {
+ return map.values().stream().flatMap(set -> set.stream().map(lookup))
+ .collect(Collectors.toCollection(Sets::newIdentityHashSet));
+ }
+
+ private DexField tryLookupInstanceField(DexField field) {
+ DexEncodedField target = appInfo.lookupInstanceTarget(field.clazz, field);
+ return target == null ? field : target.field;
+ }
+
+ private DexField tryLookupStaticField(DexField field) {
+ DexEncodedField target = appInfo.lookupStaticTarget(field.clazz, field);
+ return target == null ? field : target.field;
+ }
+
+ Set<DexField> collectFieldsRead() {
+ return Sets.union(collectReachedFields(instanceFieldsRead, this::tryLookupInstanceField),
+ collectReachedFields(staticFieldsRead, this::tryLookupStaticField));
+ }
+
+ Set<DexField> collectFieldsWritten() {
+ return Sets.union(collectReachedFields(instanceFieldsWritten, this::tryLookupInstanceField),
+ collectReachedFields(staticFieldsWritten, this::tryLookupStaticField));
+ }
+
private static class Action {
final Kind kind;
@@ -981,31 +1008,29 @@
*/
final Set<DexField> liveFields;
/**
- * Set of all fields which are read. Combines {@link #instanceFieldsRead} and
- * {@link #staticFieldsRead}.
+ * Set of all fields which may be touched by a get operation. This is actual field definitions.
*/
public final Set<DexField> fieldsRead;
/**
- * Set of all fields which are written. Combines {@link #instanceFieldsWritten} and
- * {@link #staticFieldsWritten}.
+ * Set of all fields which may be touched by a put operation. This is actual field definitions.
*/
public final Set<DexField> fieldsWritten;
/**
- * Set of all instance fields which are read.
+ * Set of all field ids used in instance field reads.
*/
- public final Set<DexField> instanceFieldsRead;
+ public final Set<DexField> instanceFieldReads;
/**
- * Set of all instance fields which are written.
+ * Set of all field ids used in instance field writes.
*/
- public final Set<DexField> instanceFieldsWritten;
+ public final Set<DexField> instanceFieldWrites;
/**
- * Set of all static fields which are read.
+ * Set of all field ids used in static static field reads.
*/
- public final Set<DexField> staticFieldsRead;
+ public final Set<DexField> staticFieldReads;
/**
- * Set of all static fields which are written.
+ * Set of all field ids used in static field writes.
*/
- public final Set<DexField> staticFieldsWritten;
+ public final Set<DexField> staticFieldWrites;
/**
* Set of all methods referenced in virtual invokes;
*/
@@ -1042,12 +1067,12 @@
this.targetedMethods = toDescriptorSet(enqueuer.targetedMethods.getItems());
this.liveMethods = toDescriptorSet(enqueuer.liveMethods.getItems());
this.liveFields = toDescriptorSet(enqueuer.liveFields.getItems());
- this.instanceFieldsRead = enqueuer.collectInstanceFieldsRead();
- this.instanceFieldsWritten = enqueuer.collectInstanceFieldsWritten();
- this.staticFieldsRead = enqueuer.collectStaticFieldsRead();
- this.staticFieldsWritten = enqueuer.collectStaticFieldsWritten();
- this.fieldsRead = Sets.union(staticFieldsRead, instanceFieldsRead);
- this.fieldsWritten = Sets.union(staticFieldsWritten, instanceFieldsWritten);
+ this.instanceFieldReads = enqueuer.collectInstanceFieldsRead();
+ this.instanceFieldWrites = enqueuer.collectInstanceFieldsWritten();
+ this.staticFieldReads = enqueuer.collectStaticFieldsRead();
+ this.staticFieldWrites = enqueuer.collectStaticFieldsWritten();
+ this.fieldsRead = enqueuer.collectFieldsRead();
+ this.fieldsWritten = enqueuer.collectFieldsWritten();
this.pinnedItems = Collections.unmodifiableSet(enqueuer.pinnedItems);
this.virtualInvokes = joinInvokedMethods(enqueuer.virtualInvokes);
this.superInvokes = joinInvokedMethods(enqueuer.superInvokes);
@@ -1055,8 +1080,8 @@
this.staticInvokes = joinInvokedMethods(enqueuer.staticInvokes);
this.noSideEffects = enqueuer.rootSet.noSideEffects;
this.assumedValues = enqueuer.rootSet.assumedValues;
- assert Sets.intersection(instanceFieldsRead, staticFieldsRead).size() == 0;
- assert Sets.intersection(instanceFieldsWritten, staticFieldsWritten).size() == 0;
+ assert Sets.intersection(instanceFieldReads, staticFieldReads).size() == 0;
+ assert Sets.intersection(instanceFieldWrites, staticFieldWrites).size() == 0;
}
private AppInfoWithLiveness(AppInfoWithLiveness previous, DexApplication application) {
@@ -1066,11 +1091,12 @@
this.targetedMethods = previous.targetedMethods;
this.liveMethods = previous.liveMethods;
this.liveFields = previous.liveFields;
- this.instanceFieldsRead = previous.instanceFieldsRead;
- this.instanceFieldsWritten = previous.instanceFieldsWritten;
- this.staticFieldsRead = previous.staticFieldsRead;
- this.staticFieldsWritten = previous.staticFieldsWritten;
+ this.instanceFieldReads = previous.instanceFieldReads;
+ this.instanceFieldWrites = previous.instanceFieldWrites;
+ this.staticFieldReads = previous.staticFieldReads;
+ this.staticFieldWrites = previous.staticFieldWrites;
this.fieldsRead = previous.fieldsRead;
+ // TODO(herhut): We remove fields that are only written, so maybe update this.
this.fieldsWritten = previous.fieldsWritten;
this.pinnedItems = previous.pinnedItems;
this.noSideEffects = previous.noSideEffects;
@@ -1079,8 +1105,8 @@
this.superInvokes = previous.superInvokes;
this.directInvokes = previous.directInvokes;
this.staticInvokes = previous.staticInvokes;
- assert Sets.intersection(instanceFieldsRead, staticFieldsRead).size() == 0;
- assert Sets.intersection(instanceFieldsWritten, staticFieldsWritten).size() == 0;
+ assert Sets.intersection(instanceFieldReads, staticFieldReads).size() == 0;
+ assert Sets.intersection(instanceFieldWrites, staticFieldWrites).size() == 0;
}
private AppInfoWithLiveness(AppInfoWithLiveness previous, GraphLense lense) {
@@ -1090,12 +1116,12 @@
this.targetedMethods = rewriteItems(previous.targetedMethods, lense::lookupMethod);
this.liveMethods = rewriteItems(previous.liveMethods, lense::lookupMethod);
this.liveFields = rewriteItems(previous.liveFields, lense::lookupField);
- this.instanceFieldsRead = rewriteItems(previous.instanceFieldsRead, lense::lookupField);
- this.instanceFieldsWritten = rewriteItems(previous.instanceFieldsWritten, lense::lookupField);
- this.staticFieldsRead = rewriteItems(previous.staticFieldsRead, lense::lookupField);
- this.staticFieldsWritten = rewriteItems(previous.staticFieldsWritten, lense::lookupField);
- this.fieldsRead = Sets.union(staticFieldsRead, instanceFieldsRead);
- this.fieldsWritten = Sets.union(staticFieldsWritten, instanceFieldsWritten);
+ this.instanceFieldReads = rewriteItems(previous.instanceFieldReads, lense::lookupField);
+ this.instanceFieldWrites = rewriteItems(previous.instanceFieldWrites, lense::lookupField);
+ this.staticFieldReads = rewriteItems(previous.staticFieldReads, lense::lookupField);
+ this.staticFieldWrites = rewriteItems(previous.staticFieldWrites, lense::lookupField);
+ this.fieldsRead = rewriteItems(previous.fieldsRead, lense::lookupField);
+ this.fieldsWritten = rewriteItems(previous.fieldsWritten, lense::lookupField);
// TODO(herhut): Migrate these to Descriptors, as well.
this.pinnedItems = previous.pinnedItems;
this.noSideEffects = previous.noSideEffects;
@@ -1104,8 +1130,8 @@
this.superInvokes = rewriteItems(previous.superInvokes, lense::lookupMethod);
this.directInvokes = rewriteItems(previous.directInvokes, lense::lookupMethod);
this.staticInvokes = rewriteItems(previous.staticInvokes, lense::lookupMethod);
- assert Sets.intersection(instanceFieldsRead, staticFieldsRead).size() == 0;
- assert Sets.intersection(instanceFieldsWritten, staticFieldsWritten).size() == 0;
+ assert Sets.intersection(instanceFieldReads, staticFieldReads).size() == 0;
+ assert Sets.intersection(instanceFieldWrites, staticFieldWrites).size() == 0;
}
private Set<DexMethod> joinInvokedMethods(Map<DexType, Set<DexMethod>> invokes) {
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index 860fc45..9fcb50a 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -41,13 +41,14 @@
"optimizationpasses",
"target");
private static final List<String> ignoredOptionalSingleArgOptions = ImmutableList
- .of("keepdirectories");
+ .of("keepdirectories", "runtype", "laststageoutput");
private static final List<String> ignoredFlagOptions = ImmutableList
.of("forceprocessing", "dontusemixedcaseclassnames",
"dontpreverify", "experimentalshrinkunusedprotofields",
"filterlibraryjarswithorginalprogramjars",
"dontskipnonpubliclibraryclasses",
"dontskipnonpubliclibraryclassmembers",
+ "overloadaggressively",
"invokebasemethod");
private static final List<String> ignoredClassDescriptorOptions = ImmutableList
.of("isclassnamestring",
diff --git a/src/main/java/com/android/tools/r8/shaking/SimpleClassMerger.java b/src/main/java/com/android/tools/r8/shaking/SimpleClassMerger.java
index a2864e4..d03c74b 100644
--- a/src/main/java/com/android/tools/r8/shaking/SimpleClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/SimpleClassMerger.java
@@ -27,9 +27,7 @@
import com.android.tools.r8.utils.Timing;
import com.google.common.base.Equivalence;
import com.google.common.base.Equivalence.Wrapper;
-import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
-import com.google.common.collect.Sets;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.Reference2IntMap;
@@ -44,6 +42,7 @@
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
+import java.util.stream.Collectors;
/**
* Merges Supertypes with a single implementation into their single subtype.
@@ -61,7 +60,7 @@
private final GraphLense.Builder renamedMembersLense = GraphLense.builder();
private final Map<DexType, DexType> mergedClasses = new IdentityHashMap<>();
private final Timing timing;
- private Set<DexMethod> invokes;
+ private Collection<DexMethod> invokes;
private int numberOfMerges = 0;
public SimpleClassMerger(DexApplication application, AppInfoWithLiveness appInfo,
@@ -81,25 +80,51 @@
&& clazz.type.getSingleSubtype() != null;
}
- private Set<DexMethod> getInvokes() {
- if (invokes == null) {
+ private void addProgramMethods(Set<Wrapper<DexMethod>> set, DexMethod method,
+ Equivalence<DexMethod> equivalence) {
+ DexClass definition = appInfo.definitionFor(method.holder);
+ if (definition != null && definition.isProgramClass()) {
+ set.add(equivalence.wrap(method));
+ }
+ }
- // TODO(herhut): Ignore invokes into the library, as those can only reference library types.
- invokes = Sets.newIdentityHashSet();
- invokes.addAll(appInfo.directInvokes);
- invokes.addAll(appInfo.staticInvokes);
- invokes.addAll(appInfo.superInvokes);
- invokes.addAll(appInfo.virtualInvokes);
- invokes.addAll(appInfo.targetedMethods);
- invokes.addAll(appInfo.liveMethods);
- for (DexEncodedMethod method : Iterables
- .filter(appInfo.pinnedItems, DexEncodedMethod.class)) {
- invokes.add(method.method);
- }
+ private Collection<DexMethod> getInvokes() {
+ if (invokes == null) {
+ // Collect all reachable methods that are not within a library class. Those defined on
+ // library classes are known not to have program classes in their signature.
+ // Also filter methods that only use types from library classes in their signatures. We
+ // know that those won't conflict.
+ Set<Wrapper<DexMethod>> filteredInvokes = new HashSet<>();
+ Equivalence<DexMethod> equivalence = MethodSignatureEquivalence.get();
+ appInfo.targetedMethods.forEach(m -> addProgramMethods(filteredInvokes, m, equivalence));
+ invokes = filteredInvokes.stream().map(Wrapper::get).filter(this::removeNonProgram)
+ .collect(Collectors.toList());
}
return invokes;
}
+ private boolean isProgramClass(DexType type) {
+ if (type.isArrayType()) {
+ type = type.toBaseType(appInfo.dexItemFactory);
+ }
+ if (type.isClassType()) {
+ DexClass clazz = appInfo.definitionFor(type);
+ if (clazz != null && clazz.isProgramClass()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean removeNonProgram(DexMethod dexMethod) {
+ for (DexType type : dexMethod.proto.parameters.values) {
+ if (isProgramClass(type)) {
+ return true;
+ }
+ }
+ return isProgramClass(dexMethod.proto.returnType);
+ }
+
public GraphLense run() {
timing.begin("merge");
GraphLense mergingGraphLense = mergeClasses(graphLense);
@@ -426,7 +451,8 @@
if (previous != null) {
if (!previous.accessFlags.isBridge()) {
if (!method.accessFlags.isBridge()) {
- throw new CompilationError("Class merging produced invalid result.");
+ throw new CompilationError(
+ "Class merging produced invalid result on: " + previous.toSourceString());
} else {
filtered.put(previous.method, previous);
}
diff --git a/src/main/java/com/android/tools/r8/shaking/TreePruner.java b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
index f801cdd..04f6786 100644
--- a/src/main/java/com/android/tools/r8/shaking/TreePruner.java
+++ b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
@@ -5,20 +5,16 @@
import com.android.tools.r8.graph.DexApplication;
import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexClassPromise;
import com.android.tools.r8.graph.DexEncodedField;
import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexLibraryClass;
import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.KeyedDexItem;
import com.android.tools.r8.graph.PresortedComparable;
import com.android.tools.r8.logging.Log;
import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
import com.android.tools.r8.utils.InternalOptions;
import java.util.ArrayList;
-import java.util.IdentityHashMap;
-import java.util.Map;
+import java.util.List;
import java.util.Set;
public class TreePruner {
@@ -52,22 +48,20 @@
}
private DexApplication.Builder removeUnused(DexApplication application) {
- return new DexApplication.Builder(application, removeUnusedClassStructure(application));
+ return new DexApplication.Builder(application)
+ .replaceProgramClasses(getNewProgramClasses(application.classes()));
}
- private Map<DexType, DexClassPromise> removeUnusedClassStructure(DexApplication application) {
- Map<DexType, DexClassPromise> classMap = new IdentityHashMap<>();
- for (DexLibraryClass clazz : application.libraryClasses()) {
- classMap.put(clazz.type, clazz);
- }
- for (DexProgramClass clazz : application.classes()) {
+ private List<DexProgramClass> getNewProgramClasses(List<DexProgramClass> classes) {
+ List<DexProgramClass> newClasses = new ArrayList<>();
+ for (DexProgramClass clazz : classes) {
if (!appInfo.liveTypes.contains(clazz.type) && !options.debugKeepRules) {
// The class is completely unused and we can remove it.
if (Log.ENABLED) {
Log.debug(getClass(), "Removing class: " + clazz);
}
} else {
- classMap.put(clazz.type, clazz);
+ newClasses.add(clazz);
if (!appInfo.instantiatedTypes.contains(clazz.type) && !options.debugKeepRules) {
// The class is only needed as a type but never instantiated. Make it abstract to reflect
// this.
@@ -89,7 +83,7 @@
clazz.staticFields = reachableFields(clazz.staticFields());
}
}
- return classMap;
+ return newClasses;
}
private <S extends PresortedComparable<S>, T extends KeyedDexItem<S>> int firstUnreachableIndex(
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
index 453a250..389db15 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -7,8 +7,8 @@
import static com.android.tools.r8.utils.FileUtils.isClassFile;
import static com.android.tools.r8.utils.FileUtils.isDexFile;
+import com.android.tools.r8.ClassFileResourceProvider;
import com.android.tools.r8.Resource;
-import com.android.tools.r8.ResourceProvider;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.ClassKind;
@@ -49,14 +49,12 @@
public class AndroidApp {
public static final String DEFAULT_PROGUARD_MAP_FILE = "proguard.map";
- public static final String DEFAULT_PROGUARD_SEEDS_FILE = "proguard.seeds";
- public static final String DEFAULT_PACKAGE_DISTRIBUTION_FILE = "package.map";
private final ImmutableList<Resource> programResources;
private final ImmutableList<Resource> classpathResources;
private final ImmutableList<Resource> libraryResources;
- private final ImmutableList<ResourceProvider> classpathResourceProviders;
- private final ImmutableList<ResourceProvider> libraryResourceProviders;
+ private final ImmutableList<ClassFileResourceProvider> classpathResourceProviders;
+ private final ImmutableList<ClassFileResourceProvider> libraryResourceProviders;
private final Resource proguardMap;
private final Resource proguardSeeds;
private final Resource packageDistribution;
@@ -67,8 +65,8 @@
ImmutableList<Resource> programResources,
ImmutableList<Resource> classpathResources,
ImmutableList<Resource> libraryResources,
- ImmutableList<ResourceProvider> classpathResourceProviders,
- ImmutableList<ResourceProvider> libraryResourceProviders,
+ ImmutableList<ClassFileResourceProvider> classpathResourceProviders,
+ ImmutableList<ClassFileResourceProvider> libraryResourceProviders,
Resource proguardMap,
Resource proguardSeeds,
Resource packageDistribution,
@@ -178,12 +176,12 @@
}
/** Get classpath resource providers. */
- public List<ResourceProvider> getClasspathResourceProviders() {
+ public List<ClassFileResourceProvider> getClasspathResourceProviders() {
return classpathResourceProviders;
}
/** Get library resource providers. */
- public List<ResourceProvider> getLibraryResourceProviders() {
+ public List<ClassFileResourceProvider> getLibraryResourceProviders() {
return libraryResourceProviders;
}
@@ -290,8 +288,6 @@
Path fileName = directory.resolve(outputMode.getFileName(dexProgramSources.get(i), i));
Files.copy(dexProgramSources.get(i).getStream(closer), fileName, options);
}
- writeProguardMap(closer, directory, overwrite);
- writeProguardSeeds(closer, directory, overwrite);
}
}
@@ -334,17 +330,6 @@
out.closeEntry();
}
}
- // Write the proguard map to the archives containing directory.
- // TODO(zerny): How do we want to determine the output location for the extra resources?
- writeProguardMap(closer, archive.getParent(), overwrite);
- writeProguardSeeds(closer, archive.getParent(), overwrite);
- }
- }
-
- private void writeProguardMap(Closer closer, Path parent, boolean overwrite) throws IOException {
- InputStream input = getProguardMap(closer);
- if (input != null) {
- Files.copy(input, parent.resolve(DEFAULT_PROGUARD_MAP_FILE), copyOptions(overwrite));
}
}
@@ -354,12 +339,16 @@
out.write(ByteStreams.toByteArray(input));
}
- private void writeProguardSeeds(Closer closer, Path parent, boolean overwrite)
- throws IOException {
+ public void writeProguardSeeds(Closer closer, OutputStream out) throws IOException {
InputStream input = getProguardSeeds(closer);
- if (input != null) {
- Files.copy(input, parent.resolve(DEFAULT_PROGUARD_SEEDS_FILE), copyOptions(overwrite));
- }
+ assert input != null;
+ out.write(ByteStreams.toByteArray(input));
+ }
+
+ public void writeMainDexList(Closer closer, OutputStream out) throws IOException {
+ InputStream input = getMainDexList(closer);
+ assert input != null;
+ out.write(ByteStreams.toByteArray(input));
}
private OpenOption[] openOptions(boolean overwrite) {
@@ -383,8 +372,8 @@
private final List<Resource> programResources = new ArrayList<>();
private final List<Resource> classpathResources = new ArrayList<>();
private final List<Resource> libraryResources = new ArrayList<>();
- private final List<ResourceProvider> classpathResourceProviders = new ArrayList<>();
- private final List<ResourceProvider> libraryResourceProviders = new ArrayList<>();
+ private final List<ClassFileResourceProvider> classpathResourceProviders = new ArrayList<>();
+ private final List<ClassFileResourceProvider> libraryResourceProviders = new ArrayList<>();
private Resource proguardMap;
private Resource proguardSeeds;
private Resource packageDistribution;
@@ -467,7 +456,7 @@
/**
* Add classpath resource provider.
*/
- public Builder addClasspathResourceProvider(ResourceProvider provider) {
+ public Builder addClasspathResourceProvider(ClassFileResourceProvider provider) {
classpathResourceProviders.add(provider);
return this;
}
@@ -492,7 +481,7 @@
/**
* Add library resource provider.
*/
- public Builder addLibraryResourceProvider(ResourceProvider provider) {
+ public Builder addLibraryResourceProvider(ClassFileResourceProvider provider) {
libraryResourceProviders.add(provider);
return this;
}
@@ -588,6 +577,14 @@
}
/**
+ * Set the main-dex list data.
+ */
+ public Builder setMainDexListData(byte[] content) {
+ mainDexList = content == null ? null : Resource.fromBytes(null, content);
+ return this;
+ }
+
+ /**
* Build final AndroidApp.
*/
public AndroidApp build() {
@@ -644,7 +641,7 @@
Resource.Kind.DEX, ByteStreams.toByteArray(stream)));
} else if (isClassFile(name)) {
containsClassData = true;
- String descriptor = PreloadedResourceProvider.guessTypeDescriptor(name);
+ String descriptor = PreloadedClassFileProvider.guessTypeDescriptor(name);
resources(classKind).add(Resource.fromBytes(Resource.Kind.CLASSFILE,
ByteStreams.toByteArray(stream), Collections.singleton(descriptor)));
}
diff --git a/src/main/java/com/android/tools/r8/utils/ClassMap.java b/src/main/java/com/android/tools/r8/utils/ClassMap.java
new file mode 100644
index 0000000..d0f2cda
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/ClassMap.java
@@ -0,0 +1,151 @@
+// Copyright (c) 2017, 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.utils;
+
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexType;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * Represents a collection of classes. Collection can be fully loaded,
+ * lazy loaded or have preloaded classes along with lazy loaded content.
+ */
+public abstract class ClassMap<T extends DexClass> {
+ // For each type which has ever been queried stores one class loaded from
+ // resources provided by different resource providers.
+ //
+ // NOTE: all access must be synchronized on `classes`.
+ private final Map<DexType, Value<T>> classes;
+
+ // Class provider if available. In case it's `null`, all classes of
+ // the collection must be pre-populated in `classes`.
+ private final ClassProvider<T> classProvider;
+
+ ClassMap(Map<DexType, Value<T>> classes, ClassProvider<T> classProvider) {
+ this.classes = classes == null ? new IdentityHashMap<>() : classes;
+ this.classProvider = classProvider;
+ assert this.classProvider == null || this.classProvider.getClassKind() == getClassKind();
+ }
+
+ /** Resolves a class conflict by selecting a class, may generate compilation error. */
+ abstract T resolveClassConflict(T a, T b);
+
+ /** Kind of the classes supported by this collection. */
+ abstract ClassKind getClassKind();
+
+ @Override
+ public String toString() {
+ synchronized (classes) {
+ return classes.size() + " loaded, provider: " +
+ (classProvider == null ? "none" : classProvider.toString());
+ }
+ }
+
+ /** Returns a definition for a class or `null` if there is no such class in the collection. */
+ public T get(DexType type) {
+ final Value<T> value = getOrCreateValue(type);
+
+ if (!value.ready) {
+ // Load the value in a context synchronized on value instance. This way
+ // we avoid locking the whole collection during expensive resource loading
+ // and classes construction operations.
+ synchronized (value) {
+ if (!value.ready && classProvider != null) {
+ classProvider.collectClass(type, clazz -> {
+ assert clazz != null;
+ assert getClassKind().isOfKind(clazz);
+ assert !value.ready;
+
+ if (clazz.type != type) {
+ throw new CompilationError("Class content provided for type descriptor "
+ + type.toSourceString() + " actually defines class " + clazz.type
+ .toSourceString());
+ }
+
+ if (value.clazz == null) {
+ value.clazz = clazz;
+ } else {
+ // The class resolution *may* generate a compilation error as one of
+ // possible resolutions. In this case we leave `value` in (false, null)
+ // state so in rare case of another thread trying to get the same class
+ // before this error is propagated it will get the same conflict.
+ T oldClass = value.clazz;
+ value.clazz = null;
+ value.clazz = resolveClassConflict(oldClass, clazz);
+ }
+ });
+ }
+ value.ready = true;
+ }
+ }
+
+ assert value.ready;
+ return value.clazz;
+ }
+
+ private Value<T> getOrCreateValue(DexType type) {
+ synchronized (classes) {
+ return classes.computeIfAbsent(type, k -> new Value<>());
+ }
+ }
+
+ /** Returns currently loaded classes */
+ public List<T> collectLoadedClasses() {
+ List<T> loadedClasses = new ArrayList<>();
+ synchronized (classes) {
+ for (Value<T> value : classes.values()) {
+ // Method collectLoadedClasses() must always be called when there
+ // is no risk of concurrent loading of the classes, otherwise the
+ // behaviour of this method is undefined. Note that value mutations
+ // are NOT synchronized in `classes`, so the assertion below does
+ // not enforce this requirement, but may help detect wrong behaviour.
+ assert value.ready : "";
+ if (value.clazz != null) {
+ loadedClasses.add(value.clazz);
+ }
+ }
+ }
+ return loadedClasses;
+ }
+
+ /** Forces loading of all the classes satisfying the criteria specified. */
+ public void forceLoad(Predicate<DexType> load) {
+ if (classProvider != null) {
+ Set<DexType> loaded;
+ synchronized (classes) {
+ loaded = classes.keySet();
+ }
+ Collection<DexType> types = classProvider.collectTypes();
+ for (DexType type : types) {
+ if (load.test(type) && !loaded.contains(type)) {
+ get(type); // force-load type.
+ }
+ }
+ }
+ }
+
+ // Represents a value in the class map.
+ final static class Value<T> {
+ volatile boolean ready;
+ T clazz;
+
+ Value() {
+ ready = false;
+ clazz = null;
+ }
+
+ Value(T clazz) {
+ this.clazz = clazz;
+ this.ready = true;
+ }
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/ClassProvider.java b/src/main/java/com/android/tools/r8/utils/ClassProvider.java
new file mode 100644
index 0000000..25abdce
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/ClassProvider.java
@@ -0,0 +1,187 @@
+// Copyright (c) 2017, 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.utils;
+
+import static com.android.tools.r8.utils.FileUtils.DEFAULT_DEX_FILENAME;
+
+import com.android.tools.r8.ClassFileResourceProvider;
+import com.android.tools.r8.Resource;
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.JarApplicationReader;
+import com.android.tools.r8.graph.JarClassFileReader;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.common.io.Closer;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+
+/** Represents a provider for classes loaded from different sources. */
+public abstract class ClassProvider<T extends DexClass> {
+ private final ClassKind classKind;
+
+ ClassProvider(ClassKind classKind) {
+ this.classKind = classKind;
+ }
+
+ /** The kind of the classes created by the provider. */
+ final ClassKind getClassKind() {
+ return classKind;
+ }
+
+ /**
+ * The provider uses the callback to return all the classes that might
+ * be associated with the descriptor asked for.
+ *
+ * NOTE: the provider is not required to cache created classes and this
+ * method may create a new class instance in case it is called twice for
+ * the same type. For this reason it is recommended that the provider
+ * user only calls this method once per any given type.
+ *
+ * NOTE: thread-safe.
+ */
+ public abstract void collectClass(DexType type, Consumer<T> classConsumer);
+
+ /**
+ * Returns all the types of classes that might be produced by this provider.
+ *
+ * NOTE: thread-safe.
+ */
+ public abstract Collection<DexType> collectTypes();
+
+ /** Create class provider for java class resource provider. */
+ public static <T extends DexClass> ClassProvider<T> forClassFileResources(
+ ClassKind classKind, ClassFileResourceProvider provider, JarApplicationReader reader) {
+ return new ClassFileResourceReader<>(classKind, provider, reader);
+ }
+
+ /** Create class provider for preloaded classes, classes may have conflicting names. */
+ public static <T extends DexClass> ClassProvider<T> forPreloadedClasses(
+ ClassKind classKind, Collection<T> classes) {
+ ImmutableListMultimap.Builder<DexType, T> builder = ImmutableListMultimap.builder();
+ for (T clazz : classes) {
+ builder.put(clazz.type, clazz);
+ }
+ return new PreloadedClassProvider<>(classKind, builder.build());
+ }
+
+ /** Create class provider for preloaded classes. */
+ public static <T extends DexClass> ClassProvider<T> combine(
+ ClassKind classKind, List<ClassProvider<T>> providers) {
+ return new CombinedClassProvider<>(classKind, providers);
+ }
+
+ private static class ClassFileResourceReader<T extends DexClass> extends ClassProvider<T> {
+ private final ClassKind classKind;
+ private final ClassFileResourceProvider provider;
+ private final JarApplicationReader reader;
+
+ private ClassFileResourceReader(
+ ClassKind classKind, ClassFileResourceProvider provider, JarApplicationReader reader) {
+ super(classKind);
+ this.classKind = classKind;
+ this.provider = provider;
+ this.reader = reader;
+ }
+
+ @Override
+ public void collectClass(DexType type, Consumer<T> classConsumer) {
+ String descriptor = type.descriptor.toString();
+ Resource resource = provider.getResource(descriptor);
+ if (resource != null) {
+ try (Closer closer = Closer.create()) {
+ JarClassFileReader classReader =
+ new JarClassFileReader(reader, classKind.bridgeConsumer(classConsumer));
+ classReader.read(DEFAULT_DEX_FILENAME, classKind, resource.getStream(closer));
+ } catch (IOException e) {
+ throw new CompilationError("Failed to load class: " + descriptor, e);
+ }
+ }
+ }
+
+ @Override
+ public Collection<DexType> collectTypes() {
+ List<DexType> types = new ArrayList<>();
+ for (String descriptor : provider.getClassDescriptors()) {
+ types.add(reader.options.itemFactory.createType(descriptor));
+ }
+ return types;
+ }
+
+ @Override
+ public String toString() {
+ return "class-resource-provider(" + provider.toString() + ")";
+ }
+ }
+
+ private static class PreloadedClassProvider<T extends DexClass> extends ClassProvider<T> {
+ private final Multimap<DexType, T> classes;
+
+ private PreloadedClassProvider(ClassKind classKind, Multimap<DexType, T> classes) {
+ super(classKind);
+ this.classes = classes;
+ }
+
+ @Override
+ public void collectClass(DexType type, Consumer<T> classConsumer) {
+ for (T clazz : classes.get(type)) {
+ classConsumer.accept(clazz);
+ }
+ }
+
+ @Override
+ public Collection<DexType> collectTypes() {
+ return classes.keys();
+ }
+
+ @Override
+ public String toString() {
+ return "preloaded(" + classes.size() + ")";
+ }
+ }
+
+ private static class CombinedClassProvider<T extends DexClass> extends ClassProvider<T> {
+ private final List<ClassProvider<T>> providers;
+
+ private CombinedClassProvider(ClassKind classKind, List<ClassProvider<T>> providers) {
+ super(classKind);
+ this.providers = providers;
+ }
+
+ @Override
+ public void collectClass(DexType type, Consumer<T> classConsumer) {
+ for (ClassProvider<T> provider : providers) {
+ provider.collectClass(type, classConsumer);
+ }
+ }
+
+ @Override
+ public Collection<DexType> collectTypes() {
+ Set<DexType> types = Sets.newIdentityHashSet();
+ for (ClassProvider<T> provider : providers) {
+ types.addAll(provider.collectTypes());
+ }
+ return types;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ String prefix = "combined(";
+ for (ClassProvider<T> provider : providers) {
+ builder.append(prefix);
+ prefix = ", ";
+ builder.append(provider);
+ }
+ return builder.append(")").toString();
+ }
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/ClasspathClassCollection.java b/src/main/java/com/android/tools/r8/utils/ClasspathClassCollection.java
new file mode 100644
index 0000000..ee00c39
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/ClasspathClassCollection.java
@@ -0,0 +1,30 @@
+// Copyright (c) 2017, 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.utils;
+
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexClasspathClass;
+
+/** Represents a collection of classpath classes. */
+public class ClasspathClassCollection extends ClassMap<DexClasspathClass> {
+ public ClasspathClassCollection(ClassProvider<DexClasspathClass> classProvider) {
+ super(null, classProvider);
+ }
+
+ @Override
+ DexClasspathClass resolveClassConflict(DexClasspathClass a, DexClasspathClass b) {
+ throw new CompilationError("Classpath type already present: " + a.type.toSourceString());
+ }
+
+ @Override
+ ClassKind getClassKind() {
+ return ClassKind.CLASSPATH;
+ }
+
+ @Override
+ public String toString() {
+ return "classpath classes: " + super.toString();
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
index ec430fd..eae33d1 100644
--- a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
@@ -88,11 +88,7 @@
String clazz = descriptor.substring(1, descriptor.length() - 1)
.replace(DESCRIPTOR_PACKAGE_SEPARATOR, JAVA_PACKAGE_SEPARATOR);
String originalName =
- classNameMapper == null ? null : classNameMapper.deobfuscateClassName(clazz);
- if (originalName == null) {
- // Class was not renamed, reconstruct the java name.
- return clazz;
- }
+ classNameMapper == null ? clazz : classNameMapper.deobfuscateClassName(clazz);
return originalName;
case '[':
return descriptorToJavaType(descriptor.substring(1, descriptor.length()), classNameMapper)
diff --git a/src/main/java/com/android/tools/r8/utils/DirectoryClassFileProvider.java b/src/main/java/com/android/tools/r8/utils/DirectoryClassFileProvider.java
new file mode 100644
index 0000000..c060ead
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/DirectoryClassFileProvider.java
@@ -0,0 +1,76 @@
+// Copyright (c) 2017, 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.utils;
+
+import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
+
+import com.android.tools.r8.ClassFileResourceProvider;
+import com.android.tools.r8.Resource;
+import com.google.common.collect.Sets;
+import java.io.File;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Lazy resource provider returning class file resources based
+ * on filesystem directory content.
+ */
+public final class DirectoryClassFileProvider implements ClassFileResourceProvider {
+ private final Path root;
+
+ private DirectoryClassFileProvider(Path root) {
+ this.root = root;
+ }
+
+ @Override
+ public Set<String> getClassDescriptors() {
+ HashSet<String> result = Sets.newHashSet();
+ collectClassDescriptors(root, result);
+ return result;
+ }
+
+ private void collectClassDescriptors(Path dir, Set<String> result) {
+ File file = dir.toFile();
+ if (file.exists()) {
+ File[] files = file.listFiles();
+ if (files != null) {
+ for (File child : files) {
+ if (child.isDirectory()) {
+ collectClassDescriptors(child.toPath(), result);
+ } else {
+ String relative = root.relativize(child.toPath()).toString();
+ if (relative.endsWith(CLASS_EXTENSION)) {
+ result.add("L" + relative.substring(
+ 0, relative.length() - CLASS_EXTENSION.length()) + ";");
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public Resource getResource(String descriptor) {
+ assert DescriptorUtils.isClassDescriptor(descriptor);
+
+ // Build expected file path based on type descriptor.
+ String classBinaryName = DescriptorUtils.getClassBinaryNameFromDescriptor(descriptor);
+ Path filePath = root.resolve(classBinaryName + CLASS_EXTENSION);
+ File file = filePath.toFile();
+
+ return (file.exists() && !file.isDirectory())
+ ? Resource.fromFile(Resource.Kind.CLASSFILE, filePath) : null;
+ }
+
+ /** Create resource provider from directory path. */
+ public static ClassFileResourceProvider fromDirectory(Path dir) {
+ return new DirectoryClassFileProvider(dir.toAbsolutePath());
+ }
+
+ @Override
+ public String toString() {
+ return "directory(" + root + ")";
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/DirectoryResourceProvider.java b/src/main/java/com/android/tools/r8/utils/DirectoryResourceProvider.java
deleted file mode 100644
index b003d86..0000000
--- a/src/main/java/com/android/tools/r8/utils/DirectoryResourceProvider.java
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (c) 2017, 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.utils;
-
-import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
-
-import com.android.tools.r8.Resource;
-import com.android.tools.r8.ResourceProvider;
-import java.io.File;
-import java.nio.file.Path;
-
-/**
- * Lazy resource provider based on filesystem directory content.
- *
- * NOTE: only handles classfile resources.
- */
-public final class DirectoryResourceProvider implements ResourceProvider {
- private final Path root;
-
- private DirectoryResourceProvider(Path root) {
- this.root = root;
- }
-
- @Override
- public Resource getResource(String descriptor) {
- assert DescriptorUtils.isClassDescriptor(descriptor);
-
- // Build expected file path based on type descriptor.
- String classBinaryName = DescriptorUtils.getClassBinaryNameFromDescriptor(descriptor);
- Path filePath = root.resolve(classBinaryName + CLASS_EXTENSION);
- File file = filePath.toFile();
-
- return (file.exists() && !file.isDirectory())
- ? Resource.fromFile(Resource.Kind.CLASSFILE, filePath) : null;
- }
-
- /** Create resource provider from directory path. */
- public static ResourceProvider fromDirectory(Path dir) {
- return new DirectoryResourceProvider(dir.toAbsolutePath());
- }
-}
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 5945741..7b1cfe1 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -7,12 +7,14 @@
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.ir.conversion.CallGraph;
import com.android.tools.r8.shaking.ProguardConfigurationRule;
import com.android.tools.r8.shaking.ProguardTypeMatcher;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.nio.file.Path;
import java.util.List;
+import java.util.function.BiFunction;
public class InternalOptions {
@@ -35,9 +37,6 @@
public boolean skipDebugLineNumberOpt = false;
public boolean skipClassMerging = true;
- public boolean lazyClasspathLoading = false;
- public boolean lazyLibraryLoading = false;
-
// Number of threads to use while processing the dex files.
public int numberOfThreads = NOT_SPECIFIED;
// Print smali disassembly.
@@ -46,8 +45,6 @@
public boolean verbose = false;
// Silencing output.
public boolean quiet = false;
- // Eagerly fill dex files as much as possible.
- public boolean fillDexFiles = false;
public List<String> methodsFilter = ImmutableList.of();
public int minApiLevel = Constants.DEFAULT_ANDROID_API;
@@ -69,6 +66,8 @@
public Path seedsFile;
public boolean printMapping;
public Path printMappingFile;
+ public boolean printMainDexList;
+ public Path printMainDexListFile;
public boolean ignoreMissingClasses = false;
public boolean skipMinification = false;
public String packagePrefix = "";
@@ -87,11 +86,14 @@
public List<String> obfuscationDictionary = ImmutableList.of();
public ImmutableList<ProguardConfigurationRule> mainDexKeepRules = ImmutableList.of();
+ public boolean minimalMainDex;
public ImmutableList<ProguardConfigurationRule> keepRules = ImmutableList.of();
public ImmutableSet<ProguardTypeMatcher> dontWarnPatterns = ImmutableSet.of();
public String warningInvalidParameterAnnotations = null;
+ public boolean overwriteOutputs; // default value is set in D/R8Command
+
public boolean printWarnings() {
boolean printed = false;
if (warningInvalidParameterAnnotations != null) {
@@ -101,9 +103,13 @@
return printed;
}
+ public boolean hasMethodsFilter() {
+ return methodsFilter.size() > 0;
+ }
+
public boolean methodMatchesFilter(DexEncodedMethod method) {
// Not specifying a filter matches all methods.
- if (methodsFilter.size() == 0) {
+ if (!hasMethodsFilter()) {
return true;
}
// Currently the filter is simple string equality on the qualified name.
@@ -133,7 +139,7 @@
public static class TestingOptions {
- public boolean randomizeCallGraphLeaves = false;
+ public BiFunction<List<DexEncodedMethod>, CallGraph.Leaves, List<DexEncodedMethod>> irOrdering;
}
public static class AttributeRemovalOptions {
diff --git a/src/main/java/com/android/tools/r8/utils/LazyClassCollection.java b/src/main/java/com/android/tools/r8/utils/LazyClassCollection.java
deleted file mode 100644
index 0e71973..0000000
--- a/src/main/java/com/android/tools/r8/utils/LazyClassCollection.java
+++ /dev/null
@@ -1,208 +0,0 @@
-// Copyright (c) 2017, 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.utils;
-
-import com.android.tools.r8.Resource;
-import com.android.tools.r8.ResourceProvider;
-import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.graph.ClassKind;
-import com.android.tools.r8.graph.DexApplication;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexClassPromise;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.JarApplicationReader;
-import com.android.tools.r8.graph.LazyClassFileLoader;
-import com.google.common.collect.ImmutableList;
-import java.util.IdentityHashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Represents a collection of classes loaded lazily from a set of lazy resource
- * providers. The collection is autonomous, it lazily collects promises but
- * does not choose the classes in cases of conflicts, delaying it until
- * the class is asked for.
- *
- * NOTE: only java class resources are allowed to be lazy loaded.
- */
-public final class LazyClassCollection {
- // Stores promises for types which have ever been asked for before. Special value
- // EmptyPromise.INSTANCE indicates there were no resources for this type in any of
- // the resource providers.
- //
- // Promises are potentially coming from different providers and chained, but in majority
- // of the cases there will only be one promise per type. We store promises for all
- // the resource providers and resolve the classes at the time it is queried.
- //
- // Must be synchronized on `classes`.
- private final Map<DexType, DexClassPromise> classes = new IdentityHashMap<>();
-
- // Available lazy resource providers.
- private final List<ResourceProvider> classpathProviders;
- private final List<ResourceProvider> libraryProviders;
-
- // Application reader to be used. Note that the reader may be reused in
- // many loaders and may be used concurrently, it is considered to be
- // thread-safe since its only state is internal options which we
- // consider immutable after they are initialized (barring dex factory
- // which is thread-safe).
- private final JarApplicationReader reader;
-
- public LazyClassCollection(JarApplicationReader reader,
- List<ResourceProvider> classpathProviders, List<ResourceProvider> libraryProviders) {
- this.classpathProviders = ImmutableList.copyOf(classpathProviders);
- this.libraryProviders = ImmutableList.copyOf(libraryProviders);
- this.reader = reader;
- }
-
- /**
- * Returns a definition for a class or `null` if there is no such class.
- * Parameter `dominator` represents the class promise that is considered
- * to be already loaded, it may be null but if specified it may affect
- * the conflict resolution. For example non-lazy loaded classpath class
- * provided as `dominator` will conflict with lazy-loaded classpath classes.
- */
- public DexClassPromise get(DexType type, DexClassPromise dominator) {
- DexClassPromise promise;
-
- // Check if the promise already exists.
- synchronized (classes) {
- promise = classes.get(type);
- }
-
- if (promise == null) {
- // Building a promise may be time consuming, we do it outside
- // the lock so others don't have to wait.
- promise = buildPromiseChain(type);
-
- synchronized (classes) {
- DexClassPromise existing = classes.get(type);
- if (existing != null) {
- promise = existing;
- } else {
- classes.put(type, promise);
- }
- }
-
- assert promise != EmptyPromise.INSTANCE;
- }
-
- return promise == EmptyPromise.INSTANCE ? dominator : chooseClass(dominator, promise);
- }
-
- // Build chain of lazy promises or `null` if none of the providers
- // provided resource for this type.
- private DexClassPromise buildPromiseChain(DexType type) {
- String descriptor = type.descriptor.toString();
- DexClassPromise promise = buildPromiseChain(
- type, descriptor, null, classpathProviders, ClassKind.CLASSPATH);
- promise = buildPromiseChain(
- type, descriptor, promise, libraryProviders, ClassKind.LIBRARY);
- return promise == null ? EmptyPromise.INSTANCE : promise;
- }
-
- private DexClassPromise buildPromiseChain(DexType type, String descriptor,
- DexClassPromise promise, List<ResourceProvider> providers, ClassKind classKind) {
- for (ResourceProvider provider : providers) {
- Resource resource = provider.getResource(descriptor);
- if (resource == null) {
- continue;
- }
-
- if (resource.kind != Resource.Kind.CLASSFILE) {
- throw new CompilationError("Resource returned by resource provider for type " +
- type.toSourceString() + " must be a class file resource.");
- }
-
- LazyClassFileLoader loader = new LazyClassFileLoader(type, resource, classKind, reader);
- promise = (promise == null) ? loader : new DexClassPromiseChain(loader, promise);
- }
- return promise;
- }
-
- // Chooses the proper promise. Recursion is not expected to be deep.
- private DexClassPromise chooseClass(DexClassPromise dominator, DexClassPromise candidate) {
- DexClassPromise best = (dominator == null) ? candidate
- : DexApplication.chooseClass(dominator, candidate, /* skipLibDups: */ true);
- return (candidate instanceof DexClassPromiseChain)
- ? chooseClass(best, ((DexClassPromiseChain) candidate).next) : best;
- }
-
- private static final class EmptyPromise implements DexClassPromise {
- static final EmptyPromise INSTANCE = new EmptyPromise();
-
- @Override
- public DexType getType() {
- throw new Unreachable();
- }
-
- @Override
- public Resource.Kind getOrigin() {
- throw new Unreachable();
- }
-
- @Override
- public boolean isProgramClass() {
- throw new Unreachable();
- }
-
- @Override
- public boolean isClasspathClass() {
- throw new Unreachable();
- }
-
- @Override
- public boolean isLibraryClass() {
- throw new Unreachable();
- }
-
- @Override
- public DexClass get() {
- throw new Unreachable();
- }
- }
-
- private static final class DexClassPromiseChain implements DexClassPromise {
- final DexClassPromise promise;
- final DexClassPromise next;
-
- private DexClassPromiseChain(DexClassPromise promise, DexClassPromise next) {
- assert promise != null;
- assert next != null;
- this.promise = promise;
- this.next = next;
- }
-
- @Override
- public DexType getType() {
- return promise.getType();
- }
-
- @Override
- public Resource.Kind getOrigin() {
- return promise.getOrigin();
- }
-
- @Override
- public boolean isProgramClass() {
- return promise.isProgramClass();
- }
-
- @Override
- public boolean isClasspathClass() {
- return promise.isClasspathClass();
- }
-
- @Override
- public boolean isLibraryClass() {
- return promise.isLibraryClass();
- }
-
- @Override
- public DexClass get() {
- return promise.get();
- }
- }
-}
diff --git a/src/main/java/com/android/tools/r8/utils/LibraryClassCollection.java b/src/main/java/com/android/tools/r8/utils/LibraryClassCollection.java
new file mode 100644
index 0000000..e11e660
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/LibraryClassCollection.java
@@ -0,0 +1,42 @@
+// Copyright (c) 2017, 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.utils;
+
+import com.android.tools.r8.Resource;
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexLibraryClass;
+import com.android.tools.r8.logging.Log;
+
+/** Represents a collection of library classes. */
+public class LibraryClassCollection extends ClassMap<DexLibraryClass> {
+ public LibraryClassCollection(ClassProvider<DexLibraryClass> classProvider) {
+ super(null, classProvider);
+ }
+
+ @Override
+ DexLibraryClass resolveClassConflict(DexLibraryClass a, DexLibraryClass b) {
+ if (a.origin != Resource.Kind.CLASSFILE || b.origin != Resource.Kind.CLASSFILE) {
+ // We only support conflicts for classes both coming from jar files.
+ throw new CompilationError(
+ "Library type already present: " + a.type.toSourceString());
+ }
+ if (Log.ENABLED) {
+ Log.warn(DexApplication.class,
+ "Class `%s` was specified twice as a library type.", a.type.toSourceString());
+ }
+ return a;
+ }
+
+ @Override
+ ClassKind getClassKind() {
+ return ClassKind.LIBRARY;
+ }
+
+ @Override
+ public String toString() {
+ return "library classes: " + super.toString();
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/PreloadedResourceProvider.java b/src/main/java/com/android/tools/r8/utils/PreloadedClassFileProvider.java
similarity index 74%
rename from src/main/java/com/android/tools/r8/utils/PreloadedResourceProvider.java
rename to src/main/java/com/android/tools/r8/utils/PreloadedClassFileProvider.java
index fcb89d4..07c136d 100644
--- a/src/main/java/com/android/tools/r8/utils/PreloadedResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/PreloadedClassFileProvider.java
@@ -7,10 +7,13 @@
import static com.android.tools.r8.utils.FileUtils.isArchive;
import static com.android.tools.r8.utils.FileUtils.isClassFile;
+import com.android.tools.r8.ClassFileResourceProvider;
import com.android.tools.r8.Resource;
-import com.android.tools.r8.ResourceProvider;
import com.android.tools.r8.errors.CompilationError;
+import com.google.common.collect.Sets;
import com.google.common.io.ByteStreams;
+
+import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Path;
@@ -18,23 +21,25 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
+import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipInputStream;
-/**
- * Lazy resource provider based on preloaded/prebuilt context.
- *
- * NOTE: only handles classfile resources.
- */
-public final class PreloadedResourceProvider implements ResourceProvider {
+/** Lazy Java class file resource provider based on preloaded/prebuilt context. */
+public final class PreloadedClassFileProvider implements ClassFileResourceProvider {
private final Map<String, byte[]> content;
- private PreloadedResourceProvider(Map<String, byte[]> content) {
+ private PreloadedClassFileProvider(Map<String, byte[]> content) {
this.content = content;
}
@Override
+ public Set<String> getClassDescriptors() {
+ return Sets.newHashSet(content.keySet());
+ }
+
+ @Override
public Resource getResource(String descriptor) {
byte[] bytes = content.get(descriptor);
if (bytes == null) {
@@ -44,7 +49,7 @@
}
/** Create preloaded content resource provider from archive file. */
- public static ResourceProvider fromArchive(Path archive) throws IOException {
+ public static ClassFileResourceProvider fromArchive(Path archive) throws IOException {
assert isArchive(archive);
Builder builder = builder();
try (ZipInputStream stream = new ZipInputStream(new FileInputStream(archive.toFile()))) {
@@ -73,13 +78,21 @@
assert name != null;
assert name.endsWith(CLASS_EXTENSION) :
"Name " + name + " must have " + CLASS_EXTENSION + " suffix";
- String descriptor = name.substring(0, name.length() - CLASS_EXTENSION.length());
+ String fileName =
+ File.separatorChar == '/' ? name.toString() :
+ name.toString().replace(File.separatorChar, '/');
+ String descriptor = fileName.substring(0, fileName.length() - CLASS_EXTENSION.length());
if (descriptor.contains(".")) {
- throw new CompilationError("Unexpected file name in the archive: " + name);
+ throw new CompilationError("Unexpected file name in the archive: " + fileName);
}
return 'L' + descriptor + ';';
}
+ @Override
+ public String toString() {
+ return content.size() + " preloaded resources";
+ }
+
/** Create a new empty builder. */
public static Builder builder() {
return new Builder();
@@ -100,9 +113,9 @@
return this;
}
- public PreloadedResourceProvider build() {
+ public PreloadedClassFileProvider build() {
assert content != null;
- PreloadedResourceProvider provider = new PreloadedResourceProvider(content);
+ PreloadedClassFileProvider provider = new PreloadedClassFileProvider(content);
content = null;
return provider;
}
diff --git a/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java b/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
new file mode 100644
index 0000000..a37eb92
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
@@ -0,0 +1,63 @@
+// Copyright (c) 2017, 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.utils;
+
+import com.android.tools.r8.Resource;
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.desugar.LambdaRewriter;
+import java.util.IdentityHashMap;
+import java.util.List;
+
+/** Represents a collection of library classes. */
+public class ProgramClassCollection extends ClassMap<DexProgramClass> {
+ public static ProgramClassCollection create(List<DexProgramClass> classes) {
+ // We have all classes preloaded, but not necessarily without conflicts.
+ IdentityHashMap<DexType, Value<DexProgramClass>> map = new IdentityHashMap<>();
+ for (DexProgramClass clazz : classes) {
+ Value<DexProgramClass> value = map.get(clazz.type);
+ if (value == null) {
+ value = new Value<>(clazz);
+ map.put(clazz.type, value);
+ } else {
+ value.clazz = resolveClassConflictImpl(value.clazz, clazz);
+ }
+ }
+ return new ProgramClassCollection(map);
+ }
+
+ private ProgramClassCollection(IdentityHashMap<DexType, Value<DexProgramClass>> classes) {
+ super(classes, null);
+ }
+
+ @Override
+ public String toString() {
+ return "program classes: " + super.toString();
+ }
+
+ @Override
+ DexProgramClass resolveClassConflict(DexProgramClass a, DexProgramClass b) {
+ return resolveClassConflictImpl(a, b);
+ }
+
+ @Override
+ ClassKind getClassKind() {
+ return ClassKind.PROGRAM;
+ }
+
+ private static DexProgramClass resolveClassConflictImpl(DexProgramClass a, DexProgramClass b) {
+ // Currently only allow collapsing synthetic lambda classes.
+ if (a.getOrigin() == Resource.Kind.DEX
+ && b.getOrigin() == Resource.Kind.DEX
+ && a.accessFlags.isSynthetic()
+ && b.accessFlags.isSynthetic()
+ && LambdaRewriter.hasLambdaClassPrefix(a.type)
+ && LambdaRewriter.hasLambdaClassPrefix(b.type)) {
+ return a;
+ }
+ throw new CompilationError("Program type already present: " + a.type.toSourceString());
+ }
+}
diff --git a/src/test/debugTestResourcesJava8/DebugDefaultMethod.java b/src/test/debugTestResourcesJava8/DebugInterfaceMethod.java
similarity index 77%
rename from src/test/debugTestResourcesJava8/DebugDefaultMethod.java
rename to src/test/debugTestResourcesJava8/DebugInterfaceMethod.java
index db216b3..b407698 100644
--- a/src/test/debugTestResourcesJava8/DebugDefaultMethod.java
+++ b/src/test/debugTestResourcesJava8/DebugInterfaceMethod.java
@@ -2,13 +2,17 @@
// 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.
-public class DebugDefaultMethod {
+public class DebugInterfaceMethod {
interface I {
default void doSomething(String msg) {
String name = getClass().getName();
System.out.println(name + ": " + msg);
}
+
+ static void printString(String msg) {
+ System.out.println(msg);
+ }
}
static class DefaultImpl implements I {
@@ -27,9 +31,14 @@
i.doSomething("Test");
}
+ private static void testStaticMethod() {
+ I.printString("I'm a static method in interface");
+ }
+
public static void main(String[] args) {
testDefaultMethod(new DefaultImpl());
testDefaultMethod(new OverrideImpl());
+ testStaticMethod();
}
}
diff --git a/src/test/debugTestResourcesJava8/DebugLambda.java b/src/test/debugTestResourcesJava8/DebugLambda.java
index c3b8695..007cd0c 100644
--- a/src/test/debugTestResourcesJava8/DebugLambda.java
+++ b/src/test/debugTestResourcesJava8/DebugLambda.java
@@ -16,8 +16,52 @@
printInt(() -> i + j);
}
- public static void main(String[] args) {
- DebugLambda.testLambda(5, 10);
+ private static void printInt2(I i) {
+ System.out.println(i.getInt());
}
+ public static void testLambdaWithMethodReference() {
+ printInt2(DebugLambda::returnOne);
+ }
+
+ private static int returnOne() {
+ return 1;
+ }
+
+ private static void printInt3(BinaryOpInterface i, int a, int b) {
+ System.out.println(i.binaryOp(a, b));
+ }
+
+ public static void testLambdaWithArguments(int i, int j) {
+ printInt3((a, b) -> {
+ return a + b;
+ }, i, j);
+ }
+
+ interface ObjectProvider {
+ Object foo(String a, String b, String c);
+ }
+
+ private static void testLambdaWithMethodReferenceAndConversion(ObjectProvider objectProvider) {
+ System.out.println(objectProvider.foo("A", "B", "C"));
+ }
+
+ public static void main(String[] args) {
+ DebugLambda.testLambda(5, 10);
+ DebugLambda.testLambdaWithArguments(5, 10);
+ DebugLambda.testLambdaWithMethodReference();
+ DebugLambda.testLambdaWithMethodReferenceAndConversion(DebugLambda::concatObjects);
+ }
+
+ private static Object concatObjects(Object... objects) {
+ StringBuilder sb = new StringBuilder();
+ for (Object o : objects) {
+ sb.append(o.toString());
+ }
+ return sb.toString();
+ }
+
+ interface BinaryOpInterface {
+ int binaryOp(int a, int b);
+ }
}
diff --git a/src/test/examples/memberrebinding/Test.java b/src/test/examples/memberrebinding/Memberrebinding.java
similarity index 97%
rename from src/test/examples/memberrebinding/Test.java
rename to src/test/examples/memberrebinding/Memberrebinding.java
index 62a2440..74c99fd 100644
--- a/src/test/examples/memberrebinding/Test.java
+++ b/src/test/examples/memberrebinding/Memberrebinding.java
@@ -6,7 +6,7 @@
import memberrebinding.subpackage.PublicClass;
import memberrebindinglib.AnIndependentInterface;
-public class Test {
+public class Memberrebinding {
public static void main(String[] args) {
ClassAtBottomOfChain bottomInstance = new ClassAtBottomOfChain();
diff --git a/src/test/examples/memberrebinding2/Test.java b/src/test/examples/memberrebinding2/Memberrebinding.java
similarity index 96%
rename from src/test/examples/memberrebinding2/Test.java
rename to src/test/examples/memberrebinding2/Memberrebinding.java
index d4b005e..13cde8c 100644
--- a/src/test/examples/memberrebinding2/Test.java
+++ b/src/test/examples/memberrebinding2/Memberrebinding.java
@@ -5,7 +5,7 @@
import memberrebinding2.subpackage.PublicClass;
-public class Test {
+public class Memberrebinding {
public static void main(String[] args) {
ClassAtBottomOfChain bottomInstance = new ClassAtBottomOfChain();
diff --git a/src/test/examples/memberrebinding2/keep-rules.txt b/src/test/examples/memberrebinding2/keep-rules.txt
new file mode 100644
index 0000000..ae40c43
--- /dev/null
+++ b/src/test/examples/memberrebinding2/keep-rules.txt
@@ -0,0 +1,12 @@
+# Copyright (c) 2016, 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.
+
+# Keep the application entry point. Get rid of everything that is not
+# reachable from there.
+-keep public class memberrebinding2.Memberrebinding {
+ public static void main(...);
+}
+
+# Remove once b/62048823 is fixed.
+-allowaccessmodification
\ No newline at end of file
diff --git a/src/test/examples/memberrebinding3/Test.java b/src/test/examples/memberrebinding3/Memberrebinding.java
similarity index 78%
rename from src/test/examples/memberrebinding3/Test.java
rename to src/test/examples/memberrebinding3/Memberrebinding.java
index d37f263..83d8bbb 100644
--- a/src/test/examples/memberrebinding3/Test.java
+++ b/src/test/examples/memberrebinding3/Memberrebinding.java
@@ -3,16 +3,19 @@
// BSD-style license that can be found in the LICENSE file.
package memberrebinding3;
-public class Test extends ClassAtBottomOfChain {
+public class Memberrebinding extends ClassAtBottomOfChain {
+ @Override
void bottomMethod() {
}
+ @Override
void middleMethod() {
}
+ @Override
void topMethod() {
}
@@ -24,6 +27,6 @@
}
public static void main(String[] args) {
- new Test().test();
+ new Memberrebinding().test();
}
}
diff --git a/src/test/examplesAndroidN/memberrebinding4/Test.java b/src/test/examplesAndroidN/memberrebinding4/Memberrebinding.java
similarity index 93%
rename from src/test/examplesAndroidN/memberrebinding4/Test.java
rename to src/test/examplesAndroidN/memberrebinding4/Memberrebinding.java
index 6fc2ad5..a699537 100644
--- a/src/test/examplesAndroidN/memberrebinding4/Test.java
+++ b/src/test/examplesAndroidN/memberrebinding4/Memberrebinding.java
@@ -5,7 +5,7 @@
import memberrebinding4.subpackage.PublicInterface;
-public class Test {
+public class Memberrebinding {
static class Inner implements PublicInterface {
diff --git a/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java
index 728f769..db81f17 100644
--- a/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java
@@ -18,6 +18,8 @@
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
@@ -80,17 +82,20 @@
Assert.assertEquals(1, descriptors.size());
String classDescriptor = descriptors.iterator().next();
classDescriptor = classDescriptor.substring(1, classDescriptor.length() - 1);
- fileToResource.put(classDescriptor + ".class", resource);
+ String classFilePath = classDescriptor + ".class";
+ if (File.separatorChar != '/') {
+ classFilePath = classFilePath.replace('/', File.separatorChar);
+ }
+ fileToResource.put(classFilePath, resource);
}
return fileToResource;
}
private Path makeRelative(Path testJarFile, Path classFile) {
- classFile = classFile.toAbsolutePath();
Path regularParent =
- testJarFile.getParent().resolve(Paths.get("classes")).toAbsolutePath();
+ testJarFile.getParent().resolve(Paths.get("classes"));
Path legacyParent = regularParent.resolve(Paths.get("..",
- regularParent.getFileName().toString() + "Legacy", "classes")).toAbsolutePath();
+ regularParent.getFileName().toString() + "Legacy", "classes"));
if (classFile.startsWith(regularParent)) {
return regularParent.relativize(classFile);
@@ -102,9 +107,9 @@
private List<String> collectClassFiles(Path testJarFile) {
List<String> result = new ArrayList<>();
// Collect Java 8 classes.
- collectClassFiles(getClassesRoot(testJarFile).toFile(), result);
+ collectClassFiles(getClassesRoot(testJarFile), result);
// Collect legacy classes.
- collectClassFiles(getLegacyClassesRoot(testJarFile).toFile(), result);
+ collectClassFiles(getLegacyClassesRoot(testJarFile), result);
Collections.sort(result);
return result;
}
@@ -121,15 +126,18 @@
return parent.resolve(legacyPath);
}
- private void collectClassFiles(File dir, List<String> result) {
- File[] files = dir.listFiles();
- if (files != null) {
- for (File file : files) {
- if (file.isDirectory()) {
- collectClassFiles(file, result);
- } else {
- result.add(file.getAbsolutePath());
+ private void collectClassFiles(Path dir, List<String> result) {
+ if (Files.exists(dir)) {
+ try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
+ for (Path entry: stream) {
+ if (Files.isDirectory(entry)) {
+ collectClassFiles(entry, result);
+ } else {
+ result.add(entry.toString());
+ }
}
+ } catch (IOException x) {
+ throw new AssertionError(x);
}
}
}
diff --git a/src/test/java/com/android/tools/r8/D8ResourceProviderRunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/D8LazyRunExamplesAndroidOTest.java
similarity index 79%
rename from src/test/java/com/android/tools/r8/D8ResourceProviderRunExamplesAndroidOTest.java
rename to src/test/java/com/android/tools/r8/D8LazyRunExamplesAndroidOTest.java
index 7cef1a4..34cd83c 100644
--- a/src/test/java/com/android/tools/r8/D8ResourceProviderRunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/D8LazyRunExamplesAndroidOTest.java
@@ -4,12 +4,12 @@
package com.android.tools.r8;
-import com.android.tools.r8.utils.DirectoryResourceProvider;
-import com.android.tools.r8.utils.PreloadedResourceProvider;
+import com.android.tools.r8.utils.DirectoryClassFileProvider;
+import com.android.tools.r8.utils.PreloadedClassFileProvider;
import java.io.IOException;
import java.nio.file.Path;
-public class D8ResourceProviderRunExamplesAndroidOTest
+public class D8LazyRunExamplesAndroidOTest
extends D8IncrementalRunExamplesAndroidOTest {
class D8LazyTestRunner extends D8IncrementalTestRunner {
@@ -25,13 +25,13 @@
private void addClasspathPath(Path location, D8Command.Builder builder) {
builder.addClasspathResourceProvider(
- DirectoryResourceProvider.fromDirectory(location.resolve("..")));
+ DirectoryClassFileProvider.fromDirectory(location.resolve("..")));
}
@Override
void addLibraryReference(D8Command.Builder builder, Path location) throws IOException {
builder.addLibraryResourceProvider(
- PreloadedResourceProvider.fromArchive(location));
+ PreloadedClassFileProvider.fromArchive(location));
}
}
diff --git a/src/test/java/com/android/tools/r8/D8RegularLazyRunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/D8NonLazyRunExamplesAndroidOTest.java
similarity index 95%
rename from src/test/java/com/android/tools/r8/D8RegularLazyRunExamplesAndroidOTest.java
rename to src/test/java/com/android/tools/r8/D8NonLazyRunExamplesAndroidOTest.java
index 38acabe..42d3c47 100644
--- a/src/test/java/com/android/tools/r8/D8RegularLazyRunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/D8NonLazyRunExamplesAndroidOTest.java
@@ -8,7 +8,7 @@
import java.nio.file.Path;
import java.nio.file.Paths;
-public class D8RegularLazyRunExamplesAndroidOTest
+public class D8NonLazyRunExamplesAndroidOTest
extends D8IncrementalRunExamplesAndroidOTest {
class D8LazyTestRunner extends D8IncrementalTestRunner {
diff --git a/src/test/java/com/android/tools/r8/JctfTestSpecifications.java b/src/test/java/com/android/tools/r8/JctfTestSpecifications.java
index b1934dd..f7cbee8 100644
--- a/src/test/java/com/android/tools/r8/JctfTestSpecifications.java
+++ b/src/test/java/com/android/tools/r8/JctfTestSpecifications.java
@@ -4686,8 +4686,7 @@
// 1) t02
// java.lang.AssertionError: java.lang.AssertionError: expected:<5> but was:<4>
- .put("lang.Thread.interrupt.Thread_interrupt_A04",
- match(D8_COMPILER, runtimes(DexVm.ART_6_0_1)))
+ .put("lang.Thread.interrupt.Thread_interrupt_A04", any())
// Been running fine for a while then this happened on a bot:
// 1) t01
// java.lang.AssertionError: expected:<BLOCKED> but was:<RUNNABLE>
diff --git a/src/test/java/com/android/tools/r8/R8EntryPointTests.java b/src/test/java/com/android/tools/r8/R8EntryPointTests.java
new file mode 100644
index 0000000..c83cea6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/R8EntryPointTests.java
@@ -0,0 +1,124 @@
+// Copyright (c) 2017, 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;
+
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.shaking.ProguardRuleParserException;
+import com.android.tools.r8.utils.FileUtils;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class R8EntryPointTests extends TestBase {
+
+ private static final String MAPPING = "mapping.txt";
+ private static final String SEEDS = "seeds.txt";
+ private static final Path INPUT_JAR =
+ Paths.get(ToolHelper.EXAMPLES_BUILD_DIR, "minification" + FileUtils.JAR_EXTENSION);
+ private static final Path PROGUARD_FLAGS =
+ Paths.get(ToolHelper.EXAMPLES_DIR, "minification", "keep-rules.txt");
+
+ private Path testFlags;
+
+ @Before
+ public void setup() throws IOException {
+ testFlags = temp.newFile("local.flags").toPath();
+ FileUtils.writeTextFile(testFlags, ImmutableList.of(
+ "-printseeds " + SEEDS,
+ "-printmapping " + MAPPING));
+ }
+
+ @Test
+ public void testRun1Dir() throws IOException, CompilationException, ProguardRuleParserException {
+ Path out = temp.newFolder("outdex").toPath();
+ R8.run(getCommand(out));
+ Assert.assertTrue(Files.isRegularFile(out.resolve(FileUtils.DEFAULT_DEX_FILENAME)));
+ Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(MAPPING)));
+ Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(SEEDS)));
+ }
+
+ @Test
+ public void testRun1Zip() throws IOException, CompilationException, ProguardRuleParserException {
+ Path out = temp.newFolder("outdex").toPath().resolve("dex.zip");
+ R8.run(getCommand(out));
+ Assert.assertTrue(Files.isRegularFile(out));
+ Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(MAPPING)));
+ Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(SEEDS)));
+ }
+
+ @Test
+ public void testRun2Dir() throws IOException, CompilationException, ProguardRuleParserException {
+ Path out = temp.newFolder("outdex").toPath();
+ ExecutorService executor = Executors.newWorkStealingPool(2);
+ try {
+ R8.run(getCommand(out), executor);
+ } finally {
+ executor.shutdown();
+ }
+ Assert.assertTrue(Files.isRegularFile(out.resolve(FileUtils.DEFAULT_DEX_FILENAME)));
+ Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(MAPPING)));
+ Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(SEEDS)));
+ }
+
+ @Test
+ public void testRun2Zip() throws IOException, CompilationException, ProguardRuleParserException {
+ Path out = temp.newFolder("outdex").toPath().resolve("dex.zip");
+ ExecutorService executor = Executors.newWorkStealingPool(2);
+ try {
+ R8.run(getCommand(out), executor);
+ } finally {
+ executor.shutdown();
+ }
+ Assert.assertTrue(Files.isRegularFile(out));
+ Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(MAPPING)));
+ Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(SEEDS)));
+ }
+
+ @Test
+ public void testMainDir() throws IOException, InterruptedException {
+ Path out = temp.newFolder("outdex").toPath();
+ ProcessResult r8 = ToolHelper.forkR8(Paths.get("."),
+ "--lib", ToolHelper.getDefaultAndroidJar(),
+ "--output", out.toString(),
+ "--pg-conf", PROGUARD_FLAGS.toString(),
+ "--pg-conf", testFlags.toString(),
+ INPUT_JAR.toString());
+ Assert.assertEquals(0, r8.exitCode);
+ Assert.assertTrue(Files.isRegularFile(out.resolve(FileUtils.DEFAULT_DEX_FILENAME)));
+ Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(MAPPING)));
+ Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(SEEDS)));
+ }
+
+ @Test
+ public void testMainZip() throws IOException, InterruptedException {
+ Path out = temp.newFolder("outdex").toPath().resolve("dex.zip");
+ ProcessResult r8 = ToolHelper.forkR8(Paths.get("."),
+ "--lib", ToolHelper.getDefaultAndroidJar(),
+ "--output", out.toString(),
+ "--pg-conf", PROGUARD_FLAGS.toString(),
+ "--pg-conf", testFlags.toString(),
+ INPUT_JAR.toString());
+ Assert.assertEquals(0, r8.exitCode);
+ Assert.assertTrue(Files.isRegularFile(out));
+ Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(MAPPING)));
+ Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(SEEDS)));
+ }
+
+ private R8Command getCommand(Path out) throws CompilationException, IOException {
+ return R8Command.builder()
+ .addLibraryFiles(Paths.get(ToolHelper.getDefaultAndroidJar()))
+ .addProgramFiles(INPUT_JAR)
+ .setOutputPath(out)
+ .addProguardConfigurationFiles(PROGUARD_FLAGS, testFlags)
+ .build();
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
index 66844af..13a4c24 100644
--- a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
@@ -989,7 +989,7 @@
builder.setMinApiLevel(minSdkVersion);
}
D8Output output = D8.run(builder.build());
- output.write(Paths.get(resultPath));
+ output.write(Paths.get(resultPath), true);
break;
}
case R8:
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
index a264f48..5e265aa 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
@@ -14,6 +14,7 @@
import com.android.tools.r8.ToolHelper.DexVm;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.shaking.ProguardRuleParserException;
+import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.JarBuilder;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -40,7 +41,6 @@
public class R8RunExamplesTest {
private static final String EXAMPLE_DIR = ToolHelper.EXAMPLES_BUILD_DIR;
- private static final String DEFAULT_DEX_FILENAME = "classes.dex";
// For local testing on a specific Art version(s) change this set. e.g. to
// ImmutableSet.of(DexVm.ART_DEFAULT) or pass the option -Ddex_vm=<version> to the Java VM.
@@ -103,8 +103,8 @@
"regress_37875803.Regress",
"regress_37955340.Regress",
"regress_62300145.Regress",
- "memberrebinding2.Test",
- "memberrebinding3.Test",
+ "memberrebinding2.Memberrebinding",
+ "memberrebinding3.Memberrebinding",
"minification.Minification",
"enclosingmethod.Main",
"interfaceinlining.Main",
@@ -123,7 +123,7 @@
private static String[] makeTest(
DexTool tool, CompilerUnderTest compiler, CompilationMode mode, String clazz) {
String pkg = clazz.substring(0, clazz.lastIndexOf('.'));
- return new String[] {pkg, tool.name(), compiler.name(), mode.name(), clazz};
+ return new String[]{pkg, tool.name(), compiler.name(), mode.name(), clazz};
}
@Rule
@@ -161,7 +161,7 @@
}
private Path getOriginalDexFile() {
- return Paths.get(EXAMPLE_DIR, pkg, DEFAULT_DEX_FILENAME);
+ return Paths.get(EXAMPLE_DIR, pkg, FileUtils.DEFAULT_DEX_FILENAME);
}
@Rule
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 28d4872..3f31686 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -96,6 +96,15 @@
/**
* Compile an application with R8.
*/
+ protected AndroidApp compileWithR8(List<Class> classes, Consumer<InternalOptions> optionsConsumer)
+ throws CompilationException, ProguardRuleParserException, ExecutionException, IOException {
+ R8Command command = ToolHelper.prepareR8CommandBuilder(readClasses(classes)).build();
+ return ToolHelper.runR8(command, optionsConsumer);
+ }
+
+ /**
+ * Compile an application with R8.
+ */
protected AndroidApp compileWithR8(AndroidApp app, Consumer<InternalOptions> optionsConsumer)
throws CompilationException, ProguardRuleParserException, ExecutionException, IOException {
R8Command command = ToolHelper.prepareR8CommandBuilder(app).build();
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index d7fd468..e74eb2c 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -14,7 +14,6 @@
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.ListUtils;
-import com.android.tools.r8.utils.OutputMode;
import com.android.tools.r8.utils.Timing;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -41,6 +40,7 @@
import java.util.function.Consumer;
import java.util.stream.Collectors;
import joptsimple.internal.Strings;
+import org.junit.Assume;
import org.junit.rules.TemporaryFolder;
public class ToolHelper {
@@ -53,6 +53,8 @@
public static final String EXAMPLES_ANDROID_O_BUILD_DIR = BUILD_DIR + "test/examplesAndroidO/";
public static final String SMALI_BUILD_DIR = BUILD_DIR + "test/smali/";
+ public static final String LINE_SEPARATOR = System.getProperty("line.separator");
+
private static final String ANDROID_JAR_PATTERN = "third_party/android_jar/lib-v%d/android.jar";
private static final int DEFAULT_MIN_SDK = 14;
@@ -379,6 +381,10 @@
return getPlatform().startsWith("Mac");
}
+ public static boolean isWindows() {
+ return getPlatform().startsWith("Windows");
+ }
+
public static boolean artSupported() {
if (!isLinux() && !isMac()) {
System.err.println("Testing on your platform is not fully supported. " +
@@ -462,9 +468,7 @@
optionsConsumer.accept(options);
}
CompilationResult result = R8.runForTesting(app, options);
- if (command.getOutputPath() != null) {
- result.androidApp.write(command.getOutputPath(), OutputMode.Indexed);
- }
+ R8.writeOutputs(command, options, result.androidApp);
return result;
}
@@ -519,6 +523,7 @@
}
public static ProcessResult runDX(String[] args) throws IOException {
+ Assume.assumeTrue(ToolHelper.artSupported());
DXCommandBuilder builder = new DXCommandBuilder();
for (String arg : args) {
builder.appendProgramArgument(arg);
@@ -601,6 +606,7 @@
}
private static ProcessResult runArtProcess(ArtCommandBuilder builder) throws IOException {
+ Assume.assumeTrue(ToolHelper.artSupported());
ProcessResult result = runProcess(builder.asProcessBuilder());
if (result.exitCode != 0) {
fail("Unexpected art failure: '" + result.stderr + "'\n" + result.stdout);
@@ -640,6 +646,7 @@
}
public static void runDex2Oat(Path file, Path outFile) throws IOException {
+ Assume.assumeTrue(ToolHelper.artSupported());
assert Files.exists(file);
assert ByteStreams.toByteArray(Files.newInputStream(file)).length > 0;
List<String> command = new ArrayList<>();
diff --git a/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java b/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java
index 4131b86..81f660a 100644
--- a/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java
+++ b/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java
@@ -8,11 +8,15 @@
import static org.junit.Assert.assertTrue;
import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.dex.Constants;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.maindexlist.MainDexListTests;
+import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.OutputMode;
import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.StringUtils.BraceType;
+import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -21,6 +25,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
@@ -95,9 +100,14 @@
}
@Test
- public void singleDexProgramIsTooLarge() throws IOException {
+ public void singleDexProgramIsTooLarge() throws IOException, ExecutionException {
+ // Generate an application that will not fit into a single dex file.
+ AndroidApp generated = MainDexListTests.generateApplication(
+ ImmutableList.of("A", "B"), Constants.ANDROID_L_API, Constants.U16BIT_MAX / 2 + 1);
+ Path applicationJar = temp.newFile("application.jar").toPath();
+ generated.write(applicationJar, OutputMode.Indexed, true);
thrown.expect(CompilationError.class);
- runDexer(MainDexListTests.getTwoLargeClassesAppPath().toString());
+ runDexer(applicationJar.toString());
}
@Test
diff --git a/src/test/java/com/android/tools/r8/d8/DexVersionTests.java b/src/test/java/com/android/tools/r8/d8/DexVersionTests.java
index 0d72a52..2cdb4d7 100644
--- a/src/test/java/com/android/tools/r8/d8/DexVersionTests.java
+++ b/src/test/java/com/android/tools/r8/d8/DexVersionTests.java
@@ -38,17 +38,17 @@
D8Command.Builder arithmeticBuilder = D8Command.builder().addProgramFiles(ARITHMETIC_JAR);
D8Command.Builder arrayAccessBuilder = D8Command.builder().addProgramFiles(ARRAYACCESS_JAR);
D8Output output = D8.run(arrayAccessBuilder.build());
- output.write(defaultApiFolder1.getRoot().toPath());
+ output.write(defaultApiFolder1.getRoot().toPath(), true);
output = D8.run(arrayAccessBuilder.setMinApiLevel(Constants.ANDROID_O_API).build());
- output.write(androidOApiFolder1.getRoot().toPath());
+ output.write(androidOApiFolder1.getRoot().toPath(), true);
output = D8.run(arrayAccessBuilder.setMinApiLevel(Constants.ANDROID_N_API).build());
- output.write(androidNApiFolder1.getRoot().toPath());
+ output.write(androidNApiFolder1.getRoot().toPath(), true);
output = D8.run(arithmeticBuilder.build());
- output.write(defaultApiFolder2.getRoot().toPath());
+ output.write(defaultApiFolder2.getRoot().toPath(), true);
output = D8.run(arithmeticBuilder.setMinApiLevel(Constants.ANDROID_O_API).build());
- output.write(androidOApiFolder2.getRoot().toPath());
+ output.write(androidOApiFolder2.getRoot().toPath(), true);
output = D8.run(arithmeticBuilder.setMinApiLevel(Constants.ANDROID_N_API).build());
- output.write(androidNApiFolder2.getRoot().toPath());
+ output.write(androidNApiFolder2.getRoot().toPath(), true);
}
private Path default1() {
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
index 9cffe15..4cef632 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -20,6 +20,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Queue;
import java.util.TreeMap;
import java.util.function.Consumer;
@@ -245,7 +246,10 @@
}
protected final JUnit3Wrapper.Command checkNoLocal() {
- return inspect(t -> Assert.assertTrue(t.getLocalNames().isEmpty()));
+ return inspect(t -> {
+ List<String> localNames = t.getLocalNames();
+ Assert.assertTrue("Local variables: " + String.join(",", localNames), localNames.isEmpty());
+ });
}
protected final JUnit3Wrapper.Command checkLine(String sourceFile, int line) {
@@ -475,11 +479,13 @@
}
public void checkLocal(String localName) {
- getVariableAt(getLocation(), localName);
+ Optional<Variable> localVar = getVariableAt(getLocation(), localName);
+ Assert.assertTrue("No local '" + localName + "'", localVar.isPresent());
}
public void checkLocal(String localName, Value expectedValue) {
- Variable localVar = getVariableAt(getLocation(), localName);
+ Optional<Variable> localVar = getVariableAt(getLocation(), localName);
+ Assert.assertTrue("No local '" + localName + "'", localVar.isPresent());
// Get value
CommandPacket commandPacket = new CommandPacket(
@@ -488,8 +494,8 @@
commandPacket.setNextValueAsThreadID(getThreadId());
commandPacket.setNextValueAsFrameID(getFrameId());
commandPacket.setNextValueAsInt(1);
- commandPacket.setNextValueAsInt(localVar.getSlot());
- commandPacket.setNextValueAsByte(localVar.getTag());
+ commandPacket.setNextValueAsInt(localVar.get().getSlot());
+ commandPacket.setNextValueAsByte(localVar.get().getTag());
ReplyPacket replyPacket = getMirror().performCommand(commandPacket);
checkReplyPacket(replyPacket, "StackFrame.GetValues command");
int valuesCount = replyPacket.getNextValueAsInt();
@@ -594,11 +600,10 @@
return index >= varStart && index < varEnd;
}
- private Variable getVariableAt(Location location, String localName) {
+ private Optional<Variable> getVariableAt(Location location, String localName) {
return getVariablesAt(location).stream()
.filter(v -> localName.equals(v.getName()))
- .findFirst()
- .get();
+ .findFirst();
}
private List<Variable> getVariablesAt(Location location) {
@@ -858,13 +863,16 @@
@Override
public void perform(JUnit3Wrapper testBase) {
- Variable v = testBase.getVariableAt(testBase.debuggeeState.location, localName);
+ Optional<Variable> localVar = testBase
+ .getVariableAt(testBase.debuggeeState.location, localName);
+ Assert.assertTrue("No local '" + localName + "'", localVar.isPresent());
+
CommandPacket setValues = new CommandPacket(StackFrameCommandSet.CommandSetID,
StackFrameCommandSet.SetValuesCommand);
setValues.setNextValueAsThreadID(testBase.getDebuggeeState().getThreadId());
setValues.setNextValueAsFrameID(testBase.getDebuggeeState().getFrameId());
setValues.setNextValueAsInt(1);
- setValues.setNextValueAsInt(v.getSlot());
+ setValues.setNextValueAsInt(localVar.get().getSlot());
setValues.setNextValueAsValue(newValue);
ReplyPacket replyPacket = testBase.getMirror().performCommand(setValues);
testBase.checkReplyPacket(replyPacket, "StackFrame.SetValues");
diff --git a/src/test/java/com/android/tools/r8/debug/DefaultMethodTest.java b/src/test/java/com/android/tools/r8/debug/InterfaceMethodTest.java
similarity index 61%
rename from src/test/java/com/android/tools/r8/debug/DefaultMethodTest.java
rename to src/test/java/com/android/tools/r8/debug/InterfaceMethodTest.java
index e0eb5ea..a7dcb60 100644
--- a/src/test/java/com/android/tools/r8/debug/DefaultMethodTest.java
+++ b/src/test/java/com/android/tools/r8/debug/InterfaceMethodTest.java
@@ -9,13 +9,13 @@
import java.util.List;
import org.junit.Test;
-public class DefaultMethodTest extends DebugTestBase {
+public class InterfaceMethodTest extends DebugTestBase {
- private static final String SOURCE_FILE = "DebugDefaultMethod.java";
+ private static final String SOURCE_FILE = "DebugInterfaceMethod.java";
@Test
public void testDefaultMethod() throws Throwable {
- String debuggeeClass = "DebugDefaultMethod";
+ String debuggeeClass = "DebugInterfaceMethod";
String parameterName = "msg";
String localVariableName = "name";
@@ -23,13 +23,17 @@
commands.add(breakpoint(debuggeeClass, "testDefaultMethod"));
commands.add(run());
commands.add(checkMethod(debuggeeClass, "testDefaultMethod"));
- commands.add(checkLine(SOURCE_FILE, 27));
+ commands.add(checkLine(SOURCE_FILE, 31));
if (!supportsDefaultMethod()) {
// We desugared default method. This means we're going to step through an extra (forward)
// method first.
commands.add(stepInto());
}
commands.add(stepInto());
+ // TODO(shertz) we should see the local variable this even when desugaring.
+ if (supportsDefaultMethod()) {
+ commands.add(checkLocal("this"));
+ }
commands.add(checkLocal(parameterName));
commands.add(stepOver());
commands.add(checkLocal(parameterName));
@@ -43,7 +47,7 @@
@Test
public void testOverrideDefaultMethod() throws Throwable {
- String debuggeeClass = "DebugDefaultMethod";
+ String debuggeeClass = "DebugInterfaceMethod";
String parameterName = "msg";
String localVariableName = "newMsg";
@@ -52,13 +56,34 @@
commands.add(run());
commands.add(run() /* resume after 1st breakpoint */);
commands.add(checkMethod(debuggeeClass, "testDefaultMethod"));
- commands.add(checkLine(SOURCE_FILE, 27));
+ commands.add(checkLine(SOURCE_FILE, 31));
commands.add(stepInto());
- commands.add(checkMethod("DebugDefaultMethod$OverrideImpl", "doSomething"));
+ commands.add(checkMethod("DebugInterfaceMethod$OverrideImpl", "doSomething"));
+ commands.add(checkLocal("this"));
+ commands.add(checkLocal(parameterName));
+ commands.add(stepOver());
+ commands.add(checkLocal("this"));
+ commands.add(checkLocal(parameterName));
+ commands.add(checkLocal(localVariableName));
+ commands.add(run());
+
+ runDebugTestJava8(debuggeeClass, commands);
+ }
+
+ @Test
+ public void testStaticMethod() throws Throwable {
+ String debuggeeClass = "DebugInterfaceMethod";
+ String parameterName = "msg";
+
+ List<Command> commands = new ArrayList<>();
+ commands.add(breakpoint(debuggeeClass, "testStaticMethod"));
+ commands.add(run());
+ commands.add(checkMethod(debuggeeClass, "testStaticMethod"));
+ commands.add(checkLine(SOURCE_FILE, 35));
+ commands.add(stepInto());
commands.add(checkLocal(parameterName));
commands.add(stepOver());
commands.add(checkLocal(parameterName));
- commands.add(checkLocal(localVariableName));
commands.add(run());
runDebugTestJava8(debuggeeClass, commands);
diff --git a/src/test/java/com/android/tools/r8/debug/LambdaTest.java b/src/test/java/com/android/tools/r8/debug/LambdaTest.java
index 52d9c70..6bdbb90 100644
--- a/src/test/java/com/android/tools/r8/debug/LambdaTest.java
+++ b/src/test/java/com/android/tools/r8/debug/LambdaTest.java
@@ -4,17 +4,18 @@
package com.android.tools.r8.debug;
+import org.junit.Assert;
import org.junit.Test;
+// TODO(shertz) test local variables
public class LambdaTest extends DebugTestBase {
public static final String SOURCE_FILE = "DebugLambda.java";
@Test
- public void testLambdaDebugging() throws Throwable {
+ public void testLambda_ExpressionOnSameLine() throws Throwable {
String debuggeeClass = "DebugLambda";
String initialMethodName = "printInt";
- // TODO(shertz) test local variables
runDebugTestJava8(debuggeeClass,
breakpoint(debuggeeClass, initialMethodName),
run(),
@@ -24,4 +25,56 @@
checkLine(SOURCE_FILE, 16),
run());
}
+
+ @Test
+ public void testLambda_StatementOnNewLine() throws Throwable {
+ String debuggeeClass = "DebugLambda";
+ String initialMethodName = "printInt3";
+ runDebugTestJava8(debuggeeClass,
+ breakpoint(debuggeeClass, initialMethodName),
+ run(),
+ checkMethod(debuggeeClass, initialMethodName),
+ checkLine(SOURCE_FILE, 32),
+ stepInto(INTELLIJ_FILTER),
+ checkLine(SOURCE_FILE, 37),
+ run());
+ }
+
+ @Test
+ public void testLambda_StaticMethodReference_Trivial() throws Throwable {
+ String debuggeeClass = "DebugLambda";
+ String initialMethodName = "printInt2";
+ runDebugTestJava8(debuggeeClass,
+ breakpoint(debuggeeClass, initialMethodName),
+ run(),
+ checkMethod(debuggeeClass, initialMethodName),
+ checkLine(SOURCE_FILE, 20),
+ stepInto(INTELLIJ_FILTER),
+ isRunningJava() ? LambdaTest::doNothing : stepInto(INTELLIJ_FILTER),
+ checkMethod(debuggeeClass, "returnOne"),
+ checkLine(SOURCE_FILE, 28),
+ checkNoLocal(),
+ run());
+ }
+
+ @Test
+ public void testLambda_StaticMethodReference_NonTrivial() throws Throwable {
+ String debuggeeClass = "DebugLambda";
+ String initialMethodName = "testLambdaWithMethodReferenceAndConversion";
+ runDebugTestJava8(debuggeeClass,
+ breakpoint(debuggeeClass, initialMethodName),
+ run(),
+ checkMethod(debuggeeClass, initialMethodName),
+ checkLine(SOURCE_FILE, 46),
+ stepInto(INTELLIJ_FILTER),
+ inspect(t -> Assert.assertTrue(t.getCurrentMethodName().startsWith("lambda$"))),
+ stepInto(INTELLIJ_FILTER),
+ checkMethod(debuggeeClass, "concatObjects"),
+ checkLine(SOURCE_FILE, 57),
+ checkLocal("objects"),
+ run());
+ }
+
+ private static void doNothing(JUnit3Wrapper jUnit3Wrapper) {
+ }
}
diff --git a/src/test/java/com/android/tools/r8/debuginfo/LocalsInSwitchTestRunner.java b/src/test/java/com/android/tools/r8/debuginfo/LocalsInSwitchTestRunner.java
index 020809a..af726bf 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/LocalsInSwitchTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/LocalsInSwitchTestRunner.java
@@ -5,6 +5,7 @@
import static org.junit.Assert.assertEquals;
+import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.utils.AndroidApp;
import org.junit.Test;
@@ -17,7 +18,8 @@
AndroidApp d8App = compileWithD8(clazz);
AndroidApp dxApp = getDxCompiledSources();
- String expected = "55\n1862\n15130\n";
+ String expected = "55" + ToolHelper.LINE_SEPARATOR + "1862" + ToolHelper.LINE_SEPARATOR
+ + "15130" + ToolHelper.LINE_SEPARATOR;
assertEquals(expected, runOnJava(clazz));
assertEquals(expected, runOnArt(d8App, clazz.getCanonicalName()));
assertEquals(expected, runOnArt(dxApp, clazz.getCanonicalName()));
diff --git a/src/test/java/com/android/tools/r8/debuginfo/SynchronizedMethodTestRunner.java b/src/test/java/com/android/tools/r8/debuginfo/SynchronizedMethodTestRunner.java
index 7370ca2..7cee4a3 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/SynchronizedMethodTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/SynchronizedMethodTestRunner.java
@@ -5,6 +5,7 @@
import static org.junit.Assert.assertEquals;
+import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.utils.AndroidApp;
import org.junit.Test;
@@ -17,7 +18,7 @@
AndroidApp d8App = compileWithD8(clazz);
AndroidApp dxApp = getDxCompiledSources();
- String expected = "42\n42\n";
+ String expected = "42" + ToolHelper.LINE_SEPARATOR + "42" + ToolHelper.LINE_SEPARATOR;
assertEquals(expected, runOnJava(clazz));
assertEquals(expected, runOnArt(d8App, clazz.getCanonicalName()));
assertEquals(expected, runOnArt(dxApp, clazz.getCanonicalName()));
diff --git a/src/test/java/com/android/tools/r8/dex/ExtraFileTest.java b/src/test/java/com/android/tools/r8/dex/ExtraFileTest.java
index 26067c7..d1e46f3 100644
--- a/src/test/java/com/android/tools/r8/dex/ExtraFileTest.java
+++ b/src/test/java/com/android/tools/r8/dex/ExtraFileTest.java
@@ -25,7 +25,7 @@
private static final String EXAMPLE_DIR = ToolHelper.EXAMPLES_BUILD_DIR;
private static final String EXAMPLE_DEX = "memberrebinding/classes.dex";
private static final String EXAMPLE_LIB = "memberrebindinglib/classes.dex";
- private static final String EXAMPLE_CLASS = "memberrebinding.Test";
+ private static final String EXAMPLE_CLASS = "memberrebinding.Memberrebinding";
private static final String EXAMPLE_PACKAGE_MAP = "memberrebinding/package.map";
private static final String EXAMPLE_PROGUARD_MAP = "memberrebinding/proguard.map";
@@ -47,6 +47,7 @@
R8Command.builder()
.addProgramFiles(original)
.setOutputPath(out)
+ .setMinApiLevel(Constants.ANDROID_L_API) // Allow native multidex.
.setProguardMapFile(proguardMap)
.setPackageDistributionFile(packageMap)
.build();
diff --git a/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java b/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java
index 80a1324..d5e66b8 100644
--- a/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java
+++ b/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java
@@ -11,6 +11,7 @@
import com.android.tools.r8.R8Command;
import com.android.tools.r8.R8RunArtTestsTest.CompilerUnderTest;
import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.dex.Constants;
import com.android.tools.r8.shaking.ProguardRuleParserException;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.ArtErrorParser;
@@ -67,7 +68,11 @@
builder.addProguardConfigurationFiles(Paths.get(pgConf));
}
builder.setMode(mode);
- outputApp = ToolHelper.runR8(builder.build());
+ outputApp = ToolHelper.runR8(builder.build(),
+ options -> {
+ options.printSeeds = false;
+ options.minApiLevel = Constants.ANDROID_L_API;
+ });
} else {
assert compiler == CompilerUnderTest.D8;
outputApp =
@@ -75,6 +80,7 @@
D8Command.builder()
.addProgramFiles(ListUtils.map(inputs, Paths::get))
.setMode(mode)
+ .setMinApiLevel(Constants.ANDROID_L_API)
.build());
}
Path out = temp.getRoot().toPath().resolve("all.zip");
diff --git a/src/test/java/com/android/tools/r8/internal/R8DisassemblerTest.java b/src/test/java/com/android/tools/r8/internal/R8DisassemblerTest.java
index 1f20559..d455841 100644
--- a/src/test/java/com/android/tools/r8/internal/R8DisassemblerTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8DisassemblerTest.java
@@ -6,8 +6,8 @@
import static com.android.tools.r8.utils.AndroidApp.DEFAULT_PROGUARD_MAP_FILE;
import com.android.tools.r8.CompilationException;
+import com.android.tools.r8.Disassemble;
import com.android.tools.r8.R8;
-import com.android.tools.r8.R8Command;
import com.android.tools.r8.shaking.ProguardRuleParserException;
import com.android.tools.r8.utils.FileUtils;
import java.io.IOException;
@@ -27,11 +27,8 @@
static final String APP_DIR = "third_party/gmscore/v5/";
- public boolean deobfuscate;
-
- @Test
- public void disassemble() throws IOException, ExecutionException, ProguardRuleParserException,
- CompilationException {
+ public void testDisassemble(boolean deobfuscate, boolean smali)
+ throws IOException, ExecutionException, ProguardRuleParserException, CompilationException {
// This test only ensures that we do not break disassembling of dex code. It does not
// check the generated code. To make it fast, we get rid of the output.
PrintStream originalOut = System.out;
@@ -40,7 +37,8 @@
}));
try {
- R8Command.Builder builder = R8Command.builder();
+ Disassemble.DisassembleCommand.Builder builder = Disassemble.DisassembleCommand.builder();
+ builder.setUseSmali(smali);
if (deobfuscate) {
builder.setProguardMapFile(Paths.get(APP_DIR, DEFAULT_PROGUARD_MAP_FILE));
}
@@ -54,4 +52,24 @@
System.setOut(originalOut);
}
}
+
+ @Test
+ public void test1() throws Exception {
+ testDisassemble(false, false);
+ }
+
+ @Test
+ public void test2() throws Exception {
+ testDisassemble(false, true);
+ }
+
+ @Test
+ public void test3() throws Exception {
+ testDisassemble(true, false);
+ }
+
+ @Test
+ public void test4() throws Exception {
+ testDisassemble(true, true);
+ }
}
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
index 2bc8dba..74f9ef9 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
@@ -10,6 +10,9 @@
import com.android.tools.r8.R8Command;
import com.android.tools.r8.Resource;
import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.ir.conversion.CallGraph;
import com.android.tools.r8.shaking.ProguardRuleParserException;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.OutputMode;
@@ -19,17 +22,30 @@
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import org.junit.Test;
public class R8GMSCoreDeterministicTest extends GMSCoreCompilationTestBase {
+ public List<DexEncodedMethod> shuffle(List<DexEncodedMethod> methods, CallGraph.Leaves leaves) {
+ Collections.shuffle(methods);
+ return methods;
+ }
+
private AndroidApp doRun()
throws IOException, ProguardRuleParserException, CompilationException, ExecutionException {
R8Command command =
R8Command.builder().addProgramFiles(Paths.get(GMSCORE_V7_DIR, GMSCORE_APK)).build();
- return ToolHelper.runR8(command, options -> options.testing.randomizeCallGraphLeaves = true);
+ return ToolHelper.runR8(
+ command, options -> {
+ // For this test just do random shuffle.
+ options.testing.irOrdering = this::shuffle;
+ // Only use one thread to process to process in the order decided by the callback.
+ options.numberOfThreads = 1;
+ options.minApiLevel = Constants.ANDROID_L_API;
+ });
}
@Test
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreFixedPointTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreFixedPointTest.java
index b6d3dac..376ccf3 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreFixedPointTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreFixedPointTest.java
@@ -7,6 +7,7 @@
import com.android.tools.r8.CompilationException;
import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.dex.Constants;
import com.android.tools.r8.shaking.ProguardRuleParserException;
import com.android.tools.r8.utils.AndroidApp;
import java.io.IOException;
@@ -21,14 +22,18 @@
throws ExecutionException, IOException, ProguardRuleParserException, CompilationException {
// First compilation.
AndroidApp app = AndroidApp.fromProgramDirectory(Paths.get(GMSCORE_V7_DIR));
- AndroidApp app1 = ToolHelper.runR8(app);
+ AndroidApp app1 =
+ ToolHelper.runR8(app, options -> options.minApiLevel = Constants.ANDROID_L_API);
// Second compilation.
// Add option --skip-outline-opt for second compilation. The second compilation can find
// additional outlining opportunities as member rebinding from the first compilation can move
// methods.
// See b/33410508 and b/33475705.
- AndroidApp app2 = ToolHelper.runR8(app1, options -> options.outline.enabled = false);
+ AndroidApp app2 = ToolHelper.runR8(app1, options -> {
+ options.outline.enabled = false;
+ options.minApiLevel = Constants.ANDROID_L_API;
+ });
// TODO: Require that the results of the two compilations are the same.
assertEquals(
diff --git a/src/test/java/com/android/tools/r8/ir/deterministic/DeterministicProcessingTest.java b/src/test/java/com/android/tools/r8/ir/deterministic/DeterministicProcessingTest.java
index eaf9fe3..6acacda 100644
--- a/src/test/java/com/android/tools/r8/ir/deterministic/DeterministicProcessingTest.java
+++ b/src/test/java/com/android/tools/r8/ir/deterministic/DeterministicProcessingTest.java
@@ -10,38 +10,121 @@
import com.android.tools.r8.CompilationException;
import com.android.tools.r8.R8Command;
import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.ir.conversion.CallGraph;
import com.android.tools.r8.shaking.ProguardRuleParserException;
import com.android.tools.r8.smali.SmaliTestBase;
import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ExecutionException;
import org.junit.Test;
public class DeterministicProcessingTest extends SmaliTestBase {
+ public List<DexEncodedMethod> shuffle(List<DexEncodedMethod> methods, CallGraph.Leaves leaves) {
+ Collections.shuffle(methods);
+ return methods;
+ }
+ // This test will process the code a number of times each time shuffling the order in which
+ // the methods are processed. It does not do a exhaustive probing of all permutations, so if
+ // this fails it might not fail consistently, but under all circumstances a failure does reveal
+ // non-determinism in the IR-processing.
@Test
- public void test()
+ public void shuffleOrderTest()
throws IOException, ExecutionException, ProguardRuleParserException, CompilationException {
- final int ITERATIONS = 10;
+ final int ITERATIONS = 25;
R8Command.Builder builder =
R8Command.builder()
.addProgramFiles(ToolHelper.getClassFileForTestClass(TestClass.class))
.addLibraryFiles(Paths.get(ToolHelper.getDefaultAndroidJar()));
- List<byte[]> results = new ArrayList<>();
+ byte[] expectedDex = null;
for (int i = 0; i < ITERATIONS; i++) {
AndroidApp result =
ToolHelper.runR8(
- builder.build(), options -> options.testing.randomizeCallGraphLeaves = true);
+ builder.build(), options -> {
+ // For this test just do random shuffle.
+ options.testing.irOrdering = this::shuffle;
+ // Only use one thread to process to process in the order decided by the callback.
+ options.numberOfThreads = 1;
+ });
List<byte[]> dex = result.writeToMemory();
assertEquals(1, dex.size());
+ if (i == 0) {
+ assert expectedDex == null;
+ expectedDex = dex.get(0);
+ } else {
+ assertArrayEquals(expectedDex, dex.get(0));
+ }
+ }
+ }
+
+ // Global variables used by the shuffler callback.
+ int iteration = 0;
+ Class testClass = null;
+
+ public List<DexEncodedMethod> permutationsOfTwo(
+ List<DexEncodedMethod> methods, CallGraph.Leaves leaves) {
+ if (!leaves.brokeCycles()) {
+ return methods;
+ }
+ methods.sort(Comparator.comparing(DexEncodedMethod::qualifiedName));
+ assertEquals(2, methods.size());
+ String className = testClass.getTypeName();
+ // Check that we are permutating the expected methods.
+ assertEquals(className + ".a", methods.get(0).qualifiedName());
+ assertEquals(className + ".b", methods.get(1).qualifiedName());
+ if (iteration == 1) {
+ Collections.swap(methods, 0, 1);
+ }
+ return methods;
+ }
+
+ public void runTest(Class clazz, boolean inline) throws Exception {
+ final int ITERATIONS = 2;
+ testClass = clazz;
+ R8Command.Builder builder =
+ R8Command.builder()
+ .addProgramFiles(ToolHelper.getClassFileForTestClass(clazz))
+ .addLibraryFiles(Paths.get(ToolHelper.getDefaultAndroidJar()));
+ List<byte[]> results = new ArrayList<>();
+ for (iteration = 0; iteration < ITERATIONS; iteration++) {
+ AndroidApp result =
+ ToolHelper.runR8(
+ builder.build(), options -> {
+ options.inlineAccessors = inline;
+ // Callback to determine IR processing order.
+ options.testing.irOrdering = this::permutationsOfTwo;
+ // Only use one thread to process to process in the order decided by the callback.
+ options.numberOfThreads = 1;
+ });
+ List<byte[]> dex = result.writeToMemory();
+ DexInspector x = new DexInspector(result);
+ assertEquals(1, dex.size());
results.add(dex.get(0));
- System.out.println(dex.get(0).length);
}
for (int i = 0; i < ITERATIONS - 1; i++) {
assertArrayEquals(results.get(i), results.get(i + 1));
}
}
+
+ @Test
+ public void testReturnArguments() throws Exception {
+ runTest(TestClassReturnsArgument.class, false);
+ }
+
+ @Test
+ public void testReturnConstant() throws Exception {
+ runTest(TestClassReturnsConstant.class, false);
+ }
+
+ @Test
+ public void testInline() throws Exception {
+ runTest(TestClassInline.class, true);
+ }
}
diff --git a/src/test/java/com/android/tools/r8/ir/deterministic/TestClassInline.java b/src/test/java/com/android/tools/r8/ir/deterministic/TestClassInline.java
new file mode 100644
index 0000000..aa50f02
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/deterministic/TestClassInline.java
@@ -0,0 +1,24 @@
+// Copyright (c) 2017, 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.deterministic;
+
+public class TestClassInline {
+
+ public static boolean alwaysFalse() {
+ return false;
+ }
+
+ public static int a() {
+ return b();
+ }
+
+ public static int b() {
+ if (alwaysFalse()) {
+ a();
+ return 0;
+ }
+ return 1;
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/deterministic/TestClassReturnsArgument.java b/src/test/java/com/android/tools/r8/ir/deterministic/TestClassReturnsArgument.java
new file mode 100644
index 0000000..ac70401
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/deterministic/TestClassReturnsArgument.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2017, 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.deterministic;
+
+import java.util.Random;
+
+public class TestClassReturnsArgument {
+
+ public static int a(int x) {
+ return b(new Random().nextInt());
+ }
+
+ public static int b(int x) {
+ a(1);
+ return x;
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/deterministic/TestClassReturnsConstant.java b/src/test/java/com/android/tools/r8/ir/deterministic/TestClassReturnsConstant.java
new file mode 100644
index 0000000..7d63f60
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/deterministic/TestClassReturnsConstant.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2017, 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.deterministic;
+
+import java.util.Random;
+
+public class TestClassReturnsConstant {
+
+ public static int a(int x) {
+ return b(new Random().nextInt());
+ }
+
+ public static int b(int x) {
+ a(x);
+ return 1;
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java
index 0df0031..08e90c0 100644
--- a/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java
@@ -36,6 +36,11 @@
public boolean argumentValueUsesHighRegister(Value value, int instructionNumber) {
return false;
}
+
+ @Override
+ public int getArgumentOrAllocateRegisterForValue(Value value, int instructionNumber) {
+ return value.getNumber();
+ }
}
@Test
diff --git a/src/test/java/com/android/tools/r8/jasmin/BooleanByteConfusion.java b/src/test/java/com/android/tools/r8/jasmin/BooleanByteConfusion.java
new file mode 100644
index 0000000..2db35c4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/jasmin/BooleanByteConfusion.java
@@ -0,0 +1,112 @@
+// Copyright (c) 2017, 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.jasmin;
+
+import static junit.framework.TestCase.assertEquals;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+
+public class BooleanByteConfusion extends JasminTestBase {
+
+ private void runTest(JasminBuilder builder, String main) throws Exception {
+ String javaResult = runOnJava(builder, main);
+ String artResult = runOnArt(builder, main);
+ assertEquals(javaResult, artResult);
+ String dxArtResult = runOnArtDx(builder, main);
+ assertEquals(javaResult, dxArtResult);
+ }
+
+ @Test
+ public void booleanByteConfusion() throws Exception {
+ JasminBuilder builder = new JasminBuilder();
+ JasminBuilder.ClassBuilder clazz = builder.addClass("Test");
+
+ // public static void foo(boolean condition) {
+ // boolean[][] array = null;
+ // if (condition) {
+ // array = new boolean[][] { {true}, {false} };
+ // }
+ // if (array != null) {
+ // System.out.println(array[0][0]);
+ // System.out.println(array[1][0]);
+ // } else {
+ // System.out.println("null array");
+ // }
+ // }
+ clazz.addStaticMethod("foo", ImmutableList.of("Z"), "V",
+ ".limit stack 10",
+ ".limit locals 10",
+ " aconst_null",
+ // Compiling with new versions of javac seem to always put in this check cast, but it
+ // is not strictly required and some versions of javac seem to not emit it. The class
+ // file is still valid and runs, so we should be able to deal with it as well.
+ //
+ // " checkcast [[Z",
+ //
+ " astore_1",
+ " iload_0",
+ " ifeq Target",
+ " iconst_2",
+ " anewarray [Z",
+ " dup",
+ " iconst_0",
+ " iconst_1",
+ " newarray boolean",
+ " dup",
+ " iconst_0",
+ " iconst_1",
+ " bastore",
+ " aastore",
+ " dup",
+ " iconst_1",
+ " iconst_1",
+ " newarray boolean",
+ " dup",
+ " iconst_0",
+ " iconst_0",
+ " bastore",
+ " aastore",
+ " astore_1",
+ "Target:",
+ " aload_1",
+ " ifnull PrintNull",
+ " getstatic java/lang/System/out Ljava/io/PrintStream;",
+ " aload_1",
+ " iconst_0",
+ " aaload",
+ " iconst_0",
+ " baload",
+ " invokevirtual java/io/PrintStream/println(Z)V",
+ " getstatic java/lang/System.out Ljava/io/PrintStream;",
+ " aload_1",
+ " iconst_1",
+ " aaload",
+ " iconst_0",
+ " baload",
+ " invokevirtual java/io/PrintStream.println(Z)V",
+ " goto Return",
+ "PrintNull:",
+ " getstatic java/lang/System.out Ljava/io/PrintStream;",
+ " ldc \"null array\"",
+ " invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V",
+ "Return:",
+ " return");
+
+ // public static void main(String args[]) {
+ // foo(true);
+ // foo(false);
+ // }
+ clazz.addMainMethod(
+ ".limit stack 2",
+ ".limit locals 10",
+ " iconst_1",
+ " invokestatic Test/foo(Z)V",
+ " iconst_0",
+ " invokestatic Test/foo(Z)V",
+ " return");
+
+ runTest(builder, clazz.name);
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/jasmin/InvalidClassNames.java b/src/test/java/com/android/tools/r8/jasmin/InvalidClassNames.java
new file mode 100644
index 0000000..1812fe1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/jasmin/InvalidClassNames.java
@@ -0,0 +1,80 @@
+// Copyright (c) 2017, 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.jasmin;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.fail;
+
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.errors.CompilationError;
+import java.util.Arrays;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class InvalidClassNames extends JasminTestBase {
+
+ public boolean runsOnJVM;
+ public String name;
+
+ public InvalidClassNames(String name, boolean runsOnJVM) {
+ this.name = name;
+ this.runsOnJVM = runsOnJVM;
+ }
+
+ private void runTest(JasminBuilder builder, String main, String expected) throws Exception {
+ if (runsOnJVM) {
+ String javaResult = runOnJava(builder, main);
+ assertEquals(expected, javaResult);
+ }
+ String artResult = null;
+ try {
+ artResult = runOnArt(builder, main);
+ } catch (CompilationError t) {
+ // Ignore.
+ } catch (Throwable t) {
+ t.printStackTrace(System.out);
+ fail("Invalid dex class names should be compilation errors.");
+ }
+ assert artResult == null : "Invalid dex class names should be rejected.";
+ }
+
+ @Parameters
+ public static Collection<Object[]> data() {
+ return Arrays.asList(new Object[][] {
+ { new String(new int[] { 0x00a0 }, 0, 1), true },
+ { new String(new int[] { 0x2000 }, 0, 1), !ToolHelper.isWindows() },
+ { new String(new int[] { 0x200f }, 0, 1), !ToolHelper.isWindows() },
+ { new String(new int[] { 0x2028 }, 0, 1), !ToolHelper.isWindows() },
+ { new String(new int[] { 0x202f }, 0, 1), !ToolHelper.isWindows() },
+ { new String(new int[] { 0xd800 }, 0, 1), false },
+ { new String(new int[] { 0xdfff }, 0, 1), false },
+ { new String(new int[] { 0xfff0 }, 0, 1), !ToolHelper.isWindows() },
+ { new String(new int[] { 0xffff }, 0, 1), !ToolHelper.isWindows() },
+ { "a/b/c/a/D/", true },
+ { "a<b", !ToolHelper.isWindows() },
+ { "a>b", !ToolHelper.isWindows() },
+ { "<a>b", !ToolHelper.isWindows() },
+ { "<a>", !ToolHelper.isWindows() }
+ });
+ }
+
+ @Test
+ public void invalidClassName() throws Exception {
+ JasminBuilder builder = new JasminBuilder();
+ JasminBuilder.ClassBuilder clazz = builder.addClass(name);
+ clazz.addMainMethod(
+ ".limit stack 2",
+ ".limit locals 1",
+ " getstatic java/lang/System/out Ljava/io/PrintStream;",
+ " ldc \"MAIN\"",
+ " invokevirtual java/io/PrintStream.print(Ljava/lang/String;)V",
+ " return");
+
+ runTest(builder, clazz.name, "MAIN");
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/jasmin/InvalidDebugInfoTests.java b/src/test/java/com/android/tools/r8/jasmin/InvalidDebugInfoTests.java
index 984e40e..155438f 100644
--- a/src/test/java/com/android/tools/r8/jasmin/InvalidDebugInfoTests.java
+++ b/src/test/java/com/android/tools/r8/jasmin/InvalidDebugInfoTests.java
@@ -5,6 +5,7 @@
import static org.junit.Assert.assertEquals;
+import com.android.tools.r8.ToolHelper;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
@@ -66,7 +67,7 @@
" invokestatic Test/foo(I)V",
" return");
- String expected = "42\n0\n";
+ String expected = "42" + ToolHelper.LINE_SEPARATOR + "0" + ToolHelper.LINE_SEPARATOR;
String javaResult = runOnJava(builder, clazz.name);
assertEquals(expected, javaResult);
String artResult = runOnArtD8(builder, clazz.name);
@@ -115,7 +116,7 @@
" invokestatic Test/foo(II)V",
" return");
- String expected = "42\n";
+ String expected = "42" + ToolHelper.LINE_SEPARATOR;
String javaResult = runOnJava(builder, clazz.name);
assertEquals(expected, javaResult);
String artResult = runOnArtD8(builder, clazz.name);
diff --git a/src/test/java/com/android/tools/r8/jasmin/InvalidFieldNames.java b/src/test/java/com/android/tools/r8/jasmin/InvalidFieldNames.java
new file mode 100644
index 0000000..791ecca
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/jasmin/InvalidFieldNames.java
@@ -0,0 +1,78 @@
+// Copyright (c) 2017, 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.jasmin;
+
+import static junit.framework.TestCase.assertEquals;
+
+import com.android.tools.r8.errors.CompilationError;
+import java.util.Arrays;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class InvalidFieldNames extends JasminTestBase {
+
+ public boolean runsOnJVM;
+ public String name;
+
+ public InvalidFieldNames(String name, boolean runsOnJVM) {
+ this.name = name;
+ this.runsOnJVM = runsOnJVM;
+ }
+
+ private void runTest(JasminBuilder builder, String main, String expected) throws Exception {
+ if (runsOnJVM) {
+ String javaResult = runOnJava(builder, main);
+ assertEquals(expected, javaResult);
+ }
+ String artResult = null;
+ try {
+ artResult = runOnArt(builder, main);
+ } catch (CompilationError t) {
+ // Ignore.
+ }
+ assert artResult == null : "Invalid dex class names should be rejected.";
+ }
+
+ @Parameters
+ public static Collection<Object[]> data() {
+ return Arrays.asList(new Object[][] {
+ { new String(new int[] { 0x00a0 }, 0, 1), true },
+ { new String(new int[] { 0x2000 }, 0, 1), true },
+ { new String(new int[] { 0x200f }, 0, 1), true },
+ { new String(new int[] { 0x2028 }, 0, 1), true },
+ { new String(new int[] { 0x202f }, 0, 1), true },
+ { new String(new int[] { 0xd800 }, 0, 1), true },
+ { new String(new int[] { 0xdfff }, 0, 1), true },
+ { new String(new int[] { 0xfff0 }, 0, 1), true },
+ { new String(new int[] { 0xffff }, 0, 1), true },
+ { "a/b", false },
+ { "<a", false },
+ { "a>", true },
+ { "a<b>", true },
+ { "<a>b", true }
+ });
+ }
+
+ @Test
+ public void invalidFieldNames() throws Exception {
+ JasminBuilder builder = new JasminBuilder();
+ JasminBuilder.ClassBuilder clazz = builder.addClass("Test");
+
+ clazz.addStaticField(name, "I", "42");
+
+ clazz.addMainMethod(
+ ".limit stack 2",
+ ".limit locals 1",
+ " getstatic java/lang/System/out Ljava/io/PrintStream;",
+ " getstatic Test/" + name + " I",
+ " invokevirtual java/io/PrintStream.print(I)V",
+ " return");
+
+ runTest(builder, clazz.name, "42");
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/jasmin/InvalidMethodNames.java b/src/test/java/com/android/tools/r8/jasmin/InvalidMethodNames.java
new file mode 100644
index 0000000..f8b7615
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/jasmin/InvalidMethodNames.java
@@ -0,0 +1,86 @@
+// Copyright (c) 2017, 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.jasmin;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.fail;
+
+import com.android.tools.r8.errors.CompilationError;
+import com.google.common.collect.ImmutableList;
+import java.util.Arrays;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class InvalidMethodNames extends JasminTestBase {
+
+ public boolean runsOnJVM;
+ public String name;
+
+ public InvalidMethodNames(String name, boolean runsOnJVM) {
+ this.name = name;
+ this.runsOnJVM = runsOnJVM;
+ }
+
+ private void runTest(JasminBuilder builder, String main, String expected) throws Exception {
+ if (runsOnJVM) {
+ String javaResult = runOnJava(builder, main);
+ assertEquals(expected, javaResult);
+ }
+ String artResult = null;
+ try {
+ artResult = runOnArt(builder, main);
+ } catch (CompilationError t) {
+ // Ignore.
+ } catch (Throwable t) {
+ t.printStackTrace(System.out);
+ fail("Invalid dex class names should be compilation errors.");
+ }
+ assert artResult == null : "Invalid dex class names should be rejected.";
+ }
+
+ @Parameters
+ public static Collection<Object[]> data() {
+ return Arrays.asList(new Object[][] {
+ { new String(new int[] { 0x00a0 }, 0, 1), true },
+ { new String(new int[] { 0x2000 }, 0, 1), true },
+ { new String(new int[] { 0x200f }, 0, 1), true },
+ { new String(new int[] { 0x2028 }, 0, 1), true },
+ { new String(new int[] { 0x202f }, 0, 1), true },
+ { new String(new int[] { 0xd800 }, 0, 1), true },
+ { new String(new int[] { 0xdfff }, 0, 1), true },
+ { new String(new int[] { 0xfff0 }, 0, 1), true },
+ { new String(new int[] { 0xffff }, 0, 1), true },
+ { "a/b", false },
+ { "<a", false },
+ { "a>", true },
+ { "<a>", false }
+ });
+ }
+
+ @Test
+ public void invalidMethodName() throws Exception {
+ JasminBuilder builder = new JasminBuilder();
+ JasminBuilder.ClassBuilder clazz = builder.addClass("Test");
+
+ clazz.addStaticMethod(name, ImmutableList.of(), "V",
+ ".limit stack 2",
+ ".limit locals 0",
+ " getstatic java/lang/System/out Ljava/io/PrintStream;",
+ " ldc \"CALLED\"",
+ " invokevirtual java/io/PrintStream.print(Ljava/lang/String;)V",
+ " return");
+
+ clazz.addMainMethod(
+ ".limit stack 0",
+ ".limit locals 1",
+ " invokestatic Test/" + name + "()V",
+ " return");
+
+ runTest(builder, clazz.name, "CALLED");
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/jasmin/InvokeSpecialTests.java b/src/test/java/com/android/tools/r8/jasmin/InvokeSpecialTests.java
index 197b4d1..c74464f 100644
--- a/src/test/java/com/android/tools/r8/jasmin/InvokeSpecialTests.java
+++ b/src/test/java/com/android/tools/r8/jasmin/InvokeSpecialTests.java
@@ -5,7 +5,9 @@
import static org.junit.Assert.assertEquals;
+import com.android.tools.r8.ToolHelper;
import com.google.common.collect.ImmutableList;
+import org.junit.AssumptionViolatedException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
@@ -72,7 +74,9 @@
// TODO(zerny): Should we fail early on the above code? Art fails with a verification error
// because Test.foo is expected to be in the direct method table.
- thrown.expect(AssertionError.class);
+ if (ToolHelper.artSupported()) {
+ thrown.expect(AssertionError.class);
+ }
String artResult = runOnArt(builder, clazz.name);
assertEquals(expected, artResult);
}
diff --git a/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java b/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
index e7511e5..7f65c60 100644
--- a/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
+++ b/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
@@ -6,6 +6,7 @@
import com.android.tools.r8.dex.ApplicationReader;
import com.android.tools.r8.graph.DexApplication;
import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.naming.MemberNaming.FieldSignature;
import com.android.tools.r8.naming.MemberNaming.MethodSignature;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.DescriptorUtils;
@@ -25,6 +26,7 @@
public static class ClassBuilder {
public final String name;
private final List<String> methods = new ArrayList<>();
+ private final List<String> fields = new ArrayList<>();
private boolean makeInit = false;
public ClassBuilder(String name) {
@@ -85,6 +87,12 @@
return new MethodSignature(name, returnJavaType, argumentJavaTypes);
}
+ public FieldSignature addStaticField(String name, String type, String value) {
+ fields.add(
+ ".field public static " + name + " " + type + (value != null ? (" = " + value) : ""));
+ return new FieldSignature(name, type);
+ }
+
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
@@ -101,6 +109,9 @@
.append(" return\n")
.append(".end method\n");
}
+ for (String field : fields) {
+ builder.append(field).append("\n");
+ }
for (String method : methods) {
builder.append(method).append("\n");
}
diff --git a/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java b/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java
index 615e289..6ceb912 100644
--- a/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java
+++ b/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java
@@ -24,6 +24,7 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringReader;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
@@ -42,7 +43,9 @@
for (ClassBuilder clazz : builder.getClasses()) {
ClassFile file = new ClassFile();
file.readJasmin(new StringReader(clazz.toString()), clazz.name, true);
- file.write(new FileOutputStream(out.toPath().resolve(clazz.name + ".class").toFile()));
+ Path path = out.toPath().resolve(clazz.name + ".class");
+ Files.createDirectories(path.getParent());
+ file.write(new FileOutputStream(path.toFile()));
}
return ToolHelper.runJava(ImmutableList.of(out.getPath()), main);
}
diff --git a/src/test/java/com/android/tools/r8/jasmin/JumpSubroutineTests.java b/src/test/java/com/android/tools/r8/jasmin/JumpSubroutineTests.java
index 34ddd69..bd060b5 100644
--- a/src/test/java/com/android/tools/r8/jasmin/JumpSubroutineTests.java
+++ b/src/test/java/com/android/tools/r8/jasmin/JumpSubroutineTests.java
@@ -8,6 +8,7 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.ToolHelper.ProcessResult;
import com.android.tools.r8.utils.AndroidApp;
import com.google.common.collect.ImmutableList;
@@ -308,7 +309,8 @@
" invokestatic Test/foo()V",
" return");
- runTest(builder, clazz.name, "Got zero\nGot non-zero\n");
+ runTest(builder, clazz.name, "Got zero" + ToolHelper.LINE_SEPARATOR + "Got non-zero"
+ + ToolHelper.LINE_SEPARATOR);
}
@Test
@@ -351,7 +353,9 @@
" invokestatic Test/foo()V",
" return");
- runTest(builder, clazz.name, "Got zero\nGot non-zero, calling nested\nIn nested subroutine\n");
+ runTest(builder, clazz.name, "Got zero" + ToolHelper.LINE_SEPARATOR
+ + "Got non-zero, calling nested" + ToolHelper.LINE_SEPARATOR + "In nested subroutine"
+ + ToolHelper.LINE_SEPARATOR);
}
@Test
@@ -527,7 +531,8 @@
" invokestatic Test/foo()V",
" return");
- runTest(builder, clazz.name, "Divided by zero\nDivided by non-zero\n");
+ runTest(builder, clazz.name, "Divided by zero" + ToolHelper.LINE_SEPARATOR
+ + "Divided by non-zero" + ToolHelper.LINE_SEPARATOR);
}
@Test
diff --git a/src/test/java/com/android/tools/r8/jasmin/TryCatchStateTests.java b/src/test/java/com/android/tools/r8/jasmin/TryCatchStateTests.java
index 92f99f8..a24ffd2 100644
--- a/src/test/java/com/android/tools/r8/jasmin/TryCatchStateTests.java
+++ b/src/test/java/com/android/tools/r8/jasmin/TryCatchStateTests.java
@@ -5,6 +5,7 @@
import static org.junit.Assert.assertEquals;
+import com.android.tools.r8.ToolHelper;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
@@ -49,7 +50,7 @@
" invokevirtual java/io/PrintStream/print(I)V",
" return");
- String expected = "0\n6";
+ String expected = "0" + ToolHelper.LINE_SEPARATOR + "6";
String javaResult = runOnJava(builder, clazz.name);
assertEquals(expected, javaResult);
String artResult = runOnArt(builder, clazz.name);
@@ -97,7 +98,7 @@
" invokevirtual java/io/PrintStream/print(I)V",
" return");
- String expected = "12\n21";
+ String expected = "12" + ToolHelper.LINE_SEPARATOR + "21";
String javaResult = runOnJava(builder, clazz.name);
assertEquals(expected, javaResult);
String artResult = runOnArt(builder, clazz.name);
@@ -148,7 +149,7 @@
" invokevirtual java/io/PrintStream/print(I)V",
" return");
- String expected = "12\n21";
+ String expected = "12" + ToolHelper.LINE_SEPARATOR + "21";
String javaResult = runOnJava(builder, clazz.name);
assertEquals(expected, javaResult);
String artResult = runOnArt(builder, clazz.name);
diff --git a/src/test/java/com/android/tools/r8/maindexlist/HelloWorldMain.java b/src/test/java/com/android/tools/r8/maindexlist/HelloWorldMain.java
new file mode 100644
index 0000000..b70e73f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/maindexlist/HelloWorldMain.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2017, 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.maindexlist;
+
+public class HelloWorldMain {
+
+ public static void main(String[] args) {
+ System.out.println("Hello, world!");
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java
new file mode 100644
index 0000000..f8ca2a8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java
@@ -0,0 +1,48 @@
+// Copyright (c) 2017, 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.maindexlist;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.utils.FileUtils;
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import org.junit.Test;
+
+public class MainDexListOutputTest extends TestBase {
+ @Test
+ public void testNoMainDex() throws Exception {
+ Path mainDexList = temp.newFile().toPath();
+ compileWithR8(ImmutableList.of(HelloWorldMain.class),
+ options -> {
+ options.printMainDexList = true;
+ options.printMainDexListFile = mainDexList;
+ });
+ // Empty main dex list.
+ assertEquals(0, FileUtils.readTextFile(mainDexList).size());
+ }
+
+ @Test
+ public void testWithMainDex() throws Exception {
+ Path mainDexRules = writeTextToTempFile(keepMainProguardConfiguration(HelloWorldMain.class));
+ R8Command command =
+ ToolHelper.prepareR8CommandBuilder(readClasses(HelloWorldMain.class))
+ .addMainDexRules(mainDexRules)
+ .build();
+ Path mainDexList = temp.newFile().toPath();
+ ToolHelper.runR8(command,
+ options -> {
+ options.printMainDexList = true;
+ options.printMainDexListFile = mainDexList;
+ });
+ // Main dex list with the single class.
+ assertEquals(
+ ImmutableList.of(HelloWorldMain.class.getTypeName().replace('.', '/') + ".class"),
+ FileUtils.readTextFile(mainDexList));
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
index 44c2156..a0a77d1 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -3,15 +3,15 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.maindexlist;
-import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.android.tools.r8.CompilationException;
+import com.android.tools.r8.D8Command;
import com.android.tools.r8.R8Command;
+import com.android.tools.r8.TestBase;
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.dex.ApplicationWriter;
import com.android.tools.r8.dex.Constants;
@@ -39,11 +39,13 @@
import com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator;
import com.android.tools.r8.ir.regalloc.RegisterAllocator;
import com.android.tools.r8.ir.synthetic.SynthesizedCode;
+import com.android.tools.r8.jasmin.JasminBuilder;
import com.android.tools.r8.naming.NamingLens;
import com.android.tools.r8.shaking.ProguardRuleParserException;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.FoundClassSubject;
import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.ListUtils;
@@ -52,122 +54,101 @@
import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.Timing;
import com.google.common.collect.ImmutableList;
-import com.google.common.io.ByteStreams;
-import com.google.common.io.Closer;
+import com.google.common.collect.Lists;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
+import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
-public class MainDexListTests {
-
- private static final Path BASE =
- Paths.get("src", "test", "java", "com", "android", "tools", "r8", "maindexlist");
-
- private static boolean verifyApplications = true;
- private static boolean regenerateApplications = false;
+public class MainDexListTests extends TestBase {
private static final int MAX_METHOD_COUNT = Constants.U16BIT_MAX;
+
private static final List<String> TWO_LARGE_CLASSES = ImmutableList.of("A", "B");
- private static final String TWO_LARGE_CLASSES_APP = "two-large-classes.zip";
+ private static final int MANY_CLASSES_COUNT = 10000;
+ private static final int MANY_CLASSES_SINGLE_DEX_METHODS_PER_CLASS = 2;
+ private static final int MANY_CLASSES_MULTI_DEX_METHODS_PER_CLASS = 10;
+ private static List<String> MANY_CLASSES;
- private static final int MANY_CLASSES_COUNT = 1000;
- private static final List<String> MANY_CLASSES;
- private static final String MANY_CLASSES_APP = "many-classes.zip";
+ @ClassRule
+ public static TemporaryFolder generatedApplicationsFolder = new TemporaryFolder();
- static {
+ // Generate the test applications in a @BeforeClass method, as they are used by several tests.
+ @BeforeClass
+ public static void generateTestApplications() throws Throwable {
ImmutableList.Builder<String> builder = ImmutableList.builder();
for (int i = 0; i < MANY_CLASSES_COUNT; ++i) {
String pkg = i % 2 == 0 ? "a" : "b";
builder.add(pkg + ".Class" + i);
}
MANY_CLASSES = builder.build();
+
+ // Generates an application with many classes, every even in one package and every odd in
+ // another. Keep the number of methods low enough for single dex application.
+ AndroidApp generated = generateApplication(
+ MANY_CLASSES, Constants.DEFAULT_ANDROID_API, MANY_CLASSES_SINGLE_DEX_METHODS_PER_CLASS);
+ generated.write(getManyClassesSingleDexAppPath(), OutputMode.Indexed, false);
+
+ // Generates an application with many classes, every even in one package and every odd in
+ // another. Add enough methods so the application cannot fit into one dex file.
+ generated = generateApplication(
+ MANY_CLASSES, Constants.ANDROID_L_API, MANY_CLASSES_MULTI_DEX_METHODS_PER_CLASS);
+ generated.write(getManyClassesMultiDexAppPath(), OutputMode.Indexed, false);
+
+ // Generates an application with two classes, each with the maximum possible number of methods.
+ generated = generateApplication(TWO_LARGE_CLASSES, Constants.ANDROID_N_API, MAX_METHOD_COUNT);
+ generated.write(getTwoLargeClassesAppPath(), OutputMode.Indexed, false);
}
- public static Path getTwoLargeClassesAppPath() {
- return BASE.resolve(TWO_LARGE_CLASSES_APP);
+ private static Path getTwoLargeClassesAppPath() {
+ return generatedApplicationsFolder.getRoot().toPath().resolve("two-large-classes.zip");
}
- public static Path getManyClassesAppPath() {
- return BASE.resolve(MANY_CLASSES_APP);
+ private static Path getManyClassesSingleDexAppPath() {
+ return generatedApplicationsFolder.getRoot().toPath().resolve("many-classes-mono.zip");
}
- // Generates an application with two classes, each with the maximum possible number of methods.
- @Test
- public void generateTwoLargeClasses() throws IOException, ExecutionException {
- if (!verifyApplications && !regenerateApplications) {
- return;
- }
- AndroidApp generated = generateApplication(TWO_LARGE_CLASSES, MAX_METHOD_COUNT);
- if (regenerateApplications) {
- generated.write(getTwoLargeClassesAppPath(), OutputMode.Indexed, true);
- } else {
- AndroidApp cached = AndroidApp.fromProgramFiles(getTwoLargeClassesAppPath());
- compareToCachedVersion(cached, generated, TWO_LARGE_CLASSES_APP);
- }
+ private static Path getManyClassesMultiDexAppPath() {
+ return generatedApplicationsFolder.getRoot().toPath().resolve("many-classes-stereo.zip");
}
- // Generates an application with many classes, every even in one package and every odd in another.
- @Test
- public void generateManyClasses() throws IOException, ExecutionException {
- if (!verifyApplications && !regenerateApplications) {
- return;
- }
- AndroidApp generated = generateApplication(MANY_CLASSES, 1);
- if (regenerateApplications) {
- generated.write(getManyClassesAppPath(), OutputMode.Indexed, true);
- } else {
- AndroidApp cached = AndroidApp.fromProgramFiles(getManyClassesAppPath());
- compareToCachedVersion(cached, generated, MANY_CLASSES_APP);
- }
- }
-
- private static void compareToCachedVersion(AndroidApp cached, AndroidApp generated, String name)
- throws IOException {
- assertEquals("On-disk cached app (" + name + ") differs in file count from regeneration"
- + "Set 'regenerateApplications = true' and rerun this test to update the cache.",
- cached.getDexProgramResources().size(), generated.getDexProgramResources().size());
- try (Closer closer = Closer.create()) {
- for (int i = 0; i < cached.getDexProgramResources().size(); i++) {
- byte[] cachedBytes =
- ByteStreams.toByteArray(cached.getDexProgramResources().get(i).getStream(closer));
- byte[] generatedBytes =
- ByteStreams.toByteArray(generated.getDexProgramResources().get(i).getStream(closer));
- assertArrayEquals("On-disk cached app differs in byte content from regeneration"
- + "Set 'regenerateApplications = true' and rerun this test to update the cache.",
- cachedBytes, generatedBytes);
- }
- }
- }
-
- @Rule
- public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
-
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
+ public void checkGeneratedFileFitInSingleDexFile() {
+ assertTrue(MANY_CLASSES_COUNT * MANY_CLASSES_SINGLE_DEX_METHODS_PER_CLASS <= MAX_METHOD_COUNT);
+ }
+
+ @Test
+ public void checkGeneratedFileNeedsTwoDexFiles() {
+ assertTrue(MANY_CLASSES_COUNT * MANY_CLASSES_MULTI_DEX_METHODS_PER_CLASS > MAX_METHOD_COUNT);
+ }
+
+ @Test
public void putFirstClassInMainDexList() throws Throwable {
- verifyMainDexContains(TWO_LARGE_CLASSES.subList(0, 1), getTwoLargeClassesAppPath());
+ verifyMainDexContains(TWO_LARGE_CLASSES.subList(0, 1), getTwoLargeClassesAppPath(), false);
}
@Test
public void putSecondClassInMainDexList() throws Throwable {
- verifyMainDexContains(TWO_LARGE_CLASSES.subList(1, 2), getTwoLargeClassesAppPath());
+ verifyMainDexContains(TWO_LARGE_CLASSES.subList(1, 2), getTwoLargeClassesAppPath(), false);
}
@Test
public void cannotFitBothIntoMainDex() throws Throwable {
thrown.expect(CompilationError.class);
- verifyMainDexContains(TWO_LARGE_CLASSES, getTwoLargeClassesAppPath());
+ verifyMainDexContains(TWO_LARGE_CLASSES, getTwoLargeClassesAppPath(), false);
}
@Test
@@ -179,7 +160,28 @@
mainDexBuilder.add(clazz);
}
}
- verifyMainDexContains(mainDexBuilder.build(), getManyClassesAppPath());
+ verifyMainDexContains(mainDexBuilder.build(), getManyClassesSingleDexAppPath(), true);
+ verifyMainDexContains(mainDexBuilder.build(), getManyClassesMultiDexAppPath(), false);
+ }
+
+ @Test
+ public void singleClassInMainDex() throws Throwable {
+ ImmutableList<String> mainDex = ImmutableList.of(MANY_CLASSES.get(0));
+ verifyMainDexContains(mainDex, getManyClassesSingleDexAppPath(), true);
+ verifyMainDexContains(mainDex, getManyClassesMultiDexAppPath(), false);
+ }
+
+ @Test
+ public void allClassesInMainDex() throws Throwable {
+ // Degenerated case with an app thats fit into a single dex, and where the main dex list
+ // contains all classes.
+ verifyMainDexContains(MANY_CLASSES, getManyClassesSingleDexAppPath(), true);
+ }
+
+ @Test
+ public void cannotFitAllIntoMainDex() throws Throwable {
+ thrown.expect(CompilationError.class);
+ verifyMainDexContains(MANY_CLASSES, getManyClassesMultiDexAppPath(), false);
}
@Test
@@ -207,11 +209,147 @@
MainDexList.parse(mainDexList, factory);
}
- private static String typeToEntry(String type) {
- return type.replace(".", "/") + CLASS_EXTENSION;
+ @Test
+ public void checkDeterminism() throws Exception {
+ // Synthesize a dex containing a few empty classes including some in the default package.
+ // Everything can fit easaly in a single dex file.
+ String[] classes = {
+ "A",
+ "B",
+ "C",
+ "D",
+ "E",
+ "F",
+ "A1",
+ "A2",
+ "A3",
+ "A4",
+ "A5",
+ "maindexlist/A",
+ "maindexlist/B",
+ "maindexlist/C",
+ "maindexlist/D",
+ "maindexlist/E",
+ "maindexlist/F",
+ "maindexlist/A1",
+ "maindexlist/A2",
+ "maindexlist/A3",
+ "maindexlist/A4",
+ "maindexlist/A5"
+ };
+ JasminBuilder jasminBuilder = new JasminBuilder();
+ for (String name : classes) {
+ jasminBuilder.addClass(name);
+ }
+ Path input = temp.newFolder().toPath().resolve("input.zip");
+ ToolHelper.runR8(jasminBuilder.build()).writeToZip(input, OutputMode.Indexed);
+
+ // Prepare different main dex lists.
+ ArrayList<Path> mainLists = new ArrayList<>();
+ // Lets first without a main dex list.
+ mainLists.add(null);
+
+ // List with all classes.
+ List<String> mainList = new ArrayList<>();
+ for (int i = 0; i < classes.length; i++) {
+ mainList.add(classes[i] + ".class");
+ }
+ addMainListFile(mainLists, mainList);
+
+ // Full list in reverse order
+ addMainListFile(mainLists, Lists.reverse(mainList));
+
+ // Partial list without first entries (those in default package).
+ mainList.clear();
+ for (int i = classes.length / 2; i < classes.length; i++) {
+ mainList.add(classes[i] + ".class");
+ }
+ addMainListFile(mainLists, mainList);
+
+ // Same in reverese order
+ addMainListFile(mainLists, Lists.reverse(mainList));
+
+ // Mixed partial list.
+ mainList.clear();
+ for (int i = 0; i < classes.length; i += 2) {
+ mainList.add(classes[i] + ".class");
+ }
+ addMainListFile(mainLists, mainList);
+
+ // Another different mixed partial list.
+ mainList.clear();
+ for (int i = 1; i < classes.length; i += 2) {
+ mainList.add(classes[i] + ".class");
+ }
+ addMainListFile(mainLists, mainList);
+
+ // Build with all main dex lists.
+ Path tmp = temp.getRoot().toPath();
+ for (int i = 0; i < mainLists.size(); i++) {
+ Path out = tmp.resolve(String.valueOf(i));
+ Files.createDirectories(out);
+ D8Command.Builder builder = D8Command.builder()
+ .addProgramFiles(input)
+ .setOutputPath(out);
+ if (mainLists.get(i) != null) {
+ builder.setMainDexListFile(mainLists.get(i));
+ }
+ ToolHelper.runD8(builder.build());
+ }
+
+ // Check: no secondary dex and resulting dex is always the same.
+ assertFalse(Files.exists(tmp.resolve(String.valueOf(0)).resolve("classes2.dex")));
+ byte[] ref = Files.readAllBytes(
+ tmp.resolve(String.valueOf(0)).resolve(FileUtils.DEFAULT_DEX_FILENAME));
+ for (int i = 1; i < mainLists.size(); i++) {
+ assertFalse(Files.exists(tmp.resolve(String.valueOf(i)).resolve("classes2.dex")));
+ byte[] checked = Files.readAllBytes(
+ tmp.resolve(String.valueOf(i)).resolve(FileUtils.DEFAULT_DEX_FILENAME));
+ assertArrayEquals(ref, checked);
+ }
}
- private void verifyMainDexContains(List<String> mainDex, Path app)
+ private void addMainListFile(ArrayList<Path> mainLists, List<String> content)
+ throws IOException {
+ Path listFile = temp.newFile().toPath();
+ FileUtils.writeTextFile(listFile, content);
+ mainLists.add(listFile);
+ }
+
+ private static String typeToEntry(String type) {
+ return type.replace(".", "/") + FileUtils.CLASS_EXTENSION;
+ }
+
+ private void failedToFindClassInExpectedFile(Path outDir, String clazz) throws IOException {
+ Files.list(outDir)
+ .filter(FileUtils::isDexFile)
+ .forEach(
+ p -> {
+ try {
+ DexInspector i = new DexInspector(AndroidApp.fromProgramFiles(p));
+ assertFalse("Found " + clazz + " in file " + p, i.clazz(clazz).isPresent());
+ } catch (IOException | ExecutionException e) {
+ e.printStackTrace();
+ }
+ });
+ fail("Failed to find class " + clazz + "in any file...");
+ }
+
+ private void assertMainDexClass(FoundClassSubject clazz, List<String> mainDex) {
+ if (!mainDex.contains(clazz.toString())) {
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < mainDex.size(); i++) {
+ builder.append(i == 0 ? "[" : ", ");
+ builder.append(mainDex.get(i));
+ }
+ builder.append("]");
+ fail("Class " + clazz + " found in main dex, " +
+ "only expected explicit main dex classes " + builder +" in main dex file");
+ }
+ }
+
+ private void doVerifyMainDexContains(
+ List<String> mainDex, Path app, boolean singleDexApp, boolean minimalMainDex)
throws IOException, CompilationException, ExecutionException, ProguardRuleParserException {
AndroidApp originalApp = AndroidApp.fromProgramFiles(app);
DexInspector originalInspector = new DexInspector(originalApp);
@@ -219,53 +357,52 @@
assertTrue("Class " + clazz + " does not exist in input",
originalInspector.clazz(clazz).isPresent());
}
- Path outDir = temp.newFolder("out").toPath();
- Path mainDexList = temp.getRoot().toPath().resolve("main-dex-list.txt");
+ Path outDir = temp.newFolder().toPath();
+ Path mainDexList = temp.newFile().toPath();
FileUtils.writeTextFile(mainDexList, ListUtils.map(mainDex, MainDexListTests::typeToEntry));
- Path packageDistribution = temp.getRoot().toPath().resolve("package.map");
- FileUtils.writeTextFile(packageDistribution, ImmutableList.of("a.*:2", "b.*:1"));
R8Command command =
R8Command.builder()
.addProgramFiles(app)
- .setPackageDistributionFile(packageDistribution)
.setMainDexListFile(mainDexList)
+ .setMinimalMainDex(minimalMainDex)
.setOutputPath(outDir)
.setTreeShaking(false)
.setMinification(false)
.build();
ToolHelper.runR8(command);
- assertTrue("Output run only produced one dex file. Invalid test",
- 1 < Files.list(outDir).filter(FileUtils::isDexFile).count());
+ if (!singleDexApp && !minimalMainDex) {
+ assertTrue("Output run only produced one dex file.",
+ 1 < Files.list(outDir).filter(FileUtils::isDexFile).count());
+ }
DexInspector inspector =
new DexInspector(AndroidApp.fromProgramFiles(outDir.resolve("classes.dex")));
for (String clazz : mainDex) {
if (!inspector.clazz(clazz).isPresent()) {
- Files.list(outDir)
- .filter(FileUtils::isDexFile)
- .forEach(
- p -> {
- try {
- DexInspector i = new DexInspector(AndroidApp.fromProgramFiles(p));
- assertFalse("Found " + clazz + " in file " + p, i.clazz(clazz).isPresent());
- } catch (IOException | ExecutionException e) {
- e.printStackTrace();
- }
- });
- fail("Failed to find class " + clazz + "in any file...");
+ failedToFindClassInExpectedFile(outDir, clazz);
}
}
+ if (minimalMainDex) {
+ inspector.forAllClasses(clazz -> assertMainDexClass(clazz, mainDex));
+ }
}
- private static AndroidApp generateApplication(List<String> classes, int methodCount)
+ private void verifyMainDexContains(List<String> mainDex, Path app, boolean singleDexApp)
+ throws Throwable {
+ doVerifyMainDexContains(mainDex, app, singleDexApp, false);
+ doVerifyMainDexContains(mainDex, app, singleDexApp, true);
+ }
+
+ public static AndroidApp generateApplication(List<String> classes, int minApi, int methodCount)
throws IOException, ExecutionException {
Timing timing = new Timing("MainDexListTests");
InternalOptions options = new InternalOptions();
+ options.minApiLevel = minApi;
DexItemFactory factory = options.itemFactory;
DexApplication.Builder builder = new DexApplication.Builder(factory, timing);
for (String clazz : classes) {
DexString desc = factory.createString(DescriptorUtils.javaTypeToDescriptor(clazz));
DexType type = factory.createType(desc);
- DexEncodedMethod[] virtualMethods = new DexEncodedMethod[methodCount];
+ DexEncodedMethod[] directMethods = new DexEncodedMethod[methodCount];
for (int i = 0; i < methodCount; i++) {
DexAccessFlags access = new DexAccessFlags();
access.setPublic();
@@ -283,11 +420,11 @@
DexAnnotationSetRefList.empty(),
code);
IRCode ir = code.buildIR(method, options);
- RegisterAllocator allocator = new LinearScanRegisterAllocator(ir);
+ RegisterAllocator allocator = new LinearScanRegisterAllocator(ir, options);
method.setCode(ir, allocator, factory);
- virtualMethods[i] = method;
+ directMethods[i] = method;
}
- builder.addClassPromise(
+ builder.addProgramClass(
new DexProgramClass(
type,
null,
@@ -298,8 +435,8 @@
DexAnnotationSet.empty(),
DexEncodedField.EMPTY_ARRAY,
DexEncodedField.EMPTY_ARRAY,
- DexEncodedMethod.EMPTY_ARRAY,
- virtualMethods));
+ directMethods,
+ DexEncodedMethod.EMPTY_ARRAY));
}
DexApplication application = builder.build();
AppInfoWithSubtyping appInfo = new AppInfoWithSubtyping(application);
@@ -342,8 +479,8 @@
}
@Override
- public boolean traceInstruction(int instructionIndex, IRBuilder builder) {
- return true;
+ public int traceInstruction(int instructionIndex, IRBuilder builder) {
+ return instructionIndex;
}
@Override
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
index ae0f650..2549ff1 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
@@ -144,7 +144,7 @@
String[] refList = new String(Files.readAllBytes(
expectedMainDexList), StandardCharsets.UTF_8).split("\n");
for (int i = 0; i < refList.length; i++) {
- String reference = refList[i];
+ String reference = refList[i].trim();
String computed = resultMainDexList.get(i);
if (reference.contains("-$$Lambda$")) {
// For lambda classes we check that there is a lambda class for the right containing
diff --git a/src/test/java/com/android/tools/r8/maindexlist/many-classes.zip b/src/test/java/com/android/tools/r8/maindexlist/many-classes.zip
deleted file mode 100644
index 3c1530d..0000000
--- a/src/test/java/com/android/tools/r8/maindexlist/many-classes.zip
+++ /dev/null
Binary files differ
diff --git a/src/test/java/com/android/tools/r8/maindexlist/two-large-classes.zip b/src/test/java/com/android/tools/r8/maindexlist/two-large-classes.zip
deleted file mode 100644
index 97b5fde..0000000
--- a/src/test/java/com/android/tools/r8/maindexlist/two-large-classes.zip
+++ /dev/null
Binary files differ
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java
index 55bd373..c617c25 100644
--- a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java
+++ b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java
@@ -105,7 +105,8 @@
}
private static void inspectOriginalMain(DexInspector inspector) {
- MethodSubject main = inspector.clazz("memberrebinding.Test").method(DexInspector.MAIN);
+ MethodSubject main = inspector.clazz("memberrebinding.Memberrebinding")
+ .method(DexInspector.MAIN);
Iterator<InvokeInstructionSubject> iterator =
main.iterateInstructions(MemberRebindingTest::coolInvokes);
assertTrue(iterator.next().holder().is("memberrebinding.ClassAtBottomOfChain"));
@@ -132,7 +133,8 @@
}
private static void inspectMain(DexInspector inspector) {
- MethodSubject main = inspector.clazz("memberrebinding.Test").method(DexInspector.MAIN);
+ MethodSubject main = inspector.clazz("memberrebinding.Memberrebinding")
+ .method(DexInspector.MAIN);
Iterator<InvokeInstructionSubject> iterator =
main.iterateInstructions(MemberRebindingTest::coolInvokes);
assertTrue(iterator.next().holder().is("memberrebinding.ClassAtBottomOfChain"));
@@ -162,7 +164,8 @@
}
private static void inspectOriginalMain2(DexInspector inspector) {
- MethodSubject main = inspector.clazz("memberrebinding2.Test").method(DexInspector.MAIN);
+ MethodSubject main = inspector.clazz("memberrebinding2.Memberrebinding")
+ .method(DexInspector.MAIN);
Iterator<FieldAccessInstructionSubject> iterator =
main.iterateInstructions(InstructionSubject::isFieldAccess);
// Run through instance put, static put, instance get and instance get.
@@ -177,7 +180,8 @@
}
private static void inspectMain2(DexInspector inspector) {
- MethodSubject main = inspector.clazz("memberrebinding2.Test").method(DexInspector.MAIN);
+ MethodSubject main = inspector.clazz("memberrebinding2.Memberrebinding")
+ .method(DexInspector.MAIN);
Iterator<FieldAccessInstructionSubject> iterator =
main.iterateInstructions(InstructionSubject::isFieldAccess);
// Run through instance put, static put, instance get and instance get.
@@ -185,7 +189,7 @@
assertTrue(iterator.next().holder().is("memberrebinding2.ClassAtBottomOfChain"));
assertTrue(iterator.next().holder().is("memberrebinding2.ClassInMiddleOfChain"));
assertTrue(iterator.next().holder().is("memberrebinding2.SuperClassOfAll"));
- assertTrue(iterator.next().holder().is("memberrebinding2.subpackage.PackagePrivateClass"));
+ assertTrue(iterator.next().holder().is("memberrebinding2.subpackage.PublicClass"));
}
assertTrue(iterator.next().holder().is("java.lang.System"));
assertFalse(iterator.hasNext());
@@ -195,7 +199,7 @@
new MethodSignature("test", "void", new String[]{});
private static void inspectOriginal3(DexInspector inspector) {
- MethodSubject main = inspector.clazz("memberrebinding3.Test").method(TEST);
+ MethodSubject main = inspector.clazz("memberrebinding3.Memberrebinding").method(TEST);
Iterator<InvokeInstructionSubject> iterator =
main.iterateInstructions(InstructionSubject::isInvoke);
assertTrue(iterator.next().holder().is("memberrebinding3.ClassAtBottomOfChain"));
@@ -205,7 +209,7 @@
}
private static void inspect3(DexInspector inspector) {
- MethodSubject main = inspector.clazz("memberrebinding3.Test").method(TEST);
+ MethodSubject main = inspector.clazz("memberrebinding3.Memberrebinding").method(TEST);
Iterator<InvokeInstructionSubject> iterator =
main.iterateInstructions(InstructionSubject::isInvoke);
assertTrue(iterator.next().holder().is("memberrebinding3.ClassAtBottomOfChain"));
@@ -215,19 +219,19 @@
}
private static void inspectOriginal4(DexInspector inspector) {
- MethodSubject main = inspector.clazz("memberrebinding4.Test").method(TEST);
+ MethodSubject main = inspector.clazz("memberrebinding4.Memberrebinding").method(TEST);
Iterator<InvokeInstructionSubject> iterator =
main.iterateInstructions(InstructionSubject::isInvoke);
- assertTrue(iterator.next().holder().is("memberrebinding4.Test$Inner"));
+ assertTrue(iterator.next().holder().is("memberrebinding4.Memberrebinding$Inner"));
assertTrue(iterator.next().holder().is("memberrebinding4.subpackage.PublicInterface"));
assertFalse(iterator.hasNext());
}
private static void inspect4(DexInspector inspector) {
- MethodSubject main = inspector.clazz("memberrebinding4.Test").method(TEST);
+ MethodSubject main = inspector.clazz("memberrebinding4.Memberrebinding").method(TEST);
Iterator<InvokeInstructionSubject> iterator =
main.iterateInstructions(InstructionSubject::isInvoke);
- assertTrue(iterator.next().holder().is("memberrebinding4.Test$Inner"));
+ assertTrue(iterator.next().holder().is("memberrebinding4.Memberrebinding$Inner"));
assertTrue(iterator.next().holder().is("memberrebinding4.subpackage.PublicInterface"));
assertFalse(iterator.hasNext());
}
diff --git a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
index 5d5bc17..05b06af 100644
--- a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
@@ -11,6 +11,7 @@
import static org.junit.Assert.fail;
import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.graph.DexAccessFlags;
import com.android.tools.r8.graph.DexItemFactory;
import java.io.IOException;
@@ -45,6 +46,8 @@
INVALID_PROGUARD_DIR + "including-2.flags";
private static final String LIBRARY_JARS =
VALID_PROGUARD_DIR + "library-jars.flags";
+ private static final String LIBRARY_JARS_WIN =
+ VALID_PROGUARD_DIR + "library-jars-win.flags";
private static final String SEEDS =
VALID_PROGUARD_DIR + "seeds.flags";
private static final String SEEDS_2 =
@@ -59,6 +62,8 @@
VALID_PROGUARD_DIR + "dontskipnonpubliclibraryclasses.flags";
private static final String DONT_SKIP_NON_PUBLIC_LIBRARY_CLASS_MEMBERS =
VALID_PROGUARD_DIR + "dontskipnonpubliclibraryclassmembers.flags";
+ private static final String OVERLOAD_AGGRESIVELY =
+ VALID_PROGUARD_DIR + "overloadaggressively.flags";
private static final String DONT_OPTIMIZE =
VALID_PROGUARD_DIR + "dontoptimize.flags";
private static final String SKIP_NON_PUBLIC_LIBRARY_CLASSES =
@@ -281,7 +286,11 @@
@Test
public void parseLibraryJars() throws IOException, ProguardRuleParserException {
ProguardConfigurationParser parser = new ProguardConfigurationParser(new DexItemFactory());
- parser.parse(Paths.get(LIBRARY_JARS));
+ if (!ToolHelper.isLinux() && !ToolHelper.isMac()) {
+ parser.parse(Paths.get(LIBRARY_JARS_WIN));
+ } else {
+ parser.parse(Paths.get(LIBRARY_JARS));
+ }
assertEquals(4, parser.getConfig().getLibraryjars().size());
}
@@ -332,6 +341,13 @@
}
@Test
+ public void parseOverloadAggressively()
+ throws IOException, ProguardRuleParserException {
+ ProguardConfigurationParser parser = new ProguardConfigurationParser(new DexItemFactory());
+ parser.parse(Paths.get(OVERLOAD_AGGRESIVELY));
+ }
+
+ @Test
public void parseDontOptimize()
throws IOException, ProguardRuleParserException {
ProguardConfigurationParser parser = new ProguardConfigurationParser(new DexItemFactory());
@@ -372,4 +388,15 @@
);
parser.parse(proguardConfig);
}
+
+ @Test
+ public void parseCustomFlags() throws Exception {
+ ProguardConfigurationParser parser = new ProguardConfigurationParser(new DexItemFactory());
+ // Custom Proguard flags -runtype and -laststageoutput are ignored.
+ Path proguardConfig = writeTextToTempFile(
+ "-runtype FINAL ",
+ "-laststageoutput /some/file/name "
+ );
+ parser.parse(proguardConfig);
+ }
}
diff --git a/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java b/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
index f5b2b1a..8f46572 100644
--- a/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
@@ -11,13 +11,16 @@
import com.android.tools.r8.R8Command;
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.errors.CompilationError;
+import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintStream;
+import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
@@ -95,7 +98,11 @@
actualMapping = new String(Files.readAllBytes(outputmapping), StandardCharsets.UTF_8);
String refMapping = new String(Files.readAllBytes(
Paths.get(EXAMPLES_DIR, "shaking1", "print-mapping.ref")), StandardCharsets.UTF_8);
- Assert.assertEquals(refMapping, actualMapping);
+ Assert.assertEquals(sorted(refMapping), sorted(actualMapping));
}
+ private static String sorted(String str) {
+ return new BufferedReader(new StringReader(str))
+ .lines().sorted().collect(Collectors.joining("\n"));
+ }
}
diff --git a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
index 4530e2e..0904569 100644
--- a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
@@ -13,6 +13,7 @@
import com.android.tools.r8.graph.DexAccessFlags;
import com.android.tools.r8.naming.MemberNaming.FieldSignature;
import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.DexInspector;
import com.android.tools.r8.utils.DexInspector.ClassSubject;
@@ -126,7 +127,11 @@
.addLibraryFiles(ListUtils.map(libs, Paths::get))
.setMinification(minify)
.build();
- ToolHelper.runR8(command, options -> options.inlineAccessors = inline);
+ ToolHelper.runR8(command, options -> {
+ options.inlineAccessors = inline;
+ options.printMapping = true;
+ options.printMappingFile = out.resolve(AndroidApp.DEFAULT_PROGUARD_MAP_FILE);
+ });
}
public static void shaking1HasNoClassUnused(DexInspector inspector) {
@@ -542,7 +547,9 @@
"assumevalues3",
"assumevalues4",
"assumevalues5",
- "annotationremoval");
+ "annotationremoval",
+ "memberrebinding2",
+ "memberrebinding3");
// Keys can be the name of the test or the name of the test followed by a colon and the name
// of the keep file.
diff --git a/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java b/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java
index 5e67ad1..ca30f76 100644
--- a/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java
+++ b/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java
@@ -93,7 +93,7 @@
assertTrue(
defBlock.getInstructions().get(defBlock.getInstructions().size() - 2).isInvoke());
for (BasicBlock returnPredecessor : block.getPredecessors()) {
- if (defBlock.isCatchSuccessor(returnPredecessor)) {
+ if (defBlock.hasCatchSuccessor(returnPredecessor)) {
hasExceptionalPredecessor = true;
} else if (defBlock == returnPredecessor) {
// Normal flow goes to return.
diff --git a/src/test/java/com/android/tools/r8/smali/SmaliDisassembleTest.java b/src/test/java/com/android/tools/r8/smali/SmaliDisassembleTest.java
index b1fd38f..66d0304 100644
--- a/src/test/java/com/android/tools/r8/smali/SmaliDisassembleTest.java
+++ b/src/test/java/com/android/tools/r8/smali/SmaliDisassembleTest.java
@@ -68,7 +68,9 @@
" mul-int/2addr v2, v1\n" +
" div-int/2addr v3, v2\n" +
" return v3\n" +
- ".end method\n";
+ ".end method\n" +
+ "\n" +
+ "# End of class LTest;\n";
assertEquals(expected, application.smali(new InternalOptions()));
@@ -122,7 +124,9 @@
" 0x00000001 -> :label_5 # 1\n" +
" 0x00000002 -> :label_7 # 2\n" +
" .end sparse-switch\n" +
- ".end method\n";
+ ".end method\n" +
+ "\n" +
+ "# End of class LTest;\n";
assertEquals(expected, application.smali(new InternalOptions()));
@@ -176,7 +180,9 @@
" :label_5\n" +
" :label_7\n" +
" .end packed-switch\n" +
- ".end method\n";
+ ".end method\n" +
+ "\n" +
+ "# End of class LTest;\n";
assertEquals(expected, application.smali(new InternalOptions()));
@@ -217,7 +223,9 @@
" 0x02 # 2\n" +
" 0xff # 255\n" +
" .end array-data\n" +
- ".end method\n";
+ ".end method\n" +
+ "\n" +
+ "# End of class LTest;\n";
assertEquals(expected, application.smali(new InternalOptions()));
@@ -258,7 +266,9 @@
" 0x0002 # 2\n" +
" 0xffff # 65535\n" +
" .end array-data\n" +
- ".end method\n";
+ ".end method\n" +
+ "\n" +
+ "# End of class LTest;\n";
assertEquals(expected, application.smali(new InternalOptions()));
@@ -299,7 +309,9 @@
" 0x00000002 # 2\n" +
" 0xffffffff # 4294967295\n" +
" .end array-data\n" +
- ".end method\n";
+ ".end method\n" +
+ "\n" +
+ "# End of class LTest;\n";
assertEquals(expected, application.smali(new InternalOptions()));
@@ -340,14 +352,15 @@
" 0x0000000000000002 # 2\n" +
" 0xffffffffffffffff # -1\n" +
" .end array-data\n" +
- ".end method\n";
+ ".end method\n" +
+ "\n" +
+ "# End of class LTest;\n";
assertEquals(expected, application.smali(new InternalOptions()));
roundTripRawSmali(expected);
}
-
@Test
public void interfaceClass() {
SmaliBuilder builder = new SmaliBuilder();
@@ -362,7 +375,33 @@
".super Ljava/lang/Object;\n" +
"\n" +
".method public abstract test()I\n" +
- ".end method\n";
+ ".end method\n" +
+ "\n" +
+ "# End of class LTest;\n";
+
+ assertEquals(expected, application.smali(new InternalOptions()));
+
+ roundTripRawSmali(expected);
+ }
+
+ @Test
+ public void implementsInterface() {
+ SmaliBuilder builder = new SmaliBuilder();
+ builder.addClass("Test", "java.lang.Object", ImmutableList.of("java.util.List"));
+ builder.addAbstractMethod("int", "test", ImmutableList.of());
+ DexApplication application = buildApplication(builder);
+ assertEquals(1, Iterables.size(application.classes()));
+
+ String expected =
+ ".class public LTest;\n" +
+ "\n" +
+ ".super Ljava/lang/Object;\n" +
+ ".implements Ljava/util/List;\n" +
+ "\n" +
+ ".method public abstract test()I\n" +
+ ".end method\n" +
+ "\n" +
+ "# End of class LTest;\n";
assertEquals(expected, application.smali(new InternalOptions()));
diff --git a/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java b/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
index 3665a86..6b377f4 100644
--- a/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
+++ b/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
@@ -31,6 +31,7 @@
import com.android.tools.r8.utils.OutputMode;
import com.android.tools.r8.utils.Smali;
import com.android.tools.r8.utils.Timing;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import java.io.IOException;
import java.nio.file.Path;
@@ -77,11 +78,27 @@
abstract class Builder {
String name;
String superName;
+ List<String> implementedInterfaces;
List<String> source = new ArrayList<>();
- Builder(String name, String superName) {
+ Builder(String name, String superName, List<String> implementedInterfaces) {
this.name = name;
this.superName = superName;
+ this.implementedInterfaces = implementedInterfaces;
+ }
+
+ protected void appendSuper(StringBuilder builder) {
+ builder.append(".super ");
+ builder.append(DescriptorUtils.javaTypeToDescriptor(superName));
+ builder.append("\n");
+ }
+
+ protected void appendImplementedInterfaces(StringBuilder builder) {
+ for (String implementedInterface : implementedInterfaces) {
+ builder.append(".implements ");
+ builder.append(DescriptorUtils.javaTypeToDescriptor(implementedInterface));
+ builder.append("\n");
+ }
}
protected void writeSource(StringBuilder builder) {
@@ -94,8 +111,8 @@
public class ClassBuilder extends Builder {
- ClassBuilder(String name, String superName) {
- super(name, superName);
+ ClassBuilder(String name, String superName, List<String> implementedInterfaces) {
+ super(name, superName, implementedInterfaces);
}
public String toString() {
@@ -103,9 +120,9 @@
builder.append(".class public ");
builder.append(DescriptorUtils.javaTypeToDescriptor(name));
builder.append("\n");
- builder.append(".super ");
- builder.append(DescriptorUtils.javaTypeToDescriptor(superName));
- builder.append("\n\n");
+ appendSuper(builder);
+ appendImplementedInterfaces(builder);
+ builder.append("\n");
writeSource(builder);
return builder.toString();
}
@@ -114,7 +131,7 @@
public class InterfaceBuilder extends Builder {
InterfaceBuilder(String name, String superName) {
- super(name, superName);
+ super(name, superName, ImmutableList.of());
}
public String toString() {
@@ -122,9 +139,9 @@
builder.append(".class public interface abstract ");
builder.append(DescriptorUtils.javaTypeToDescriptor(name));
builder.append("\n");
- builder.append(".super ");
- builder.append(DescriptorUtils.javaTypeToDescriptor(superName));
- builder.append("\n\n");
+ appendSuper(builder);
+ appendImplementedInterfaces(builder);
+ builder.append("\n");
writeSource(builder);
return builder.toString();
}
@@ -162,9 +179,13 @@
}
public void addClass(String name, String superName) {
+ addClass(name, superName, ImmutableList.of());
+ }
+
+ public void addClass(String name, String superName, List<String> implementedInterfaces) {
assert !classes.containsKey(name);
currentClassName = name;
- classes.put(name, new ClassBuilder(name, superName));
+ classes.put(name, new ClassBuilder(name, superName, implementedInterfaces));
}
public void addInterface(String name) {
diff --git a/src/test/java/com/android/tools/r8/utils/ArtCommandBuilderTest.java b/src/test/java/com/android/tools/r8/utils/ArtCommandBuilderTest.java
index 0344a27..fa3497b 100644
--- a/src/test/java/com/android/tools/r8/utils/ArtCommandBuilderTest.java
+++ b/src/test/java/com/android/tools/r8/utils/ArtCommandBuilderTest.java
@@ -9,10 +9,17 @@
import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
import com.android.tools.r8.ToolHelper.DexVm;
import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Before;
import org.junit.Test;
public class ArtCommandBuilderTest {
+ @Before
+ public void setUp() {
+ Assume.assumeTrue(ToolHelper.artSupported());
+ }
+
@Test
public void noArguments() {
ArtCommandBuilder builder = new ArtCommandBuilder();
diff --git a/src/test/proguard/valid/library-jars-win.flags b/src/test/proguard/valid/library-jars-win.flags
new file mode 100644
index 0000000..c6ffbb4
--- /dev/null
+++ b/src/test/proguard/valid/library-jars-win.flags
@@ -0,0 +1,2 @@
+-libraryjars some/library.jar
+-libraryjars some/library.jar;some/other/library.jar;yet/another/library.jar
\ No newline at end of file
diff --git a/src/test/proguard/valid/overloadaggressively.flags b/src/test/proguard/valid/overloadaggressively.flags
new file mode 100644
index 0000000..312ebff
--- /dev/null
+++ b/src/test/proguard/valid/overloadaggressively.flags
@@ -0,0 +1,5 @@
+# Copyright (c) 2017, 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.
+
+-overloadaggressively
diff --git a/third_party/android_cts_baseline.tar.gz.sha1 b/third_party/android_cts_baseline.tar.gz.sha1
index 7126671..5efd8e5 100644
--- a/third_party/android_cts_baseline.tar.gz.sha1
+++ b/third_party/android_cts_baseline.tar.gz.sha1
@@ -1 +1 @@
-3914f251fc8b8669b2ec92766696267c1a9ac7ff
\ No newline at end of file
+499d86b9b9eba837261c2d10e5b76f3a3a9a9952
\ No newline at end of file
diff --git a/third_party/android_jar/lib-v21.tar.gz.sha1 b/third_party/android_jar/lib-v21.tar.gz.sha1
new file mode 100644
index 0000000..8d42abc
--- /dev/null
+++ b/third_party/android_jar/lib-v21.tar.gz.sha1
@@ -0,0 +1 @@
+83d20da35ba7b7b1a8b48a2da8dfe3b95d59392a
\ No newline at end of file
diff --git a/third_party/gmail/gmail_android_170604.16.tar.gz.sha1 b/third_party/gmail/gmail_android_170604.16.tar.gz.sha1
new file mode 100644
index 0000000..f57ba90
--- /dev/null
+++ b/third_party/gmail/gmail_android_170604.16.tar.gz.sha1
@@ -0,0 +1 @@
+161c569821a5c9b4cb8e99de764f3449191af084
\ No newline at end of file
diff --git a/third_party/gmscore/latest.tar.gz.sha1 b/third_party/gmscore/latest.tar.gz.sha1
new file mode 100644
index 0000000..0a0d4d4
--- /dev/null
+++ b/third_party/gmscore/latest.tar.gz.sha1
@@ -0,0 +1 @@
+d38c56b1c4583393fda2bff933a8d09443e5c792
\ No newline at end of file
diff --git a/third_party/jctf.tar.gz.sha1 b/third_party/jctf.tar.gz.sha1
index 73cc756..35ec1ba 100644
--- a/third_party/jctf.tar.gz.sha1
+++ b/third_party/jctf.tar.gz.sha1
@@ -1 +1 @@
-d9256a5987d50372f916d367cb8e8e5286a10caa
+93c13a164ca54db72273642d27308e325526fd38
\ No newline at end of file
diff --git a/third_party/proguard/proguard_internal_159423826.tar.gz.sha1 b/third_party/proguard/proguard_internal_159423826.tar.gz.sha1
new file mode 100644
index 0000000..8fdbf19
--- /dev/null
+++ b/third_party/proguard/proguard_internal_159423826.tar.gz.sha1
@@ -0,0 +1 @@
+933e970c611a306ef5d755234820188bc50c4c60
\ No newline at end of file
diff --git a/third_party/youtube/youtube.android_11.47.tar.gz.sha1 b/third_party/youtube/youtube.android_11.47.tar.gz.sha1
deleted file mode 100644
index db94984..0000000
--- a/third_party/youtube/youtube.android_11.47.tar.gz.sha1
+++ /dev/null
@@ -1 +0,0 @@
-3c763fc5b033497dabdfff2ecd27475cfa7759a4
\ No newline at end of file
diff --git a/third_party/youtube/youtube.android_12.22.tar.gz.sha1 b/third_party/youtube/youtube.android_12.22.tar.gz.sha1
new file mode 100644
index 0000000..8f6813c
--- /dev/null
+++ b/third_party/youtube/youtube.android_12.22.tar.gz.sha1
@@ -0,0 +1 @@
+73c4880898d734064815d0426d8fe84ee6d075b4
\ No newline at end of file
diff --git a/tools/compare_cts_results.py b/tools/compare_cts_results.py
index 95e29d1..00901ec 100755
--- a/tools/compare_cts_results.py
+++ b/tools/compare_cts_results.py
@@ -9,9 +9,10 @@
from os.path import basename
import argparse
import os
-import re
import sys
+import utils
+
class Module:
def __init__(self):
self.test_cases = {}
@@ -90,35 +91,19 @@
# Read CTS test_result.xml from file and merge into result_tree
def add_to_result_tree(result_tree, file_xml, file_idx):
- re_module = re.compile('<Module name="([^"]*)"')
- re_test_case = re.compile('<TestCase name="([^"]*)"')
- re_test = re.compile('<Test result="(pass|fail)" name="([^"]*)"')
module = None
test_case = None
- with open(file_xml) as f:
- for line in f:
- m = re_module.search(line)
- if m:
- module_name = m.groups()[0]
- module = result_tree.setdefault(module_name, Module())
- module.set_file_index_present(file_idx)
- continue
-
- m = re_test_case.search(line)
- if m:
- test_case_name = m.groups()[0]
- test_case = module.get_test_case_maybe_create(test_case_name)
- test_case.set_file_index_present(file_idx)
- continue
-
- m = re_test.search(line)
- if m:
- outcome = m.groups()[0]
- test_name = m.groups()[1]
- assert outcome in ["fail", "pass"]
-
- v = test_case.get_test_maybe_create(test_name)
- v.set_file_index_outcome(outcome == 'pass', file_idx)
+ for x in utils.read_cts_test_result(file_xml):
+ if type(x) is utils.CtsModule:
+ module = result_tree.setdefault(x.name, Module())
+ module.set_file_index_present(file_idx)
+ elif type(x) is utils.CtsTestCase:
+ test_case = module.get_test_case_maybe_create(x.name)
+ test_case.set_file_index_present(file_idx)
+ else:
+ assert(type(x) is utils.CtsTest)
+ v = test_case.get_test_maybe_create(x.name)
+ v.set_file_index_outcome(x.outcome, file_idx)
# main tree_report function
def tree_report(result_tree, files, diff_only):
diff --git a/tools/gmail_data.py b/tools/gmail_data.py
new file mode 100644
index 0000000..d09a3e8
--- /dev/null
+++ b/tools/gmail_data.py
@@ -0,0 +1,35 @@
+# Copyright (c) 2017, 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.
+
+import glob
+import os
+import utils
+
+THIRD_PARTY = os.path.join(utils.REPO_ROOT, 'third_party')
+BASE = os.path.join(THIRD_PARTY, 'gmail')
+
+V170604_16_BASE = os.path.join(BASE, 'gmail_android_170604.16')
+V170604_16_PREFIX = os.path.join(V170604_16_BASE, 'Gmail_release_unstripped')
+
+# NOTE: We always use android.jar for SDK v25 for now.
+ANDROID_JAR = os.path.join(THIRD_PARTY, 'android_jar', 'lib-v25', 'android.jar')
+
+VERSIONS = {
+ '170604.16': {
+ 'dex' : {
+ 'inputs': [os.path.join(V170604_16_BASE, 'Gmail_release_unsigned.apk')],
+ 'pgmap': '%s_proguard.map' % V170604_16_PREFIX,
+ 'libraries' : [ANDROID_JAR],
+ 'r8-flags': '--ignore-missing-classes',
+ },
+ 'deploy' : {
+ 'inputs': ['%s_deploy.jar' % V170604_16_PREFIX],
+ 'pgconf': ['%s_proguard.config' % V170604_16_PREFIX],
+ },
+ 'proguarded' : {
+ 'inputs': ['%s_proguard.jar' % V170604_16_PREFIX],
+ 'pgmap': '%s_proguard.map' % V170604_16_PREFIX,
+ }
+ },
+}
diff --git a/tools/gradle.py b/tools/gradle.py
index 1945557..f30b3f4 100755
--- a/tools/gradle.py
+++ b/tools/gradle.py
@@ -14,7 +14,10 @@
GRADLE_DIR = os.path.join(utils.REPO_ROOT, 'third_party', 'gradle')
GRADLE_SHA1 = os.path.join(GRADLE_DIR, 'gradle.tar.gz.sha1')
-GRADLE = os.path.join(GRADLE_DIR, 'gradle', 'bin', 'gradle')
+if utils.IsWindows():
+ GRADLE = os.path.join(GRADLE_DIR, 'gradle', 'bin', 'gradle.bat')
+else:
+ GRADLE = os.path.join(GRADLE_DIR, 'gradle', 'bin', 'gradle')
def PrintCmd(s):
if type(s) is list:
diff --git a/tools/proguard.py b/tools/proguard.py
new file mode 100755
index 0000000..58c049f
--- /dev/null
+++ b/tools/proguard.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python
+# Copyright (c) 2017, 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.
+
+# Run ProGuard, Google's internal version
+
+from __future__ import print_function
+import os
+import subprocess
+import sys
+
+import utils
+
+PROGUARD_JAR = os.path.join(utils.REPO_ROOT, 'third_party', 'proguard',
+ 'proguard_internal_159423826', 'ProGuard_deploy.jar')
+
+def run(args):
+ cmd = ['java', '-jar', PROGUARD_JAR]
+ cmd.extend(args)
+ print('-- ' + ' '.join(cmd))
+ subprocess.check_call(cmd)
+
+def Main():
+ run(sys.argv[1:])
+
+if __name__ == '__main__':
+ sys.exit(Main())
diff --git a/tools/run-r8-on-gmscore.py b/tools/run-r8-on-gmscore.py
index c00a58d..6d09760 100755
--- a/tools/run-r8-on-gmscore.py
+++ b/tools/run-r8-on-gmscore.py
@@ -5,7 +5,8 @@
import sys
-import run_r8_on_app
+import run_on_app
if __name__ == '__main__':
- sys.exit(run_r8_on_app.main())
+ # Default compiler is R8.
+ sys.exit(run_on_app.main())
diff --git a/tools/run_on_app.py b/tools/run_on_app.py
new file mode 100755
index 0000000..ddc1e47
--- /dev/null
+++ b/tools/run_on_app.py
@@ -0,0 +1,190 @@
+#!/usr/bin/env python
+# Copyright (c) 2017, 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.
+
+import optparse
+import os
+import r8
+import d8
+import sys
+import utils
+import time
+
+import gmscore_data
+import youtube_data
+import gmail_data
+
+TYPES = ['dex', 'deploy', 'proguarded']
+APPS = ['gmscore', 'youtube', 'gmail']
+
+def ParseOptions():
+ result = optparse.OptionParser()
+ result.add_option('--compiler',
+ help='',
+ default='r8',
+ choices=['d8', 'r8'])
+ result.add_option('--app',
+ help='',
+ default='gmscore',
+ choices=APPS)
+ result.add_option('--type',
+ help='Default for R8: deploy, for D8: proguarded',
+ choices=TYPES)
+ result.add_option('--out',
+ help='',
+ default=os.getcwd())
+ result.add_option('--no-build',
+ help='',
+ default=False,
+ action='store_true')
+ result.add_option('--no-libraries',
+ help='',
+ default=False,
+ action='store_true')
+ result.add_option('--no-debug',
+ help='Run without debug asserts.',
+ default=False,
+ action='store_true')
+ result.add_option('--version',
+ help='')
+ result.add_option('-k',
+ help='Override the default ProGuard keep rules')
+ result.add_option('--compiler-flags',
+ help='Additional option(s) for the compiler. ' +
+ 'If passing several options use a quoted string.')
+ result.add_option('--r8-flags',
+ help='Additional option(s) for the compiler. ' +
+ 'Same as --compiler-flags, keeping it for backward compatibility. ' +
+ 'If passing several options use a quoted string.')
+ result.add_option('--track-memory-to-file',
+ help='Track how much memory the jvm is using while ' +
+ ' compiling. Output to the specified file.')
+ result.add_option('--profile',
+ help='Profile R8 run.',
+ default=False,
+ action='store_true')
+ result.add_option('--dump-args-file',
+ help='Dump a file with the arguments for the specified ' +
+ 'configuration. For use as a @<file> argument to perform ' +
+ 'the run.')
+ result.add_option('--print-runtimeraw',
+ metavar='BENCHMARKNAME',
+ help='Prints the line \'<BENCHMARKNAME>(RunTimeRaw):' +
+ ' <elapsed> ms\' at the end where <elapsed> is' +
+ ' the elapsed time in milliseconds.')
+ return result.parse_args()
+
+# Most apps have the -printmapping and -printseeds in the Proguard
+# configuration. However we don't want to write these files in these
+# locations. Instead generate an auxiliary Proguard configuration
+# placing these two output files together with the dex output.
+def GenerateAdditionalProguardConfiguration(temp, outdir):
+ name = "output.config"
+ with open(os.path.join(temp, name), 'w') as file:
+ file.write('-printmapping ' + os.path.join(outdir, 'proguard.map') + "\n")
+ file.write('-printseeds ' + os.path.join(outdir, 'proguard.seeds') + "\n")
+ return os.path.abspath(file.name)
+
+def main():
+ app_provided_pg_conf = False;
+ (options, args) = ParseOptions()
+ outdir = options.out
+ data = None
+ if options.app == 'gmscore':
+ options.version = options.version or 'v9'
+ data = gmscore_data
+ elif options.app == 'youtube':
+ options.version = options.version or '12.22'
+ data = youtube_data
+ elif options.app == 'gmail':
+ options.version = options.version or '170604.16'
+ data = gmail_data
+ else:
+ raise 'Unexpected'
+
+ if not options.version in data.VERSIONS.keys():
+ print 'No version %s for application %s' % (options.version, options.app)
+ print 'Valid versions are %s' % data.VERSIONS.keys()
+ return 1
+
+ version = data.VERSIONS[options.version]
+
+ if not options.type:
+ options.type = 'deploy' if options.compiler == 'r8' \
+ else 'proguarded'
+
+ if options.type not in version:
+ print 'No type %s for version %s' % (options.type, options.version)
+ print 'Valid types are %s' % version.keys()
+ return 1
+ values = version[options.type]
+ inputs = None
+ # For R8 'deploy' the JAR is located using the Proguard configuration -injars option.
+ if 'inputs' in values and (options.compiler != 'r8' or options.type != 'deploy'):
+ inputs = values['inputs']
+
+ args.extend(['--output', outdir])
+
+ if options.compiler == 'r8':
+ if 'pgmap' in values:
+ args.extend(['--pg-map', values['pgmap']])
+ if 'pgconf' in values and not options.k:
+ for pgconf in values['pgconf']:
+ args.extend(['--pg-conf', pgconf])
+ app_provided_pg_conf = True
+ if options.k:
+ args.extend(['--pg-conf', options.k])
+ if 'multidexrules' in values:
+ for rules in values['multidexrules']:
+ args.extend(['--multidex-rules', rules])
+
+ if not options.no_libraries and 'libraries' in values:
+ for lib in values['libraries']:
+ args.extend(['--lib', lib])
+
+ if not outdir.endswith('.zip') and not outdir.endswith('.jar') and not os.path.exists(outdir):
+ os.makedirs(outdir)
+
+ if options.compiler == 'r8':
+ if 'r8-flags' in values:
+ args.extend(values['r8-flags'].split(' '))
+
+ if options.compiler_flags:
+ args.extend(options.compiler_flags.split(' '))
+ if options.r8_flags:
+ args.extend(options.r8_flags.split(' '))
+
+ if inputs:
+ args.extend(inputs)
+
+ t0 = time.time()
+
+ if options.dump_args_file:
+ with open(options.dump_args_file, 'w') as args_file:
+ args_file.writelines([arg + os.linesep for arg in args])
+ else:
+ if options.compiler == 'd8':
+ d8.run(args, not options.no_build, not options.no_debug, options.profile,
+ options.track_memory_to_file)
+ else:
+ with utils.TempDir() as temp:
+ if app_provided_pg_conf:
+ # Ensure that output of -printmapping and -printseeds go to the output
+ # location and not where the app Proguard configuration places them.
+ if outdir.endswith('.zip') or outdir.endswith('.jar'):
+ pg_outdir = os.path.dirname(outdir)
+ else:
+ pg_outdir = outdir
+ additional_pg_conf = GenerateAdditionalProguardConfiguration(
+ temp, os.path.abspath(pg_outdir))
+ args.extend(['--pg-conf', additional_pg_conf])
+ r8.run(args, not options.no_build, not options.no_debug, options.profile,
+ options.track_memory_to_file)
+
+ if options.print_runtimeraw:
+ print('{}(RunTimeRaw): {} ms'
+ .format(options.print_runtimeraw, 1000.0 * (time.time() - t0)))
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/tools/run_proguard_dx_on_gmscore.py b/tools/run_proguard_dx_on_gmscore.py
new file mode 100755
index 0000000..a67868b
--- /dev/null
+++ b/tools/run_proguard_dx_on_gmscore.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python
+# Copyright (c) 2017, 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.
+
+# Run ProGuard and the DX tool on GmsCore V10.
+
+from __future__ import print_function
+from os import makedirs
+from os.path import exists, join
+from subprocess import check_call
+import argparse
+import gmscore_data
+import os
+import stat
+import sys
+import time
+
+import proguard
+import utils
+
+BLAZE_BUILD_DIR = join(gmscore_data.V10_BASE,
+ 'blaze-out/intel-linux-android-4.8-libcxx-x86-opt/bin/java/com/google/'
+ 'android/gmscore/integ')
+PROGUARDED_OUTPUT = join(BLAZE_BUILD_DIR,
+ 'GmsCore_prod_alldpi_release_all_locales_proguard.jar')
+GMSCORE_SEEDS_FILE = join(BLAZE_BUILD_DIR,
+ 'GmsCore_prod_alldpi_release_all_locales_proguard.seeds')
+DX_EXECUTABLE = join(utils.REPO_ROOT, 'tools/linux/dx/bin/dx')
+
+def parse_arguments():
+ parser = argparse.ArgumentParser(
+ description = 'Run ProGuard and the DX tool on GmsCore V10.')
+ parser.add_argument('--out',
+ help = 'Output directory for the DX tool.',
+ default = os.getcwd())
+ parser.add_argument('--print-runtimeraw',
+ metavar = 'BENCHMARKNAME',
+ help = 'Prints the line \'<BENCHMARKNAME>(RunTimeRaw): <elapsed>' +
+ ' ms\' at the end where <elapsed> is the elapsed time in' +
+ ' milliseconds.')
+ return parser.parse_args()
+
+def Main():
+ options = parse_arguments()
+
+ outdir = options.out
+
+ args = ['-forceprocessing']
+
+ if not outdir.endswith('.zip') and not outdir.endswith('.jar') \
+ and not exists(outdir):
+ makedirs(outdir)
+
+ version = gmscore_data.VERSIONS['v10']
+ values = version['deploy']
+ assert 'pgconf' in values
+
+ for pgconf in values['pgconf']:
+ args.extend(['@' + pgconf])
+
+ # Remove write-protection from seeds file. The seeds file is an output of
+ # ProGuard so it aborts if this is not writeable.
+ st = os.stat(GMSCORE_SEEDS_FILE)
+ os.chmod(GMSCORE_SEEDS_FILE,
+ st.st_mode | stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
+
+ t0 = time.time()
+
+ proguard.run(args)
+
+ # run dex on the result
+ check_call([DX_EXECUTABLE, '-JXmx5256M', '--multi-dex', '--output=' + outdir,
+ '--dex', PROGUARDED_OUTPUT])
+
+ if options.print_runtimeraw:
+ print('{}(RunTimeRaw): {} ms'
+ .format(options.print_runtimeraw, 1000.0 * (time.time() - t0)))
+
+if __name__ == '__main__':
+ sys.exit(Main())
diff --git a/tools/run_r8_on_app.py b/tools/run_r8_on_app.py
deleted file mode 100755
index 736d428..0000000
--- a/tools/run_r8_on_app.py
+++ /dev/null
@@ -1,123 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) 2017, 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.
-
-import optparse
-import os
-import r8
-import sys
-
-import gmscore_data
-import youtube_data
-
-TYPES = ['dex', 'deploy', 'proguarded']
-APPS = ['gmscore', 'youtube']
-
-def ParseOptions():
- result = optparse.OptionParser()
- result.add_option('--app',
- help='',
- default='gmscore',
- choices=APPS)
- result.add_option('--type',
- help='',
- default='deploy',
- choices=TYPES)
- result.add_option('--out',
- help='',
- default=os.getcwd())
- result.add_option('--no-build',
- help='',
- default=False,
- action='store_true')
- result.add_option('--no-libraries',
- help='',
- default=False,
- action='store_true')
- result.add_option('--no-debug',
- help='Run without debug asserts.',
- default=False,
- action='store_true')
- result.add_option('--version',
- help='')
- result.add_option('-k',
- help='Override the default ProGuard keep rules')
- result.add_option('--r8-flags',
- help='Additional option(s) for R8. ' +
- 'If passing several options use a quoted string.')
- result.add_option('--track-memory-to-file',
- help='Track how much memory the jvm is using while ' +
- ' compiling. Output to the specified file.')
- result.add_option('--profile',
- help='Profile R8 run.',
- default=False,
- action='store_true')
- result.add_option('--dump-args-file',
- help='Dump a file with the arguments for the specified ' +
- 'configuration. For use as a @<file> argument to perform ' +
- 'the run.')
- return result.parse_args()
-
-def main():
- (options, args) = ParseOptions()
- outdir = options.out
- data = None
- if options.app == 'gmscore':
- options.version = options.version or 'v9'
- data = gmscore_data
- elif options.app == 'youtube':
- options.version = options.version or '12.10'
- data = youtube_data
- else:
- raise 'Unexpected'
-
- if not options.version in data.VERSIONS.keys():
- print 'No version %s for application %s' % (options.version, options.app)
- print 'Valid versions are %s' % data.VERSIONS.keys()
- return 1
-
- version = data.VERSIONS[options.version]
-
- if options.type not in version:
- print 'No type %s for version %s' % (options.type, options.version)
- print 'Valid types are %s' % version.keys()
- return 1
- values = version[options.type]
- inputs = None
- # For 'deploy' the JAR is located using the Proguard configuration -injars option.
- if 'inputs' in values and options.type != 'deploy':
- inputs = values['inputs']
-
- args.extend(['--output', outdir])
- if 'pgmap' in values:
- args.extend(['--pg-map', values['pgmap']])
- if 'pgconf' in values and not options.k:
- for pgconf in values['pgconf']:
- args.extend(['--pg-conf', pgconf])
- if options.k:
- args.extend(['--pg-conf', options.k])
- if not options.no_libraries and 'libraries' in values:
- for lib in values['libraries']:
- args.extend(['--lib', lib])
-
- if not outdir.endswith('.zip') and not outdir.endswith('.jar') and not os.path.exists(outdir):
- os.makedirs(outdir)
-
- if 'r8-flags' in values:
- args.extend(values['r8-flags'].split(' '))
- if options.r8_flags:
- args.extend(options.r8_flags.split(' '))
-
- if inputs:
- args.extend(inputs)
-
- if options.dump_args_file:
- with open(options.dump_args_file, 'w') as args_file:
- args_file.writelines([arg + os.linesep for arg in args])
- else:
- r8.run(args, not options.no_build, not options.no_debug, options.profile,
- options.track_memory_to_file)
-
-if __name__ == '__main__':
- sys.exit(main())
diff --git a/tools/test.bat b/tools/test.bat
new file mode 100644
index 0000000..a343096
--- /dev/null
+++ b/tools/test.bat
@@ -0,0 +1,9 @@
+@echo off
+:: Copyright (c) 2017, 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.
+setlocal
+
+PATH=%~dp0;%PATH%
+
+python "%~dp0\test.py" %*
\ No newline at end of file
diff --git a/tools/test.py b/tools/test.py
index 2f5baf8..49a6f3c 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -7,6 +7,7 @@
# if an argument is given, run only tests with that pattern. This script will
# force the tests to run, even if no input changed.
+import os
import gradle
import optparse
import sys
@@ -47,6 +48,9 @@
result.add_option('--jctf_compile_only',
help="Don't run, only compile JCTF tests.",
default=False, action='store_true')
+ result.add_option('--disable_assertions',
+ help="Disable assertions when running tests.",
+ default=False, action='store_true')
return result.parse_args()
@@ -74,15 +78,23 @@
gradle_args.append('-Ponly_jctf')
if options.jctf_compile_only:
gradle_args.append('-Pjctf_compile_only')
+ if options.disable_assertions:
+ gradle_args.append('-Pdisable_assertions')
if len(args) > 0:
gradle_args.append('--tests')
gradle_args.append(args[0])
-
+ if os.name == 'nt':
+ # temporary hack
+ gradle_args.append('-Pno_internal')
+ gradle_args.append('-x')
+ gradle_args.append('createJctfTests')
+ gradle_args.append('-x')
+ gradle_args.append('jctfCommonJar')
+ gradle_args.append('-x')
+ gradle_args.append('jctfTestsClasses')
vms_to_test = [options.dex_vm] if options.dex_vm != "all" else ALL_ART_VMS
for art_vm in vms_to_test:
gradle.RunGradle(gradle_args + ['-Pdex_vm=%s' % art_vm])
if __name__ == '__main__':
sys.exit(Main())
-
-
diff --git a/tools/test_android_cts.py b/tools/test_android_cts.py
index 384ce70..606611d 100755
--- a/tools/test_android_cts.py
+++ b/tools/test_android_cts.py
@@ -14,11 +14,9 @@
# cd build/aosp
# repo manifest -o ../../third_party/aosp_manifest.xml -r
#
-# The baseline is the `test_result.xml` file which is created with an AOSP
-# build which uses the default (JACK) toolset.
-#
-# Use this script, with '--tool=jack' to reproduce the baseline results
-#
+# The baseline is a set of `test_result.xml` files in
+# third_party/android_cts_baseline/jack. The current test considered a success
+# if all tests pass that consistently pass in the baseline.
from __future__ import print_function
from glob import glob
@@ -30,12 +28,13 @@
import os
import re
import sys
+import time
import gradle
import utils
-CTS_BASELINE = join(utils.REPO_ROOT,
- 'third_party/android_cts_baseline/test_result.xml')
+CTS_BASELINE_FILES_DIR = join(utils.REPO_ROOT,
+ 'third_party/android_cts_baseline/jack')
AOSP_MANIFEST_XML = join(utils.REPO_ROOT, 'third_party',
'aosp_manifest.xml')
AOSP_HELPER_SH = join(utils.REPO_ROOT, 'scripts', 'aosp_helper.sh')
@@ -96,97 +95,33 @@
return False
return True
-# Read the xml test result file into an in-memory tree:
-# Extract only the Module/TestCase/Test names and outcome (True|False for
-# PASS|FAIL):
-#
-# tree[module_name][testcase_name][test_name] = True|False
-#
-def read_test_result_into_tree(filename):
- re_module = re.compile('<Module name="([^"]*)"')
- re_testcase = re.compile('<TestCase name="([^"]*)"')
- re_test = re.compile('<Test result="(pass|fail)" name="([^"]*)"')
+# Return list of fully qualified names of tests passing in
+# all the files.
+def consistently_passing_tests_from_test_results(filenames):
tree = {}
module = None
testcase = None
- with open(filename) as f:
- for line in f:
- m = re_module.search(line)
- if m:
- module_name = m.groups()[0]
- tree[module_name] = {}
- module = tree[module_name]
- continue
+ # Build a tree with leaves True|False|None for passing, failing and flaky
+ # tests.
+ for f in filenames:
+ for x in utils.read_cts_test_result(f):
+ if type(x) is utils.CtsModule:
+ module = tree.setdefault(x.name, {})
+ elif type(x) is utils.CtsTestCase:
+ testcase = module.setdefault(x.name, {})
+ else:
+ outcome = testcase.setdefault(x.name, x.outcome)
+ if outcome is not None and outcome != x.outcome:
+ testcase[x.name] = None
- m = re_testcase.search(line)
- if m:
- testcase_name = m.groups()[0]
- module[testcase_name] = {}
- testcase = module[testcase_name]
- continue
+ result = []
+ for module_name, module in tree.iteritems():
+ for test_case_name, test_case in module.iteritems():
+ result.extend(['{}/{}/{}'.format(module_name, test_case_name, test_name)
+ for test_name, test in test_case.iteritems()
+ if test])
- m = re_test.search(line)
- if m:
- outcome = m.groups()[0]
- test_name = m.groups()[1]
- assert outcome in ["fail", "pass"]
- testcase[test_name] = outcome == "pass"
- return tree
-
-# Report the items with the title
-def report_key_diff(title, items, prefix = ''):
- if len(items) > 0:
- print(title, ":")
- for x in items:
- print("- {}{}".format(prefix, x))
- print()
-
-
-def diff_sets(base_minus_result_title, result_minus_base_title,
- base_set, result_set, prefix = ''):
- base_minus_result = base_set - result_set
- result_minus_base = result_set - base_set
- report_key_diff(base_minus_result_title, base_minus_result, prefix)
- report_key_diff(result_minus_base_title, result_minus_base, prefix)
- return len(base_minus_result) > 0 or len(result_minus_base) > 0
-
-def diff_tree_report(baseline_tree, result_tree):
- baseline_modules = set(baseline_tree.keys())
- result_modules = set(result_tree.keys())
- differ = diff_sets('Modules missing from current result',
- 'New modules appeared in current result',
- baseline_modules, result_modules)
- for module in (result_modules & baseline_modules):
- baseline_module = baseline_tree[module]
- result_module = result_tree[module]
- baseline_testcases = set(baseline_module.keys())
- result_testcases = set(result_module.keys())
- differ = diff_sets('Test cases missing from current result',
- 'New test cases appeared in current result',
- baseline_testcases, result_testcases, module + '/') \
- or differ
- for testcase in (result_testcases & baseline_testcases):
- baseline_testcase = baseline_module[testcase]
- result_testcase = result_module[testcase]
- baseline_tests = set(baseline_testcase.keys())
- result_tests = set(result_testcase.keys())
- differ = diff_sets('Tests missing from current result',
- 'New tests appeared in current result',
- baseline_tests, result_tests, module + '/' + testcase + '/') \
- or differ
- need_newline_at_end = False
- for test in (result_tests & baseline_tests):
- baseline_outcome = baseline_testcase[test]
- result_outcome = result_testcase[test]
- if baseline_outcome != result_outcome:
- differ = True
- print('Test: {}/{}/{}, change: {}'.format(
- module, testcase, test,
- 'PASS -> FAIL' if baseline_outcome else 'FAIL -> PASS'))
- need_newline_at_end = True
- if need_newline_at_end:
- print()
- return differ
+ return result
def setup_and_clean(tool_is_d8, clean_dex):
# Two output dirs, one for the android image and one for cts tests.
@@ -296,8 +231,6 @@
re_summary = re.compile('<Summary ')
summaries = [('Summary from current test results: ', results_xml)]
- if not args.no_baseline:
- summaries.append(('Summary from baseline: ', CTS_BASELINE))
for (title, result_file) in summaries:
print(title, result_file)
@@ -312,10 +245,31 @@
else:
print('Comparing test results to baseline:\n')
- result_tree = read_test_result_into_tree(results_xml)
- baseline_tree = read_test_result_into_tree(CTS_BASELINE)
+ passing_tests = consistently_passing_tests_from_test_results([results_xml])
+ baseline_results = \
+ [f for f in glob(join(CTS_BASELINE_FILES_DIR, '*.xml'))]
+ assert len(baseline_results) != 0
- r = EXIT_FAILURE if diff_tree_report(baseline_tree, result_tree) else 0
+ passing_tests_in_baseline = \
+ consistently_passing_tests_from_test_results(baseline_results)
+
+ missing_or_failing_tests = \
+ set(passing_tests_in_baseline) - set(passing_tests)
+
+ num_tests = len(missing_or_failing_tests)
+ if num_tests != 0:
+ if num_tests > 1:
+ text = '{} tests that consistently pass in the baseline' \
+ ' are missing or failing in the current test:'.format(num_tests)
+ else:
+ text = '1 test that consistently passes in the baseline' \
+ ' is missing or failing in the current test:'
+ print(text)
+ for t in missing_or_failing_tests:
+ print(t)
+ r = EXIT_FAILURE
+ else:
+ r = 0
if args.save_result:
copy2(results_xml, args.save_result)
diff --git a/tools/utils.py b/tools/utils.py
index 67cac46..8c4f710 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -6,6 +6,7 @@
import hashlib
import os
+import re
import shutil
import subprocess
import sys
@@ -13,7 +14,6 @@
TOOLS_DIR = os.path.abspath(os.path.normpath(os.path.join(__file__, '..')))
REPO_ROOT = os.path.realpath(os.path.join(TOOLS_DIR, '..'))
-DOWNLOAD_DEPS = os.path.join(REPO_ROOT, 'scripts', 'download-deps.sh')
def PrintCmd(s):
if type(s) is list:
@@ -22,8 +22,13 @@
# I know this will hit os on windows eventually if we don't do this.
sys.stdout.flush()
+def IsWindows():
+ return os.name == 'nt'
+
def DownloadFromGoogleCloudStorage(sha1_file, bucket='r8-deps'):
- cmd = ["download_from_google_storage", "-n", "-b", bucket, "-u", "-s",
+ suffix = '.bat' if IsWindows() else ''
+ download_script = 'download_from_google_storage%s' % suffix
+ cmd = [download_script, '-n', '-b', bucket, '-u', '-s',
sha1_file]
PrintCmd(cmd)
subprocess.check_call(cmd)
@@ -63,9 +68,46 @@
def __enter__(self):
self._old_cwd = os.getcwd()
- print "Enter directory = ", self._working_directory
+ print 'Enter directory = ', self._working_directory
os.chdir(self._working_directory)
def __exit__(self, *_):
- print "Enter directory = ", self._old_cwd
+ print 'Enter directory = ', self._old_cwd
os.chdir(self._old_cwd)
+
+# Reading Android CTS test_result.xml
+
+class CtsModule(object):
+ def __init__(self, module_name):
+ self.name = module_name
+
+class CtsTestCase(object):
+ def __init__(self, test_case_name):
+ self.name = test_case_name
+
+class CtsTest(object):
+ def __init__(self, test_name, outcome):
+ self.name = test_name
+ self.outcome = outcome
+
+# Generator yielding CtsModule, CtsTestCase or CtsTest from
+# reading through a CTS test_result.xml file.
+def read_cts_test_result(file_xml):
+ re_module = re.compile('<Module name="([^"]*)"')
+ re_test_case = re.compile('<TestCase name="([^"]*)"')
+ re_test = re.compile('<Test result="(pass|fail)" name="([^"]*)"')
+ with open(file_xml) as f:
+ for line in f:
+ m = re_module.search(line)
+ if m:
+ yield CtsModule(m.groups()[0])
+ continue
+ m = re_test_case.search(line)
+ if m:
+ yield CtsTestCase(m.groups()[0])
+ continue
+ m = re_test.search(line)
+ if m:
+ outcome = m.groups()[0]
+ assert outcome in ['fail', 'pass']
+ yield CtsTest(m.groups()[1], outcome == 'pass')
diff --git a/tools/windows/README.dx b/tools/windows/README.dx
new file mode 100644
index 0000000..348dc4a
--- /dev/null
+++ b/tools/windows/README.dx
@@ -0,0 +1,25 @@
+Dx version: 1.12
+dx.bat and dx.jar are fetched from SDK build tools 24.0.0.
+dx.bat has been modified so that the code that checks if 'java' if on current path
+is removed and replaced by a direct reference to it.
+This is done because this relies on a tool found in the SDK and not present in the
+current package.
+Diff:
+
+26,29c26,29
+< set java_exe=
+< if exist "%~dp0..\tools\lib\find_java.bat" call "%~dp0..\tools\lib\find_java.bat"
+< if exist "%~dp0..\..\tools\lib\find_java.bat" call "%~dp0..\..\tools\lib\find_java.bat"
+< if not defined java_exe goto :EOF
+---
+> REM set java_exe=
+> REM if exist "%~dp0..\tools\lib\find_java.bat" call "%~dp0..\tools\lib\find_java.bat"
+> REM if exist "%~dp0..\..\tools\lib\find_java.bat" call "%~dp0..\..\tools\lib\find_java.bat"
+> REM if not defined java_exe goto :EOF
+87c87
+< call "%java_exe%" %javaOpts% -Djava.ext.dirs="%frameworkdir%" -jar "%jarpath%" %params%
+---
+> call java %javaOpts% -Djava.ext.dirs="%frameworkdir%" -jar "%jarpath%" %params%
+
+dexmerger.bat has been copied from dx.bat, and the command line has been updated
+according to the SDK dexmerger bash script to call the right class.
\ No newline at end of file
diff --git a/tools/windows/dx.tar.gz.sha1 b/tools/windows/dx.tar.gz.sha1
new file mode 100644
index 0000000..0f3e345
--- /dev/null
+++ b/tools/windows/dx.tar.gz.sha1
@@ -0,0 +1 @@
+9adeae753e17fa0a663e4d458b406a39ded27621
\ No newline at end of file
diff --git a/tools/youtube_data.py b/tools/youtube_data.py
index 0fb39e3..b32a372 100644
--- a/tools/youtube_data.py
+++ b/tools/youtube_data.py
@@ -9,37 +9,20 @@
THIRD_PARTY = os.path.join(utils.REPO_ROOT, 'third_party')
BASE = os.path.join(THIRD_PARTY, 'youtube')
-V11_47_BASE = os.path.join(BASE, 'youtube.android_11.47')
-V11_47_PREFIX = os.path.join(V11_47_BASE, 'YouTubeRelease')
-
V12_10_BASE = os.path.join(BASE, 'youtube.android_12.10')
V12_10_PREFIX = os.path.join(V12_10_BASE, 'YouTubeRelease')
V12_17_BASE = os.path.join(BASE, 'youtube.android_12.17')
V12_17_PREFIX = os.path.join(V12_17_BASE, 'YouTubeRelease')
+V12_22_BASE = os.path.join(BASE, 'youtube.android_12.22')
+V12_22_PREFIX = os.path.join(V12_22_BASE, 'YouTubeRelease')
+
# NOTE: we always use android.jar for SDK v25, later we might want to revise it
# to use proper android.jar version for each of youtube version separately.
ANDROID_JAR = os.path.join(THIRD_PARTY, 'android_jar', 'lib-v25', 'android.jar')
VERSIONS = {
- '11.47': {
- 'dex' : {
- 'inputs': [os.path.join(V11_47_BASE, 'YouTubeRelease_unsigned.apk')],
- 'pgmap': '%s_proguard.map' % V11_47_PREFIX,
- 'libraries' : [ANDROID_JAR],
- 'r8-flags': '--ignore-missing-classes',
- },
- 'deploy' : {
- 'inputs': ['%s_deploy.jar' % V11_47_PREFIX],
- 'pgconf': ['%s_proguard.config' % V11_47_PREFIX,
- '%s/proguardsettings/YouTubeRelease_proguard.config' % THIRD_PARTY],
- },
- 'proguarded' : {
- 'inputs': ['%s_proguard.jar' % V11_47_PREFIX],
- 'pgmap': '%s_proguard.map' % V11_47_PREFIX
- }
- },
'12.10': {
'dex' : {
'inputs': [os.path.join(V12_10_BASE, 'YouTubeRelease_unsigned.apk')],
@@ -74,4 +57,26 @@
'pgmap': '%s_proguard.map' % V12_17_PREFIX
}
},
+ '12.22': {
+ 'dex' : {
+ 'inputs': [os.path.join(V12_22_BASE, 'YouTubeRelease_unsigned.apk')],
+ 'pgmap': '%s_proguard.map' % V12_22_PREFIX,
+ 'libraries' : [ANDROID_JAR],
+ 'r8-flags': '--ignore-missing-classes',
+ },
+ 'deploy' : {
+ 'inputs': ['%s_deploy.jar' % V12_22_PREFIX],
+ 'pgconf': [
+ '%s_proguard.config' % V12_22_PREFIX,
+ '%s/proguardsettings/YouTubeRelease_proguard.config' % THIRD_PARTY],
+ 'multidexrules' : [
+ os.path.join(V12_22_BASE, 'mainDexClasses.rules'),
+ os.path.join(V12_22_BASE, 'main-dex-classes-release.cfg'),
+ os.path.join(V12_22_BASE, 'main_dex_YouTubeRelease_proguard.cfg')],
+ },
+ 'proguarded' : {
+ 'inputs': ['%s_proguard.jar' % V12_22_PREFIX],
+ 'pgmap': '%s_proguard.map' % V12_22_PREFIX
+ }
+ },
}