Reproduce NullPointerException when inlining outline
This reproduces a NullPointerException that manifests in the inlining of
an outline, caused by the inability to map back an outline position
using the outline translating table in Code.newInlineePosition.
Bug: b/514117718
Change-Id: I57ebe9b4d2d109ddbc515ece370fb6d7c10fc7c7
diff --git a/src/test/java8/ir/com/android/tools/r8/ir/optimize/inliner/SingleCallerInlineOutlineWithNonThrowingPreludeTest.java b/src/test/java8/ir/com/android/tools/r8/ir/optimize/inliner/SingleCallerInlineOutlineWithNonThrowingPreludeTest.java
new file mode 100644
index 0000000..3e90293
--- /dev/null
+++ b/src/test/java8/ir/com/android/tools/r8/ir/optimize/inliner/SingleCallerInlineOutlineWithNonThrowingPreludeTest.java
@@ -0,0 +1,155 @@
+// Copyright (c) 2026, 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.inliner;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.codeinspector.AssertUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.android.tools.r8.utils.internal.BooleanUtils;
+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 SingleCallerInlineOutlineWithNonThrowingPreludeTest extends TestBase {
+
+ @Parameter(0)
+ public boolean allowInliningOfOutlines;
+
+ @Parameter(1)
+ public TestParameters parameters;
+
+ @Parameters(name = "{1}, allow inlining: {0}")
+ public static List<Object[]> data() {
+ return buildParameters(
+ BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
+ }
+
+ @Test
+ public void test() throws Exception {
+ AssertUtils.assertFailsCompilationIf(
+ allowInliningOfOutlines,
+ () ->
+ testForR8(parameters)
+ .addInnerClasses(getClass())
+ .addKeepMainRule(Main.class)
+ .addOptionsModification(
+ options -> {
+ options.outline.threshold = 2;
+ options.outline.minSize = 2;
+ options.getTestingOptions().allowInliningOfOutlines = allowInliningOfOutlines;
+ })
+ .collectSyntheticItems()
+ .enableInliningAnnotations()
+ .compile()
+ .inspectWithSyntheticItems(
+ (inspector, syntheticItems) -> {
+ ClassSubject printerClassSubject = inspector.clazz(Printer.class);
+ assertThat(printerClassSubject, isPresent());
+
+ MethodSubject helloMethodSubject =
+ printerClassSubject.uniqueMethodWithOriginalName("hello");
+ assertThat(helloMethodSubject, isPresent());
+
+ MethodSubject worldMethodSubject =
+ printerClassSubject.uniqueMethodWithOriginalName("world");
+ assertThat(worldMethodSubject, isPresent());
+
+ ClassSubject mainClassSubject = inspector.clazz(Main.class);
+ assertThat(mainClassSubject, isPresent());
+
+ MethodSubject outlineCandidateMethodSubject =
+ mainClassSubject.uniqueMethodWithOriginalName("outlineCandidate");
+ assertThat(outlineCandidateMethodSubject, isPresent());
+
+ ClassSubject outlineClassSubject =
+ inspector.clazz(syntheticItems.syntheticOutlineClass(Main.class, 0));
+ // Note that the outline class is present even when `allowInliningOfOutlines`
+ // is true, since we do not run another round of tree shaking after the single
+ // caller inliner pass.
+ assertThat(outlineClassSubject, isPresent());
+
+ if (allowInliningOfOutlines) {
+ assertThat(
+ outlineCandidateMethodSubject,
+ allOf(
+ invokesMethod(helloMethodSubject),
+ invokesMethod(worldMethodSubject)));
+ } else {
+ assertThat(
+ outlineCandidateMethodSubject,
+ invokesMethod(outlineClassSubject.uniqueMethod()));
+ }
+ })
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutputLines("Hello 3.0, world!"));
+ }
+
+ static class Main {
+
+ static boolean alwaysFalse;
+
+ public static void main(String[] args) {
+ float x = args.length > 0 ? Float.parseFloat(args[0]) : 1.0f;
+ float y = args.length > 1 ? Float.parseFloat(args[1]) : 2.0f;
+ outlineCandidate(x, y);
+
+ // This will be pruned in the primary optimization pass. At this point R8 still does not know
+ // that the `alwaysFalse` field is false.
+ if (alwaysFalse()) {
+ alwaysFalse = true;
+ }
+
+ // This will be pruned in the second optimization pass after the outliner has run. After the
+ // second round of tree shaking, outlineCandidate2() will be removed, and the synthetic
+ // outline will have a single caller.
+ if (alwaysFalse) {
+ outlineCandidate2(x, y);
+ }
+ }
+
+ @NeverInline
+ static boolean alwaysFalse() {
+ return false;
+ }
+
+ @NeverInline
+ static void outlineCandidate(float x, float y) {
+ float z = x + y;
+ Printer.hello(z);
+ Printer.world();
+ }
+
+ @NeverInline
+ static void outlineCandidate2(float x, float y) {
+ float z = x + y;
+ Printer.hello(z);
+ Printer.world();
+ }
+ }
+
+ static class Printer {
+
+ @NeverInline
+ static void hello(float z) {
+ System.out.print("Hello " + z);
+ }
+
+ @NeverInline
+ static void world() {
+ System.out.println(", world!");
+ }
+ }
+}