Reapply "Add support for outlining StringBuilders"

This reverts commit ff0c12bd8a973c2bd12b63f1b5fbac5cb5901c0d.

Change-Id: I1af86778c9f2bffa0ed1e8b55c0290b1dc980696
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 003e17f..2287bbc 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
@@ -96,10 +96,19 @@
     return prev;
   }
 
+  public boolean hasNext() {
+    return next != null;
+  }
+
   public Instruction getNext() {
     return next;
   }
 
+  @SuppressWarnings("TypeParameterUnusedInFormals")
+  public <T extends Instruction> T nextUntilExclusive(Predicate<Instruction> predicate) {
+    return hasNext() ? getNext().nextUntilInclusive(predicate) : null;
+  }
+
   @SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"})
   public <T extends Instruction> T nextUntilInclusive(Predicate<Instruction> predicate) {
     Instruction current = this;
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
index a7d82e0..8f61c05 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ThrowBlockOutlineMarker.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ThrowBlockOutlineMarker.java
@@ -37,7 +37,7 @@
         ListUtils.mapOrElse(
             inValues,
             (i, argument) -> {
-              if (outline.getParentOrSelf().isArgumentConstant(i)) {
+              if (outline.isArgumentConstant(i)) {
                 argument.removeUser(this);
                 return null;
               }
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
index ae16771..353aba6 100644
--- 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
@@ -295,6 +295,14 @@
     return materializedOutlineMethod != null;
   }
 
+  public boolean isStringBuilderToStringOutline() {
+    return !isThrowOutline();
+  }
+
+  public boolean isThrowOutline() {
+    return proto.getReturnType().isVoidType();
+  }
+
   public void materialize(AppView<?> appView, MethodProcessingContext methodProcessingContext) {
     assert verifyNotMerged();
     SyntheticItems syntheticItems = appView.getSyntheticItems();
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
index e9de503..04e8567 100644
--- 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
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
@@ -18,7 +19,6 @@
 import com.android.tools.r8.ir.code.Phi;
 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.UnusedArgument;
 import com.android.tools.r8.ir.code.Value;
@@ -41,10 +41,12 @@
 
   private final AppView<?> appView;
   private final DeadCodeRemover deadCodeRemover;
+  private final DexItemFactory factory;
 
   ThrowBlockOutlineMarkerRewriter(AppView<?> appView) {
     this.appView = appView;
     this.deadCodeRemover = new DeadCodeRemover(appView);
+    this.factory = appView.dexItemFactory();
   }
 
   public void processOutlineMethod(
@@ -109,80 +111,92 @@
 
   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();
-          outlineMarker.detachConstantOutlineArguments(outline);
-          if (outline.isMaterialized()) {
-            // Insert a call to the materialized outline method and load the return value.
-            BasicBlockInstructionListIterator instructionIterator =
-                block.listIterator(block.exit());
-            InvokeStatic invoke =
-                InvokeStatic.builder()
-                    .setArguments(outlineMarker.inValues())
-                    .setIsInterface(false)
-                    .setMethod(outline.getMaterializedOutlineMethod())
-                    .setPosition(throwInstruction)
-                    .build();
+      ThrowBlockOutlineMarker outlineMarker =
+          block.entry().nextUntilInclusive(Instruction::isThrowBlockOutlineMarker);
+      while (outlineMarker != null) {
+        ThrowBlockOutline outline = outlineMarker.getOutline().getParentOrSelf();
+        Instruction outlineEnd = getOutlineEnd(block, outline, outlineMarker);
+        outlineMarker.detachConstantOutlineArguments(outline);
+        if (outline.isMaterialized()) {
+          // Insert a call to the materialized outline method and load the return value.
+          BasicBlockInstructionListIterator instructionIterator = block.listIterator(outlineEnd);
+          InvokeStatic.Builder invokeBuilder =
+              InvokeStatic.builder()
+                  .setArguments(outlineMarker.inValues())
+                  .setIsInterface(false)
+                  .setMethod(outline.getMaterializedOutlineMethod())
+                  .setPosition(outlineEnd);
+          if (outline.isStringBuilderToStringOutline()) {
+            invokeBuilder.setFreshOutValue(
+                code, factory.stringType.toTypeElement(appView), outlineEnd.getLocalInfo());
+          }
+          InvokeStatic invoke = invokeBuilder.build();
+          if (outline.isStringBuilderToStringOutline()) {
+            outlineEnd.replace(invoke);
+            outlineEnd = invoke;
+          } else {
+            assert outline.isThrowOutline();
             instructionIterator.add(invoke);
-            Value returnValue = addReturnOrThrowValue(code, instructionIterator);
+            Value returnOrThrowValue = addReturnOrThrowValue(code, instructionIterator);
 
             // Replace the throw instruction by a normal return, but throw null in initializers.
             if (code.context().getDefinition().isInstanceInitializer()) {
-              throwInstruction.replaceValue(0, returnValue);
+              outlineEnd.replaceValue(0, returnOrThrowValue);
             } else {
               Return returnInstruction =
                   Return.builder()
                       .setPositionForNonThrowingInstruction(
-                          throwInstruction.getPosition(), appView.options())
-                      .setReturnValue(returnValue)
+                          outlineEnd.getPosition(), appView.options())
+                      .setReturnValue(returnOrThrowValue)
                       .build();
               block.replaceLastInstruction(returnInstruction);
+              outlineEnd = returnInstruction;
             }
+          }
 
-            // Remove all outlined instructions bottom up.
-            instructionIterator = block.listIterator(invoke);
-            for (Instruction instruction = instructionIterator.previous();
-                instruction != outlineMarker;
-                instruction = instructionIterator.previous()) {
-              Value outValue = instruction.outValue();
-              if (outValue == null || !outValue.hasNonDebugUsers()) {
-                // Remove all debug users of the out-value.
-                if (outValue != null && outValue.hasDebugUsers()) {
-                  for (Instruction debugUser : outValue.debugUsers()) {
-                    debugUser.getDebugValues().remove(outValue);
-                    if (debugUser.isDebugLocalRead() && debugUser.getDebugValues().isEmpty()) {
-                      debugUser.remove();
-                    }
+          // Remove all outlined instructions bottom up.
+          instructionIterator = block.listIterator(invoke);
+          for (Instruction outlinedInstruction = instructionIterator.previous();
+              outlinedInstruction != outlineMarker;
+              outlinedInstruction = instructionIterator.previous()) {
+            assert !outlinedInstruction.isThrowBlockOutlineMarker();
+            Value outValue = outlinedInstruction.outValue();
+            if (outValue == null || !outValue.hasNonDebugUsers()) {
+              // Remove all debug users of the out-value.
+              if (outValue != null && outValue.hasDebugUsers()) {
+                for (Instruction debugUser : outValue.debugUsers()) {
+                  debugUser.getDebugValues().remove(outValue);
+                  if (debugUser.isDebugLocalRead() && debugUser.getDebugValues().isEmpty()) {
+                    debugUser.remove();
                   }
-                  outValue.clearDebugUsers();
                 }
-                // We are not using `removeOrReplaceByDebugLocalRead` here due to the backwards
-                // iteration.
-                if (instruction.getDebugValues().isEmpty()) {
-                  instruction.remove();
-                } else {
-                  DebugLocalRead replacement = new DebugLocalRead();
-                  instruction.replace(replacement);
-                  Instruction previous = instructionIterator.previous();
-                  assert previous == replacement;
-                }
+                outValue.clearDebugUsers();
+              }
+              // We are not using `removeOrReplaceByDebugLocalRead` here due to the backwards
+              // iteration.
+              if (outlinedInstruction.getDebugValues().isEmpty()) {
+                outlinedInstruction.remove();
+              } else {
+                DebugLocalRead replacement = new DebugLocalRead();
+                outlinedInstruction.replace(replacement);
+                Instruction previous = instructionIterator.previous();
+                assert previous == replacement;
               }
             }
           }
-
-          // Finally delete the outline marker.
-          outlineMarker.removeOrReplaceByDebugLocalRead();
-
-          // Blocks cannot start with DebugLocalRead.
-          while (block.entry().isDebugLocalRead()) {
-            block.entry().moveDebugValues(block.entry().getNext());
-            block.entry().remove();
-          }
         }
+
+        // Finally delete the outline marker.
+        outlineMarker.removeOrReplaceByDebugLocalRead();
+
+        // Blocks cannot start with DebugLocalRead.
+        while (block.entry().isDebugLocalRead()) {
+          block.entry().moveDebugValues(block.entry().getNext());
+          block.entry().remove();
+        }
+
+        // Continue searching for outline markers from the end of the current outline.
+        outlineMarker = outlineEnd.nextUntilExclusive(Instruction::isThrowBlockOutlineMarker);
       }
       assert block.streamInstructions().noneMatch(Instruction::isThrowBlockOutlineMarker);
     }
@@ -216,4 +230,15 @@
       return null;
     }
   }
+
+  private Instruction getOutlineEnd(
+      BasicBlock block, ThrowBlockOutline outline, ThrowBlockOutlineMarker outlineMarker) {
+    if (outline.isThrowOutline()) {
+      return block.exit();
+    } else {
+      // The end of a StringBuilder#toString outline is the call to StringBuilder#toString.
+      return outlineMarker.nextUntilExclusive(
+          i -> ThrowBlockOutlinerScanner.isStringBuilderToString(i, factory));
+    }
+  }
 }
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
index 7f6ea7d..ce48c06 100644
--- 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
@@ -15,6 +15,10 @@
       SystemPropertyUtils.parseSystemPropertyOrDefault(
           "com.android.tools.r8.throwblockoutliner.enable", false);
 
+  public boolean enableStringBuilderOutlining =
+      SystemPropertyUtils.parseSystemPropertyOrDefault(
+          "com.android.tools.r8.throwblockoutliner.enablestringbuilder", false);
+
   public final int costInBytesForTesting =
       SystemPropertyUtils.parseSystemPropertyOrDefault(
           "com.android.tools.r8.throwblockoutliner.cost", -1);
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
index 582fc97..2fda1c4 100644
--- 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
@@ -42,6 +42,7 @@
 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.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;
@@ -92,6 +93,28 @@
       if (block.exit().isThrow()) {
         new ThrowBlockOutlinerScannerForThrow(code, block).tryBuildOutline();
       }
+      if (!appView.options().getThrowBlockOutlinerOptions().enableStringBuilderOutlining) {
+        continue;
+      }
+      Instruction previousOutlineEnd = null;
+      for (Instruction instruction : block.getInstructions()) {
+        // If we encounter an outline marker it is because we were able to outline the tail of the
+        // block above. Since the remainder of the block has been outlined, abort further outlining.
+        if (instruction.isThrowBlockOutlineMarker()) {
+          assert block.exit().isThrow();
+          break;
+        }
+        if (isStringBuilderToString(instruction, factory)) {
+          InvokeVirtual invoke = instruction.asInvokeVirtual();
+          ThrowBlockOutline outline =
+              new ThrowBlockOutlinerScannerForStringBuilderToString(
+                      code, block, invoke, previousOutlineEnd)
+                  .tryBuildOutline();
+          if (outline != null) {
+            previousOutlineEnd = instruction;
+          }
+        }
+      }
     }
     if (code.metadata().mayHaveThrowBlockOutlineMarker()) {
       if (appView.enableWholeProgramOptimizations()) {
@@ -113,55 +136,68 @@
     return outlines.getOutlines();
   }
 
+  static boolean isStringBuilderToString(Instruction instruction, DexItemFactory factory) {
+    InvokeVirtual invoke = instruction.asInvokeVirtual();
+    return invoke != null
+        && invoke.getInvokedMethod().match(factory.objectMembers.toString)
+        && invoke.getReceiver().getType().isClassType(factory.stringBuilderType)
+        && invoke.hasOutValue();
+  }
+
   private abstract class ThrowBlockOutlinerScannerForInstruction {
 
     final IRCode code;
     final BasicBlock block;
+    final Instruction previousOutlineEnd;
 
     private boolean hasRunPrefixer;
 
-    ThrowBlockOutlinerScannerForInstruction(IRCode code, BasicBlock block) {
+    ThrowBlockOutlinerScannerForInstruction(
+        IRCode code, BasicBlock block, Instruction previousOutlineEnd) {
       this.code = code;
       this.block = block;
+      this.previousOutlineEnd = previousOutlineEnd;
     }
 
     void processInstruction(Instruction instruction, Consumer<OutlineBuilder> continuation) {
-      switch (instruction.opcode()) {
-        case ASSUME:
-          processAssume(instruction.asAssume(), continuation);
-          return;
-        case CONST_NUMBER:
-        case CONST_STRING:
-          processConstInstruction(instruction.asConstInstruction(), continuation);
-          return;
-        case DEBUG_LOCAL_READ:
-        case DEBUG_POSITION:
-          processNonMaterializingDebugInstruction(instruction, continuation);
-          return;
-        case INVOKE_DIRECT:
-          if (instruction.isInvokeConstructor(factory)) {
-            processStringBuilderConstructorCall(instruction.asInvokeDirect(), continuation);
+      if (instruction != previousOutlineEnd) {
+        switch (instruction.opcode()) {
+          case ASSUME:
+            processAssume(instruction.asAssume(), continuation);
             return;
-          }
-          break;
-        case INVOKE_STATIC:
-          processStringFormatOrValueOf(instruction.asInvokeStatic(), continuation);
-          return;
-        case INVOKE_VIRTUAL:
-          processStringBuilderAppendOrToString(instruction.asInvokeVirtual(), continuation);
-          return;
-        case MOVE:
-          if (instruction.isDebugLocalWrite()) {
-            processDebugLocalWrite(instruction.asDebugLocalWrite(), continuation);
+          case CONST_NUMBER:
+          case CONST_STRING:
+            processConstInstruction(instruction.asConstInstruction(), continuation);
             return;
-          }
-          assert false;
-          break;
-        case NEW_INSTANCE:
-          processNewInstanceInstruction(instruction.asNewInstance(), continuation);
-          return;
-        default:
-          break;
+          case DEBUG_LOCAL_READ:
+          case DEBUG_POSITION:
+            processNonMaterializingDebugInstruction(instruction, continuation);
+            return;
+          case INVOKE_DIRECT:
+            if (instruction.isInvokeConstructor(factory)) {
+              processStringBuilderConstructorCall(instruction.asInvokeDirect(), continuation);
+              return;
+            }
+            break;
+          case INVOKE_STATIC:
+            processStringFormatOrValueOf(instruction.asInvokeStatic(), continuation);
+            return;
+          case INVOKE_VIRTUAL:
+            processStringBuilderAppendOrToString(instruction.asInvokeVirtual(), continuation);
+            return;
+          case MOVE:
+            if (instruction.isDebugLocalWrite()) {
+              processDebugLocalWrite(instruction.asDebugLocalWrite(), continuation);
+              return;
+            }
+            assert false;
+            break;
+          case NEW_INSTANCE:
+            processNewInstanceInstruction(instruction.asNewInstance(), continuation);
+            return;
+          default:
+            break;
+        }
       }
       // Unhandled instruction. Start the outline at the successor instruction.
       startOutline(instruction.getNext(), continuation);
@@ -300,7 +336,7 @@
           });
     }
 
-    private void processStringBuilderAppendOrToString(
+    void processStringBuilderAppendOrToString(
         InvokeVirtual invoke, Consumer<OutlineBuilder> continuation) {
       DexMethod invokedMethod = invoke.getInvokedMethod();
       if (!factory.stringBuilderMethods.isAppendMethod(invokedMethod)
@@ -357,12 +393,75 @@
     }
   }
 
+  private class ThrowBlockOutlinerScannerForStringBuilderToString
+      extends ThrowBlockOutlinerScannerForInstruction {
+
+    private final InvokeVirtual stringBuilderToStringInstruction;
+
+    private ThrowBlockOutline outline;
+
+    ThrowBlockOutlinerScannerForStringBuilderToString(
+        IRCode code,
+        BasicBlock block,
+        InvokeVirtual stringBuilderToStringInstruction,
+        Instruction previousOutlineEnd) {
+      super(code, block, previousOutlineEnd);
+      this.stringBuilderToStringInstruction = stringBuilderToStringInstruction;
+    }
+
+    ThrowBlockOutline tryBuildOutline() {
+      // Recursively build up the outline method. On successful outline creation, the resulting
+      // LirCode is passed to the continuation function.
+      processStringBuilderToString(
+          outlineBuilder -> {
+            // On successful outline creation, store the outline for later processing.
+            DexProto proto = outlineBuilder.getProto(appView, factory.stringType);
+            if (proto == null) {
+              return;
+            }
+            LirCode<?> lirCode = outlineBuilder.buildLirCode(appView, code.context());
+            outline = outlines.add(lirCode, proto, code.context());
+            assert proto.isIdenticalTo(outline.getProto());
+            List<Value> arguments = outlineBuilder.buildArguments();
+            outline.addUser(code.reference(), arguments, getAbstractValueFactory());
+
+            // 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()
+                    .setArguments(arguments)
+                    .setOutline(outline)
+                    .setPosition(Position.none())
+                    .build();
+            block.listIterator(insertionPoint).add(marker);
+          });
+      return outline;
+    }
+
+    private void processStringBuilderToString(Consumer<OutlineBuilder> continuation) {
+      processStringBuilderAppendOrToString(
+          stringBuilderToStringInstruction,
+          outlineBuilder -> {
+            Value outlinedStringValue =
+                outlineBuilder.getOutlinedValue(stringBuilderToStringInstruction.outValue());
+            outlineBuilder.add(
+                Return.builder()
+                    .setPosition(Position.syntheticNone())
+                    .setReturnValue(outlinedStringValue)
+                    .build());
+            continuation.accept(outlineBuilder);
+          });
+    }
+  }
+
   private class ThrowBlockOutlinerScannerForThrow extends ThrowBlockOutlinerScannerForInstruction {
 
     private final Throw throwInstruction;
 
     ThrowBlockOutlinerScannerForThrow(IRCode code, BasicBlock block) {
-      super(code, block);
+      super(code, block, null);
       this.throwInstruction = block.exit().asThrow();
     }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
index a3de438..3ad5193 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
@@ -322,7 +322,7 @@
     assertEquals(Collections.emptyList(), synthesizedJavaLambdaClasses);
 
     assertEquals(
-        Collections.singleton("java.lang.StringBuilder"),
+        Collections.emptySet(),
         collectTypes(clazz.uniqueMethodWithOriginalName("testStatelessLambda")));
 
     assertEquals(
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerConstArgumentTest.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerConstArgumentTest.java
index 2badee6..87e57c2 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerConstArgumentTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerConstArgumentTest.java
@@ -22,6 +22,8 @@
 import it.unimi.dsi.fastutil.ints.IntArraySet;
 import it.unimi.dsi.fastutil.ints.IntSet;
 import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
 import org.junit.Test;
 
 public class ThrowBlockOutlinerConstArgumentTest extends ThrowBlockOutlinerTestBase {
@@ -62,10 +64,12 @@
 
   @Override
   public void inspectOutlines(Collection<ThrowBlockOutline> outlines, DexItemFactory factory) {
-    // Verify that we have two outlines with one and three users, respectively.
-    assertEquals(2, outlines.size());
+    // Verify that we have two throw block outlines with one and three users, respectively.
+    List<ThrowBlockOutline> throwOutlines =
+        outlines.stream().filter(ThrowBlockOutline::isThrowOutline).collect(Collectors.toList());
+    assertEquals(2, throwOutlines.size());
     IntSet numberOfUsers = new IntArraySet();
-    for (ThrowBlockOutline outline : outlines) {
+    for (ThrowBlockOutline outline : throwOutlines) {
       numberOfUsers.add(outline.getNumberOfUsers());
     }
     assertTrue(numberOfUsers.contains(1));
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/stringbuilders/StringBuilderOutlinerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/stringbuilders/StringBuilderOutlinerTest.java
new file mode 100644
index 0000000..b64c5ba
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/stringbuilders/StringBuilderOutlinerTest.java
@@ -0,0 +1,71 @@
+// 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.stringbuilders;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.SingleTestRunResult;
+import com.android.tools.r8.TestCompilerBuilder;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.ir.optimize.outliner.exceptions.ThrowBlockOutline;
+import com.android.tools.r8.ir.optimize.outliner.exceptions.ThrowBlockOutlinerTestBase;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.Collection;
+import org.junit.Test;
+
+public class StringBuilderOutlinerTest extends ThrowBlockOutlinerTestBase {
+
+  @Test
+  public void testD8() throws Exception {
+    runTest(testForD8(parameters));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assumeRelease();
+    runTest(testForR8(parameters).addKeepMainRule(Main.class));
+  }
+
+  private void runTest(
+      TestCompilerBuilder<?, ?, ?, ? extends SingleTestRunResult<?>, ?> testBuilder)
+      throws Exception {
+    testBuilder
+        .addInnerClasses(getClass())
+        .apply(this::configure)
+        .compile()
+        .inspect(this::inspectOutput)
+        .run(parameters.getRuntime(), Main.class, "Hel", "lo", ", world", "!")
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  @Override
+  public void inspectOutlines(Collection<ThrowBlockOutline> outlines, DexItemFactory factory) {
+    assertEquals(1, outlines.size());
+    ThrowBlockOutline outline = outlines.iterator().next();
+    assertEquals(2, outline.getNumberOfUsers());
+  }
+
+  private void inspectOutput(CodeInspector inspector) {
+    MethodSubject mainMethod = inspector.clazz(Main.class).mainMethod();
+    assertTrue(mainMethod.streamInstructions().noneMatch(InstructionSubject::isNewInstance));
+  }
+
+  @Override
+  public boolean shouldOutline(ThrowBlockOutline outline) {
+    return true;
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      String s1 = new StringBuilder().append(args[0]).append(args[1]).toString();
+      String s2 = new StringBuilder().append(args[2]).append(args[3]).toString();
+      System.out.print(s1);
+      System.out.println(s2);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
index 9646cc8..ef9fece 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
@@ -6,12 +6,10 @@
 
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_3_72;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_5_0;
-import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_6_0;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_9_21;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_2_0_20;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_2_1_10;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
-import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsentIf;
 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;
@@ -60,7 +58,7 @@
   }
 
   @Test
-  public void testJStyleLambdas() throws Exception {
+  public void testJStyleLambdasNoClassInlining() throws Exception {
     // SAM interfaces lambdas are implemented by invoke dynamic in kotlin 1.5 unlike 1.4 where a
     // class is generated for each. In CF we leave invokeDynamic but for DEX we desugar the classes
     // and merge them.
@@ -87,14 +85,18 @@
                                 "class_inliner_lambda_j_style.MainKt$testStateful3$1");
                           } else if (testParameters.isDexRuntime()) {
                             Set<Set<DexType>> mergeGroups = inspector.getMergeGroups();
-                            assertEquals(1, mergeGroups.size());
-                            inspector.assertIsCompleteMergeGroup(
-                                "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda0",
-                                "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda2",
-                                "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda4",
-                                "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda3",
-                                "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda5",
-                                "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda6");
+                            assertEquals(2, mergeGroups.size());
+                            inspector
+                                .assertIsCompleteMergeGroup(
+                                    "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda1",
+                                    "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda3",
+                                    "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda4",
+                                    "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda5",
+                                    "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda6",
+                                    "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda7")
+                                .assertIsCompleteMergeGroup(
+                                    "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda2",
+                                    "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticThrowBlockOutline0");
                           }
                           inspector.assertNoOtherClassesMerged();
                         })
@@ -103,25 +105,16 @@
             inspector -> {
               if (testParameters.isCfRuntime() && !hasKotlinCGeneratedLambdaClasses) {
                 assertEquals(5, inspector.allClasses().size());
-              } else if (!hasKotlinCGeneratedLambdaClasses) {
-                assertThat(
-                    inspector.clazz(
-                        "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda1"),
-                    isPresent());
-                assertThat(
-                    inspector.clazz(
-                        "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda2"),
-                    isAbsent());
               } else {
-                assertThat(
-                    inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateless$1"),
-                    isAbsent());
-                assertThat(
-                    inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful$1"),
-                    isPresent());
+                assertEquals(7, inspector.allClasses().size());
               }
             });
+  }
 
+  @Test
+  public void testJStyleLambdas() throws Exception {
+    boolean hasKotlinCGeneratedLambdaClasses = kotlinParameters.isOlderThan(KOTLINC_1_5_0);
+    String mainClassName = "class_inliner_lambda_j_style.MainKt";
     runTest(
             "class_inliner_lambda_j_style",
             mainClassName,
@@ -136,39 +129,14 @@
             inspector -> {
               if (testParameters.isCfRuntime() && !hasKotlinCGeneratedLambdaClasses) {
                 assertEquals(5, inspector.allClasses().size());
-                return;
-              }
-              if (!hasKotlinCGeneratedLambdaClasses) {
-                // Kotlin 1.6.20 and later do not create intrinsics.stringPlus for two argument
-                // string concatination. That allow R8's stringbuilder optimization to reduce the
-                // size of strings and therefore inline the synthetic lambda.
-                assertThat(
-                    inspector.clazz(
-                        "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda1"),
-                    isAbsentIf(kotlinParameters.isNewerThan(KOTLINC_1_6_0)));
               } else {
-                assertThat(
-                    inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateless$1"),
-                    isAbsent());
-              }
-
-              if (hasKotlinCGeneratedLambdaClasses) {
-                assertThat(
-                    testParameters.isCfRuntime()
-                        ? inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful2$1")
-                        : inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful$2"),
-                    isPresent());
-              } else {
-                assertThat(
-                    inspector.clazz(
-                        "class_inliner_lambda_j_style.MainKt$$ExternalSyntheticLambda2"),
-                    isAbsent());
+                assertEquals(7, inspector.allClasses().size());
               }
             });
   }
 
   @Test
-  public void testKStyleLambdas() throws Exception {
+  public void testKStyleLambdasNoClassInlining() throws Exception {
     String mainClassName = "class_inliner_lambda_k_style.MainKt";
     runTest(
             "class_inliner_lambda_k_style",
@@ -264,7 +232,11 @@
                     isPresent());
               }
             });
+  }
 
+  @Test
+  public void testKStyleLambdas() throws Exception {
+    String mainClassName = "class_inliner_lambda_k_style.MainKt";
     runTest(
             "class_inliner_lambda_k_style",
             mainClassName,
diff --git a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingCapturesKotlinStyleTest.java b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingCapturesKotlinStyleTest.java
index c9467fa..b02f228 100644
--- a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingCapturesKotlinStyleTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingCapturesKotlinStyleTest.java
@@ -13,10 +13,11 @@
 import com.android.tools.r8.KotlinTestParameters;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
 import com.google.common.collect.ImmutableList;
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -81,7 +82,20 @@
         .addProgramFiles(getProgramFiles())
         .addKeepMainRule(getMainClassName())
         .addHorizontallyMergedClassesInspector(
-            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+            inspector -> {
+              if (parameters.isDexRuntime()
+                  && kotlinParameters.getLambdaGeneration().isInvokeDynamic()) {
+                inspector
+                    .assertIsCompleteMergeGroup(
+                        SyntheticItemsTestUtils.syntheticThrowBlockOutlineClass(
+                            getMainClassReference(), 0),
+                        SyntheticItemsTestUtils.syntheticThrowBlockOutlineClass(
+                            getMainClassReference(), 1))
+                    .assertNoOtherClassesMerged();
+              } else {
+                inspector.assertNoClassesMerged();
+              }
+            })
         .allowAccessModification(allowAccessModification)
         .setMinApi(parameters)
         .compile()
@@ -138,6 +152,10 @@
     return getTestName() + ".MainKt";
   }
 
+  private ClassReference getMainClassReference() {
+    return Reference.classFromTypeName(getMainClassName());
+  }
+
   private List<Path> getProgramFiles() {
     Path kotlinJarFile =
         getCompileMemoizer(getKotlinFilesInResource(getTestName()), getTestName())
diff --git a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingKeepAttributesKotlinStyleTest.java b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingKeepAttributesKotlinStyleTest.java
index da524c4..56178bd 100644
--- a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingKeepAttributesKotlinStyleTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingKeepAttributesKotlinStyleTest.java
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.kotlin.lambda;
 
-import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_9_21;
 import static com.android.tools.r8.shaking.ProguardKeepAttributes.ENCLOSING_METHOD;
 import static com.android.tools.r8.shaking.ProguardKeepAttributes.INNER_CLASSES;
 import static com.android.tools.r8.shaking.ProguardKeepAttributes.SIGNATURE;
@@ -189,21 +188,6 @@
         ClassReference mainKt = Reference.classFromTypeName(getMainClassName());
         List<ClassReference> mergeGroup =
             ImmutableList.of(
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 9),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 10),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 11),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 12),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 13),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 14),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 19),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 18),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 20),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 21),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 22),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 23));
-        List<ClassReference> otherMergeGroup =
-            ImmutableList.of(
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 0),
                 SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 1),
                 SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 2),
                 SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 3),
