Merge commit '2678e9554995f4cafee6fe3cf886e9683014afe7' into dev-release
Change-Id: I0e2091a4e2510b84afa6388e4d2e93db9fdee741
diff --git a/AUTHORS b/AUTHORS
index df9cd8d..5905529 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -6,6 +6,10 @@
Google Inc.
Uber Technologies Inc.
Square Inc.
+Meta Platforms, Inc.
Albert Jin <albert.jin@gmail.com>
Kevin Sun <snxngxng@gmail.com>
+Søren Gjesse <soren@gjesse.dk>
+Andreas Gampe <agampe@meta.com>
+Tiangong Li <tgli@meta.com>
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index 5d310a10..6e873c7 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -179,15 +179,30 @@
return [output_api.PresubmitError(diff)]
return []
+def IsTestFile(file):
+ localPath = file.LocalPath()
+ return localPath.endswith('.java') and '/test/' in localPath
+
def CheckForAddedDisassemble(input_api, output_api):
results = []
for (file, line_nr, line) in input_api.RightHandSideLines():
- if file.LocalPath().endswith('.java') and '.disassemble()' in line:
+ if IsTestFile(file) and '.disassemble()' in line:
results.append(
output_api.PresubmitError(
'Test call to disassemble\n%s:%s %s' % (file.LocalPath(), line_nr, line)))
return results
+def CheckForAddedAllowXxxxxxMessages(input_api, output_api):
+ results = []
+ for (file, line_nr, line) in input_api.RightHandSideLines():
+ if (IsTestFile(file)
+ and ('.allowStdoutMessages()' in line or '.allowStderrMessages()' in line)):
+ results.append(
+ output_api.PresubmitError(
+ 'Test call to allowStdoutMessages or allowStderrMessages\n%s:%s %s'
+ % (file.LocalPath(), line_nr, line)))
+ return results
+
def CheckForAddedPartialDebug(input_api, output_api):
results = []
for (file, line_nr, line) in input_api.RightHandSideLines():
@@ -242,6 +257,7 @@
results.extend(
CheckDeterministicDebuggingChanged(input_api, output_api, branch))
results.extend(CheckForAddedDisassemble(input_api, output_api))
+ results.extend(CheckForAddedAllowXxxxxxMessages(input_api, output_api))
results.extend(CheckForAddedPartialDebug(input_api, output_api))
results.extend(CheckForCopyRight(input_api, output_api, branch))
return results
diff --git a/README.md b/README.md
index 487df6d..021b3f5 100644
--- a/README.md
+++ b/README.md
@@ -216,22 +216,28 @@
If your contribution is owned by your employer you need the
[Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate).
-Once the license agreement is in place, please send an email to
-[r8-dev@googlegroups.com](mailto:r8-dev@googlegroups.com) to be added as a
-contributor.
+Signing the CLA should be enough to be approved as a contributer. However, please send an email to
+[r8-dev@googlegroups.com](mailto:r8-dev@googlegroups.com) to introduce yourself and your plans
+for contributing to the project.
-After being added as a contributer you can upload your patches
-using `git cl` which is available in `depot_tools`. Once you have a
+To create an account for the code review tool (required for uploading changes)
+please navigate to https://r8-review.googlesource.com/ and sign in with the
+account used to sign the CLA. First time you do that you will be asked to
+_Create Gerrit Account_. Plase follow that process.
+
+You can now upload your patches. Once you have a
change that you are happy with you should make sure that it passes
all tests and then upload the change to our code review tool using:
$ git cl upload
On your first upload you will be asked to acquire credentials. Follow the
-instructions given by `git cl upload`.
+instructions given by `git cl upload` (as of Aug 5 2025 that is running
+`git credential-luci login`, the previous `.gitcookies` based authentication
+scheme has been deprecated.).
On successful uploads a link to the code review is printed in the
-output of the upload command. In the code review tool you can
+output of the `git cl upload` command. In the code review tool you can
assign reviewers and mark the change ready for review. At that
point the code review tool will send emails to reviewers.
diff --git a/infra/config/global/generated/cr-buildbucket.cfg b/infra/config/global/generated/cr-buildbucket.cfg
index 3a60dff..72dfcc9 100644
--- a/infra/config/global/generated/cr-buildbucket.cfg
+++ b/infra/config/global/generated/cr-buildbucket.cfg
@@ -779,7 +779,7 @@
swarming_tags: "vpython:native-python-wrapper"
dimensions: "cpu:x86-64"
dimensions: "normal:true"
- dimensions: "os:Ubuntu-22.04"
+ dimensions: "os:Ubuntu-20.04"
dimensions: "pool:luci.r8.ci"
exe {
cipd_package: "infra_internal/recipe_bundles/chrome-internal.googlesource.com/chrome/tools/build"
@@ -817,7 +817,7 @@
swarming_tags: "vpython:native-python-wrapper"
dimensions: "cpu:x86-64"
dimensions: "normal:true"
- dimensions: "os:Ubuntu-22.04"
+ dimensions: "os:Ubuntu-20.04"
dimensions: "pool:luci.r8.ci"
exe {
cipd_package: "infra_internal/recipe_bundles/chrome-internal.googlesource.com/chrome/tools/build"
diff --git a/infra/config/global/generated/project.cfg b/infra/config/global/generated/project.cfg
index 2475c82..b5be1d4 100644
--- a/infra/config/global/generated/project.cfg
+++ b/infra/config/global/generated/project.cfg
@@ -7,7 +7,7 @@
name: "r8"
access: "group:all"
lucicfg {
- version: "1.45.3"
+ version: "1.45.6"
package_dir: ".."
config_dir: "generated"
entry_point: "main.star"
diff --git a/infra/config/global/main.star b/infra/config/global/main.star
index 862f835..eb4ad17 100755
--- a/infra/config/global/main.star
+++ b/infra/config/global/main.star
@@ -168,7 +168,7 @@
default_timeout = time.hour * 6
-def get_dimensions(windows = False, internal = False, archive = False, noble=True):
+def get_dimensions(windows = False, internal = False, archive = False, jammy=True):
# We use the following setup:
# windows -> always windows machine
# internal -> always internal, single small, machine
@@ -181,7 +181,7 @@
if windows:
dimensions["os"] = "Windows-11"
else:
- if noble:
+ if jammy:
dimensions["os"] = "Ubuntu-22.04"
else:
dimensions["os"] = "Ubuntu-20.04"
@@ -404,7 +404,7 @@
r8_tester_with_default(
"linux-android-5",
["--dex_vm=5.1.1", "--all_tests", "--command_cache_dir=/tmp/ccache"],
- dimensions = get_dimensions(noble=True),
+ dimensions = get_dimensions(jammy=False),
)
r8_tester_with_default(
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/EmptyReflectiveOperationReceiver.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/EmptyReflectiveOperationReceiver.java
index 22f06cb..7b6ad44 100644
--- a/src/assistant/java/com/android/tools/r8/assistant/runtime/EmptyReflectiveOperationReceiver.java
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/EmptyReflectiveOperationReceiver.java
@@ -28,13 +28,14 @@
@Override
public void onClassGetDeclaredMethod(
- Stack stack, Class<?> clazz, String method, Class<?>... parameters) {}
+ Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters) {}
@Override
public void onClassGetDeclaredMethods(Stack stack, Class<?> clazz) {}
@Override
- public void onClassGetDeclaredField(Stack stack, Class<?> clazz, String fieldName) {}
+ public void onClassGetDeclaredField(
+ Stack stack, Class<?> fieldType, Class<?> clazz, String fieldName) {}
@Override
public void onClassGetDeclaredFields(Stack stack, Class<?> clazz) {}
@@ -47,13 +48,13 @@
@Override
public void onClassGetMethod(
- Stack stack, Class<?> clazz, String method, Class<?>... parameters) {}
+ Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters) {}
@Override
public void onClassGetMethods(Stack stack, Class<?> clazz) {}
@Override
- public void onClassGetField(Stack stack, Class<?> clazz, String fieldName) {}
+ public void onClassGetField(Stack stack, Class<?> fieldType, Class<?> clazz, String fieldName) {}
@Override
public void onClassGetFields(Stack stack, Class<?> clazz) {}
@@ -83,14 +84,8 @@
public void onClassFlag(Stack stack, Class<?> clazz, ClassFlag classFlag) {}
@Override
- public void onAtomicIntegerFieldUpdaterNewUpdater(Stack stack, Class<?> clazz, String name) {}
-
- @Override
- public void onAtomicLongFieldUpdaterNewUpdater(Stack stack, Class<?> clazz, String name) {}
-
- @Override
- public void onAtomicReferenceFieldUpdaterNewUpdater(
- Stack stack, Class<?> clazz, Class<?> fieldClass, String name) {}
+ public void onAtomicFieldUpdaterNewUpdater(
+ Stack stack, Class<?> fieldClass, Class<?> clazz, String name) {}
@Override
public void onServiceLoaderLoad(Stack stack, Class<?> clazz, ClassLoader classLoader) {}
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveEventType.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveEventType.java
new file mode 100644
index 0000000..7b8b3eb
--- /dev/null
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveEventType.java
@@ -0,0 +1,37 @@
+// Copyright (c) 2025, 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.assistant.runtime;
+
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+
+@KeepForApi
+public enum ReflectiveEventType {
+ CLASS_NEW_INSTANCE,
+ CLASS_GET_DECLARED_METHOD,
+ CLASS_GET_DECLARED_METHODS,
+ CLASS_GET_DECLARED_FIELD,
+ CLASS_GET_DECLARED_FIELDS,
+ CLASS_GET_DECLARED_CONSTRUCTOR,
+ CLASS_GET_DECLARED_CONSTRUCTORS,
+ CLASS_GET_METHOD,
+ CLASS_GET_METHODS,
+ CLASS_GET_FIELD,
+ CLASS_GET_FIELDS,
+ CLASS_GET_CONSTRUCTOR,
+ CLASS_GET_CONSTRUCTORS,
+ CLASS_GET_NAME,
+ CLASS_FOR_NAME,
+ CLASS_GET_COMPONENT_TYPE,
+ CLASS_GET_PACKAGE,
+ CLASS_IS_ASSIGNABLE_FROM,
+ CLASS_GET_SUPERCLASS,
+ CLASS_AS_SUBCLASS,
+ CLASS_IS_INSTANCE,
+ CLASS_CAST,
+ CLASS_FLAG,
+ ATOMIC_FIELD_UPDATER_NEW_UPDATER,
+ SERVICE_LOADER_LOAD,
+ PROXY_NEW_PROXY_INSTANCE;
+}
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationJsonLogger.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationJsonLogger.java
new file mode 100644
index 0000000..57e3cdf
--- /dev/null
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationJsonLogger.java
@@ -0,0 +1,271 @@
+// Copyright (c) 2025, 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.assistant.runtime;
+
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.ATOMIC_FIELD_UPDATER_NEW_UPDATER;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_AS_SUBCLASS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_CAST;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_FLAG;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_FOR_NAME;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_COMPONENT_TYPE;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_CONSTRUCTOR;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_CONSTRUCTORS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_DECLARED_CONSTRUCTOR;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_DECLARED_CONSTRUCTORS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_DECLARED_FIELD;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_DECLARED_FIELDS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_DECLARED_METHOD;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_DECLARED_METHODS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_FIELD;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_FIELDS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_METHOD;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_METHODS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_NAME;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_PACKAGE;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_SUPERCLASS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_IS_ASSIGNABLE_FROM;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_IS_INSTANCE;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_NEW_INSTANCE;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.PROXY_NEW_PROXY_INSTANCE;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.SERVICE_LOADER_LOAD;
+
+import com.android.tools.r8.assistant.runtime.ReflectiveOracle.Stack;
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.lang.reflect.InvocationHandler;
+
+// This logs the information in JSON-like format,
+// manually encoded to avoid loading gson on the mobile.
+@KeepForApi
+public class ReflectiveOperationJsonLogger implements ReflectiveOperationReceiver {
+
+ private final FileWriter output;
+
+ public ReflectiveOperationJsonLogger() throws IOException {
+ String property = System.getProperty("com.android.tools.r8.reflectiveJsonLogger", "log.txt");
+ File file = new File(property);
+ file.createNewFile();
+ this.output = new FileWriter(file);
+ output.write("[");
+ }
+
+ public void finished() throws IOException {
+ output.write("{}]");
+ output.close();
+ }
+
+ private String[] methodToString(
+ Class<?> returnType, Class<?> holder, String method, Class<?>... parameters) {
+ String[] methodStrings = new String[parameters.length + 3];
+ methodStrings[0] = printClass(returnType);
+ methodStrings[1] = printClass(holder);
+ methodStrings[2] = method;
+ for (int i = 0; i < parameters.length; i++) {
+ methodStrings[i + 3] = printClass(parameters[i]);
+ }
+ return methodStrings;
+ }
+
+ private String[] constructorToString(Class<?> holder, Class<?>... parameters) {
+ return methodToString(Void.TYPE, holder, "<init>", parameters);
+ }
+
+ private String printClass(Class<?> clazz) {
+ return clazz == null ? "null" : clazz.getName();
+ }
+
+ private String printClassLoader(ClassLoader classLoader) {
+ return classLoader == null ? "null" : printClass(classLoader.getClass());
+ }
+
+ private void output(ReflectiveEventType event, Stack stack, String... args) {
+ try {
+ output.write("{\"event\": \"");
+ output.write(event.name());
+ output.write("\"");
+ if (stack != null) {
+ output.write(", \"stack\": ");
+ printArray(stack.stackTraceElementsAsString());
+ }
+ assert args != null;
+ output.write(", \"args\": ");
+ printArray(args);
+ output.write("},\n");
+ output.flush();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void printArray(String... args) throws IOException {
+ output.write("[");
+ for (int i = 0; i < args.length; i++) {
+ output.write("\"");
+ output.write(args[i]);
+ output.write("\"");
+ if (i != args.length - 1) {
+ output.write(", ");
+ }
+ }
+ output.write("]");
+ }
+
+ @Override
+ public void onClassNewInstance(Stack stack, Class<?> clazz) {
+ output(CLASS_NEW_INSTANCE, stack, printClass(clazz));
+ }
+
+ @Override
+ public void onClassGetDeclaredMethod(
+ Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters) {
+ output(CLASS_GET_DECLARED_METHOD, stack, methodToString(returnType, clazz, method, parameters));
+ }
+
+ @Override
+ public void onClassGetDeclaredMethods(Stack stack, Class<?> clazz) {
+ output(CLASS_GET_DECLARED_METHODS, stack, printClass(clazz));
+ }
+
+ @Override
+ public void onClassGetDeclaredField(
+ Stack stack, Class<?> fieldType, Class<?> clazz, String fieldName) {
+ output(CLASS_GET_DECLARED_FIELD, stack, printClass(fieldType), printClass(clazz), fieldName);
+ }
+
+ @Override
+ public void onClassGetDeclaredFields(Stack stack, Class<?> clazz) {
+ output(CLASS_GET_DECLARED_FIELDS, stack, printClass(clazz));
+ }
+
+ @Override
+ public void onClassGetDeclaredConstructor(Stack stack, Class<?> clazz, Class<?>... parameters) {
+ output(CLASS_GET_DECLARED_CONSTRUCTOR, stack, constructorToString(clazz, parameters));
+ }
+
+ @Override
+ public void onClassGetDeclaredConstructors(Stack stack, Class<?> clazz) {
+ output(CLASS_GET_DECLARED_CONSTRUCTORS, stack, printClass(clazz));
+ }
+
+ @Override
+ public void onClassGetMethod(
+ Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters) {
+ output(CLASS_GET_METHOD, stack, methodToString(returnType, clazz, method, parameters));
+ }
+
+ @Override
+ public void onClassGetMethods(Stack stack, Class<?> clazz) {
+ output(CLASS_GET_METHODS, stack, printClass(clazz));
+ }
+
+ @Override
+ public void onClassGetField(Stack stack, Class<?> fieldType, Class<?> clazz, String fieldName) {
+ output(CLASS_GET_FIELD, stack, printClass(fieldType), printClass(clazz), fieldName);
+ }
+
+ @Override
+ public void onClassGetFields(Stack stack, Class<?> clazz) {
+ output(CLASS_GET_FIELDS, stack, printClass(clazz));
+ }
+
+ @Override
+ public void onClassGetConstructor(Stack stack, Class<?> clazz, Class<?>... parameters) {
+ output(CLASS_GET_CONSTRUCTOR, stack, constructorToString(clazz, parameters));
+ }
+
+ @Override
+ public void onClassGetConstructors(Stack stack, Class<?> clazz) {
+ output(CLASS_GET_CONSTRUCTORS, stack, printClass(clazz));
+ }
+
+ @Override
+ public void onClassGetName(Stack stack, Class<?> clazz, NameLookupType lookupType) {
+ output(CLASS_GET_NAME, stack, printClass(clazz), lookupType.name());
+ }
+
+ @Override
+ public void onClassForName(
+ Stack stack, String className, boolean initialize, ClassLoader classLoader) {
+ output(
+ CLASS_FOR_NAME,
+ stack,
+ className,
+ Boolean.toString(initialize),
+ printClassLoader(classLoader));
+ }
+
+ @Override
+ public void onClassGetComponentType(Stack stack, Class<?> clazz) {
+ output(CLASS_GET_COMPONENT_TYPE, stack, printClass(clazz));
+ }
+
+ @Override
+ public void onClassGetPackage(Stack stack, Class<?> clazz) {
+ output(CLASS_GET_PACKAGE, stack, printClass(clazz));
+ }
+
+ @Override
+ public void onClassIsAssignableFrom(Stack stack, Class<?> clazz, Class<?> sup) {
+ output(CLASS_IS_ASSIGNABLE_FROM, stack, printClass(clazz), printClass(sup));
+ }
+
+ @Override
+ public void onClassGetSuperclass(Stack stack, Class<?> clazz) {
+ output(CLASS_GET_SUPERCLASS, stack, printClass(clazz));
+ }
+
+ @Override
+ public void onClassAsSubclass(Stack stack, Class<?> holder, Class<?> clazz) {
+ output(CLASS_AS_SUBCLASS, stack, printClass(holder), printClass(clazz));
+ }
+
+ @Override
+ public void onClassIsInstance(Stack stack, Class<?> holder, Object object) {
+ output(CLASS_IS_INSTANCE, stack, printClass(holder), printClass(object.getClass()));
+ }
+
+ @Override
+ public void onClassCast(Stack stack, Class<?> holder, Object object) {
+ output(CLASS_CAST, stack, printClass(holder), printClass(object.getClass()));
+ }
+
+ @Override
+ public void onClassFlag(Stack stack, Class<?> clazz, ClassFlag classFlag) {
+ output(CLASS_FLAG, stack, printClass(clazz), classFlag.name());
+ }
+
+ @Override
+ public void onAtomicFieldUpdaterNewUpdater(
+ Stack stack, Class<?> fieldClass, Class<?> clazz, String name) {
+ output(
+ ATOMIC_FIELD_UPDATER_NEW_UPDATER, stack, printClass(fieldClass), printClass(clazz), name);
+ }
+
+ @Override
+ public void onServiceLoaderLoad(Stack stack, Class<?> clazz, ClassLoader classLoader) {
+ output(SERVICE_LOADER_LOAD, stack, printClass(clazz), printClassLoader(classLoader));
+ }
+
+ @Override
+ public void onProxyNewProxyInstance(
+ Stack stack,
+ ClassLoader classLoader,
+ Class<?>[] interfaces,
+ InvocationHandler invocationHandler) {
+ String[] methodStrings = new String[interfaces.length + 2];
+ methodStrings[0] = printClassLoader(classLoader);
+ methodStrings[1] = invocationHandler.toString();
+ for (int i = 0; i < interfaces.length; i++) {
+ methodStrings[i + 2] = printClass(interfaces[i]);
+ }
+ output(PROXY_NEW_PROXY_INSTANCE, stack, methodStrings);
+ }
+
+ public boolean requiresStackInformation() {
+ return true;
+ }
+}
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java
index 7d59d7c..d6f1ead 100644
--- a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java
@@ -44,7 +44,7 @@
@Override
public void onClassGetDeclaredMethod(
- Stack stack, Class<?> clazz, String method, Class<?>... parameters) {
+ Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters) {
System.out.println(
"Reflectively got declared method "
+ printMethod(method, parameters)
@@ -58,7 +58,8 @@
}
@Override
- public void onClassGetDeclaredField(Stack stack, Class<?> clazz, String fieldName) {
+ public void onClassGetDeclaredField(
+ Stack stack, Class<?> fieldType, Class<?> clazz, String fieldName) {
System.out.println("Reflectively got declared field " + fieldName + " on " + clazz.getName());
}
@@ -82,7 +83,8 @@
}
@Override
- public void onClassGetMethod(Stack stack, Class<?> clazz, String method, Class<?>... parameters) {
+ public void onClassGetMethod(
+ Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters) {
System.out.println(
"Reflectively got method " + printMethod(method, parameters) + " on " + clazz.getName());
}
@@ -93,7 +95,7 @@
}
@Override
- public void onClassGetField(Stack stack, Class<?> clazz, String fieldName) {
+ public void onClassGetField(Stack stack, Class<?> fieldType, Class<?> clazz, String fieldName) {
System.out.println("Reflectively got field " + fieldName + " on " + clazz.getName());
}
@@ -168,20 +170,8 @@
}
@Override
- public void onAtomicIntegerFieldUpdaterNewUpdater(Stack stack, Class<?> clazz, String name) {
- System.out.println(
- "Reflectively got AtomicIntegerFieldUpdater.newUpdater on " + clazz + "#" + name);
- }
-
- @Override
- public void onAtomicLongFieldUpdaterNewUpdater(Stack stack, Class<?> clazz, String name) {
- System.out.println(
- "Reflectively got AtomicLongFieldUpdater.newUpdater on " + clazz + "#" + name);
- }
-
- @Override
- public void onAtomicReferenceFieldUpdaterNewUpdater(
- Stack stack, Class<?> clazz, Class<?> fieldClass, String name) {
+ public void onAtomicFieldUpdaterNewUpdater(
+ Stack stack, Class<?> fieldClass, Class<?> clazz, String name) {
System.out.println(
"Reflectively got AtomicReferenceFieldUpdater.newUpdater on "
+ fieldClass
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationReceiver.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationReceiver.java
index 206a1f4..4c13dda 100644
--- a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationReceiver.java
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationReceiver.java
@@ -18,11 +18,12 @@
void onClassNewInstance(Stack stack, Class<?> clazz);
- void onClassGetDeclaredMethod(Stack stack, Class<?> clazz, String method, Class<?>... parameters);
+ void onClassGetDeclaredMethod(
+ Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters);
void onClassGetDeclaredMethods(Stack stack, Class<?> clazz);
- void onClassGetDeclaredField(Stack stack, Class<?> clazz, String fieldName);
+ void onClassGetDeclaredField(Stack stack, Class<?> fieldType, Class<?> clazz, String fieldName);
void onClassGetDeclaredFields(Stack stack, Class<?> clazz);
@@ -30,11 +31,12 @@
void onClassGetDeclaredConstructors(Stack stack, Class<?> clazz);
- void onClassGetMethod(Stack stack, Class<?> clazz, String method, Class<?>... parameters);
+ void onClassGetMethod(
+ Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters);
void onClassGetMethods(Stack stack, Class<?> clazz);
- void onClassGetField(Stack stack, Class<?> clazz, String fieldName);
+ void onClassGetField(Stack stack, Class<?> fieldType, Class<?> clazz, String fieldName);
void onClassGetFields(Stack stack, Class<?> clazz);
@@ -60,12 +62,8 @@
void onClassIsAssignableFrom(Stack stack, Class<?> clazz, Class<?> sup);
- void onAtomicIntegerFieldUpdaterNewUpdater(Stack stack, Class<?> clazz, String name);
-
- void onAtomicLongFieldUpdaterNewUpdater(Stack stack, Class<?> clazz, String name);
-
- void onAtomicReferenceFieldUpdaterNewUpdater(
- Stack stack, Class<?> clazz, Class<?> fieldClass, String name);
+ void onAtomicFieldUpdaterNewUpdater(
+ Stack stack, Class<?> fieldClass, Class<?> clazz, String name);
void onServiceLoaderLoad(Stack stack, Class<?> clazz, ClassLoader classLoader);
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java
index 32c841c..7a055b4 100644
--- a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java
@@ -7,6 +7,7 @@
import com.android.tools.r8.assistant.runtime.ReflectiveOperationReceiver.NameLookupType;
import com.android.tools.r8.keepanno.annotations.KeepForApi;
import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
import java.util.Arrays;
@KeepForApi
@@ -65,6 +66,14 @@
}
return sb.toString();
}
+
+ public String[] stackTraceElementsAsString() {
+ String[] result = new String[stackTraceElements.length];
+ for (int i = 0; i < stackTraceElements.length; i++) {
+ result[i] = stackTraceElements[i].toString();
+ }
+ return result;
+ }
}
public static void onClassNewInstance(Class<?> clazz) {
@@ -81,7 +90,14 @@
}
public static void onClassGetDeclaredMethod(Class<?> clazz, String name, Class<?>... parameters) {
- getInstance().onClassGetDeclaredMethod(Stack.createStack(), clazz, name, parameters);
+ Class<?> returnType = null;
+ try {
+ Method declaredMethod = clazz.getDeclaredMethod(name, parameters);
+ returnType = declaredMethod.getReturnType();
+ } catch (NoSuchMethodException e) {
+ }
+ getInstance()
+ .onClassGetDeclaredMethod(Stack.createStack(), returnType, clazz, name, parameters);
}
public static void onClassGetDeclaredMethods(Class<?> clazz) {
@@ -89,7 +105,12 @@
}
public static void onClassGetDeclaredField(Class<?> clazz, String fieldName) {
- getInstance().onClassGetDeclaredField(Stack.createStack(), clazz, fieldName);
+ Class<?> fieldType = null;
+ try {
+ fieldType = clazz.getDeclaredField(fieldName).getType();
+ } catch (NoSuchFieldException e) {
+ }
+ getInstance().onClassGetDeclaredField(Stack.createStack(), fieldType, clazz, fieldName);
}
public static void onClassGetDeclaredFields(Class<?> clazz) {
@@ -105,7 +126,12 @@
}
public static void onClassGetMethod(Class<?> clazz, String name, Class<?>[] parameterTypes) {
- getInstance().onClassGetMethod(Stack.createStack(), clazz, name, parameterTypes);
+ Class<?> returnType = null;
+ try {
+ returnType = clazz.getMethod(name, parameterTypes).getReturnType();
+ } catch (NoSuchMethodException e) {
+ }
+ getInstance().onClassGetMethod(Stack.createStack(), returnType, clazz, name, parameterTypes);
}
public static void onClassGetMethods(Class<?> clazz) {
@@ -113,7 +139,12 @@
}
public static void onClassGetField(Class<?> clazz, String name) {
- getInstance().onClassGetField(Stack.createStack(), clazz, name);
+ Class<?> fieldType = null;
+ try {
+ fieldType = clazz.getField(name).getType();
+ } catch (NoSuchFieldException e) {
+ }
+ getInstance().onClassGetField(Stack.createStack(), fieldType, clazz, name);
}
public static void onClassGetFields(Class<?> clazz) {
@@ -221,17 +252,16 @@
}
public static void onAtomicIntegerFieldUpdaterNewUpdater(Class<?> clazz, String name) {
- getInstance().onAtomicIntegerFieldUpdaterNewUpdater(Stack.createStack(), clazz, name);
+ getInstance().onAtomicFieldUpdaterNewUpdater(Stack.createStack(), int.class, clazz, name);
}
public static void onAtomicLongFieldUpdaterNewUpdater(Class<?> clazz, String name) {
- getInstance().onAtomicLongFieldUpdaterNewUpdater(Stack.createStack(), clazz, name);
+ getInstance().onAtomicFieldUpdaterNewUpdater(Stack.createStack(), long.class, clazz, name);
}
public static void onAtomicReferenceFieldUpdaterNewUpdater(
Class<?> clazz, Class<?> fieldClass, String name) {
- getInstance()
- .onAtomicReferenceFieldUpdaterNewUpdater(Stack.createStack(), clazz, fieldClass, name);
+ getInstance().onAtomicFieldUpdaterNewUpdater(Stack.createStack(), fieldClass, clazz, name);
}
public static void onServiceLoaderLoad(Class<?> clazz) {
diff --git a/src/main/java/com/android/tools/r8/R8Assistant.java b/src/main/java/com/android/tools/r8/R8Assistant.java
index bbcba15..d340c28 100644
--- a/src/main/java/com/android/tools/r8/R8Assistant.java
+++ b/src/main/java/com/android/tools/r8/R8Assistant.java
@@ -32,6 +32,11 @@
public static void run(R8AssistantCommand command) throws CompilationFailedException {
InternalOptions options = command.getInternalOptions();
+ runForTest(command, options);
+ }
+
+ public static void runForTest(R8AssistantCommand command, InternalOptions options)
+ throws CompilationFailedException {
ExceptionUtils.withCompilationHandler(
options.reporter,
() -> runInternal(command, options, ThreadUtils.getExecutorService(options)));
diff --git a/src/main/java/com/android/tools/r8/R8AssistantCommand.java b/src/main/java/com/android/tools/r8/R8AssistantCommand.java
index 55efbd7..c939563 100644
--- a/src/main/java/com/android/tools/r8/R8AssistantCommand.java
+++ b/src/main/java/com/android/tools/r8/R8AssistantCommand.java
@@ -5,6 +5,8 @@
import com.android.tools.r8.assistant.ClassInjectionHelper;
import com.android.tools.r8.assistant.runtime.EmptyReflectiveOperationReceiver;
+import com.android.tools.r8.assistant.runtime.ReflectiveEventType;
+import com.android.tools.r8.assistant.runtime.ReflectiveOperationJsonLogger;
import com.android.tools.r8.assistant.runtime.ReflectiveOperationLogger;
import com.android.tools.r8.assistant.runtime.ReflectiveOperationReceiver;
import com.android.tools.r8.assistant.runtime.ReflectiveOracle;
@@ -148,6 +150,8 @@
R8AssistantCommand makeCommand() {
injectClasses(
EmptyReflectiveOperationReceiver.class,
+ ReflectiveEventType.class,
+ ReflectiveOperationJsonLogger.class,
ReflectiveOperationLogger.class,
ReflectiveOperationReceiver.NameLookupType.class,
ReflectiveOperationReceiver.ClassFlag.class,
diff --git a/src/main/java/com/android/tools/r8/assistant/postprocessing/ReflectiveOperationJsonParser.java b/src/main/java/com/android/tools/r8/assistant/postprocessing/ReflectiveOperationJsonParser.java
new file mode 100644
index 0000000..f1a4a73
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/assistant/postprocessing/ReflectiveOperationJsonParser.java
@@ -0,0 +1,55 @@
+// Copyright (c) 2025, 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.assistant.postprocessing;
+
+import com.android.tools.r8.assistant.postprocessing.model.ReflectiveEvent;
+import com.android.tools.r8.assistant.runtime.ReflectiveEventType;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ReflectiveOperationJsonParser {
+
+ private final DexItemFactory factory;
+
+ public ReflectiveOperationJsonParser(DexItemFactory factory) {
+ this.factory = factory;
+ }
+
+ public List<ReflectiveEvent> parse(Path file) throws IOException {
+ List<ReflectiveEvent> result = new ArrayList<>();
+ String contents = Files.readString(file) + "{}]";
+ JsonArray events = new JsonParser().parse(contents).getAsJsonArray();
+ for (JsonElement eventElement : events) {
+ JsonObject event = eventElement.getAsJsonObject();
+ if (event.isEmpty()) {
+ break;
+ }
+ ReflectiveEventType eventType = ReflectiveEventType.valueOf(event.get("event").getAsString());
+ JsonElement stackElement = event.get("stack");
+ String[] stack = stackElement != null ? toStringArray(stackElement) : null;
+ JsonElement argsElement = event.get("args");
+ String[] args = argsElement != null ? toStringArray(argsElement) : null;
+ result.add(ReflectiveEvent.instantiate(eventType, stack, args, factory));
+ }
+ return result;
+ }
+
+ private String[] toStringArray(JsonElement argsElement) {
+ JsonArray jsonArray = argsElement.getAsJsonArray();
+ String[] strings = new String[jsonArray.size()];
+ for (int i = 0; i < strings.length; i++) {
+ strings[i] = jsonArray.get(i).getAsString();
+ }
+ return strings;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/assistant/postprocessing/model/AtomicFieldUpdaterNewUpdater.java b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/AtomicFieldUpdaterNewUpdater.java
new file mode 100644
index 0000000..d2d7678
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/AtomicFieldUpdaterNewUpdater.java
@@ -0,0 +1,54 @@
+// Copyright (c) 2025, 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.assistant.postprocessing.model;
+
+import com.android.tools.r8.assistant.runtime.ReflectiveEventType;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.shaking.KeepFieldInfo;
+import com.android.tools.r8.shaking.KeepInfoCollectionExported;
+
+public class AtomicFieldUpdaterNewUpdater extends ReflectiveEvent {
+ private final DexField field;
+
+ protected AtomicFieldUpdaterNewUpdater(
+ ReflectiveEventType eventType, String[] stack, String[] args, DexItemFactory factory) {
+ super(eventType, stack);
+ assert args.length == 3;
+ DexType type = toType(args[0], factory);
+ DexType holder = toType(args[1], factory);
+ String fieldName = args[2];
+ field = factory.createField(holder, type, fieldName);
+ }
+
+ public DexField getField() {
+ return field;
+ }
+
+ @Override
+ public boolean isAtomicFieldUpdaterNewUpdater() {
+ return true;
+ }
+
+ @Override
+ public AtomicFieldUpdaterNewUpdater asAtomicFieldUpdaterNewUpdater() {
+ return this;
+ }
+
+ @Override
+ public String getContentsString() {
+ return field.toString();
+ }
+
+ @Override
+ public boolean isKeptBy(KeepInfoCollectionExported keepInfoCollectionExported) {
+ KeepFieldInfo keepFieldInfo =
+ keepInfoCollectionExported.getKeepFieldInfo(field.asFieldReference());
+ // TODO(b/428836085): Check inner properties of the keep rules, holder, type and name may have
+ // to be preserved.
+ return keepFieldInfo != null;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ClassGetMember.java b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ClassGetMember.java
new file mode 100644
index 0000000..a9c2597
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ClassGetMember.java
@@ -0,0 +1,75 @@
+// Copyright (c) 2025, 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.assistant.postprocessing.model;
+
+import com.android.tools.r8.assistant.runtime.ReflectiveEventType;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMember;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.shaking.KeepFieldInfo;
+import com.android.tools.r8.shaking.KeepInfoCollectionExported;
+import com.android.tools.r8.shaking.KeepMethodInfo;
+
+public class ClassGetMember extends ReflectiveEvent {
+
+ private final DexMember<?, ?> member;
+
+ protected ClassGetMember(
+ ReflectiveEventType eventType, String[] stack, String[] args, DexItemFactory factory) {
+ super(eventType, stack);
+ DexType type = toTypeOrTripleStar(args[0], factory);
+ DexType holder = toType(args[1], factory);
+ String name = args[2];
+ if (eventType == ReflectiveEventType.CLASS_GET_FIELD
+ || eventType == ReflectiveEventType.CLASS_GET_DECLARED_FIELD) {
+ member = factory.createField(holder, type, name);
+ return;
+ }
+ assert eventType == ReflectiveEventType.CLASS_GET_METHOD
+ || eventType == ReflectiveEventType.CLASS_GET_DECLARED_METHOD
+ || eventType == ReflectiveEventType.CLASS_GET_CONSTRUCTOR
+ || eventType == ReflectiveEventType.CLASS_GET_DECLARED_CONSTRUCTOR;
+ DexType[] typeArgs = new DexType[args.length - 3];
+ for (int i = 3; i < args.length; i++) {
+ typeArgs[i - 3] = toType(args[i], factory);
+ }
+ member = factory.createMethod(holder, factory.createProto(type, typeArgs), name);
+ }
+
+ public DexMember<?, ?> getMember() {
+ return member;
+ }
+
+ @Override
+ public boolean isClassGetMember() {
+ return true;
+ }
+
+ @Override
+ public ClassGetMember asClassGetMember() {
+ return this;
+ }
+
+ @Override
+ public String getContentsString() {
+ return member.toSourceString();
+ }
+
+ @Override
+ public boolean isKeptBy(KeepInfoCollectionExported keepInfoCollectionExported) {
+ if (member.isDexField()) {
+ KeepFieldInfo keepFieldInfo =
+ keepInfoCollectionExported.getKeepFieldInfo(member.asDexField().asFieldReference());
+ // TODO(b/428836085): Check inner properties of the keep rules, holder, type and name may have
+ // to be preserved.
+ return keepFieldInfo != null;
+ }
+ KeepMethodInfo keepMethodInfo =
+ keepInfoCollectionExported.getKeepMethodInfo(member.asDexMethod().asMethodReference());
+ // TODO(b/428836085): Check inner properties of the keep rules, holder, type and name may have
+ // to be preserved.
+ return keepMethodInfo != null;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ClassGetMembers.java b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ClassGetMembers.java
new file mode 100644
index 0000000..dc41cf4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ClassGetMembers.java
@@ -0,0 +1,46 @@
+// Copyright (c) 2025, 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.assistant.postprocessing.model;
+
+import com.android.tools.r8.assistant.runtime.ReflectiveEventType;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.shaking.KeepInfoCollectionExported;
+
+public class ClassGetMembers extends ReflectiveEvent {
+
+ private final DexType holder;
+
+ protected ClassGetMembers(
+ ReflectiveEventType eventType, String[] stack, String[] args, DexItemFactory factory) {
+ super(eventType, stack);
+ holder = toType(args[0], factory);
+ }
+
+ public DexType getHolder() {
+ return holder;
+ }
+
+ @Override
+ public boolean isClassGetMembers() {
+ return true;
+ }
+
+ @Override
+ public ClassGetMembers asClassGetMembers() {
+ return this;
+ }
+
+ @Override
+ public String getContentsString() {
+ return holder.toSourceString();
+ }
+
+ @Override
+ public boolean isKeptBy(KeepInfoCollectionExported keepInfoCollectionExported) {
+ // TODO(b/428836085): What does this mean? One member has to be kept?
+ return keepInfoCollectionExported.getKeepClassInfo(holder.asTypeReference()) != null;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ClassGetName.java b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ClassGetName.java
new file mode 100644
index 0000000..d8f599b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ClassGetName.java
@@ -0,0 +1,55 @@
+// Copyright (c) 2025, 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.assistant.postprocessing.model;
+
+import com.android.tools.r8.assistant.runtime.ReflectiveEventType;
+import com.android.tools.r8.assistant.runtime.ReflectiveOperationReceiver.NameLookupType;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.shaking.KeepInfoCollectionExported;
+
+public class ClassGetName extends ReflectiveEvent {
+
+ private final DexType type;
+ private final NameLookupType nameLookupType;
+
+ protected ClassGetName(
+ ReflectiveEventType eventType, String[] stack, String[] args, DexItemFactory factory) {
+ super(eventType, stack);
+ assert args.length == 2;
+ type = toType(args[0], factory);
+ nameLookupType = NameLookupType.valueOf(args[1]);
+ }
+
+ @Override
+ public boolean isClassGetName() {
+ return true;
+ }
+
+ @Override
+ public ClassGetName asClassGetName() {
+ return this;
+ }
+
+ public DexType getType() {
+ return type;
+ }
+
+ public NameLookupType getNameLookupType() {
+ return nameLookupType;
+ }
+
+ @Override
+ public String getContentsString() {
+ return type + ", " + nameLookupType;
+ }
+
+ @Override
+ public boolean isKeptBy(KeepInfoCollectionExported keepInfoCollectionExported) {
+ // TODO(b/428836085): Check inner properties of the keep rules, holder, type and name may have
+ // to be preserved.
+ return keepInfoCollectionExported.getKeepClassInfo(type.asTypeReference()) != null;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ReflectiveEvent.java b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ReflectiveEvent.java
new file mode 100644
index 0000000..dc49336
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ReflectiveEvent.java
@@ -0,0 +1,138 @@
+// Copyright (c) 2025, 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.assistant.postprocessing.model;
+
+import com.android.tools.r8.assistant.runtime.ReflectiveEventType;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.shaking.KeepInfoCollectionExported;
+import com.android.tools.r8.utils.DescriptorUtils;
+import java.util.Arrays;
+
+public abstract class ReflectiveEvent {
+
+ private final ReflectiveEventType eventType;
+ private final String[] stack;
+
+ protected static DexType toTypeOrTripleStar(String javaType, DexItemFactory factory) {
+ if (javaType == null) {
+ return factory.createType("***");
+ }
+ return toType(javaType, factory);
+ }
+
+ protected static DexType toType(String javaType, DexItemFactory factory) {
+ return factory.createType(DescriptorUtils.javaTypeToDescriptor(javaType));
+ }
+
+ protected ReflectiveEvent(ReflectiveEventType eventType, String[] stack) {
+ this.eventType = eventType;
+ this.stack = stack;
+ }
+
+ public ReflectiveEventType getEventType() {
+ return eventType;
+ }
+
+ public boolean isAtomicFieldUpdaterNewUpdater() {
+ return false;
+ }
+
+ public AtomicFieldUpdaterNewUpdater asAtomicFieldUpdaterNewUpdater() {
+ return null;
+ }
+
+ public boolean isClassGetName() {
+ return false;
+ }
+
+ public ClassGetName asClassGetName() {
+ return null;
+ }
+
+ public boolean isClassGetMember() {
+ return false;
+ }
+
+ public ClassGetMember asClassGetMember() {
+ return null;
+ }
+
+ public boolean isClassGetMembers() {
+ return false;
+ }
+
+ public ClassGetMembers asClassGetMembers() {
+ return null;
+ }
+
+ public abstract String getContentsString();
+
+ public abstract boolean isKeptBy(KeepInfoCollectionExported keepInfoCollectionExported);
+
+ @Override
+ public String toString() {
+ return eventType + (stack != null ? "[s]" : "") + "(" + getContentsString() + ")";
+ }
+
+ public static ReflectiveEvent instantiate(
+ ReflectiveEventType eventType, String[] stack, String[] args, DexItemFactory factory) {
+ switch (eventType) {
+ case CLASS_NEW_INSTANCE:
+ break;
+ case CLASS_GET_DECLARED_METHOD:
+ case CLASS_GET_DECLARED_FIELD:
+ case CLASS_GET_DECLARED_CONSTRUCTOR:
+ case CLASS_GET_METHOD:
+ case CLASS_GET_FIELD:
+ case CLASS_GET_CONSTRUCTOR:
+ return new ClassGetMember(eventType, stack, args, factory);
+ case CLASS_GET_DECLARED_METHODS:
+ case CLASS_GET_DECLARED_FIELDS:
+ case CLASS_GET_DECLARED_CONSTRUCTORS:
+ case CLASS_GET_METHODS:
+ case CLASS_GET_FIELDS:
+ case CLASS_GET_CONSTRUCTORS:
+ return new ClassGetMembers(eventType, stack, args, factory);
+ case CLASS_GET_NAME:
+ return new ClassGetName(eventType, stack, args, factory);
+ case CLASS_FOR_NAME:
+ break;
+ case CLASS_GET_COMPONENT_TYPE:
+ break;
+ case CLASS_GET_PACKAGE:
+ break;
+ case CLASS_IS_ASSIGNABLE_FROM:
+ break;
+ case CLASS_GET_SUPERCLASS:
+ break;
+ case CLASS_AS_SUBCLASS:
+ break;
+ case CLASS_IS_INSTANCE:
+ break;
+ case CLASS_CAST:
+ break;
+ case CLASS_FLAG:
+ break;
+ case ATOMIC_FIELD_UPDATER_NEW_UPDATER:
+ return new AtomicFieldUpdaterNewUpdater(eventType, stack, args, factory);
+ case SERVICE_LOADER_LOAD:
+ break;
+ case PROXY_NEW_PROXY_INSTANCE:
+ break;
+ }
+ return new ReflectiveEvent(eventType, stack) {
+ @Override
+ public String getContentsString() {
+ return Arrays.toString(args);
+ }
+
+ @Override
+ public boolean isKeptBy(KeepInfoCollectionExported keepInfoCollectionExported) {
+ return false;
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/cf/CfPrinter.java b/src/main/java/com/android/tools/r8/cf/CfPrinter.java
index 67b2e25..9571467 100644
--- a/src/main/java/com/android/tools/r8/cf/CfPrinter.java
+++ b/src/main/java/com/android/tools/r8/cf/CfPrinter.java
@@ -137,7 +137,7 @@
public CfPrinter(CfCode code, DexEncodedMethod method, RetracerForCodePrinting retracer) {
this.retracer = retracer;
indent = " ";
- instructionIndexSpace = ("" + code.getInstructions().size()).length();
+ instructionIndexSpace = ("" + code.getInstructionCount()).length();
labelToIndex = new Reference2IntOpenHashMap<>();
sortedLabels = new ArrayList<>();
for (CfInstruction instruction : code.getInstructions()) {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfFrameVerifier.java b/src/main/java/com/android/tools/r8/cf/code/CfFrameVerifier.java
index 86ea3c0..afb1260 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfFrameVerifier.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfFrameVerifier.java
@@ -97,7 +97,7 @@
// Linear scan over instructions.
CfFrameState state = initialState.asContinue().getValue();
int actualInstructionIndexForReporting = 0;
- for (int i = 0; i < code.getInstructions().size(); i++) {
+ for (int i = 0; i < code.getInstructionCount(); i++) {
CfInstruction instruction = code.getInstruction(i);
assert !state.isError();
if (instruction.isLabel()) {
@@ -125,7 +125,7 @@
state = instruction.evaluate(state, appView, config);
if (instruction.isJumpWithNormalTarget()) {
CfInstruction fallthroughInstruction =
- (i + 1) < code.getInstructions().size() ? code.getInstruction(i + 1) : null;
+ (i + 1) < code.getInstructionCount() ? code.getInstruction(i + 1) : null;
TraversalContinuation<CfCodeDiagnostics, CfFrameState> traversalContinuation =
instruction.traverseNormalTargets(
(target, currentState) -> {
@@ -336,10 +336,10 @@
if (!instruction.isJump()) {
return TraversalContinuation.doContinue(state);
}
- if (instructionIndex == code.getInstructions().size() - 1) {
+ if (instructionIndex == code.getInstructionCount() - 1) {
return TraversalContinuation.doContinue(CfFrameState.bottom());
}
- if (instructionIndex == code.getInstructions().size() - 2
+ if (instructionIndex == code.getInstructionCount() - 2
&& code.getInstruction(instructionIndex + 1).isLabel()) {
return TraversalContinuation.doContinue(CfFrameState.bottom());
}
@@ -375,7 +375,7 @@
if (!isReturnOrThrow) {
return false;
}
- for (int i = code.getInstructions().size() - 1; i >= 0; i--) {
+ for (int i = code.getInstructionCount() - 1; i >= 0; i--) {
CfInstruction instr = code.getInstruction(i);
if (instr == instruction) {
return true;
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 1e5c7d9..da40bbf 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -42,6 +42,7 @@
import com.android.tools.r8.graph.InnerClassAttribute;
import com.android.tools.r8.graph.ObjectToOffsetMapping;
import com.android.tools.r8.metadata.impl.BuildMetadataFactory;
+import com.android.tools.r8.metadata.impl.R8StatsMetadataImpl;
import com.android.tools.r8.naming.KotlinModuleSynthesizer;
import com.android.tools.r8.naming.NamingLens;
import com.android.tools.r8.naming.ProguardMapSupplier.ProguardMapId;
@@ -655,6 +656,8 @@
assert appView.hasClassHierarchy();
options.r8BuildMetadataConsumer.accept(
BuildMetadataFactory.create(appView.withClassHierarchy(), virtualFiles));
+ } else if (appView.hasClassHierarchy()) {
+ assert R8StatsMetadataImpl.Counters.create(appView.withClassHierarchy()).validate();
}
}
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index b9af38e..0a8556c 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -37,6 +37,7 @@
import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfoFactory;
import com.android.tools.r8.ir.optimize.library.LibraryMemberOptimizer;
import com.android.tools.r8.ir.optimize.library.LibraryMethodSideEffectModelCollection;
+import com.android.tools.r8.ir.optimize.outliner.exceptions.ThrowBlockOutliner;
import com.android.tools.r8.naming.NamingLens;
import com.android.tools.r8.naming.SeedMapper;
import com.android.tools.r8.optimize.MemberRebindingIdentityLens;
@@ -138,6 +139,7 @@
private ArgumentPropagator argumentPropagator;
private final LibraryMemberOptimizer libraryMemberOptimizer;
private final ProtoShrinker protoShrinker;
+ private ThrowBlockOutliner throwBlockOutliner;
// Optimization results.
private boolean allCodeProcessed = false;
@@ -218,6 +220,7 @@
if (enableWholeProgramOptimizations() && options().isOptimizedResourceShrinking()) {
resourceShrinkerState = ResourceShrinkerUtils.createResourceShrinkerState(this);
}
+ this.throwBlockOutliner = ThrowBlockOutliner.create(this);
timing.end();
this.libraryMethodSideEffectModelCollection =
timing.time("Library side-effects", () -> new LibraryMethodSideEffectModelCollection(this));
@@ -586,6 +589,17 @@
}
}
+ public void unsetThrowBlockOutliner() {
+ throwBlockOutliner = null;
+ }
+
+ public <E extends Throwable> void withThrowBlockOutliner(
+ ThrowingConsumer<ThrowBlockOutliner, E> consumer) throws E {
+ if (throwBlockOutliner != null) {
+ consumer.accept(throwBlockOutliner);
+ }
+ }
+
public LibraryMemberOptimizer libraryMethodOptimizer() {
return libraryMemberOptimizer;
}
diff --git a/src/main/java/com/android/tools/r8/graph/CfCode.java b/src/main/java/com/android/tools/r8/graph/CfCode.java
index 0b3838a..a7a6922 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -40,6 +40,7 @@
import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
import com.android.tools.r8.naming.NamingLens;
import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ListUtils;
import com.android.tools.r8.utils.RetracerForCodePrinting;
import com.android.tools.r8.utils.structural.CompareToVisitor;
import com.android.tools.r8.utils.structural.HashingVisitor;
@@ -293,7 +294,11 @@
}
public List<CfInstruction> getInstructions() {
- return Collections.unmodifiableList(instructions);
+ return ListUtils.unmodifiableForTesting(instructions);
+ }
+
+ public int getInstructionCount() {
+ return instructions.size();
}
public void setInstructions(List<CfInstruction> instructions) {
@@ -301,7 +306,7 @@
}
public List<LocalVariableInfo> getLocalVariables() {
- return Collections.unmodifiableList(localVariables);
+ return ListUtils.unmodifiableForTesting(localVariables);
}
@Override
@@ -668,7 +673,7 @@
MutableMethodConversionOptions conversionOptions) {
try {
return internalBuild(
- Collections.unmodifiableList(localVariables),
+ ListUtils.unmodifiableForTesting(localVariables),
context,
method,
appView,
diff --git a/src/main/java/com/android/tools/r8/graph/DefaultInstanceInitializerCode.java b/src/main/java/com/android/tools/r8/graph/DefaultInstanceInitializerCode.java
index 229e8c0..a19de7a 100644
--- a/src/main/java/com/android/tools/r8/graph/DefaultInstanceInitializerCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DefaultInstanceInitializerCode.java
@@ -124,7 +124,7 @@
|| !cfCode.getTryCatchRanges().isEmpty()) {
return false;
}
- if (cfCode.getInstructions().size() > 6) {
+ if (cfCode.getInstructionCount() > 6) {
// Default instance initializers typically have the following instruction sequence:
// [CfLabel, CfPosition, CfLoad, CfInvoke, CfReturnVoid, CfLabel].
return false;
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index 40e474c..3b71570 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -7,6 +7,7 @@
import com.android.tools.r8.dex.IndexedItemCollection;
import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
import com.android.tools.r8.ir.analysis.type.DynamicType;
import com.android.tools.r8.ir.analysis.type.DynamicTypeWithUpperBound;
import com.android.tools.r8.ir.analysis.type.Nullability;
@@ -85,10 +86,22 @@
return DynamicType.create(appView, toTypeElement(appView, nullability));
}
+ public ClassTypeElement toNonNullClassTypeElement(AppView<?> appView) {
+ return toClassTypeElement(appView, Nullability.definitelyNotNull());
+ }
+
public TypeElement toNonNullTypeElement(AppView<?> appView) {
return toTypeElement(appView, Nullability.definitelyNotNull());
}
+ public ClassTypeElement toClassTypeElement(AppView<?> appView) {
+ return toClassTypeElement(appView, Nullability.maybeNull());
+ }
+
+ public ClassTypeElement toClassTypeElement(AppView<?> appView, Nullability nullability) {
+ return TypeElement.fromDexClassType(this, nullability, appView);
+ }
+
public TypeElement toTypeElement(AppView<?> appView) {
return toTypeElement(appView, Nullability.maybeNull());
}
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/ClassInitializerAssertionEnablingAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/ClassInitializerAssertionEnablingAnalysis.java
index 5f99150..77a4cd8 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/ClassInitializerAssertionEnablingAnalysis.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/ClassInitializerAssertionEnablingAnalysis.java
@@ -174,8 +174,8 @@
@SuppressWarnings("ReferenceEquality")
private boolean hasJavacClinitAssertionCode(CfCode code) {
- for (int i = 0; i < code.getInstructions().size(); i++) {
- CfInstruction instruction = code.getInstructions().get(i);
+ for (int i = 0; i < code.getInstructionCount(); i++) {
+ CfInstruction instruction = code.getInstruction(i);
if (instruction.isInvoke()) {
// Check for the generated instruction sequence by looking for the call to
// desiredAssertionStatus() followed by the expected instruction types and finally checking
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventClassMethodAndDefaultMethodCollisions.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventClassMethodAndDefaultMethodCollisions.java
index 064b2e0..9a07dbb 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventClassMethodAndDefaultMethodCollisions.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventClassMethodAndDefaultMethodCollisions.java
@@ -1,7 +1,6 @@
// Copyright (c) 2020, 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.horizontalclassmerging.policies;
import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
@@ -159,7 +158,11 @@
DexMethodSignatureSet signatures = DexMethodSignatureSet.createLinked();
for (DexProgramClass clazz : group) {
- signatures.addAllMethods(clazz.methods());
+ for (DexEncodedMethod method : clazz.methods()) {
+ if (!method.isInitializer()) {
+ signatures.add(method);
+ }
+ }
}
Map<DispatchSignature, HorizontalMergeGroup> newGroups = new LinkedHashMap<>();
@@ -182,10 +185,4 @@
}
return removeTrivialGroups(newGroups.values());
}
-
- @Override
- public boolean shouldSkipPolicy() {
- return appView.options().isGeneratingDex()
- && !appView.options().canUseDefaultAndStaticInterfaceMethods();
- }
}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/framework/intraprocedural/cf/CfBlock.java b/src/main/java/com/android/tools/r8/ir/analysis/framework/intraprocedural/cf/CfBlock.java
index dc0c539..b12e8cb 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/framework/intraprocedural/cf/CfBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/framework/intraprocedural/cf/CfBlock.java
@@ -41,8 +41,8 @@
public CfInstruction getFallthroughInstruction(CfCode code) {
int fallthroughInstructionIndex = getLastInstructionIndex() + 1;
- return fallthroughInstructionIndex < code.getInstructions().size()
- ? code.getInstructions().get(fallthroughInstructionIndex)
+ return fallthroughInstructionIndex < code.getInstructionCount()
+ ? code.getInstruction(fallthroughInstructionIndex)
: null;
}
@@ -59,7 +59,7 @@
}
public CfInstruction getLastInstruction(CfCode code) {
- return code.getInstructions().get(lastInstructionIndex);
+ return code.getInstruction(lastInstructionIndex);
}
public int getLastInstructionIndex() {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/framework/intraprocedural/cf/CfControlFlowGraph.java b/src/main/java/com/android/tools/r8/ir/analysis/framework/intraprocedural/cf/CfControlFlowGraph.java
index 919e8d7..2d66d50 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/framework/intraprocedural/cf/CfControlFlowGraph.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/framework/intraprocedural/cf/CfControlFlowGraph.java
@@ -281,7 +281,7 @@
}
private void removeBlockForTrailingLabel() {
- CfInstruction lastInstruction = code.getInstruction(code.getInstructions().size() - 1);
+ CfInstruction lastInstruction = code.getInstruction(code.getInstructionCount() - 1);
if (lastInstruction.isLabel() && isBlockEntry(lastInstruction)) {
blocks.remove(lastInstruction);
}
@@ -292,7 +292,7 @@
}
private boolean isBlockExit(int instructionIndex) {
- int lastInstructionIndex = code.getInstructions().size() - 1;
+ int lastInstructionIndex = code.getInstructionCount() - 1;
if (instructionIndex == lastInstructionIndex) {
return true;
}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeElement.java
index 3f1fc3d..2b4ce3b 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeElement.java
@@ -432,6 +432,15 @@
return fromDexType(appView.dexItemFactory().stringType, nullability, appView).asClassType();
}
+ public static ClassTypeElement fromDexClassType(
+ DexType type, Nullability nullability, AppView<?> appView) {
+ assert type.isClassType();
+ return appView
+ .dexItemFactory()
+ .createReferenceTypeElement(type, nullability, appView)
+ .asClassType();
+ }
+
public static TypeElement fromDexType(DexType type, Nullability nullability, AppView<?> appView) {
return fromDexType(type, nullability, appView, false);
}
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 4059618..c21dc95 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
@@ -58,6 +58,7 @@
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
+import java.util.stream.Stream;
/**
* Basic block abstraction.
@@ -813,6 +814,10 @@
};
}
+ public Stream<Instruction> streamInstructions() {
+ return getInstructions().stream();
+ }
+
public boolean isEmpty() {
return instructions.isEmpty();
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/DefaultInstructionVisitor.java b/src/main/java/com/android/tools/r8/ir/code/DefaultInstructionVisitor.java
index 2e48a35..d23c1d9 100644
--- a/src/main/java/com/android/tools/r8/ir/code/DefaultInstructionVisitor.java
+++ b/src/main/java/com/android/tools/r8/ir/code/DefaultInstructionVisitor.java
@@ -365,6 +365,11 @@
}
@Override
+ public T visit(ThrowBlockOutlineMarker instruction) {
+ return null;
+ }
+
+ @Override
public T visit(UnusedArgument instruction) {
return null;
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
index 3f61db3..f6fa052 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -12,6 +12,7 @@
import com.android.tools.r8.graph.DebugLocalInfo;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexString;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.ProgramMethod;
@@ -167,6 +168,14 @@
return method;
}
+ public DexMethod reference() {
+ return context().getReference();
+ }
+
+ public boolean isD8R8Synthesized() {
+ return context().getDefinition().isD8R8Synthesized();
+ }
+
@Deprecated
public DexEncodedMethod method() {
return method.getDefinition();
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java b/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
index 9941e6d..80de495 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
@@ -318,6 +318,10 @@
return result;
}
+ public boolean mayHaveThrowBlockOutlineMarker() {
+ return get(Opcodes.THROW_BLOCK_OUTLINE_MARKER);
+ }
+
public boolean mayHaveUshr() {
return get(Opcodes.USHR);
}
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 e410c59..0733f34 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
@@ -99,6 +99,18 @@
return next;
}
+ @SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"})
+ public <T extends Instruction> T nextUntilInclusive(Predicate<Instruction> predicate) {
+ Instruction current = this;
+ do {
+ if (predicate.test(current)) {
+ return (T) current;
+ }
+ current = current.getNext();
+ } while (current != null);
+ return null;
+ }
+
@Override
public final Position getPosition() {
assert position != null;
@@ -1198,6 +1210,14 @@
return null;
}
+ public boolean isThrowBlockOutlineMarker() {
+ return false;
+ }
+
+ public ThrowBlockOutlineMarker asThrowBlockOutlineMarker() {
+ return null;
+ }
+
public boolean isStaticFieldInstruction() {
return false;
}
@@ -1434,6 +1454,10 @@
return null;
}
+ public InvokeDirect asInvokeConstructor(DexItemFactory dexItemFactory) {
+ return null;
+ }
+
public boolean isInvokeConstructor(DexItemFactory dexItemFactory) {
return false;
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionVisitor.java b/src/main/java/com/android/tools/r8/ir/code/InstructionVisitor.java
index d54b02f..16f398f 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstructionVisitor.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionVisitor.java
@@ -148,6 +148,8 @@
T visit(Throw instruction);
+ T visit(ThrowBlockOutlineMarker instruction);
+
T visit(UnusedArgument instruction);
T visit(Ushr instruction);
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 c448a2d..7ec0d42 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
@@ -124,6 +124,11 @@
}
@Override
+ public InvokeDirect asInvokeConstructor(DexItemFactory dexItemFactory) {
+ return isInvokeConstructor(dexItemFactory) ? this : null;
+ }
+
+ @Override
public boolean isInvokeConstructor(DexItemFactory dexItemFactory) {
return getInvokedMethod().isInstanceInitializer(dexItemFactory);
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/Opcodes.java b/src/main/java/com/android/tools/r8/ir/code/Opcodes.java
index 64d8f37..a0bc2f2 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Opcodes.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Opcodes.java
@@ -80,4 +80,5 @@
int RESOURCE_CONST_NUMBER = 71;
int ORIGINAL_FIELD_WITNESS = 72;
int STORE_STORE_FENCE = 73;
+ int THROW_BLOCK_OUTLINE_MARKER = 74;
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/ThrowBlockOutlineMarker.java b/src/main/java/com/android/tools/r8/ir/code/ThrowBlockOutlineMarker.java
new file mode 100644
index 0000000..bd119cb
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/code/ThrowBlockOutlineMarker.java
@@ -0,0 +1,135 @@
+// Copyright (c) 2025, 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.code;
+
+import com.android.tools.r8.cf.LoadStoreHelper;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.conversion.CfBuilder;
+import com.android.tools.r8.ir.conversion.DexBuilder;
+import com.android.tools.r8.ir.optimize.DeadCodeRemover.DeadInstructionResult;
+import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
+import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.ir.optimize.outliner.exceptions.ThrowBlockOutline;
+import com.android.tools.r8.lightir.LirBuilder;
+
+public class ThrowBlockOutlineMarker extends Instruction {
+
+ private final ThrowBlockOutline outline;
+
+ public ThrowBlockOutlineMarker(ThrowBlockOutline outline) {
+ super(null);
+ this.outline = outline;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public ThrowBlockOutline getOutline() {
+ return outline;
+ }
+
+ @Override
+ public DeadInstructionResult canBeDeadCode(AppView<?> appView, IRCode code) {
+ return DeadInstructionResult.notDead();
+ }
+
+ @Override
+ public int opcode() {
+ return Opcodes.THROW_BLOCK_OUTLINE_MARKER;
+ }
+
+ @Override
+ public <T> T accept(InstructionVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+
+ @Override
+ public void buildCf(CfBuilder builder) {
+ throw new Unreachable();
+ }
+
+ @Override
+ public void buildLir(LirBuilder<Value, ?> builder) {
+ builder.addThrowBlockOutlineMarker(outline);
+ }
+
+ @Override
+ public void buildDex(DexBuilder builder) {
+ throw new Unreachable();
+ }
+
+ @Override
+ public boolean hasInvariantOutType() {
+ return true;
+ }
+
+ @Override
+ public ConstraintWithTarget inliningConstraint(
+ InliningConstraints inliningConstraints, ProgramMethod context) {
+ throw new Unreachable();
+ }
+
+ @Override
+ public void insertLoadAndStores(LoadStoreHelper helper) {
+ throw new Unreachable();
+ }
+
+ @Override
+ public boolean instructionMayTriggerMethodInvocation(AppView<?> appView, ProgramMethod context) {
+ return false;
+ }
+
+ @Override
+ public boolean isThrowBlockOutlineMarker() {
+ return true;
+ }
+
+ @Override
+ public ThrowBlockOutlineMarker asThrowBlockOutlineMarker() {
+ return this;
+ }
+
+ @Override
+ public int maxInValueRegister() {
+ throw new Unreachable();
+ }
+
+ @Override
+ public int maxOutValueRegister() {
+ throw new Unreachable();
+ }
+
+ @Override
+ public String toString() {
+ return "ThrowBlockOutlineMarker";
+ }
+
+ @Override
+ public boolean identicalNonValueNonPositionParts(Instruction other) {
+ return false;
+ }
+
+ public static class Builder extends BuilderBase<Builder, ThrowBlockOutlineMarker> {
+
+ private ThrowBlockOutline outline;
+
+ public Builder setOutline(ThrowBlockOutline outline) {
+ this.outline = outline;
+ return this;
+ }
+
+ @Override
+ public ThrowBlockOutlineMarker build() {
+ return amend(new ThrowBlockOutlineMarker(outline));
+ }
+
+ @Override
+ public Builder self() {
+ return this;
+ }
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
index 54e7d5c..a3bfb70 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
@@ -231,8 +231,8 @@
this.method = method;
this.appView = appView;
int cfPositionCount = 0;
- for (int i = 0; i < code.getInstructions().size(); i++) {
- CfInstruction instruction = code.getInstructions().get(i);
+ for (int i = 0; i < code.getInstructionCount(); i++) {
+ CfInstruction instruction = code.getInstruction(i);
if (instruction instanceof CfLabel) {
labelOffsets.put((CfLabel) instruction, instructionOffset(i));
}
@@ -274,7 +274,7 @@
@Override
public int instructionCount() {
- return code.getInstructions().size();
+ return code.getInstructionCount();
}
@Override
@@ -300,7 +300,7 @@
@Override
public int traceInstruction(int instructionIndex, IRBuilder builder) {
- CfInstruction instruction = code.getInstructions().get(instructionIndex);
+ CfInstruction instruction = code.getInstruction(instructionIndex);
assert appView.options().isGeneratingClassFiles()
== internalOutputMode.isGeneratingClassFiles();
if (instruction.canThrow()) {
@@ -355,7 +355,7 @@
}
private int[] getTargets(int instructionIndex) {
- CfInstruction instruction = code.getInstructions().get(instructionIndex);
+ CfInstruction instruction = code.getInstruction(instructionIndex);
assert isControlFlow(instruction);
CfLabel target = instruction.getTarget();
if (instruction.isReturn() || instruction instanceof CfThrow) {
@@ -517,7 +517,7 @@
// ensure it is marked as such (via an explict 'end' marker) and thus be live in predecessors.
// In this case we insert an 'end' point on all explicit goto instructions ensuring that any
// back-edge will explicitly keep locals live at that point.
- if (!hasExitingInstruction && code.getInstructions().get(predecessorOffset) instanceof CfGoto) {
+ if (!hasExitingInstruction && code.getInstruction(predecessorOffset) instanceof CfGoto) {
assert !isExceptional;
for (Entry<DebugLocalInfo> entry : atSource.int2ReferenceEntrySet()) {
if (atTarget.get(entry.getIntKey()) == entry.getValue()) {
@@ -535,7 +535,7 @@
buildExceptionalExitForMethodSynchronization(builder, instructionIndex);
return;
}
- CfInstruction instruction = code.getInstructions().get(instructionIndex);
+ CfInstruction instruction = code.getInstruction(instructionIndex);
currentInstructionIndex = instructionIndex;
if (firstBlockInstruction) {
currentBlockInfo = builder.getCFG().get(instructionIndex);
@@ -647,7 +647,7 @@
private boolean isFirstFrameInBlock() {
for (int i = currentBlockIndex; i < currentInstructionIndex; i++) {
- CfInstruction cfInstruction = code.getInstructions().get(i);
+ CfInstruction cfInstruction = code.getInstruction(i);
if (cfInstruction.isPosition() || cfInstruction.isLabel()) {
continue;
}
@@ -676,8 +676,8 @@
if (type.isUninitializedNew()) {
int labelOffset = getLabelOffset(type.getUninitializedLabel());
int insnOffset = labelOffset + 1;
- while (insnOffset < code.getInstructions().size()) {
- CfInstruction instruction = code.getInstructions().get(insnOffset);
+ while (insnOffset < code.getInstructionCount()) {
+ CfInstruction instruction = code.getInstruction(insnOffset);
if (!(instruction instanceof CfLabel)
&& !(instruction instanceof CfFrame)
&& !(instruction instanceof CfPosition)) {
@@ -686,7 +686,7 @@
}
insnOffset += 1;
}
- CfInstruction instruction = code.getInstructions().get(insnOffset);
+ CfInstruction instruction = code.getInstruction(insnOffset);
assert instruction instanceof CfNew;
return ((CfNew) instruction).getType();
}
@@ -791,7 +791,7 @@
if (inPrelude) {
return getIncomingLocal(register);
}
- assert !isControlFlow(code.getInstructions().get(currentInstructionIndex))
+ assert !isControlFlow(code.getInstruction(currentInstructionIndex))
: "Outgoing local is undefined for control-flow instructions";
return outgoingLocals.get(register);
}
@@ -802,7 +802,7 @@
outgoingLocals = incomingLocals;
return;
}
- CfInstruction currentInstruction = code.getInstructions().get(currentInstructionIndex);
+ CfInstruction currentInstruction = code.getInstruction(currentInstructionIndex);
outgoingLocals =
!isControlFlow(currentInstruction)
? getLocalVariables(currentInstructionIndex + 1).locals
@@ -897,7 +897,7 @@
return isCurrentlyGeneratingMethodSynchronization()
// In the prelude we may be materializing arguments from call sites in R8.
|| inPrelude
- || code.getInstructions().get(currentInstructionIndex).canThrow();
+ || code.getInstruction(currentInstructionIndex).canThrow();
}
@Override
@@ -922,20 +922,20 @@
.collect(Collectors.toList()),
method.getReference());
}
- while (offset + 1 < code.getInstructions().size()) {
- CfInstruction insn = code.getInstructions().get(offset);
+ while (offset + 1 < code.getInstructionCount()) {
+ CfInstruction insn = code.getInstruction(offset);
if (!(insn instanceof CfLabel) && !(insn instanceof CfFrame)) {
break;
}
offset += 1;
}
- while (offset >= 0 && !(code.getInstructions().get(offset) instanceof CfPosition)) {
+ while (offset >= 0 && !(code.getInstruction(offset) instanceof CfPosition)) {
offset -= 1;
}
if (offset < 0) {
return canonicalPositions.getPreamblePosition();
}
- return getCanonicalPosition(((CfPosition) code.getInstructions().get(offset)).getPosition());
+ return getCanonicalPosition(((CfPosition) code.getInstruction(offset)).getPosition());
}
@Override
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 8d39236..a83f45f 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
@@ -816,6 +816,8 @@
assert code.verifyNoNullabilityBottomTypes();
assert code.verifyTypes(appView);
+ appView.withThrowBlockOutliner(outliner -> outliner.scan(code));
+
previous = printMethod(code, "Optimized IR (SSA)", previous);
timing.begin("Finalize IR");
finalizeIR(code, feedback, bytecodeMetadataProviderBuilder.build(), timing, previous);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/MethodConversionOptions.java b/src/main/java/com/android/tools/r8/ir/conversion/MethodConversionOptions.java
index f514b6e..51ef202 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/MethodConversionOptions.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/MethodConversionOptions.java
@@ -90,7 +90,7 @@
public static class MutableMethodConversionOptions extends MethodConversionOptions {
- private final Target target;
+ private Target target;
private boolean finalizeAfterLensCodeRewriter;
private MutableMethodConversionOptions(Target target) {
@@ -102,6 +102,12 @@
return this;
}
+ public MutableMethodConversionOptions setIsGeneratingLir() {
+ assert isGeneratingDex();
+ target = Target.LIR;
+ return this;
+ }
+
@Override
public boolean isGeneratingLir() {
return target == Target.LIR;
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryD8L8IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryD8L8IRConverter.java
index 29925a7..5f63174 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryD8L8IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryD8L8IRConverter.java
@@ -281,6 +281,9 @@
ClassConverter.create(appView, this, methodProcessor, interfaceProcessor)
.convertClasses(executorService, timing);
+ // Process computed throw outlines.
+ appView.withThrowBlockOutliner(outliner -> outliner.tearDownScanner(executorService));
+
// The synthesis of accessibility bridges in nest based access desugaring will schedule and
// await the processing of synthesized methods.
instructionDesugaring.processClasspath(methodProcessor, executorService, timing);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutline.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutline.java
new file mode 100644
index 0000000..dc871e1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutline.java
@@ -0,0 +1,101 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.optimize.outliner.exceptions;
+
+import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.lightir.LirCode;
+import com.android.tools.r8.lightir.LirConstant;
+import com.android.tools.r8.synthesis.SyntheticItems;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.HashingVisitor;
+import com.google.common.collect.ConcurrentHashMultiset;
+import com.google.common.collect.Multiset;
+
+public class ThrowBlockOutline implements LirConstant {
+
+ @SuppressWarnings("UnusedVariable")
+ private final LirCode<?> lirCode;
+ private final Multiset<DexMethod> users = ConcurrentHashMultiset.create();
+
+ private ProgramMethod materializedOutlineMethod;
+
+ ThrowBlockOutline(LirCode<?> lirCode) {
+ this.lirCode = lirCode;
+ }
+
+ public void addUser(DexMethod user) {
+ users.add(user);
+ }
+
+ @Override
+ public LirConstantOrder getLirConstantOrder() {
+ return LirConstantOrder.THROW_BLOCK_OUTLINE;
+ }
+
+ public ProgramMethod getMaterializedOutlineMethod() {
+ return materializedOutlineMethod;
+ }
+
+ public int getNumberOfUsers() {
+ return users.size();
+ }
+
+ public ProgramMethod getSynthesizingContext(AppView<?> appView) {
+ DexMethod shortestUser = null;
+ for (DexMethod user : users) {
+ if (shortestUser == null) {
+ shortestUser = user;
+ } else {
+ int userLength = user.getHolderType().getDescriptor().length();
+ int shortestUserLength = shortestUser.getHolderType().getDescriptor().length();
+ if (userLength < shortestUserLength) {
+ shortestUser = user;
+ } else if (userLength == shortestUserLength && user.compareTo(shortestUser) < 0) {
+ shortestUser = user;
+ }
+ }
+ }
+ assert shortestUser != null;
+ return appView.definitionFor(shortestUser).asProgramMethod();
+ }
+
+ public Multiset<DexMethod> getUsers() {
+ return users;
+ }
+
+ @Override
+ public int internalLirConstantAcceptCompareTo(LirConstant other, CompareToVisitor visitor) {
+ throw new Unreachable();
+ }
+
+ @Override
+ public void internalLirConstantAcceptHashing(HashingVisitor visitor) {
+ throw new Unreachable();
+ }
+
+ public boolean isMaterialized() {
+ return materializedOutlineMethod != null;
+ }
+
+ public void materialize(AppView<?> appView, MethodProcessingContext methodProcessingContext) {
+ SyntheticItems syntheticItems = appView.getSyntheticItems();
+ DexProto emptyProto = appView.dexItemFactory().objectMembers.constructor.getProto();
+ materializedOutlineMethod =
+ syntheticItems.createMethod(
+ kinds -> kinds.THROW_BLOCK_OUTLINE,
+ methodProcessingContext.createUniqueContext(),
+ appView,
+ builder ->
+ builder
+ .setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
+ .setCode(methodSig -> lirCode)
+ .setProto(emptyProto));
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlineMarkerRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlineMarkerRewriter.java
new file mode 100644
index 0000000..e69cdba
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlineMarkerRewriter.java
@@ -0,0 +1,111 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.optimize.outliner.exceptions;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.BasicBlockInstructionListIterator;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.Return;
+import com.android.tools.r8.ir.code.Throw;
+import com.android.tools.r8.ir.code.ThrowBlockOutlineMarker;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.IRFinalizer;
+import com.android.tools.r8.ir.optimize.DeadCodeRemover;
+import com.android.tools.r8.lightir.LirCode;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.timing.Timing;
+
+/** Rewriter that processes {@link ThrowBlockOutlineMarker} instructions. */
+public class ThrowBlockOutlineMarkerRewriter {
+
+ private final AppView<?> appView;
+ private final DeadCodeRemover deadCodeRemover;
+
+ ThrowBlockOutlineMarkerRewriter(AppView<?> appView) {
+ this.appView = appView;
+ this.deadCodeRemover = new DeadCodeRemover(appView);
+ }
+
+ public void processMethod(ProgramMethod method) {
+ assert method.getDefinition().hasCode();
+ assert method.getDefinition().getCode().isLirCode();
+ // Build IR.
+ LirCode<?> lirCode = method.getDefinition().getCode().asLirCode();
+ IRCode code = lirCode.buildIR(method, appView);
+ assert code.getConversionOptions().isGeneratingDex();
+
+ // Process IR.
+ processOutlineMarkers(code);
+
+ // Convert to DEX.
+ IRFinalizer<?> finalizer = code.getConversionOptions().getFinalizer(deadCodeRemover, appView);
+ Code dexCode = finalizer.finalizeCode(code, BytecodeMetadataProvider.empty(), Timing.empty());
+ method.setCode(dexCode, appView);
+ }
+
+ // TODO(b/434769547): This simply removes all outline markers. We should materialize the outlines
+ // that have enough uses and rewrite the corresponding markers to call the materialized outline
+ // methods.
+ private void processOutlineMarkers(IRCode code) {
+ for (BasicBlock block : code.getBlocks()) {
+ Throw throwInstruction = block.exit().asThrow();
+ if (throwInstruction != null) {
+ ThrowBlockOutlineMarker outlineMarker =
+ block.entry().nextUntilInclusive(Instruction::isThrowBlockOutlineMarker);
+ if (outlineMarker != null) {
+ ThrowBlockOutline outline = outlineMarker.getOutline();
+ if (outline.isMaterialized()) {
+ // Insert a call to the materialized outline method and load the return value.
+ BasicBlockInstructionListIterator instructionIterator =
+ block.listIterator(outlineMarker);
+ instructionIterator.add(
+ InvokeStatic.builder()
+ .setIsInterface(false)
+ .setMethod(outline.getMaterializedOutlineMethod())
+ .setPosition(throwInstruction)
+ .build());
+ Value returnValue = addReturnValue(code, instructionIterator);
+
+ // Replace the throw instruction by a normal return.
+ Return returnInstruction =
+ Return.builder().setPosition(Position.none()).setReturnValue(returnValue).build();
+ block.replaceLastInstruction(returnInstruction);
+
+ // Remove all outlined instructions bottom up.
+ instructionIterator = block.listIterator(returnInstruction);
+ while (instructionIterator.previous() != outlineMarker) {
+ instructionIterator.removeOrReplaceByDebugLocalRead();
+ }
+ }
+
+ // Finally delete the outline marker.
+ outlineMarker.removeOrReplaceByDebugLocalRead();
+ }
+ }
+ assert block.streamInstructions().noneMatch(Instruction::isThrowBlockOutlineMarker);
+ }
+ }
+
+ private Value addReturnValue(IRCode code, BasicBlockInstructionListIterator instructionIterator) {
+ InternalOptions options = appView.options();
+ DexType returnType = code.context().getReturnType();
+ if (returnType.isVoidType()) {
+ return null;
+ } else if (returnType.isPrimitiveType()) {
+ return instructionIterator.insertConstNumberInstruction(
+ code, options, 0, returnType.toTypeElement(appView));
+ } else {
+ assert returnType.isReferenceType();
+ return instructionIterator.insertConstNullInstruction(code, options);
+ }
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutliner.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutliner.java
new file mode 100644
index 0000000..c323bca
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutliner.java
@@ -0,0 +1,164 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.optimize.outliner.exceptions;
+
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
+import com.android.tools.r8.contexts.CompilationContext.ProcessorContext;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.lightir.LirConstant;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.collections.ProgramMethodMap;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import com.google.common.collect.Sets;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
+
+public class ThrowBlockOutliner {
+
+ private final AppView<?> appView;
+
+ // Scans IR code during IR conversion. Responsible for computing candidate outlines.
+ private ThrowBlockOutlinerScanner scanner;
+
+ private ThrowBlockOutliner(AppView<?> appView) {
+ this.appView = appView;
+ this.scanner = new ThrowBlockOutlinerScanner(appView);
+ }
+
+ public static ThrowBlockOutliner create(AppView<?> appView) {
+ return appView.options().getThrowBlockOutlinerOptions().isEnabled(appView)
+ ? new ThrowBlockOutliner(appView)
+ : null;
+ }
+
+ public void scan(IRCode code) {
+ // Notify the scanner.
+ if (scanner != null) {
+ scanner.run(code);
+ }
+ }
+
+ public void tearDownScanner(ExecutorService executorService) throws ExecutionException {
+ // Unset the scanner, which is responsible for computing outline candidates.
+ assert scanner != null;
+ Collection<ThrowBlockOutline> outlines = scanner.getOutlines();
+ scanner = null;
+
+ // Create outlines.
+ materializeOutlines(outlines, executorService);
+ assert supplyOutlineConsumerForTesting(outlines);
+
+ // Convert LIR to DEX.
+ processMethods(outlines, executorService);
+
+ // TODO(b/434769547): Instead of unsetting the outliner here, we should compute a specification
+ // of the outlining that needs to happen and the methods that need to be reprocessed.
+ appView.unsetThrowBlockOutliner();
+ }
+
+ private void materializeOutlines(
+ Collection<ThrowBlockOutline> outlines, ExecutorService executorService)
+ throws ExecutionException {
+ // Find the outlines that we need to synthesize from each method.
+ ProgramMethodMap<List<ThrowBlockOutline>> synthesizingContexts = ProgramMethodMap.create();
+ for (ThrowBlockOutline outline : outlines) {
+ if (outline.getUsers().size() > 1) {
+ ProgramMethod synthesizingContext = outline.getSynthesizingContext(appView);
+ synthesizingContexts
+ .computeIfAbsent(synthesizingContext, ignoreKey(ArrayList::new))
+ .add(outline);
+ }
+ }
+
+ // Sort the outlines per synthesizing context so that the synthesis order is deterministic.
+ // We use the constant pool index of the outline as sorting key.
+ synthesizingContexts.forEach(
+ (synthesizingContext, outlinesFromSynthesizingContext) -> {
+ if (outlinesFromSynthesizingContext.size() == 1) {
+ return;
+ }
+ LirConstant[] constantPool =
+ synthesizingContext.getDefinition().getCode().asLirCode().getConstantPool();
+ Reference2IntMap<ThrowBlockOutline> outlineConstantPoolIndices =
+ new Reference2IntOpenHashMap<>();
+ for (int i = 0; i < constantPool.length; i++) {
+ LirConstant constant = constantPool[i];
+ if (constant instanceof ThrowBlockOutline) {
+ outlineConstantPoolIndices.put((ThrowBlockOutline) constant, i);
+ }
+ }
+ assert outlinesFromSynthesizingContext.stream()
+ .allMatch(outlineConstantPoolIndices::containsKey);
+ ListUtils.destructiveSort(
+ outlinesFromSynthesizingContext,
+ Comparator.comparingInt(outlineConstantPoolIndices::getInt));
+ });
+
+ // Synthesize the outlines concurrently.
+ ProcessorContext processorContext = appView.createProcessorContext();
+ ThreadUtils.processMap(
+ synthesizingContexts,
+ (synthesizingContext, outlinesFromSynthesizingContext) -> {
+ MethodProcessingContext methodProcessingContext =
+ processorContext.createMethodProcessingContext(synthesizingContext);
+ for (ThrowBlockOutline outline : outlinesFromSynthesizingContext) {
+ outline.materialize(appView, methodProcessingContext);
+ }
+ },
+ appView.options().getThreadingModule(),
+ executorService);
+ }
+
+ private void processMethods(
+ Collection<ThrowBlockOutline> outlines, ExecutorService executorService)
+ throws ExecutionException {
+ ProgramMethodSet methodsToProcess = getMethodsToReprocess(outlines);
+ ThrowBlockOutlineMarkerRewriter rewriter = new ThrowBlockOutlineMarkerRewriter(appView);
+ ThreadUtils.processItems(
+ methodsToProcess,
+ rewriter::processMethod,
+ appView.options().getThreadingModule(),
+ executorService);
+ }
+
+ private ProgramMethodSet getMethodsToReprocess(Collection<ThrowBlockOutline> outlines) {
+ ProgramMethodSet methodsToProcess = ProgramMethodSet.create();
+ Set<DexMethod> seenUsers = Sets.newIdentityHashSet();
+ for (ThrowBlockOutline outline : outlines) {
+ for (DexMethod user : outline.getUsers()) {
+ if (seenUsers.add(user)) {
+ ProgramMethod methodToProcess = appView.definitionFor(user).asProgramMethod();
+ methodsToProcess.add(methodToProcess);
+ }
+ }
+ if (outline.getMaterializedOutlineMethod() != null) {
+ methodsToProcess.add(outline.getMaterializedOutlineMethod());
+ }
+ }
+ return methodsToProcess;
+ }
+
+ private boolean supplyOutlineConsumerForTesting(Collection<ThrowBlockOutline> outlines) {
+ Consumer<Collection<ThrowBlockOutline>> consumer =
+ appView.options().getThrowBlockOutlinerOptions().outlineConsumerForTesting;
+ if (consumer != null) {
+ consumer.accept(outlines);
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerLirCodeEquivalence.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerLirCodeEquivalence.java
new file mode 100644
index 0000000..5213ce9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerLirCodeEquivalence.java
@@ -0,0 +1,43 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.optimize.outliner.exceptions;
+
+import com.android.tools.r8.lightir.LirCode;
+import com.google.common.base.Equivalence;
+import java.util.Arrays;
+
+/** An equivalence for the simple type of LirCode produced by the ThrowBlockOutliner. */
+public class ThrowBlockOutlinerLirCodeEquivalence extends Equivalence<LirCode<?>> {
+
+ private static final ThrowBlockOutlinerLirCodeEquivalence INSTANCE =
+ new ThrowBlockOutlinerLirCodeEquivalence();
+
+ public static ThrowBlockOutlinerLirCodeEquivalence get() {
+ return INSTANCE;
+ }
+
+ @Override
+ protected boolean doEquivalent(LirCode<?> lirCode, LirCode<?> other) {
+ assert verifyLirCode(lirCode);
+ assert verifyLirCode(other);
+ return lirCode.getInstructionCount() == other.getInstructionCount()
+ && Arrays.equals(lirCode.getInstructionBytes(), other.getInstructionBytes())
+ && Arrays.equals(lirCode.getConstantPool(), other.getConstantPool());
+ }
+
+ @Override
+ protected int doHash(LirCode<?> lirCode) {
+ assert verifyLirCode(lirCode);
+ return 31 * (31 + Arrays.hashCode(lirCode.getConstantPool()))
+ + Arrays.hashCode(lirCode.getInstructionBytes());
+ }
+
+ private boolean verifyLirCode(LirCode<?> lirCode) {
+ assert !lirCode.hasArguments();
+ assert !lirCode.hasDebugLocalInfoTable();
+ assert !lirCode.hasPositionTable();
+ assert !lirCode.hasTryCatchTable();
+ return true;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerOptions.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerOptions.java
new file mode 100644
index 0000000..ec77462
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerOptions.java
@@ -0,0 +1,26 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.optimize.outliner.exceptions;
+
+import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.graph.AppView;
+import java.util.Collection;
+import java.util.function.Consumer;
+
+public class ThrowBlockOutlinerOptions {
+
+ public boolean enable = false;
+
+ public Consumer<Collection<ThrowBlockOutline>> outlineConsumerForTesting = null;
+
+ public boolean isEnabled(AppView<?> appView) {
+ if (!appView.options().isGeneratingDex() || !enable) {
+ return false;
+ }
+ if (appView.enableWholeProgramOptimizations()) {
+ throw new Unimplemented();
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerScanner.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerScanner.java
new file mode 100644
index 0000000..ca72989
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerScanner.java
@@ -0,0 +1,251 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.optimize.outliner.exceptions;
+
+import static java.util.Collections.emptyList;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.IRMetadata;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.ir.code.NewInstance;
+import com.android.tools.r8.ir.code.NumberGenerator;
+import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.Position.SyntheticPosition;
+import com.android.tools.r8.ir.code.Throw;
+import com.android.tools.r8.ir.code.ThrowBlockOutlineMarker;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.IRToLirFinalizer;
+import com.android.tools.r8.ir.conversion.MethodConversionOptions;
+import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
+import com.android.tools.r8.lightir.LirCode;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.timing.Timing;
+import com.google.common.base.Equivalence.Wrapper;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
+
+public class ThrowBlockOutlinerScanner {
+
+ private static final IRMetadata metadata = IRMetadata.unknown();
+
+ private final AppView<?> appView;
+ private final DexItemFactory factory;
+
+ private final Map<Wrapper<LirCode<?>>, ThrowBlockOutline> outlines = new ConcurrentHashMap<>();
+
+ ThrowBlockOutlinerScanner(AppView<?> appView) {
+ this.appView = appView;
+ this.factory = appView.dexItemFactory();
+ }
+
+ public void run(IRCode code) {
+ assert !code.metadata().mayHaveThrowBlockOutlineMarker();
+ for (BasicBlock block : getThrowBlocks(code)) {
+ processThrowBlock(code, block);
+ }
+ if (code.metadata().mayHaveThrowBlockOutlineMarker()) {
+ assert code.getConversionOptions().isGeneratingDex();
+ code.mutateConversionOptions(MutableMethodConversionOptions::setIsGeneratingLir);
+ } else {
+ assert code.streamInstructions().noneMatch(Instruction::isThrowBlockOutlineMarker);
+ }
+ }
+
+ public Collection<ThrowBlockOutline> getOutlines() {
+ return outlines.values();
+ }
+
+ private List<BasicBlock> getThrowBlocks(IRCode code) {
+ boolean seenReturn = false;
+ List<BasicBlock> throwBlocks = new ArrayList<>();
+ for (BasicBlock block : code.getBlocks()) {
+ if (block.exit().isReturn()) {
+ seenReturn = true;
+ } else if (block.exit().isThrow()) {
+ throwBlocks.add(block);
+ }
+ }
+ // Never outline from methods that always throw.
+ return seenReturn ? throwBlocks : emptyList();
+ }
+
+ private void processThrowBlock(IRCode code, BasicBlock block) {
+ // Recursively build up the outline method. On successful outline creation, the resulting
+ // LirCode is passed to the continuation function.
+ processThrowInstruction(
+ block,
+ block.exit().asThrow(),
+ outlineBuilder -> {
+ // On successful outline creation, store the outline for later processing.
+ LirCode<?> lirCode = outlineBuilder.build(appView, code.context());
+ Wrapper<LirCode<?>> lirCodeWrapper =
+ ThrowBlockOutlinerLirCodeEquivalence.get().wrap(lirCode);
+ ThrowBlockOutline outline =
+ outlines.computeIfAbsent(lirCodeWrapper, w -> new ThrowBlockOutline(w.get()));
+ outline.addUser(code.reference());
+
+ // Insert a synthetic marker instruction that references the outline so that we know where
+ // to materialize the outline call.
+ Instruction insertionPoint = outlineBuilder.getFirstOutlinedInstruction();
+ assert insertionPoint.getBlock() == block;
+ ThrowBlockOutlineMarker marker =
+ ThrowBlockOutlineMarker.builder()
+ .setOutline(outline)
+ .setPosition(Position.none())
+ .build();
+ block.listIterator(insertionPoint).add(marker);
+ });
+ }
+
+ private void processThrowInstruction(
+ BasicBlock throwBlock, Throw throwInstruction, Consumer<OutlineBuilder> continuation) {
+ Value exceptionValue = throwInstruction.exception();
+ if (!exceptionValue.isDefinedByInstructionSatisfying(
+ i -> i.isNewInstance() && i.getBlock() == throwBlock)) {
+ // Exception is not created in the throw block.
+ return;
+ }
+ assert throwInstruction.hasPrev();
+ processExceptionConstructorCall(
+ throwBlock,
+ throwInstruction.getPrev(),
+ outlineBuilder -> {
+ Value outlinedExceptionValue = outlineBuilder.getOutlinedValue(exceptionValue);
+ outlineBuilder.add(
+ Throw.builder()
+ .setExceptionValue(outlinedExceptionValue)
+ .setPosition(Position.syntheticNone())
+ .build());
+ continuation.accept(outlineBuilder);
+ });
+ }
+
+ private void processExceptionConstructorCall(
+ BasicBlock throwBlock, Instruction instruction, Consumer<OutlineBuilder> continuation) {
+ InvokeDirect invoke = instruction.asInvokeConstructor(factory);
+ if (invoke == null) {
+ return;
+ }
+ DexMethod constructor = invoke.getInvokedMethod();
+ if (!constructor.getParameters().isEmpty()) {
+ // TODO(b/434769547): Handle constructors with arguments.
+ return;
+ }
+ processNewExceptionInstruction(
+ throwBlock,
+ invoke.getPrev(),
+ outlineBuilder -> {
+ Value outlinedExceptionValue = outlineBuilder.getOutlinedValue(invoke.getReceiver());
+ outlineBuilder.add(
+ InvokeDirect.builder()
+ .setMethod(constructor)
+ .setPosition(Position.syntheticNone())
+ .setSingleArgument(outlinedExceptionValue)
+ .build());
+ continuation.accept(outlineBuilder);
+ });
+ }
+
+ private void processNewExceptionInstruction(
+ BasicBlock throwBlock, Instruction instruction, Consumer<OutlineBuilder> continuation) {
+ NewInstance newInstance = instruction.asNewInstance();
+ if (newInstance == null) {
+ return;
+ }
+ // Check that this is the thrown exception.
+ if (newInstance.outValue() != throwBlock.exit().asThrow().exception()) {
+ return;
+ }
+ OutlineBuilder outlineBuilder = new OutlineBuilder(newInstance);
+ NewInstance outlinedNewInstance =
+ NewInstance.builder()
+ .setFreshOutValue(
+ outlineBuilder.valueNumberGenerator,
+ newInstance.getType().toNonNullClassTypeElement(appView))
+ .setType(newInstance.getType())
+ .setPosition(Position.syntheticNone())
+ .build();
+ outlineBuilder.add(outlinedNewInstance);
+ outlineBuilder.map(newInstance.outValue(), outlinedNewInstance.outValue());
+ continuation.accept(outlineBuilder);
+ }
+
+ private static class OutlineBuilder {
+
+ private final Instruction firstOutlinedInstruction;
+
+ private final BasicBlock outlinedBlock = new BasicBlock(metadata);
+
+ // Map from non-outlined values to their corresponding outlined values.
+ private final Map<Value, Value> outlinedValues = new IdentityHashMap<>();
+
+ private final NumberGenerator blockNumberGenerator = new NumberGenerator();
+ private final NumberGenerator valueNumberGenerator = new NumberGenerator();
+
+ OutlineBuilder(Instruction firstOutlinedInstruction) {
+ this.firstOutlinedInstruction = firstOutlinedInstruction;
+ outlinedBlock.setNumber(blockNumberGenerator.next());
+ }
+
+ void add(Instruction instruction) {
+ outlinedBlock.add(instruction, metadata);
+ }
+
+ void map(Value value, Value outlinedValue) {
+ assert !outlinedValues.containsKey(value);
+ outlinedValues.put(value, outlinedValue);
+ }
+
+ Instruction getFirstOutlinedInstruction() {
+ return firstOutlinedInstruction;
+ }
+
+ Value getOutlinedValue(Value value) {
+ Value outlinedValue = outlinedValues.get(value);
+ assert outlinedValue != null;
+ return outlinedValue;
+ }
+
+ LirCode<?> build(AppView<?> appView, ProgramMethod context) {
+ outlinedBlock.setFilled();
+ IRCode outlineCode =
+ new IRCode(
+ appView.options(),
+ null,
+ SyntheticPosition.syntheticNone(),
+ ListUtils.newLinkedList(outlinedBlock),
+ valueNumberGenerator,
+ blockNumberGenerator,
+ metadata,
+ MethodConversionOptions.forLirPhase(appView)) {
+
+ @Override
+ public DexMethod reference() {
+ return context.getReference();
+ }
+
+ @Override
+ public boolean isD8R8Synthesized() {
+ return true;
+ }
+ };
+ LirCode<?> lirCode =
+ new IRToLirFinalizer(appView)
+ .finalizeCode(outlineCode, BytecodeMetadataProvider.empty(), Timing.empty());
+ return lirCode;
+ }
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/lightir/IR2LirConverter.java b/src/main/java/com/android/tools/r8/lightir/IR2LirConverter.java
index a207282..8adf2d3 100644
--- a/src/main/java/com/android/tools/r8/lightir/IR2LirConverter.java
+++ b/src/main/java/com/android/tools/r8/lightir/IR2LirConverter.java
@@ -39,11 +39,7 @@
this.strategy = strategy;
this.bytecodeMetadataProvider = bytecodeMetadataProvider;
this.builder =
- new LirBuilder<>(
- irCode.context().getReference(),
- irCode.context().getDefinition().isD8R8Synthesized(),
- strategy,
- options)
+ new LirBuilder<>(irCode.reference(), irCode.isD8R8Synthesized(), strategy, options)
.prepareForBytecodeInstructionMetadata(bytecodeMetadataProvider.size());
}
diff --git a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
index 01b5401..6890bd0 100644
--- a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
+++ b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
@@ -97,12 +97,14 @@
import com.android.tools.r8.ir.code.StringSwitch;
import com.android.tools.r8.ir.code.Sub;
import com.android.tools.r8.ir.code.Throw;
+import com.android.tools.r8.ir.code.ThrowBlockOutlineMarker;
import com.android.tools.r8.ir.code.Ushr;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.code.ValueType;
import com.android.tools.r8.ir.code.Xor;
import com.android.tools.r8.ir.conversion.ExtraParameter;
import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
+import com.android.tools.r8.ir.optimize.outliner.exceptions.ThrowBlockOutline;
import com.android.tools.r8.lightir.LirBuilder.IntSwitchPayload;
import com.android.tools.r8.lightir.LirBuilder.StringSwitchPayload;
import com.android.tools.r8.lightir.LirCode.PositionEntry;
@@ -856,6 +858,11 @@
}
@Override
+ public void onThrowBlockOutlineMarker(ThrowBlockOutline outline) {
+ addInstruction(new ThrowBlockOutlineMarker(outline));
+ }
+
+ @Override
public void onReturnVoid() {
addInstruction(new Return());
closeCurrentBlock();
diff --git a/src/main/java/com/android/tools/r8/lightir/LirBuilder.java b/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
index 5aedb4f..bdb4c85 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
@@ -33,6 +33,7 @@
import com.android.tools.r8.ir.code.Position;
import com.android.tools.r8.ir.code.Position.SyntheticPosition;
import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.optimize.outliner.exceptions.ThrowBlockOutline;
import com.android.tools.r8.lightir.LirCode.DebugLocalInfoTable;
import com.android.tools.r8.lightir.LirCode.LinePositionEntry;
import com.android.tools.r8.lightir.LirCode.PositionEntry;
@@ -775,6 +776,10 @@
return addOneValueInstruction(LirOpcodes.ATHROW, exception);
}
+ public LirBuilder<V, EV> addThrowBlockOutlineMarker(ThrowBlockOutline outline) {
+ return addOneItemInstruction(LirOpcodes.THROWBLOCKOUTLINEMARKER, outline);
+ }
+
public LirBuilder<V, EV> addReturn(V value) {
return addOneValueInstruction(LirOpcodes.ARETURN, value);
}
diff --git a/src/main/java/com/android/tools/r8/lightir/LirCode.java b/src/main/java/com/android/tools/r8/lightir/LirCode.java
index bf79eea..8bacebb 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirCode.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirCode.java
@@ -476,6 +476,10 @@
return strategyInfo;
}
+ public boolean hasArguments() {
+ return argumentCount > 0;
+ }
+
public int getArgumentCount() {
return argumentCount;
}
@@ -500,6 +504,10 @@
return constants;
}
+ public boolean hasPositionTable() {
+ return positionTable.length > 0;
+ }
+
public PositionEntry[] getPositionTable() {
return positionTable;
}
@@ -512,6 +520,10 @@
return tryCatchTable;
}
+ public boolean hasDebugLocalInfoTable() {
+ return debugLocalInfoTable != null;
+ }
+
public DebugLocalInfoTable<EV> getDebugLocalInfoTable() {
return debugLocalInfoTable;
}
diff --git a/src/main/java/com/android/tools/r8/lightir/LirConstant.java b/src/main/java/com/android/tools/r8/lightir/LirConstant.java
index c889c00..d0ee9d9 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirConstant.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirConstant.java
@@ -26,7 +26,8 @@
FILL_ARRAY,
NAME_COMPUTATION,
RECORD_FIELD_VALUES,
- ORIGINAL_FIELD_WITNESS
+ ORIGINAL_FIELD_WITNESS,
+ THROW_BLOCK_OUTLINE
}
class LirConstantStructuralAcceptor implements StructuralAcceptor<LirConstant> {
diff --git a/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java b/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java
index 39522bf..cd1fed4 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java
@@ -215,6 +215,7 @@
int RESOURCENUMBER = 228;
int ORIGINALFIELDWITNESS = 229;
int STORESTOREFENCE = 230;
+ int THROWBLOCKOUTLINEMARKER = 231;
static String toString(int opcode) {
switch (opcode) {
@@ -558,6 +559,8 @@
return "STRINGSWITCH";
case RESOURCENUMBER:
return "RESOURCENUMBER";
+ case THROWBLOCKOUTLINEMARKER:
+ return "THROWBLOCKOUTLINEMARKER";
default:
throw new Unreachable("Unexpected LIR opcode: " + opcode);
diff --git a/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java b/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
index 77a8ed6..5ab0417 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
@@ -18,6 +18,7 @@
import com.android.tools.r8.ir.code.IfType;
import com.android.tools.r8.ir.code.MemberType;
import com.android.tools.r8.ir.code.NumericType;
+import com.android.tools.r8.ir.optimize.outliner.exceptions.ThrowBlockOutline;
import com.android.tools.r8.lightir.LirBuilder.FillArrayPayload;
import com.android.tools.r8.lightir.LirBuilder.IntSwitchPayload;
import com.android.tools.r8.lightir.LirBuilder.NameComputationPayload;
@@ -470,6 +471,10 @@
onInstruction();
}
+ public void onThrowBlockOutlineMarker(ThrowBlockOutline outline) {
+ onInstruction();
+ }
+
public void onReturnVoid() {
onInstruction();
}
@@ -1311,6 +1316,13 @@
onStoreStoreFence(value);
return;
}
+ case LirOpcodes.THROWBLOCKOUTLINEMARKER:
+ {
+ ThrowBlockOutline outline =
+ (ThrowBlockOutline) getConstantItem(view.getNextConstantOperand());
+ onThrowBlockOutlineMarker(outline);
+ return;
+ }
default:
throw new Unimplemented("No dispatch for opcode " + LirOpcodes.toString(opcode));
}
diff --git a/src/main/java/com/android/tools/r8/metadata/impl/R8StatsMetadataImpl.java b/src/main/java/com/android/tools/r8/metadata/impl/R8StatsMetadataImpl.java
index 05b8e2f..4928feb 100644
--- a/src/main/java/com/android/tools/r8/metadata/impl/R8StatsMetadataImpl.java
+++ b/src/main/java/com/android/tools/r8/metadata/impl/R8StatsMetadataImpl.java
@@ -70,7 +70,7 @@
return noShrinkingPercentage;
}
- private static class Counters {
+ public static class Counters {
private int itemsCount = 0;
private int noObfuscationCount = 0;
@@ -79,12 +79,13 @@
private Counters() {}
- static Counters create(AppView<? extends AppInfoWithClassHierarchy> appView) {
+ public static Counters create(AppView<? extends AppInfoWithClassHierarchy> appView) {
Counters counters = new Counters();
for (DexProgramClass clazz : appView.appInfo().classes()) {
counters.add(appView, clazz);
clazz.forEachProgramMember(member -> counters.add(appView, member));
}
+ assert counters.validate();
return counters;
}
@@ -111,9 +112,34 @@
}
float toPercentageWithTwoDecimals(int count) {
- // Multiply by 100 twice to get percentage with two decimals.
- float number = (float) (count * 100 * 100) / itemsCount;
- return (float) Math.round(number) / 100;
+ if (itemsCount == 0) {
+ return 0f;
+ }
+ float fraction = (float) count / itemsCount;
+ assert verifyValidFraction(fraction);
+ float percentage = fraction * 100;
+ assert verifyValidPercentage(percentage);
+ // Multiply and divide by 100 to get percentage with two decimals.
+ return (float) Math.round(percentage * 100) / 100;
+ }
+
+ public boolean validate() {
+ assert verifyValidPercentage(getNoObfuscationPercentage());
+ assert verifyValidPercentage(getNoOptimizationPercentage());
+ assert verifyValidPercentage(getNoShrinkingPercentage());
+ return true;
+ }
+
+ private boolean verifyValidFraction(float f) {
+ assert 0f <= f;
+ assert f <= 1f;
+ return true;
+ }
+
+ private boolean verifyValidPercentage(float f) {
+ assert 0f <= f;
+ assert f <= 100f;
+ return true;
}
}
}
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 7fcedfc..42adbc6 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
@@ -323,7 +323,10 @@
|| !packageDescriptor.equals(target.getHolderType().getPackageDescriptor())) {
DexClass bridgeHolder =
findHolderForVisibilityBridge(originalClass, target.getHolder(), packageDescriptor);
- assert bridgeHolder != null;
+ if (bridgeHolder == null) {
+ // The original class does not have access to the target so we cannot insert a valid bridge.
+ return target.getReference().withHolder(originalClass, appView.dexItemFactory());
+ }
if (bridgeHolder.isClasspathClass()) {
// Intentionally empty. We do not need to insert a bridge on a classpath class.
} else if (bridgeHolder.isLibraryClass()) {
diff --git a/src/main/java/com/android/tools/r8/partial/R8PartialCompilationConfiguration.java b/src/main/java/com/android/tools/r8/partial/R8PartialCompilationConfiguration.java
index 486fe2f..961e95f 100644
--- a/src/main/java/com/android/tools/r8/partial/R8PartialCompilationConfiguration.java
+++ b/src/main/java/com/android/tools/r8/partial/R8PartialCompilationConfiguration.java
@@ -19,9 +19,11 @@
import com.android.tools.r8.utils.ConsumerUtils;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ListUtils;
import com.google.common.base.Splitter;
import java.util.Arrays;
import java.util.Collection;
+import java.util.List;
import java.util.Random;
import java.util.function.Consumer;
@@ -96,6 +98,10 @@
return disabledConfiguration;
}
+ private static List<String> splitPatternList(String patterns) {
+ return ListUtils.map(Splitter.on(",").splitToList(patterns), String::strip);
+ }
+
public static R8PartialCompilationConfiguration fromIncludeExcludePatterns(
String includePatterns, String excludePatterns) {
boolean enabled = includePatterns != null || excludePatterns != null;
@@ -104,10 +110,10 @@
}
Builder builder = builder();
if (includePatterns != null) {
- Splitter.on(",").splitToList(includePatterns).forEach(builder::addJavaTypeIncludePattern);
+ splitPatternList(includePatterns).forEach(builder::addJavaTypeIncludePattern);
}
if (excludePatterns != null) {
- Splitter.on(",").splitToList(excludePatterns).forEach(builder::addJavaTypeExcludePattern);
+ splitPatternList(excludePatterns).forEach(builder::addJavaTypeExcludePattern);
}
return builder.build();
}
diff --git a/src/main/java/com/android/tools/r8/partial/predicate/R8PartialPredicateCollection.java b/src/main/java/com/android/tools/r8/partial/predicate/R8PartialPredicateCollection.java
index 4322fe2..b554cd7 100644
--- a/src/main/java/com/android/tools/r8/partial/predicate/R8PartialPredicateCollection.java
+++ b/src/main/java/com/android/tools/r8/partial/predicate/R8PartialPredicateCollection.java
@@ -34,7 +34,10 @@
}
public boolean test(DexProgramClass clazz) {
- DexString descriptor = clazz.getType().getDescriptor();
+ return test(clazz.getType().getDescriptor());
+ }
+
+ public boolean test(DexString descriptor) {
for (R8PartialPredicate predicate : predicates) {
if (predicate.test(descriptor)) {
return true;
diff --git a/src/main/java/com/android/tools/r8/profile/art/ArtProfileOptions.java b/src/main/java/com/android/tools/r8/profile/art/ArtProfileOptions.java
index f3144f5..d558858 100644
--- a/src/main/java/com/android/tools/r8/profile/art/ArtProfileOptions.java
+++ b/src/main/java/com/android/tools/r8/profile/art/ArtProfileOptions.java
@@ -70,7 +70,9 @@
return enableCompletenessCheckForTesting
&& !options.getLibraryDesugaringOptions().isDesugaredLibraryCompilation()
&& !options.getStartupOptions().isStartupCompletenessCheckForTestingEnabled()
- && !options.getInstrumentationOptions().isInstrumentationEnabled();
+ && !options.getInstrumentationOptions().isInstrumentationEnabled()
+ // TODO(b/434769547): Add support for throw block outlines.
+ && !options.getThrowBlockOutlinerOptions().enable;
}
public boolean isNopCheckForTestingEnabled() {
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 f2bf9a4..afe30c8 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -165,6 +165,7 @@
import com.google.common.collect.Sets;
import it.unimi.dsi.fastutil.objects.Object2BooleanArrayMap;
import it.unimi.dsi.fastutil.objects.Object2BooleanMap;
+import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
@@ -4692,6 +4693,19 @@
if (options.testing.enqueuerInspector != null) {
options.testing.enqueuerInspector.accept(appInfoWithLiveness, mode);
}
+ if (mode.isFinalTreeShaking()) {
+ if (options.testing.exportFinalKeepInfoCollectionToDirectory != null) {
+ try {
+ keepInfo.exportToDirectory(options.testing.exportFinalKeepInfoCollectionToDirectory);
+ } catch (IOException e) {
+ options.reporter.error(
+ "Could not export initial keep info collection: " + e.getMessage());
+ }
+ }
+ if (options.testing.finalKeepInfoCollectionConsumer != null) {
+ options.testing.finalKeepInfoCollectionConsumer.accept(keepInfo.exportToCollection());
+ }
+ }
return new EnqueuerResult(appInfoWithLiveness);
}
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoAnalysis.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoAnalysis.java
index 792ad59..b42c6db 100644
--- a/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoAnalysis.java
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerMockitoAnalysis.java
@@ -67,9 +67,10 @@
}
private boolean isReflectiveMockInvoke(DexMethod invokedMethod) {
- return invokedMethod.holder.isIdenticalTo(mockitoType)
+ return invokedMethod.getHolderType().isIdenticalTo(mockitoType)
&& (invokedMethod.getName().isIdenticalTo(mockString)
- || invokedMethod.getName().isIdenticalTo(spyString));
+ || invokedMethod.getName().isIdenticalTo(spyString))
+ && !invokedMethod.getParameters().isEmpty();
}
@Override
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollectionExported.java b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollectionExported.java
index 92aaad2..62f7ef3 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollectionExported.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollectionExported.java
@@ -121,6 +121,25 @@
classInfos = mapBuilder.build();
}
+ public KeepClassInfo getKeepClassInfo(TypeReference typeReference) {
+ ExportedClassInfo info = classInfos.get(typeReference);
+ return info == null ? null : info.keepClassInfo;
+ }
+
+ public KeepMethodInfo getKeepMethodInfo(MethodReference methodReference) {
+ if (!classInfos.containsKey(methodReference.getHolderClass())) {
+ return null;
+ }
+ return classInfos.get(methodReference.getHolderClass()).methodInfos.get(methodReference);
+ }
+
+ public KeepFieldInfo getKeepFieldInfo(FieldReference fieldReference) {
+ if (!classInfos.containsKey(fieldReference.getHolderClass())) {
+ return null;
+ }
+ return classInfos.get(fieldReference.getHolderClass()).fieldInfos.get(fieldReference);
+ }
+
private ExportedClassInfo.Builder getBuilder(
Map<TypeReference, ExportedClassInfo.Builder> classInfosBuilder,
TypeReference typeReference) {
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
index f50b1db..6b75e1d 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -118,6 +118,8 @@
generator.forSingleMethod("DesugaredLibraryBridge");
public final SyntheticKind NON_STARTUP_IN_STARTUP_OUTLINE =
generator.forSingleMethodWithGlobalMerging("NonStartupInStartupOutline");
+ public final SyntheticKind THROW_BLOCK_OUTLINE =
+ generator.forSingleMethodWithGlobalMerging("ThrowBlockOutline");
private final List<SyntheticKind> ALL_KINDS;
private String lazyVersionHash = null;
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 6bedcb4..91abc0b 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -72,6 +72,7 @@
import com.android.tools.r8.ir.desugar.nest.Nest;
import com.android.tools.r8.ir.optimize.Inliner;
import com.android.tools.r8.ir.optimize.enums.EnumDataMap;
+import com.android.tools.r8.ir.optimize.outliner.exceptions.ThrowBlockOutlinerOptions;
import com.android.tools.r8.metadata.D8BuildMetadata;
import com.android.tools.r8.metadata.R8BuildMetadata;
import com.android.tools.r8.naming.ClassNameMapper;
@@ -100,6 +101,7 @@
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.shaking.Enqueuer;
import com.android.tools.r8.shaking.GlobalKeepInfoConfiguration;
+import com.android.tools.r8.shaking.KeepInfoCollectionExported;
import com.android.tools.r8.shaking.KeepSpecificationSource;
import com.android.tools.r8.shaking.ProguardConfiguration;
import com.android.tools.r8.shaking.ProguardConfigurationRule;
@@ -118,6 +120,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
+import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
@@ -1042,6 +1045,8 @@
new VerticalClassMergerOptions(this);
private final OpenClosedInterfacesOptions openClosedInterfacesOptions =
new OpenClosedInterfacesOptions();
+ private final ThrowBlockOutlinerOptions throwBlockOutlinerOptions =
+ new ThrowBlockOutlinerOptions();
private final ProtoShrinkingOptions protoShrinking = new ProtoShrinkingOptions();
private final RedundantBridgeRemovalOptions redundantBridgeRemovalOptions =
new RedundantBridgeRemovalOptions();
@@ -1196,6 +1201,10 @@
return syntheticItemsOptions;
}
+ public ThrowBlockOutlinerOptions getThrowBlockOutlinerOptions() {
+ return throwBlockOutlinerOptions;
+ }
+
public TraceReferencesOptions getTraceReferencesOptions() {
return traceReferencesOptions;
}
@@ -2188,6 +2197,11 @@
public boolean enableEmbeddedKeepAnnotations =
System.getProperty("com.android.tools.r8.enableKeepAnnotations") != null;
public boolean reverseClassSortingForDeterminism = false;
+ public Path exportFinalKeepInfoCollectionToDirectory =
+ System.getProperty("com.android.tools.r8.exportInitialKeepInfoCollection") != null
+ ? Paths.get(System.getProperty("com.android.tools.r8.exportInitialKeepInfoCollection"))
+ : null;
+ public Consumer<KeepInfoCollectionExported> finalKeepInfoCollectionConsumer = null;
public boolean enableAutoCloseableDesugaring = true;
public boolean enableNumberUnboxer = false;
diff --git a/src/main/java/com/android/tools/r8/utils/ThreadUtils.java b/src/main/java/com/android/tools/r8/utils/ThreadUtils.java
index b5802d9..acc0349 100644
--- a/src/main/java/com/android/tools/r8/utils/ThreadUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ThreadUtils.java
@@ -8,6 +8,7 @@
import com.android.tools.r8.threading.TaskCollection;
import com.android.tools.r8.threading.ThreadingModule;
import com.android.tools.r8.utils.ListUtils.ReferenceAndIntConsumer;
+import com.android.tools.r8.utils.collections.DexClassAndMemberMap;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ExecutionException;
@@ -193,9 +194,16 @@
ThreadingModule threadingModule,
ExecutorService executorService)
throws ExecutionException {
+ if (items instanceof DexClassAndMemberMap) {
+ return processItemsWithResults(
+ (Consumer<T> keyConsumer) -> items.forEach((k, v) -> keyConsumer.accept(k)),
+ (key, index) -> consumer.apply(key, items.get(key)),
+ threadingModule,
+ executorService);
+ }
return processItemsWithResults(
items.entrySet(),
- arg -> consumer.apply(arg.getKey(), arg.getValue()),
+ entry -> consumer.apply(entry.getKey(), entry.getValue()),
threadingModule,
executorService);
}
diff --git a/src/test/java/com/android/tools/r8/assistant/AtomicUpdaterJsonTest.java b/src/test/java/com/android/tools/r8/assistant/AtomicUpdaterJsonTest.java
new file mode 100644
index 0000000..c3b55b4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/assistant/AtomicUpdaterJsonTest.java
@@ -0,0 +1,96 @@
+// Copyright (c) 2025, 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.assistant;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.assistant.JavaLangClassTestClass.Bar;
+import com.android.tools.r8.assistant.JavaLangClassTestClass.Foo;
+import com.android.tools.r8.assistant.postprocessing.ReflectiveOperationJsonParser;
+import com.android.tools.r8.assistant.postprocessing.model.AtomicFieldUpdaterNewUpdater;
+import com.android.tools.r8.assistant.postprocessing.model.ReflectiveEvent;
+import com.android.tools.r8.assistant.runtime.ReflectiveOperationJsonLogger;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.shaking.KeepInfoCollectionExported;
+import com.android.tools.r8.utils.Box;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class AtomicUpdaterJsonTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withNativeMultidexDexRuntimes().withMaximumApiLevel().build();
+ }
+
+ @Test
+ public void testInstrumentationWithCustomOracle() throws Exception {
+ Path path = Paths.get(temp.newFile().getAbsolutePath());
+ Box<DexItemFactory> factoryBox = new Box<>();
+ testForAssistant()
+ .addProgramClasses(AtomicUpdaterTestClass.class, Foo.class, Bar.class)
+ .addInstrumentationClasses(Instrumentation.class)
+ .setCustomReflectiveOperationReceiver(Instrumentation.class)
+ .setMinApi(parameters)
+ .addOptionsModification(opt -> factoryBox.set(opt.itemFactory))
+ .compile()
+ .addVmArguments("-Dcom.android.tools.r8.reflectiveJsonLogger=" + path)
+ .run(parameters.getRuntime(), AtomicUpdaterTestClass.class)
+ .assertSuccess();
+ List<ReflectiveEvent> reflectiveEvents =
+ new ReflectiveOperationJsonParser(factoryBox.get()).parse(path);
+ assertEquals(3, reflectiveEvents.size());
+ String name = AtomicUpdaterTestClass.class.getName();
+
+ assertTrue(reflectiveEvents.get(0).isAtomicFieldUpdaterNewUpdater());
+ AtomicFieldUpdaterNewUpdater updater0 =
+ reflectiveEvents.get(0).asAtomicFieldUpdaterNewUpdater();
+ assertEquals("int " + name + ".i", updater0.getField().toSourceString());
+
+ assertTrue(reflectiveEvents.get(1).isAtomicFieldUpdaterNewUpdater());
+ AtomicFieldUpdaterNewUpdater updater1 =
+ reflectiveEvents.get(1).asAtomicFieldUpdaterNewUpdater();
+ assertEquals("long " + name + ".l", updater1.getField().toSourceString());
+
+ assertTrue(reflectiveEvents.get(2).isAtomicFieldUpdaterNewUpdater());
+ AtomicFieldUpdaterNewUpdater updater2 =
+ reflectiveEvents.get(2).asAtomicFieldUpdaterNewUpdater();
+ assertEquals("java.lang.Object " + name + ".o", updater2.getField().toSourceString());
+
+ Box<KeepInfoCollectionExported> keepInfoBox = new Box<>();
+ testForR8(parameters)
+ .addProgramClasses(AtomicUpdaterTestClass.class, Foo.class, Bar.class)
+ .addOptionsModification(
+ opt -> opt.testing.finalKeepInfoCollectionConsumer = keepInfoBox::set)
+ .setMinApi(parameters)
+ .addKeepMainRule(AtomicUpdaterTestClass.class)
+ .run(parameters.getRuntime(), AtomicUpdaterTestClass.class)
+ .assertSuccessWithOutputLines("42", "42", "42");
+
+ KeepInfoCollectionExported keepInfoCollectionExported = keepInfoBox.get();
+
+ assertTrue(updater0.isKeptBy(keepInfoCollectionExported));
+ assertTrue(updater1.isKeptBy(keepInfoCollectionExported));
+ assertTrue(updater2.isKeptBy(keepInfoCollectionExported));
+ }
+
+ public static class Instrumentation extends ReflectiveOperationJsonLogger {
+ public Instrumentation() throws IOException {}
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/assistant/AtomicUpdaterTest.java b/src/test/java/com/android/tools/r8/assistant/AtomicUpdaterTest.java
index 360ea26..616bfb3 100644
--- a/src/test/java/com/android/tools/r8/assistant/AtomicUpdaterTest.java
+++ b/src/test/java/com/android/tools/r8/assistant/AtomicUpdaterTest.java
@@ -49,18 +49,8 @@
public static class Instrumentation extends EmptyReflectiveOperationReceiver {
@Override
- public void onAtomicIntegerFieldUpdaterNewUpdater(Stack stack, Class<?> clazz, String name) {
- System.out.println("int " + clazz.getName() + "#" + name);
- }
-
- @Override
- public void onAtomicLongFieldUpdaterNewUpdater(Stack stack, Class<?> clazz, String name) {
- System.out.println("long " + clazz.getName() + "#" + name);
- }
-
- @Override
- public void onAtomicReferenceFieldUpdaterNewUpdater(
- Stack stack, Class<?> clazz, Class<?> fieldClass, String name) {
+ public void onAtomicFieldUpdaterNewUpdater(
+ Stack stack, Class<?> fieldClass, Class<?> clazz, String name) {
System.out.println(fieldClass.getName() + " " + clazz.getName() + "#" + name);
}
}
diff --git a/src/test/java/com/android/tools/r8/assistant/JavaLangClassJsonTest.java b/src/test/java/com/android/tools/r8/assistant/JavaLangClassJsonTest.java
new file mode 100644
index 0000000..089824f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/assistant/JavaLangClassJsonTest.java
@@ -0,0 +1,161 @@
+// Copyright (c) 2025, 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.assistant;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.assistant.JavaLangClassTestClass.Bar;
+import com.android.tools.r8.assistant.JavaLangClassTestClass.Foo;
+import com.android.tools.r8.assistant.postprocessing.ReflectiveOperationJsonParser;
+import com.android.tools.r8.assistant.postprocessing.model.ClassGetMember;
+import com.android.tools.r8.assistant.postprocessing.model.ClassGetMembers;
+import com.android.tools.r8.assistant.postprocessing.model.ClassGetName;
+import com.android.tools.r8.assistant.postprocessing.model.ReflectiveEvent;
+import com.android.tools.r8.assistant.runtime.ReflectiveEventType;
+import com.android.tools.r8.assistant.runtime.ReflectiveOperationJsonLogger;
+import com.android.tools.r8.assistant.runtime.ReflectiveOperationReceiver.NameLookupType;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.Box;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class JavaLangClassJsonTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withNativeMultidexDexRuntimes().withMaximumApiLevel().build();
+ }
+
+ @Test
+ public void testInstrumentationWithCustomOracle() throws Exception {
+ Path path = Paths.get(temp.newFile().getAbsolutePath());
+ Box<DexItemFactory> factoryBox = new Box<>();
+ testForAssistant()
+ .addProgramClasses(JavaLangClassTestClass.class, Foo.class, Bar.class)
+ .addInstrumentationClasses(Instrumentation.class)
+ .setCustomReflectiveOperationReceiver(Instrumentation.class)
+ .setMinApi(parameters)
+ .addOptionsModification(opt -> factoryBox.set(opt.itemFactory))
+ .compile()
+ .addVmArguments("-Dcom.android.tools.r8.reflectiveJsonLogger=" + path)
+ .run(parameters.getRuntime(), JavaLangClassTestClass.class)
+ .assertSuccess();
+ List<ReflectiveEvent> reflectiveEvents =
+ new ReflectiveOperationJsonParser(factoryBox.get()).parse(path);
+ Assert.assertEquals(28, reflectiveEvents.size());
+
+ assertTrue(reflectiveEvents.get(3).isClassGetMember());
+ ClassGetMember updater00 = reflectiveEvents.get(3).asClassGetMember();
+ assertEquals(ReflectiveEventType.CLASS_GET_DECLARED_METHOD, updater00.getEventType());
+ assertEquals(
+ Reference.methodFromMethod(Foo.class.getDeclaredMethod("barr")),
+ updater00.getMember().asDexMethod().asMethodReference());
+
+ assertTrue(reflectiveEvents.get(4).isClassGetMember());
+ ClassGetMember updater01 = reflectiveEvents.get(4).asClassGetMember();
+ assertEquals(ReflectiveEventType.CLASS_GET_DECLARED_FIELD, updater01.getEventType());
+ assertEquals(
+ Reference.fieldFromField(Foo.class.getDeclaredField("a")),
+ updater01.getMember().asDexField().asFieldReference());
+
+ assertTrue(reflectiveEvents.get(6).isClassGetMembers());
+ ClassGetMembers updater02 = reflectiveEvents.get(6).asClassGetMembers();
+ assertEquals(ReflectiveEventType.CLASS_GET_DECLARED_METHODS, updater02.getEventType());
+ assertEquals(Foo.class.getName(), updater02.getHolder().toSourceString());
+
+ assertTrue(reflectiveEvents.get(7).isClassGetMembers());
+ ClassGetMembers updater03 = reflectiveEvents.get(7).asClassGetMembers();
+ assertEquals(ReflectiveEventType.CLASS_GET_DECLARED_FIELDS, updater03.getEventType());
+ assertEquals(Foo.class.getName(), updater03.getHolder().toSourceString());
+
+ assertTrue(reflectiveEvents.get(8).isClassGetMember());
+ ClassGetMember updater04 = reflectiveEvents.get(8).asClassGetMember();
+ assertEquals(ReflectiveEventType.CLASS_GET_DECLARED_CONSTRUCTOR, updater04.getEventType());
+ assertEquals(
+ Reference.methodFromMethod(Foo.class.getDeclaredConstructor()),
+ updater04.getMember().asDexMethod().asMethodReference());
+
+ assertTrue(reflectiveEvents.get(9).isClassGetMembers());
+ ClassGetMembers updater05 = reflectiveEvents.get(9).asClassGetMembers();
+ assertEquals(ReflectiveEventType.CLASS_GET_DECLARED_CONSTRUCTORS, updater05.getEventType());
+ assertEquals(Foo.class.getName(), updater05.getHolder().toSourceString());
+
+ assertTrue(reflectiveEvents.get(10).isClassGetName());
+ ClassGetName updater0 = reflectiveEvents.get(10).asClassGetName();
+ assertEquals(Foo.class.getName(), updater0.getType().toSourceString());
+ assertEquals(NameLookupType.NAME, updater0.getNameLookupType());
+
+ assertTrue(reflectiveEvents.get(11).isClassGetName());
+ ClassGetName updater1 = reflectiveEvents.get(11).asClassGetName();
+ assertEquals(Foo.class.getName(), updater1.getType().toSourceString());
+ assertEquals(NameLookupType.CANONICAL_NAME, updater1.getNameLookupType());
+
+ assertTrue(reflectiveEvents.get(12).isClassGetName());
+ ClassGetName updater2 = reflectiveEvents.get(12).asClassGetName();
+ assertEquals(Foo.class.getName(), updater2.getType().toSourceString());
+ assertEquals(NameLookupType.SIMPLE_NAME, updater2.getNameLookupType());
+
+ assertTrue(reflectiveEvents.get(13).isClassGetName());
+ ClassGetName updater3 = reflectiveEvents.get(13).asClassGetName();
+ assertEquals(Foo.class.getName(), updater3.getType().toSourceString());
+ assertEquals(NameLookupType.TYPE_NAME, updater3.getNameLookupType());
+
+ assertTrue(reflectiveEvents.get(19).isClassGetMembers());
+ ClassGetMembers updater19 = reflectiveEvents.get(19).asClassGetMembers();
+ assertEquals(ReflectiveEventType.CLASS_GET_METHODS, updater19.getEventType());
+ assertEquals(Bar.class.getName(), updater19.getHolder().toSourceString());
+
+ assertTrue(reflectiveEvents.get(20).isClassGetMembers());
+ ClassGetMembers updater20 = reflectiveEvents.get(20).asClassGetMembers();
+ assertEquals(ReflectiveEventType.CLASS_GET_FIELDS, updater20.getEventType());
+ assertEquals(Bar.class.getName(), updater20.getHolder().toSourceString());
+
+ assertTrue(reflectiveEvents.get(21).isClassGetMembers());
+ ClassGetMembers updater21 = reflectiveEvents.get(21).asClassGetMembers();
+ assertEquals(ReflectiveEventType.CLASS_GET_CONSTRUCTORS, updater21.getEventType());
+ assertEquals(Bar.class.getName(), updater21.getHolder().toSourceString());
+
+ assertTrue(reflectiveEvents.get(22).isClassGetMember());
+ ClassGetMember updater22 = reflectiveEvents.get(22).asClassGetMember();
+ assertEquals(ReflectiveEventType.CLASS_GET_METHOD, updater22.getEventType());
+ assertEquals(
+ Reference.methodFromMethod(Bar.class.getMethod("bar")),
+ updater22.getMember().asDexMethod().asMethodReference());
+
+ assertTrue(reflectiveEvents.get(23).isClassGetMember());
+ ClassGetMember updater23 = reflectiveEvents.get(23).asClassGetMember();
+ assertEquals(ReflectiveEventType.CLASS_GET_FIELD, updater23.getEventType());
+ assertEquals(
+ Reference.fieldFromField(Bar.class.getField("i")),
+ updater23.getMember().asDexField().asFieldReference());
+
+ assertTrue(reflectiveEvents.get(24).isClassGetMember());
+ ClassGetMember updater24 = reflectiveEvents.get(24).asClassGetMember();
+ assertEquals(ReflectiveEventType.CLASS_GET_CONSTRUCTOR, updater24.getEventType());
+ assertEquals(
+ Reference.methodFromMethod(Bar.class.getConstructor()),
+ updater24.getMember().asDexMethod().asMethodReference());
+ }
+
+ public static class Instrumentation extends ReflectiveOperationJsonLogger {
+ public Instrumentation() throws IOException {}
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/assistant/JavaLangClassTest.java b/src/test/java/com/android/tools/r8/assistant/JavaLangClassTest.java
index 6f6e8ed..f4f0eff 100644
--- a/src/test/java/com/android/tools/r8/assistant/JavaLangClassTest.java
+++ b/src/test/java/com/android/tools/r8/assistant/JavaLangClassTest.java
@@ -74,7 +74,7 @@
@Override
public void onClassGetDeclaredMethod(
- Stack stack, Class<?> clazz, String method, Class<?>... parameters) {
+ Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters) {
printNumIfTrue(clazz.getName().endsWith("Foo"), 2);
}
@@ -84,7 +84,8 @@
}
@Override
- public void onClassGetDeclaredField(Stack stack, Class<?> clazz, String fieldName) {
+ public void onClassGetDeclaredField(
+ Stack stack, Class<?> fieldType, Class<?> clazz, String fieldName) {
printNumIfTrue(
clazz.getName().endsWith("Foo") && (fieldName.equals("a") || fieldName.equals("b")), 3);
}
@@ -106,7 +107,7 @@
@Override
public void onClassGetMethod(
- Stack stack, Class<?> clazz, String method, Class<?>... parameters) {
+ Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters) {
printNumIfTrue(clazz.getName().endsWith("Bar"), 20);
}
@@ -116,7 +117,7 @@
}
@Override
- public void onClassGetField(Stack stack, Class<?> clazz, String fieldName) {
+ public void onClassGetField(Stack stack, Class<?> fieldType, Class<?> clazz, String fieldName) {
printNumIfTrue(clazz.getName().endsWith("Bar"), 21);
}
diff --git a/src/test/java/com/android/tools/r8/assistant/R8AssistentReflectiveInstrumentationTest.java b/src/test/java/com/android/tools/r8/assistant/R8AssistentReflectiveInstrumentationTest.java
index 2332575..bcf3dc6 100644
--- a/src/test/java/com/android/tools/r8/assistant/R8AssistentReflectiveInstrumentationTest.java
+++ b/src/test/java/com/android/tools/r8/assistant/R8AssistentReflectiveInstrumentationTest.java
@@ -159,7 +159,7 @@
@Override
public void onClassGetDeclaredMethod(
- Stack stack, Class<?> clazz, String method, Class<?>... parameters) {
+ Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters) {
if (!clazz.equals(Bar.class) || !method.equals("callMe")) {
throw new RuntimeException("Wrong method passed");
}
@@ -200,7 +200,7 @@
@Override
public void onClassGetDeclaredMethod(
- Stack stack, Class<?> clazz, String method, Class<?>... parameters) {
+ Stack stack, Class<?> returnType, Class<?> clazz, String method, Class<?>... parameters) {
System.out.println("Custom receiver method " + method);
}
}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/DefaultInterfaceMethodCollisionWithClassMethodAfterClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/DefaultInterfaceMethodCollisionWithClassMethodAfterClassMergingTest.java
new file mode 100644
index 0000000..7c8d366
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/DefaultInterfaceMethodCollisionWithClassMethodAfterClassMergingTest.java
@@ -0,0 +1,111 @@
+// Copyright (c) 2025, 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.classmerging.horizontal;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoUnusedInterfaceRemoval;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.apimodel.ApiModelingTestHelper;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class DefaultInterfaceMethodCollisionWithClassMethodAfterClassMergingTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ @Test
+ public void testInterfaceOnProgram() throws Exception {
+ testForR8(parameters)
+ .addInnerClasses(getClass())
+ .addKeepClassAndMembersRules(Main.class)
+ .addHorizontallyMergedClassesInspector(
+ inspector ->
+ inspector
+ .applyIf(
+ !parameters.canUseDefaultAndStaticInterfaceMethods(),
+ i -> i.assertIsCompleteMergeGroup(A.class, B.class))
+ .assertNoOtherClassesMerged())
+ .enableInliningAnnotations()
+ .enableNoUnusedInterfaceRemovalAnnotations()
+ .enableNoVerticalClassMergingAnnotations()
+ .setMinApi(parameters)
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutputLines("I", "B");
+ }
+
+ @Test
+ public void testInterfaceOnLibrary() throws Exception {
+ testForR8(parameters)
+ .addProgramClasses(A.class, B.class, Main.class)
+ .addLibraryClasses(I.class)
+ .addLibraryFiles(ToolHelper.getMostRecentAndroidJar())
+ .addKeepClassAndMembersRules(Main.class)
+ .addHorizontallyMergedClassesInspector(
+ HorizontallyMergedClassesInspector::assertNoClassesMerged)
+ .apply(ApiModelingTestHelper.setMockApiLevelForClass(I.class, AndroidApiLevel.B))
+ .apply(
+ ApiModelingTestHelper.setMockApiLevelForMethod(
+ I.class.getDeclaredMethod("m"), AndroidApiLevel.B))
+ .compile()
+ .addRunClasspathClasses(I.class)
+ .run(parameters.getRuntime(), Main.class)
+ .applyIf(
+ parameters.canUseDefaultAndStaticInterfaceMethods(),
+ rr -> rr.assertSuccessWithOutputLines("I", "B"),
+ rr -> rr.assertSuccessWithOutputLines("Caught java.lang.AbstractMethodError", "B"));
+ }
+
+ static class Main {
+
+ public static void main(String[] args) {
+ try {
+ test(new A());
+ } catch (AbstractMethodError e) {
+ System.out.println("Caught " + e.getClass().getName());
+ }
+ test(new B());
+ }
+
+ // @Keep
+ static void test(I i) {
+ i.m();
+ }
+ }
+
+ @NoUnusedInterfaceRemoval
+ @NoVerticalClassMerging
+ interface I {
+
+ @NeverInline
+ default void m() {
+ System.out.println("I");
+ }
+ }
+
+ static class A implements I {}
+
+ static class B implements I {
+
+ @Override
+ public void m() {
+ System.out.println("B");
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/globalsynthetics/GlobalSyntheticGeneratorAGPUseTest.java b/src/test/java/com/android/tools/r8/globalsynthetics/GlobalSyntheticGeneratorAGPUseTest.java
new file mode 100644
index 0000000..6a277f2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/globalsynthetics/GlobalSyntheticGeneratorAGPUseTest.java
@@ -0,0 +1,80 @@
+// Copyright (c) 2025, 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.globalsynthetics;
+
+import static com.android.tools.r8.ToolHelper.getAndroidJar;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentIf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertNull;
+
+import com.android.tools.r8.D8;
+import com.android.tools.r8.D8Command;
+import com.android.tools.r8.GlobalSyntheticsGenerator;
+import com.android.tools.r8.GlobalSyntheticsGeneratorCommand;
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.nio.file.Path;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class GlobalSyntheticGeneratorAGPUseTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameter(1)
+ public boolean emitLambdaMethodAnnotations;
+
+ @Parameters(name = "{0}, emitLambdaMethodAnnotations = {1}")
+ public static List<Object[]> data() {
+ return buildParameters(getTestParameters().withNoneRuntime().build(), BooleanUtils.values());
+ }
+
+ @Test
+ public void test() throws Exception {
+ try {
+ if (emitLambdaMethodAnnotations) {
+ assertNull(System.getProperty("com.android.tools.r8.emitLambdaMethodAnnotations"));
+ System.setProperty("com.android.tools.r8.emitLambdaMethodAnnotations", "");
+ }
+ Path globals = temp.newFile("all.globals").toPath();
+ GlobalSyntheticsGenerator.run(
+ GlobalSyntheticsGeneratorCommand.builder()
+ .addLibraryFiles(getAndroidJar(36))
+ .setGlobalSyntheticsOutput(globals)
+ .build());
+
+ Path globalsDex = temp.newFile("globals.zip").toPath();
+ D8.run(
+ D8Command.builder()
+ .addLibraryFiles(getAndroidJar(36))
+ .setMinApiLevel(21)
+ .addGlobalSyntheticsFiles(globals)
+ .setOutput(globalsDex, OutputMode.DexIndexed)
+ .build());
+
+ CodeInspector inspector = new CodeInspector(globalsDex);
+ assertThat(inspector.clazz("java.lang.Record"), isPresent());
+ // Added in API level 24.
+ assertThat(inspector.clazz("android.os.HardwarePropertiesManager"), isPresent());
+ // Added in API level 36.
+ assertThat(inspector.clazz("android.os.Build$VERSION_CODES_FULL"), isPresent());
+ // TODO(b/417709154): Should this always be part of global synthetics?
+ assertThat(
+ inspector.clazz("com.android.tools.r8.annotations.LambdaMethod"),
+ isPresentIf(emitLambdaMethodAnnotations));
+ } finally {
+ System.clearProperty("com.android.tools.r8.emitLambdaMethodAnnotations");
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerNoArgumentsTest.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerNoArgumentsTest.java
new file mode 100644
index 0000000..ed330c9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerNoArgumentsTest.java
@@ -0,0 +1,135 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.optimize.outliner.exceptions;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.isInvokeWithTarget;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.BooleanBox;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import it.unimi.dsi.fastutil.ints.IntArraySet;
+import it.unimi.dsi.fastutil.ints.IntSet;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ThrowBlockOutlinerNoArgumentsTest extends TestBase {
+
+ @Parameter(0)
+ public boolean minimizeSyntheticNames;
+
+ @Parameter(1)
+ public TestParameters parameters;
+
+ @Parameters(name = "{1}, minimizeSyntheticNames: {0}")
+ public static List<Object[]> data() {
+ return buildParameters(
+ BooleanUtils.values(), getTestParameters().withDexRuntimesAndAllApiLevels().build());
+ }
+
+ @Test
+ public void test() throws Exception {
+ BooleanBox receivedCallback = new BooleanBox();
+ TestCompileResult<?, ?> compileResult =
+ testForD8(parameters)
+ .addInnerClasses(getClass())
+ .addOptionsModification(
+ options -> {
+ options.desugarSpecificOptions().minimizeSyntheticNames = minimizeSyntheticNames;
+ assertFalse(options.getThrowBlockOutlinerOptions().enable);
+ options.getThrowBlockOutlinerOptions().enable = true;
+ options.getThrowBlockOutlinerOptions().outlineConsumerForTesting =
+ outlines -> {
+ inspectOutlines(outlines);
+ receivedCallback.set();
+ };
+ })
+ .release()
+ .setMinApi(parameters)
+ .compile()
+ .inspect(this::inspectOutput);
+ assertTrue(receivedCallback.isTrue());
+
+ for (int i = 0; i < 3; i++) {
+ compileResult
+ .run(parameters.getRuntime(), Main.class, Integer.toString(i))
+ .assertFailureWithErrorThatThrows(IllegalArgumentException.class);
+ }
+ compileResult
+ .run(parameters.getRuntime(), Main.class, Integer.toString(3))
+ .assertFailureWithErrorThatThrows(RuntimeException.class);
+ compileResult
+ .run(parameters.getRuntime(), Main.class, Integer.toString(42))
+ .assertSuccessWithEmptyOutput();
+ }
+
+ private void inspectOutlines(Collection<ThrowBlockOutline> outlines) {
+ // Verify that we have two outlines with one and three users, respectively.
+ assertEquals(2, outlines.size());
+ IntSet numberOfUsers = new IntArraySet();
+ for (ThrowBlockOutline outline : outlines) {
+ numberOfUsers.add(outline.getNumberOfUsers());
+ }
+ assertTrue(numberOfUsers.contains(1));
+ assertTrue(numberOfUsers.contains(3));
+ }
+
+ private void inspectOutput(CodeInspector inspector) {
+ assertEquals(2, inspector.allClasses().size());
+
+ MethodSubject mainMethodSubject = inspector.clazz(Main.class).mainMethod();
+ assertThat(mainMethodSubject, isPresent());
+
+ ClassSubject outlineClassSubject =
+ inspector.clazz(
+ minimizeSyntheticNames
+ ? SyntheticItemsTestUtils.syntheticClassWithMinimalName(Main.class, 0)
+ : SyntheticItemsTestUtils.syntheticThrowBlockOutlineClass(Main.class, 0));
+ assertThat(outlineClassSubject, isPresent());
+ assertEquals(1, outlineClassSubject.allMethods().size());
+
+ MethodSubject outlineMethodSubject = outlineClassSubject.uniqueMethod();
+ assertEquals(
+ 3,
+ mainMethodSubject
+ .streamInstructions()
+ .filter(isInvokeWithTarget(outlineMethodSubject))
+ .count());
+ }
+
+ static class Main {
+
+ public static void main(String[] args) {
+ int i = Integer.parseInt(args[0]);
+ if (i == 0) {
+ throw new IllegalArgumentException();
+ }
+ if (i == 1) {
+ throw new IllegalArgumentException();
+ }
+ if (i == 2) {
+ throw new IllegalArgumentException();
+ }
+ if (i == 3) {
+ throw new RuntimeException();
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java
index 2497a5b..1cdc98e 100644
--- a/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java
@@ -437,7 +437,8 @@
KotlinCompileMemoizer compilation,
List<byte[]> classFileData,
String mainClass,
- ExpectedRules expectedRules)
+ ExpectedRules expectedRules,
+ String expectedOutput)
throws Exception {
// TODO(b/392865072): Legacy R8 fails with AssertionError: Synthetic class kinds should agree.
assumeFalse(parameters.isLegacyR8());
@@ -491,20 +492,22 @@
}
})
.run(mainClass)
- .assertSuccessWithOutput(getExpectedOutputForKotlin());
+ .assertSuccessWithOutput(expectedOutput);
}
protected void runTestExtractedRulesKotlin(
KotlinCompileMemoizer compilation, String mainClass, ExpectedRules expectedRules)
throws Exception {
- runTestExtractedRulesKotlin(compilation, ImmutableList.of(), mainClass, expectedRules);
+ runTestExtractedRulesKotlin(
+ compilation, ImmutableList.of(), mainClass, expectedRules, getExpectedOutputForKotlin());
}
protected void runTestExtractedRulesKotlin(
KotlinCompileMemoizer compilation,
BiFunction<ClassReference, byte[], byte[]> transformerForClass,
String mainClass,
- ExpectedRules expectedRules)
+ ExpectedRules expectedRules,
+ String expectedOutput)
throws Exception {
List<byte[]> result = new ArrayList<>();
ZipUtils.iter(
@@ -517,6 +520,16 @@
result.add(
transformerForClass.apply(classReference, ByteStreams.toByteArray(inputStream)));
});
- runTestExtractedRulesKotlin(null, result, mainClass, expectedRules);
+ runTestExtractedRulesKotlin(null, result, mainClass, expectedRules, expectedOutput);
+ }
+
+ protected void runTestExtractedRulesKotlin(
+ KotlinCompileMemoizer compilation,
+ BiFunction<ClassReference, byte[], byte[]> transformerForClass,
+ String mainClass,
+ ExpectedRules expectedRules)
+ throws Exception {
+ runTestExtractedRulesKotlin(
+ compilation, transformerForClass, mainClass, expectedRules, getExpectedOutputForKotlin());
}
}
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionForInstantiationAnyArgsConstructorTest.java b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionForInstantiationAnyArgsConstructorTest.java
new file mode 100644
index 0000000..dfb602f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionForInstantiationAnyArgsConstructorTest.java
@@ -0,0 +1,224 @@
+// Copyright (c) 2025, 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.keepanno.androidx;
+
+import static com.android.tools.r8.ToolHelper.getFilesInTestFolderRelativeToClass;
+
+import androidx.annotation.keep.UsesReflectionToConstruct;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.transformers.ClassFileTransformer.AnnotationBuilder;
+import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.function.Consumer;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.objectweb.asm.Type;
+
+@RunWith(Parameterized.class)
+public class KeepUsesReflectionForInstantiationAnyArgsConstructorTest
+ extends KeepAnnoTestExtractedRulesBase {
+
+ @Parameterized.Parameters(name = "{0}, {1}")
+ public static Collection<Object[]> data() {
+ // Test with Android 14, which has `java.lang.ClassValue` to avoid having to deal with R8
+ // missing class warnings for tests using the kotlin-reflect library.
+ return buildParameters(
+ createParameters(
+ getTestParameters()
+ .withDexRuntime(Version.V14_0_0)
+ .withDefaultCfRuntime()
+ .withMaximumApiLevel()
+ .build()),
+ getKotlinTestParameters().withLatestCompiler().build());
+ }
+
+ @Override
+ protected String getExpectedOutputForJava() {
+ return StringUtils.lines("4");
+ }
+
+ @Override
+ protected String getExpectedOutputForKotlin() {
+ return StringUtils.lines(
+ "fun `<init>`(): com.android.tools.r8.keepanno.androidx.kt.KeptClass", "<init>()", "4");
+ }
+
+ private static Collection<Path> getKotlinSources() {
+ try {
+ return getFilesInTestFolderRelativeToClass(
+ KeepUsesReflectionForInstantiationAnyArgsConstructorTest.class,
+ "kt",
+ "AnyArgsConstructor.kt",
+ "KeptClass.kt");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ compilationResults = getCompileMemoizerWithKeepAnnoLib(getKotlinSources());
+ }
+
+ private static ExpectedRules getExpectedRulesJava(Class<?> conditionClass) {
+ return getExpectedRulesJava(conditionClass, null);
+ }
+
+ private static ExpectedRules getExpectedRulesJava(
+ Class<?> conditionClass, String conditionMembers) {
+ Consumer<ExpectedKeepRule.Builder> setCondition =
+ b -> b.setConditionClass(conditionClass).setConditionMembers(conditionMembers);
+ ExpectedRules.Builder builder =
+ ExpectedRules.builder()
+ .add(
+ ExpectedKeepRule.builder()
+ .apply(setCondition)
+ .setConsequentClass(KeptClass.class)
+ .setConsequentMembers("{ void <init>(...); }")
+ .build());
+ addConsequentKotlinMetadata(builder, b -> b.apply(setCondition));
+ return builder.build();
+ }
+
+ private static ExpectedRules getExpectedRulesKotlin(String conditionClass) {
+ return getExpectedRulesKotlin(conditionClass, null);
+ }
+
+ private static ExpectedRules getExpectedRulesKotlin(
+ String conditionClass, String conditionMembers) {
+ Consumer<ExpectedKeepRule.Builder> setCondition =
+ b ->
+ b.setConditionClass("com.android.tools.r8.keepanno.androidx.kt.AnyArgsConstructor")
+ .setConditionMembers(conditionMembers);
+ ExpectedRules.Builder builder =
+ ExpectedRules.builder()
+ .add(
+ ExpectedKeepRule.builder()
+ .apply(setCondition)
+ .setConsequentClass("com.android.tools.r8.keepanno.androidx.kt.KeptClass")
+ .setConsequentMembers("{ void <init>(...); }")
+ .build());
+ addConsequentKotlinMetadata(builder, b -> b.apply(setCondition));
+ return builder.build();
+ }
+
+ private static void buildAnyConstructor(AnnotationBuilder builder, Object clazz) {
+ if (clazz instanceof String) {
+ builder.setField("className", clazz);
+ } else {
+ assert clazz instanceof Class<?> || clazz instanceof Type;
+ builder.setField("classConstant", clazz);
+ }
+ // No parameterTypes or parameterTypeNames means any constructor.
+ }
+
+ @Test
+ public void testAnyConstructor() throws Exception {
+ runTestExtractedRulesJava(
+ AnyConstructor.class,
+ ImmutableList.of(KeptClass.class),
+ ImmutableList.of(
+ setAnnotationOnMethod(
+ AnyConstructor.class,
+ MethodPredicate.onName("foo"),
+ UsesReflectionToConstruct.class,
+ builder -> buildAnyConstructor(builder, KeptClass.class))),
+ getExpectedRulesJava(AnyConstructor.class, "{ void foo(java.lang.Class); }"));
+ }
+
+ @Test
+ public void testAnyConstructorAnnotateClass() throws Exception {
+ runTestExtractedRulesJava(
+ AnyConstructor.class,
+ ImmutableList.of(KeptClass.class),
+ ImmutableList.of(
+ setAnnotationOnClass(
+ AnyConstructor.class,
+ UsesReflectionToConstruct.class,
+ builder -> buildAnyConstructor(builder, KeptClass.class))),
+ getExpectedRulesJava(AnyConstructor.class));
+ }
+
+ @Test
+ public void testAnyConstructorKotlin() throws Exception {
+ runTestExtractedRulesKotlin(
+ compilationResults,
+ (classReference, classFileBytes) ->
+ setAnnotationOnMethod(
+ classReference,
+ classFileBytes,
+ Reference.classFromTypeName(
+ "com.android.tools.r8.keepanno.androidx.kt.AnyArgsConstructor"),
+ MethodPredicate.onName("foo"),
+ UsesReflectionToConstruct.class,
+ builder ->
+ buildAnyConstructor(
+ builder,
+ Type.getType(
+ DescriptorUtils.javaTypeToDescriptor(
+ "com.android.tools.r8.keepanno.androidx.kt.KeptClass")))),
+ "com.android.tools.r8.keepanno.androidx.kt.AnyArgsConstructorKt",
+ getExpectedRulesKotlin(
+ "com.android.tools.r8.keepanno.androidx.kt.AnyArgsConstructor",
+ "{ void foo(kotlin.reflect.KClass); }"));
+ }
+
+ @Test
+ public void testAnyConstructorKotlinAnnotateClass() throws Exception {
+ runTestExtractedRulesKotlin(
+ compilationResults,
+ (classReference, classFileBytes) ->
+ setAnnotationOnClass(
+ classReference,
+ classFileBytes,
+ Reference.classFromTypeName(
+ "com.android.tools.r8.keepanno.androidx.kt.AnyArgsConstructor"),
+ UsesReflectionToConstruct.class,
+ builder ->
+ buildAnyConstructor(
+ builder,
+ Type.getType(
+ DescriptorUtils.javaTypeToDescriptor(
+ "com.android.tools.r8.keepanno.androidx.kt.KeptClass")))),
+ "com.android.tools.r8.keepanno.androidx.kt.AnyArgsConstructorKt",
+ getExpectedRulesKotlin("com.android.tools.r8.keepanno.androidx.kt.AnyArgsConstructor"),
+ // TODO(b/437277192): Constructors should be kept.
+ parameters.isExtractRules()
+ ? StringUtils.lines("null", "0")
+ : getExpectedOutputForKotlin());
+ }
+
+ // Test class without annotation to be used by multiple tests inserting annotations using a
+ // transformer.
+ static class AnyConstructor {
+
+ public void foo(Class<KeptClass> clazz) throws Exception {
+ if (clazz != null) {
+ System.out.println(clazz.getDeclaredConstructors().length);
+ }
+ }
+
+ public static void main(String[] args) throws Exception {
+ new AnyConstructor().foo(System.nanoTime() > 0 ? KeptClass.class : null);
+ }
+ }
+
+ static class KeptClass {
+ KeptClass() {}
+
+ KeptClass(int i) {}
+
+ KeptClass(long j) {}
+
+ KeptClass(String s1, String s2, String s3) {}
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/kt/AnyArgsConstructor.kt b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/AnyArgsConstructor.kt
new file mode 100644
index 0000000..c891829
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/AnyArgsConstructor.kt
@@ -0,0 +1,19 @@
+// Copyright (c) 2025, 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.keepanno.androidx.kt
+
+import kotlin.reflect.KClass
+import kotlin.reflect.full.primaryConstructor
+
+class AnyArgsConstructor {
+ fun foo(clazz: KClass<KeptClass>?) {
+ println(clazz?.primaryConstructor)
+ clazz?.primaryConstructor?.call()
+ println(clazz?.constructors?.size)
+ }
+}
+
+fun main() {
+ AnyArgsConstructor().foo(if (System.nanoTime() > 0) KeptClass::class else null)
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/kt/KeptClass.kt b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/KeptClass.kt
index 89e2d02..e96643d 100644
--- a/src/test/java/com/android/tools/r8/keepanno/androidx/kt/KeptClass.kt
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/KeptClass.kt
@@ -16,7 +16,7 @@
println("<init>(Long)")
}
- constructor(s: String) : this() {
- println("<init>(String)")
+ constructor(s1: String, s2: String, s3: String) : this() {
+ println("<init>(String, String, String)")
}
}
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingClasspathSplitTest.java b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingClasspathSplitTest.java
index e87a04d..89d12eb 100644
--- a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingClasspathSplitTest.java
+++ b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingClasspathSplitTest.java
@@ -8,6 +8,8 @@
import com.android.tools.r8.TestCompileResult;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.ThrowableConsumer;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.graph.AccessFlags;
import com.android.tools.r8.memberrebinding.testclasses.MemberRebindingClasspathSplitTestClasses;
import com.google.common.collect.ImmutableList;
import java.util.List;
@@ -24,20 +26,22 @@
private static class TestConfig {
private final String desc;
-
private final ThrowableConsumer<? super TestBuilder<?, ?>> addToClasspath;
private final ThrowableConsumer<? super TestBuilder<?, ?>> addToProgrampath;
private final ThrowableConsumer<? super TestCompileResult<?, ?>> addToRunClasspath;
+ private final boolean expectFailure;
private TestConfig(
String desc,
ThrowableConsumer<? super TestBuilder<?, ?>> addToClasspath,
ThrowableConsumer<? super TestBuilder<?, ?>> addToProgrampath,
- ThrowableConsumer<? super TestCompileResult<?, ?>> addToRunClasspath) {
+ ThrowableConsumer<? super TestCompileResult<?, ?>> addToRunClasspath,
+ boolean expectFailure) {
this.desc = desc;
this.addToClasspath = addToClasspath;
this.addToProgrampath = addToProgrampath;
this.addToRunClasspath = addToRunClasspath;
+ this.expectFailure = expectFailure;
}
@Override
@@ -66,7 +70,8 @@
b.addRunClasspathClasses(
MemberRebindingClasspathSplitTestClasses.getA(),
MemberRebindingClasspathSplitTestClasses.getB());
- }),
+ },
+ false),
new TestConfig(
"Both A and B on classpath, m() bridge removed from B",
b -> {
@@ -83,7 +88,8 @@
transformer(MemberRebindingClasspathSplitTestClasses.getB())
.removeMethodsWithName("m")
.transform());
- }),
+ },
+ false),
new TestConfig(
"A on classpath and B on programpath",
b -> {
@@ -94,7 +100,8 @@
},
b -> {
b.addRunClasspathClasses(MemberRebindingClasspathSplitTestClasses.getA());
- }),
+ },
+ false),
new TestConfig(
"A on classpath and B on programpath, m() bridge removed from B",
b -> {
@@ -108,7 +115,26 @@
},
b -> {
b.addRunClasspathClasses(MemberRebindingClasspathSplitTestClasses.getA());
- })));
+ },
+ false),
+ new TestConfig(
+ "Both A and B on classpath but neither A or B is public",
+ b -> {
+ b.addClasspathClasses(MemberRebindingClasspathSplitTestClasses.getA());
+ b.addClasspathClassFileData(
+ transformer(MemberRebindingClasspathSplitTestClasses.getB())
+ .setAccessFlags(AccessFlags::unsetPublic)
+ .transform());
+ },
+ b -> {},
+ b -> {
+ b.addRunClasspathClasses(MemberRebindingClasspathSplitTestClasses.getA());
+ b.addRunClasspathClassFileData(
+ transformer(MemberRebindingClasspathSplitTestClasses.getB())
+ .setAccessFlags(AccessFlags::unsetPublic)
+ .transform());
+ },
+ true)));
}
@Test
@@ -124,7 +150,12 @@
.compile()
.apply(split.addToRunClasspath)
.run(parameters.getRuntime(), Main.class)
- .assertSuccessWithOutputLines("A", "A");
+ .applyIf(
+ split.expectFailure && parameters.isDexRuntimeVersionOlderThanOrEqual(Version.V4_4_4),
+ rr -> rr.assertFailureWithErrorThatThrows(NoClassDefFoundError.class),
+ split.expectFailure,
+ rr -> rr.assertFailureWithErrorThatThrows(IllegalAccessError.class),
+ rr -> rr.assertSuccessWithOutputLines("A", "A"));
}
public static class C extends MemberRebindingClasspathSplitTestClasses.B {
diff --git a/src/test/java/com/android/tools/r8/partial/ExperimentalApiPatternParseTest.java b/src/test/java/com/android/tools/r8/partial/ExperimentalApiPatternParseTest.java
new file mode 100644
index 0000000..64447c9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/partial/ExperimentalApiPatternParseTest.java
@@ -0,0 +1,72 @@
+// Copyright (c) 2025, 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.partial;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.partial.predicate.R8PartialPredicateCollection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ExperimentalApiPatternParseTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withNoneRuntime().build();
+ }
+
+ @Test
+ public void testPackagePrefix() throws Exception {
+ DexItemFactory factory = new DexItemFactory();
+ for (String pattern :
+ new String[] {
+ "a.**,b.**", "a.**, b.**", " a.**,b.** ", "\ta.**\t,\nb.**\u2000\u2001\u2002"
+ }) {
+ R8PartialCompilationConfiguration config =
+ R8PartialCompilationConfiguration.fromIncludeExcludePatterns(pattern, pattern);
+ for (R8PartialPredicateCollection predicate :
+ new R8PartialPredicateCollection[] {
+ config.getIncludePredicates(), config.getExcludePredicates()
+ }) {
+ assertTrue(predicate.test(factory.createString("La/A;")));
+ assertTrue(predicate.test(factory.createString("La/a/A;")));
+ assertTrue(predicate.test(factory.createString("Lb/A;")));
+ assertTrue(predicate.test(factory.createString("Lb/a/A;")));
+ }
+ }
+ }
+
+ @Test
+ public void testClassPrefix() throws Exception {
+ DexItemFactory factory = new DexItemFactory();
+ for (String pattern :
+ new String[] {"a.*,b.A*", "a.*, b.A*", " a.*,b.A* ", "\ta.*\t,\nb.A*\u2000\u2001\u2002"}) {
+ R8PartialCompilationConfiguration config =
+ R8PartialCompilationConfiguration.fromIncludeExcludePatterns(pattern, pattern);
+ for (R8PartialPredicateCollection predicate :
+ new R8PartialPredicateCollection[] {
+ config.getIncludePredicates(), config.getExcludePredicates()
+ }) {
+ assertTrue(predicate.test(factory.createString("La/A;")));
+ assertTrue(predicate.test(factory.createString("La/Aa;")));
+ assertFalse(predicate.test(factory.createString("La/a/A;")));
+ assertTrue(predicate.test(factory.createString("Lb/A;")));
+ assertTrue(predicate.test(factory.createString("Lb/Aa;")));
+ assertFalse(predicate.test(factory.createString("Lb/Aa/a;")));
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/SoftPinClassInConditionWithMethodTest.java b/src/test/java/com/android/tools/r8/shaking/SoftPinClassInConditionWithMethodTest.java
new file mode 100644
index 0000000..9f8760f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/SoftPinClassInConditionWithMethodTest.java
@@ -0,0 +1,94 @@
+// Copyright (c) 2025, 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.shaking;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class SoftPinClassInConditionWithMethodTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ private static final String EXPECTED_OUTPUT = StringUtils.lines("2");
+
+ @Test
+ public void testD8() throws Exception {
+ parameters.assumeDexRuntime();
+ testForD8(parameters)
+ .addInnerClasses(getClass())
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT);
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ testForR8(parameters)
+ .addInnerClasses(getClass())
+ .addKeepMainRule(TestClass.class)
+ .addKeepRules(
+ "-if class "
+ + A.class.getTypeName()
+ + " { void foo(java.lang.Class); }"
+ + " -keepclasseswithmembers class "
+ + B.class.getTypeName()
+ + " { <init>(...); }")
+ .run(parameters.getRuntime(), TestClass.class)
+ .inspect(
+ inspector -> {
+ // Class A is still present with no members and no reference to it at all. It could
+ // have been removed.
+ assertThat(inspector.clazz(A.class), isPresent());
+ assertTrue(inspector.clazz(A.class).allMethods().isEmpty());
+ assertEquals(
+ 0,
+ inspector
+ .clazz(TestClass.class)
+ .mainMethod()
+ .streamInstructions()
+ .filter(InstructionSubject::isNewInstance)
+ .count());
+ })
+ .assertSuccessWithOutput(EXPECTED_OUTPUT);
+ }
+
+ static class A {
+ void foo(Class<?> clazz) {
+ System.out.println(clazz.getDeclaredConstructors().length);
+ }
+ }
+
+ static class B {
+ B() {}
+
+ B(int i) {}
+ }
+
+ static class TestClass {
+
+ public static void main(String[] args) {
+ new A().foo(System.nanoTime() > 0 ? B.class : null);
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/SoftPinClassInConditionWithOnlyInstanceInitializerTest.java b/src/test/java/com/android/tools/r8/shaking/SoftPinClassInConditionWithOnlyInstanceInitializerTest.java
new file mode 100644
index 0000000..c84c6bf
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/SoftPinClassInConditionWithOnlyInstanceInitializerTest.java
@@ -0,0 +1,86 @@
+// Copyright (c) 2025, 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.shaking;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class SoftPinClassInConditionWithOnlyInstanceInitializerTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ private static final String EXPECTED_OUTPUT = StringUtils.lines("2");
+ private static final String UNEXPECTED_OUTPUT = StringUtils.lines("0");
+
+ @Test
+ public void testD8() throws Exception {
+ parameters.assumeDexRuntime();
+ testForD8(parameters)
+ .addInnerClasses(getClass())
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT);
+ }
+
+ // TODO(b/437277192): Reproduction.
+ @Test
+ public void testR8() throws Exception {
+ testForR8(parameters)
+ .addInnerClasses(getClass())
+ .addKeepMainRule(TestClass.class)
+ .addKeepRules(
+ "-if class "
+ + A.class.getTypeName()
+ + " -keepclasseswithmembers class "
+ + B.class.getTypeName()
+ + " { <init>(...); }")
+ .run(parameters.getRuntime(), TestClass.class)
+ .inspect(
+ inspector -> {
+ // Class A is removed (which is fine, but the B constructors should still be there).
+ assertThat(inspector.clazz(A.class), isAbsent());
+ assertThat(inspector.clazz(B.class), isPresent());
+ assertTrue(inspector.clazz(B.class).allMethods().isEmpty());
+ })
+ .assertSuccessWithOutput(UNEXPECTED_OUTPUT);
+ }
+
+ static class A {
+ void foo(Class<?> clazz) {
+ System.out.println(clazz.getDeclaredConstructors().length);
+ }
+ }
+
+ static class B {
+ B() {}
+
+ B(int i) {}
+ }
+
+ static class TestClass {
+
+ public static void main(String[] args) {
+ new A().foo(System.nanoTime() > 0 ? B.class : null);
+ }
+ }
+}
diff --git a/src/test/testbase/java/com/android/tools/r8/AssistantTestBuilder.java b/src/test/testbase/java/com/android/tools/r8/AssistantTestBuilder.java
index c007ec3..e4fd8b3 100644
--- a/src/test/testbase/java/com/android/tools/r8/AssistantTestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/AssistantTestBuilder.java
@@ -11,6 +11,7 @@
import com.android.tools.r8.benchmarks.BenchmarkResults;
import com.android.tools.r8.errors.Unimplemented;
import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.ExceptionUtils;
import com.android.tools.r8.utils.InternalOptions;
import java.io.IOException;
import java.io.UncheckedIOException;
@@ -100,7 +101,13 @@
if (customReflectiveOperationReceiver != null) {
builder.setReflectiveReceiverClassDescriptor(customReflectiveOperationReceiver);
}
- R8Assistant.run(builder.build());
+ R8AssistantCommand build = builder.build();
+ InternalOptions options = build.getInternalOptions();
+ if (optionsConsumer != null) {
+ ExceptionUtils.withCompilationHandler(
+ options.reporter, () -> optionsConsumer.accept(options));
+ }
+ R8Assistant.runForTest(build, options);
return new AssistantTestCompileResult(
initialCompilation,
getState(),
diff --git a/src/test/testbase/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java b/src/test/testbase/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java
index 753027f..bf980e0 100644
--- a/src/test/testbase/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java
+++ b/src/test/testbase/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java
@@ -101,6 +101,10 @@
methodWithReceiverForForwarding.getMethodDescriptor());
}
+ public static ClassReference syntheticThrowBlockOutlineClass(Class<?> clazz, int id) {
+ return syntheticClass(clazz, naming.THROW_BLOCK_OUTLINE, id);
+ }
+
public static ClassReference syntheticOutlineClass(Class<?> clazz, int id) {
return syntheticClass(clazz, naming.OUTLINE, id);
}
diff --git a/tools/r8_release.py b/tools/r8_release.py
index 598ff46..1d9ecb1 100755
--- a/tools/r8_release.py
+++ b/tools/r8_release.py
@@ -17,7 +17,7 @@
import utils
-R8_DEV_BRANCH = '8.13'
+R8_DEV_BRANCH = '9.0'
R8_VERSION_FILE = os.path.join('src', 'main', 'java', 'com', 'android', 'tools',
'r8', 'Version.java')
THIS_FILE_RELATIVE = os.path.join('tools', 'r8_release.py')