@@ -212,24 +196,24 @@
                 SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 6),
                 SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 7),
                 SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 8),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 9),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 10),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 11),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 12),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 13),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 14),
                 SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 15),
                 SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 16),
-                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 17));
-        inspector
-            .applyIf(
-                kotlinc.getCompilerVersion().isGreaterThanOrEqualTo(KOTLINC_1_9_21)
-                    && parameters.isDexRuntime()
-                    && lambdaGeneration.isInvokeDynamic(),
-                i ->
-                    i.assertIsCompleteMergeGroup(
-                        ImmutableList.<ClassReference>builder()
-                            .addAll(mergeGroup)
-                            .addAll(otherMergeGroup)
-                            .build()),
-                i ->
-                    i.assertIsCompleteMergeGroup(mergeGroup)
-                        .assertIsCompleteMergeGroup(otherMergeGroup))
-            .assertNoOtherClassesMerged();
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 17),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 18),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 19),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 20),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 21),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 22),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 23),
+                SyntheticItemsTestUtils.syntheticLambdaClass(mainKt, 24),
+                SyntheticItemsTestUtils.syntheticThrowBlockOutlineClass(mainKt, 0));
+        inspector.assertIsCompleteMergeGroup(mergeGroup).assertNoOtherClassesMerged();
       }
     }
   }
diff --git a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingTrivialJavaStyleTest.java b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingTrivialJavaStyleTest.java
index ae02a7f..ca2d90c 100644
--- a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingTrivialJavaStyleTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingTrivialJavaStyleTest.java
@@ -173,13 +173,24 @@
               },
               i -> {
                 i.assertIsCompleteMergeGroup(
-                        SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 0),
-                        SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 1),
+                        SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 3),
+                        SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 4),
                         SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 0))
                     .assertIsCompleteMergeGroup(
-                        SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 2),
-                        SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 3),
-                        SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 1));
+                        SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 7),
+                        SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 8),
+                        SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 2))
+                    .assertIsCompleteMergeGroup(
+                        SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 5),
+                        SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 6),
+                        SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 1))
+                    .assertIsCompleteMergeGroup(
+                        SyntheticItemsTestUtils.syntheticThrowBlockOutlineClass(
+                            mainClassReference, 0),
+                        SyntheticItemsTestUtils.syntheticThrowBlockOutlineClass(
+                            mainClassReference, 1),
+                        SyntheticItemsTestUtils.syntheticThrowBlockOutlineClass(
+                            mainClassReference, 2));
                 for (int id = 4; id < 30; id++) {
                   inspector.assertClassReferencesNotMerged(
                       SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, id));
@@ -189,48 +200,8 @@
               SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 2),
               SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 3));
     } else {
-      inspector
-          .assertIsCompleteMergeGroup(
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 1),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 2),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 3),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 4))
-          .assertIsCompleteMergeGroup(
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 0),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 9),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 10),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 11),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 12),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 21),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 22),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 23),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 24),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 25),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 26),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 27),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 28))
-          .assertIsCompleteMergeGroup(
-              SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 0),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 13),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 14))
-          .assertIsCompleteMergeGroup(
-              SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 1),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 15),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 16))
-          .assertIsCompleteMergeGroup(
-              SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 2),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 17),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 18))
-          .assertIsCompleteMergeGroup(
-              SyntheticItemsTestUtils.syntheticLambdaClass(innerClassReference, 3),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 19),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 20))
-          .assertIsCompleteMergeGroup(
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 5),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 6))
-          .assertIsCompleteMergeGroup(
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 7),
-              SyntheticItemsTestUtils.syntheticLambdaClass(mainClassReference, 8));
+      assertEquals(10, inspector.getMergeGroups().size());
+      assertEquals(34, inspector.getSources().size());
     }
   }
 
diff --git a/src/test/testbase/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/testbase/java/com/android/tools/r8/TestCompilerBuilder.java
index 25efdbf..86abea7 100644
--- a/src/test/testbase/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -65,6 +65,7 @@
       options -> {
         options.testing.enableTestAssertions = true;
         options.getThrowBlockOutlinerOptions().enable = true;
+        options.getThrowBlockOutlinerOptions().enableStringBuilderOutlining = true;
       };
 
   public static final Consumer<InternalOptions> DEFAULT_D8_OPTIONS = DEFAULT_OPTIONS;
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 9b5bb95..e70142c 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
@@ -105,6 +105,10 @@
   }
 
   public static ClassReference syntheticThrowBlockOutlineClass(Class<?> clazz, int id) {
+    return syntheticThrowBlockOutlineClass(Reference.classFromClass(clazz), id);
+  }
+
+  public static ClassReference syntheticThrowBlockOutlineClass(ClassReference clazz, int id) {
     return syntheticClass(clazz, naming.THROW_BLOCK_OUTLINE, id);
   }