Extend throw block outliner to exceptions with arguments

This adds support for passing arbitrary values to the outlined exception. The outliner captures these values as arguments of the outlined method.

This also adds support for outlining uses of StringBuilder.

To keep track of which values from a given outline user must be passed as arguments to the outline method, this CL extends the ThrowBlockOutlineMarker to have as in-values the list of SSA values that are arguments to the outline method.

Bug: b/434769547
Change-Id: I0ca6343daf412347be4194ec8965e01df372651e
diff --git a/src/main/java/com/android/tools/r8/graph/DexTypeUtils.java b/src/main/java/com/android/tools/r8/graph/DexTypeUtils.java
index 205ab44..a9cd08c 100644
--- a/src/main/java/com/android/tools/r8/graph/DexTypeUtils.java
+++ b/src/main/java/com/android/tools/r8/graph/DexTypeUtils.java
@@ -21,7 +21,7 @@
       AppView<? extends AppInfoWithClassHierarchy> appView, Iterable<DexType> types) {
     TypeElement join =
         TypeElement.join(Iterables.transform(types, type -> type.toTypeElement(appView)), appView);
-    return toDexType(appView, join);
+    return toDexType(appView.dexItemFactory(), join);
   }
 
   public static boolean isApiSafe(
@@ -35,19 +35,17 @@
     return isApiSafe(appView, computeLeastUpperBound(appView, types));
   }
 
-  public static DexType toDexType(
-      AppView<? extends AppInfoWithClassHierarchy> appView, TypeElement type) {
-    DexItemFactory dexItemFactory = appView.dexItemFactory();
+  public static DexType toDexType(DexItemFactory factory, TypeElement type) {
     if (type.isPrimitiveType()) {
-      return type.asPrimitiveType().toDexType(dexItemFactory);
+      return type.asPrimitiveType().toDexType(factory);
     }
     if (type.isArrayType()) {
       ArrayTypeElement arrayType = type.asArrayType();
-      DexType baseType = toDexType(appView, arrayType.getBaseType());
-      return baseType.toArrayType(arrayType.getNesting(), dexItemFactory);
+      DexType baseType = toDexType(factory, arrayType.getBaseType());
+      return baseType.toArrayType(arrayType.getNesting(), factory);
     }
     assert type.isClassType();
-    return type.asClassType().toDexType(dexItemFactory);
+    return type.asClassType().toDexType(factory);
   }
 
   public static DexType findApiSafeUpperBound(
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 bd119cb..6b4b88f 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
@@ -14,13 +14,14 @@
 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;
+import java.util.List;
 
 public class ThrowBlockOutlineMarker extends Instruction {
 
   private final ThrowBlockOutline outline;
 
-  public ThrowBlockOutlineMarker(ThrowBlockOutline outline) {
-    super(null);
+  public ThrowBlockOutlineMarker(ThrowBlockOutline outline, List<Value> arguments) {
+    super(null, arguments);
     this.outline = outline;
   }
 
@@ -54,7 +55,7 @@
 
   @Override
   public void buildLir(LirBuilder<Value, ?> builder) {
-    builder.addThrowBlockOutlineMarker(outline);
+    builder.addThrowBlockOutlineMarker(outline, inValues);
   }
 
   @Override
@@ -115,8 +116,14 @@
 
   public static class Builder extends BuilderBase<Builder, ThrowBlockOutlineMarker> {
 
+    private List<Value> arguments;
     private ThrowBlockOutline outline;
 
+    public Builder setArguments(List<Value> arguments) {
+      this.arguments = arguments;
+      return this;
+    }
+
     public Builder setOutline(ThrowBlockOutline outline) {
       this.outline = outline;
       return this;
@@ -124,7 +131,7 @@
 
     @Override
     public ThrowBlockOutlineMarker build() {
-      return amend(new ThrowBlockOutlineMarker(outline));
+      return amend(new ThrowBlockOutlineMarker(outline, arguments));
     }
 
     @Override
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 dc871e1..480f608 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
@@ -10,6 +10,7 @@
 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.ir.code.Value;
 import com.android.tools.r8.lightir.LirCode;
 import com.android.tools.r8.lightir.LirConstant;
 import com.android.tools.r8.synthesis.SyntheticItems;
@@ -17,21 +18,27 @@
 import com.android.tools.r8.utils.structural.HashingVisitor;
 import com.google.common.collect.ConcurrentHashMultiset;
 import com.google.common.collect.Multiset;
+import java.util.List;
 
 public class ThrowBlockOutline implements LirConstant {
 
   @SuppressWarnings("UnusedVariable")
   private final LirCode<?> lirCode;
+
+  private final DexProto proto;
   private final Multiset<DexMethod> users = ConcurrentHashMultiset.create();
 
   private ProgramMethod materializedOutlineMethod;
 
-  ThrowBlockOutline(LirCode<?> lirCode) {
+  ThrowBlockOutline(LirCode<?> lirCode, DexProto proto) {
     this.lirCode = lirCode;
+    this.proto = proto;
   }
 
-  public void addUser(DexMethod user) {
+  public void addUser(DexMethod user, List<Value> unusedArguments) {
     users.add(user);
+    // TODO(TODO(b/434769547)): Compute abstraction of arguments to enable interprocedural constant
+    //  propagation.
   }
 
   @Override
@@ -47,6 +54,10 @@
     return users.size();
   }
 
+  public DexProto getProto() {
+    return proto;
+  }
+
   public ProgramMethod getSynthesizingContext(AppView<?> appView) {
     DexMethod shortestUser = null;
     for (DexMethod user : users) {
@@ -86,7 +97,6 @@
 
   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,
@@ -96,6 +106,6 @@
                 builder
                     .setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
                     .setCode(methodSig -> lirCode)
-                    .setProto(emptyProto));
+                    .setProto(proto));
   }
 }
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 e69cdba..00ba07a 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
@@ -52,9 +52,6 @@
     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();
@@ -66,13 +63,15 @@
           if (outline.isMaterialized()) {
             // Insert a call to the materialized outline method and load the return value.
             BasicBlockInstructionListIterator instructionIterator =
-                block.listIterator(outlineMarker);
-            instructionIterator.add(
+                block.listIterator(block.exit());
+            InvokeStatic invoke =
                 InvokeStatic.builder()
+                    .setArguments(outlineMarker.inValues())
                     .setIsInterface(false)
                     .setMethod(outline.getMaterializedOutlineMethod())
                     .setPosition(throwInstruction)
-                    .build());
+                    .build();
+            instructionIterator.add(invoke);
             Value returnValue = addReturnValue(code, instructionIterator);
 
             // Replace the throw instruction by a normal return.
@@ -81,9 +80,13 @@
             block.replaceLastInstruction(returnInstruction);
 
             // Remove all outlined instructions bottom up.
-            instructionIterator = block.listIterator(returnInstruction);
-            while (instructionIterator.previous() != outlineMarker) {
-              instructionIterator.removeOrReplaceByDebugLocalRead();
+            instructionIterator = block.listIterator(invoke);
+            for (Instruction instruction = instructionIterator.previous();
+                instruction != outlineMarker;
+                instruction = instructionIterator.previous()) {
+              if (instruction.hasUnusedOutValue()) {
+                instruction.removeOrReplaceByDebugLocalRead();
+              }
             }
           }
 
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
index c323bca..a131719 100644
--- 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
@@ -65,9 +65,6 @@
 
     // 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();
   }
 
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
index 5213ce9..63e62d0 100644
--- 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
@@ -21,7 +21,8 @@
   protected boolean doEquivalent(LirCode<?> lirCode, LirCode<?> other) {
     assert verifyLirCode(lirCode);
     assert verifyLirCode(other);
-    return lirCode.getInstructionCount() == other.getInstructionCount()
+    return lirCode.getArgumentCount() == other.getArgumentCount()
+        && lirCode.getInstructionCount() == other.getInstructionCount()
         && Arrays.equals(lirCode.getInstructionBytes(), other.getInstructionBytes())
         && Arrays.equals(lirCode.getConstantPool(), other.getConstantPool());
   }
@@ -34,7 +35,6 @@
   }
 
   private boolean verifyLirCode(LirCode<?> lirCode) {
-    assert !lirCode.hasArguments();
     assert !lirCode.hasDebugLocalInfoTable();
     assert !lirCode.hasPositionTable();
     assert !lirCode.hasTryCatchTable();
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 349bf86..980668a 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
@@ -3,18 +3,29 @@
 // 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.ir.code.Opcodes.CONST_NUMBER;
+import static com.android.tools.r8.ir.code.Opcodes.CONST_STRING;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_DIRECT;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_VIRTUAL;
+import static com.android.tools.r8.ir.code.Opcodes.NEW_INSTANCE;
 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.DexProto;
+import com.android.tools.r8.graph.DexTypeUtils;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
+import com.android.tools.r8.ir.code.Argument;
 import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.BasicBlockInstructionListIterator;
+import com.android.tools.r8.ir.code.ConstInstruction;
 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.InvokeVirtual;
 import com.android.tools.r8.ir.code.NewInstance;
 import com.android.tools.r8.ir.code.NumberGenerator;
 import com.android.tools.r8.ir.code.Position;
@@ -30,10 +41,13 @@
 import com.android.tools.r8.utils.timing.Timing;
 import com.google.common.base.Equivalence.Wrapper;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Consumer;
 
@@ -102,12 +116,17 @@
           block.exit().asThrow(),
           outlineBuilder -> {
             // On successful outline creation, store the outline for later processing.
-            LirCode<?> lirCode = outlineBuilder.build(appView, code.context());
+            LirCode<?> lirCode = outlineBuilder.buildLirCode(appView, code.context());
             Wrapper<LirCode<?>> lirCodeWrapper =
                 ThrowBlockOutlinerLirCodeEquivalence.get().wrap(lirCode);
+            DexProto proto = outlineBuilder.getProto(factory);
             ThrowBlockOutline outline =
-                outlines.computeIfAbsent(lirCodeWrapper, w -> new ThrowBlockOutline(w.get()));
-            outline.addUser(code.reference());
+                outlines.computeIfAbsent(
+                    lirCodeWrapper, w -> new ThrowBlockOutline(w.get(), proto));
+            // TODO(b/434769547): This may not hold. We should compute the join.
+            assert proto.isIdenticalTo(outline.getProto());
+            List<Value> arguments = outlineBuilder.buildArguments();
+            outline.addUser(code.reference(), arguments);
 
             // Insert a synthetic marker instruction that references the outline so that we know
             // where to materialize the outline call.
@@ -115,6 +134,7 @@
             assert insertionPoint.getBlock() == block;
             ThrowBlockOutlineMarker marker =
                 ThrowBlockOutlineMarker.builder()
+                    .setArguments(arguments)
                     .setOutline(outline)
                     .setPosition(Position.none())
                     .build();
@@ -131,11 +151,16 @@
         return;
       }
       assert throwInstruction.hasPrev();
+      // We always expect the constructor call corresponding to the thrown exception to be last.
       processExceptionConstructorCall(
           throwBlock,
           throwInstruction.getPrev(),
           outlineBuilder -> {
             Value outlinedExceptionValue = outlineBuilder.getOutlinedValue(exceptionValue);
+            if (outlinedExceptionValue == null) {
+              // Fail as we were unable to outline the corresponding new-instance instruction.
+              return;
+            }
             outlineBuilder.add(
                 Throw.builder()
                     .setExceptionValue(outlinedExceptionValue)
@@ -145,53 +170,170 @@
           });
     }
 
+    private void processInstruction(
+        BasicBlock throwBlock, Instruction instruction, Consumer<OutlineBuilder> continuation) {
+      switch (instruction.opcode()) {
+        case CONST_NUMBER:
+        case CONST_STRING:
+          processConstInstruction(throwBlock, instruction.asConstInstruction(), continuation);
+          return;
+        case INVOKE_DIRECT:
+          if (instruction.isInvokeConstructor(factory)) {
+            processStringBuilderConstructorCall(
+                throwBlock, instruction.asInvokeDirect(), continuation);
+            return;
+          }
+          break;
+        case INVOKE_VIRTUAL:
+          processStringBuilderAppendOrToString(
+              throwBlock, instruction.asInvokeVirtual(), continuation);
+          return;
+        case NEW_INSTANCE:
+          processNewInstanceInstruction(throwBlock, instruction.asNewInstance(), continuation);
+          return;
+        default:
+          break;
+      }
+      // Unhandled instruction. Start the outline at the successor instruction.
+      startOutline(instruction.getNext(), continuation);
+    }
+
+    private void processConstInstruction(
+        BasicBlock throwBlock,
+        ConstInstruction instruction,
+        Consumer<OutlineBuilder> continuation) {
+      processPredecessorInstructionOrStartOutline(throwBlock, instruction, continuation);
+    }
+
+    private void processPredecessorInstructionOrFail(
+        BasicBlock throwBlock, Instruction instruction, Consumer<OutlineBuilder> continuation) {
+      if (instruction.hasPrev()) {
+        processInstruction(throwBlock, instruction.getPrev(), continuation);
+      } else {
+        // Intentionally empty. Not calling the continuation corresponds to dropping the outline.
+      }
+    }
+
+    private void processPredecessorInstructionOrStartOutline(
+        BasicBlock throwBlock, Instruction instruction, Consumer<OutlineBuilder> continuation) {
+      if (instruction.hasPrev()) {
+        processInstruction(throwBlock, instruction.getPrev(), continuation);
+      } else {
+        startOutline(instruction, continuation);
+      }
+    }
+
+    private void processNewInstanceInstruction(
+        BasicBlock throwBlock, NewInstance newInstance, Consumer<OutlineBuilder> continuation) {
+      if (newInstance.outValue() != throwBlock.exit().asThrow().exception()
+          && newInstance.getType().isNotIdenticalTo(appView.dexItemFactory().stringBuilderType)) {
+        // Unhandled instruction.
+        startOutline(newInstance.getNext(), continuation);
+        return;
+      }
+      processPredecessorInstructionOrStartOutline(
+          throwBlock,
+          newInstance,
+          outlineBuilder -> {
+            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 void processExceptionConstructorCall(
         BasicBlock throwBlock, Instruction instruction, Consumer<OutlineBuilder> continuation) {
       InvokeDirect invoke = instruction.asInvokeConstructor(factory);
       if (invoke == null) {
+        // Not a constructor call.
         return;
       }
-      DexMethod constructor = invoke.getInvokedMethod();
-      if (!constructor.getParameters().isEmpty()) {
-        // TODO(b/434769547): Handle constructors with arguments.
+      if (invoke.getReceiver() != throwBlock.exit().asThrow().exception()) {
+        // Not the constructor call corresponding to the thrown exception.
         return;
       }
-      processNewExceptionInstruction(
+      // This instruction is guaranteed to have a predecessor since the handling of the throw
+      // instruction checks if the new-instance instruction is in the throw block.
+      assert instruction.hasPrev();
+      processExceptionOrStringBuilderConstructorCall(throwBlock, invoke, continuation);
+    }
+
+    private void processStringBuilderConstructorCall(
+        BasicBlock throwBlock, InvokeDirect invoke, Consumer<OutlineBuilder> continuation) {
+      if (!factory.stringBuilderMethods.isConstructorMethod(invoke.getInvokedMethod())) {
+        // Unhandled instruction.
+        startOutline(invoke.getNext(), continuation);
+        return;
+      }
+      processExceptionOrStringBuilderConstructorCall(throwBlock, invoke, continuation);
+    }
+
+    private void processExceptionOrStringBuilderConstructorCall(
+        BasicBlock throwBlock, InvokeDirect invoke, Consumer<OutlineBuilder> continuation) {
+      processPredecessorInstructionOrFail(
           throwBlock,
-          invoke.getPrev(),
+          invoke,
           outlineBuilder -> {
-            Value outlinedExceptionValue = outlineBuilder.getOutlinedValue(invoke.getReceiver());
+            if (outlineBuilder.getOutlinedValue(invoke.getReceiver()) == null) {
+              // Fail as we were unable to outline the corresponding new-instance instruction.
+              return;
+            }
             outlineBuilder.add(
                 InvokeDirect.builder()
-                    .setMethod(constructor)
+                    .setArguments(
+                        ListUtils.map(
+                            invoke.arguments(), outlineBuilder::getOutlinedValueOrCreateArgument))
+                    .setMethod(invoke.getInvokedMethod())
                     .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) {
+    private void processStringBuilderAppendOrToString(
+        BasicBlock throwBlock, InvokeVirtual invoke, Consumer<OutlineBuilder> continuation) {
+      DexMethod invokedMethod = invoke.getInvokedMethod();
+      if (!factory.stringBuilderMethods.isAppendMethod(invokedMethod)
+          && !invokedMethod.match(factory.stringBuilderMethods.toString)) {
+        // Unhandled instruction.
+        startOutline(invoke.getNext(), continuation);
         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());
+      processPredecessorInstructionOrStartOutline(
+          throwBlock,
+          invoke,
+          outlineBuilder -> {
+            InvokeVirtual.Builder outlinedInvokeBuilder =
+                InvokeVirtual.builder()
+                    .setArguments(
+                        ListUtils.map(
+                            invoke.arguments(), outlineBuilder::getOutlinedValueOrCreateArgument))
+                    .setMethod(invoke.getInvokedMethod())
+                    .setPosition(Position.syntheticNone());
+            if (invoke.hasOutValue()) {
+              outlinedInvokeBuilder.setFreshOutValue(
+                  outlineBuilder.valueNumberGenerator, invoke.getOutType());
+            }
+            InvokeVirtual outlinedInvoke = outlinedInvokeBuilder.build();
+            outlineBuilder.add(outlinedInvoke);
+            if (invoke.hasOutValue()) {
+              outlineBuilder.map(invoke.outValue(), outlinedInvoke.outValue());
+            }
+            continuation.accept(outlineBuilder);
+          });
+    }
+
+    private void startOutline(
+        Instruction firstOutlinedInstruction, Consumer<OutlineBuilder> continuation) {
+      OutlineBuilder outlineBuilder = new OutlineBuilder(firstOutlinedInstruction);
       continuation.accept(outlineBuilder);
     }
   }
@@ -200,6 +342,7 @@
 
     private final Instruction firstOutlinedInstruction;
 
+    private final List<Argument> outlinedArguments = new ArrayList<>();
     private final BasicBlock outlinedBlock = new BasicBlock(metadata);
 
     // Map from non-outlined values to their corresponding outlined values.
@@ -214,9 +357,22 @@
     }
 
     void add(Instruction instruction) {
+      assert !instruction.isArgument();
       outlinedBlock.add(instruction, metadata);
     }
 
+    Argument addArgument(Value value) {
+      Argument outlinedArgument =
+          Argument.builder()
+              .setFreshOutValue(valueNumberGenerator, value.getType())
+              .setIndex(outlinedArguments.size())
+              .setPosition(Position.none())
+              .build();
+      outlinedArguments.add(outlinedArgument);
+      map(value, outlinedArgument.outValue());
+      return outlinedArgument;
+    }
+
     void map(Value value, Value outlinedValue) {
       assert !outlinedValues.containsKey(value);
       outlinedValues.put(value, outlinedValue);
@@ -227,12 +383,46 @@
     }
 
     Value getOutlinedValue(Value value) {
-      Value outlinedValue = outlinedValues.get(value);
-      assert outlinedValue != null;
-      return outlinedValue;
+      return outlinedValues.get(value);
     }
 
-    LirCode<?> build(AppView<?> appView, ProgramMethod context) {
+    Value getOutlinedValueOrCreateArgument(Value value) {
+      Value outlinedValue = getOutlinedValue(value);
+      if (outlinedValue != null) {
+        return outlinedValue;
+      }
+      return addArgument(value).outValue();
+    }
+
+    DexProto getProto(DexItemFactory factory) {
+      return factory.createProto(
+          factory.voidType,
+          ListUtils.map(
+              outlinedArguments,
+              outlinedArgument -> DexTypeUtils.toDexType(factory, outlinedArgument.getOutType())));
+    }
+
+    List<Value> buildArguments() {
+      if (outlinedArguments.isEmpty()) {
+        return Collections.emptyList();
+      }
+      List<Value> arguments = Arrays.asList(new Value[outlinedArguments.size()]);
+      for (Entry<Value, Value> entry : outlinedValues.entrySet()) {
+        Value value = entry.getKey();
+        Value outlinedValue = entry.getValue();
+        if (outlinedValue.isArgument()) {
+          Argument outlinedArgument = outlinedValue.getDefinition().asArgument();
+          arguments.set(outlinedArgument.getIndexRaw(), value);
+        }
+      }
+      return arguments;
+    }
+
+    LirCode<?> buildLirCode(AppView<?> appView, ProgramMethod context) {
+      BasicBlockInstructionListIterator outlinedBlockIterator = outlinedBlock.listIterator();
+      for (Argument outlinedArgument : outlinedArguments) {
+        outlinedBlockIterator.add(outlinedArgument);
+      }
       outlinedBlock.setFilled();
       IRCode outlineCode =
           new IRCode(
@@ -258,6 +448,7 @@
       LirCode<?> lirCode =
           new IRToLirFinalizer(appView)
               .finalizeCode(outlineCode, BytecodeMetadataProvider.empty(), Timing.empty());
+      assert lirCode.getArgumentCount() == outlinedArguments.size();
       return lirCode;
     }
   }
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 6890bd0..18671a3 100644
--- a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
+++ b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
@@ -858,8 +858,8 @@
     }
 
     @Override
-    public void onThrowBlockOutlineMarker(ThrowBlockOutline outline) {
-      addInstruction(new ThrowBlockOutlineMarker(outline));
+    public void onThrowBlockOutlineMarker(ThrowBlockOutline outline, List<EV> arguments) {
+      addInstruction(new ThrowBlockOutlineMarker(outline, getValues(arguments)));
     }
 
     @Override
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 bdb4c85..c253f6d 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
@@ -776,8 +776,10 @@
     return addOneValueInstruction(LirOpcodes.ATHROW, exception);
   }
 
-  public LirBuilder<V, EV> addThrowBlockOutlineMarker(ThrowBlockOutline outline) {
-    return addOneItemInstruction(LirOpcodes.THROWBLOCKOUTLINEMARKER, outline);
+  public LirBuilder<V, EV> addThrowBlockOutlineMarker(
+      ThrowBlockOutline outline, List<V> arguments) {
+    return addInstructionTemplate(
+        LirOpcodes.THROWBLOCKOUTLINEMARKER, Collections.singletonList(outline), arguments);
   }
 
   public LirBuilder<V, EV> addReturn(V value) {
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 5ab0417..3cb0332 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
@@ -471,7 +471,7 @@
     onInstruction();
   }
 
-  public void onThrowBlockOutlineMarker(ThrowBlockOutline outline) {
+  public void onThrowBlockOutlineMarker(ThrowBlockOutline outline, List<EV> arguments) {
     onInstruction();
   }
 
@@ -1320,7 +1320,8 @@
         {
           ThrowBlockOutline outline =
               (ThrowBlockOutline) getConstantItem(view.getNextConstantOperand());
-          onThrowBlockOutlineMarker(outline);
+          List<EV> arguments = getInvokeInstructionArguments(view);
+          onThrowBlockOutlineMarker(outline, arguments);
           return;
         }
       default:
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
new file mode 100644
index 0000000..acbd830
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerConstArgumentTest.java
@@ -0,0 +1,127 @@
+// 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.CoreMatchers.containsString;
+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.TestParametersCollection;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.BooleanBox;
+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 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 ThrowBlockOutlinerConstArgumentTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimesAndAllApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    BooleanBox receivedCallback = new BooleanBox();
+    TestCompileResult<?, ?> compileResult =
+        testForD8(parameters)
+            .addInnerClasses(getClass())
+            .addOptionsModification(
+                options -> {
+                  assertFalse(options.getThrowBlockOutlinerOptions().enable);
+                  options.getThrowBlockOutlinerOptions().enable = true;
+                  options.getThrowBlockOutlinerOptions().outlineConsumerForTesting =
+                      outlines -> {
+                        inspectOutlines(outlines);
+                        receivedCallback.set();
+                      };
+                })
+            .release()
+            .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)
+          .assertFailureWithErrorThatMatches(containsString("Unexpected " + i));
+    }
+    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());
+
+    ClassSubject outlineClassSubject =
+        inspector.clazz(SyntheticItemsTestUtils.syntheticThrowBlockOutlineClass(Main.class, 0));
+    assertThat(outlineClassSubject, isPresent());
+    assertEquals(1, outlineClassSubject.allMethods().size());
+
+    MethodSubject outlineMethodSubject = outlineClassSubject.uniqueMethod();
+    MethodSubject mainMethodSubject = inspector.clazz(Main.class).mainMethod();
+    assertThat(mainMethodSubject, isPresent());
+    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("Unexpected 0");
+      }
+      if (i == 1) {
+        throw new IllegalArgumentException("Unexpected 1");
+      }
+      String arg0 = "Unexpected " + args[0];
+      if (i == 2) {
+        throw new IllegalArgumentException(arg0);
+      }
+      if (i == 3) {
+        throw new RuntimeException();
+      }
+    }
+  }
+}
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
index ed330c9..0988ba4 100644
--- 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
@@ -62,7 +62,6 @@
                       };
                 })
             .release()
-            .setMinApi(parameters)
             .compile()
             .inspect(this::inspectOutput);
     assertTrue(receivedCallback.isTrue());
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerSharedStringBuilderTest.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerSharedStringBuilderTest.java
new file mode 100644
index 0000000..2e130f7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/exceptions/ThrowBlockOutlinerSharedStringBuilderTest.java
@@ -0,0 +1,127 @@
+// 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.CoreMatchers.containsString;
+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.TestParametersCollection;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.BooleanBox;
+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 java.util.Collection;
+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 ThrowBlockOutlinerSharedStringBuilderTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimesAndAllApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    BooleanBox receivedCallback = new BooleanBox();
+    TestCompileResult<?, ?> compileResult =
+        testForD8(parameters)
+            .addInnerClasses(getClass())
+            .addOptionsModification(
+                options -> {
+                  assertFalse(options.getThrowBlockOutlinerOptions().enable);
+                  options.getThrowBlockOutlinerOptions().enable = true;
+                  options.getThrowBlockOutlinerOptions().outlineConsumerForTesting =
+                      outlines -> {
+                        inspectOutlines(outlines);
+                        receivedCallback.set();
+                      };
+                })
+            .release()
+            .compile()
+            .inspect(this::inspectOutput);
+    assertTrue(receivedCallback.isTrue());
+
+    compileResult
+        .run(parameters.getRuntime(), Main.class, "0", "1")
+        .assertFailureWithErrorThatThrows(IllegalArgumentException.class)
+        .assertFailureWithErrorThatMatches(containsString("i=0, j=1"));
+    compileResult
+        .run(parameters.getRuntime(), Main.class, "1", "0")
+        .assertFailureWithErrorThatThrows(IllegalArgumentException.class)
+        .assertFailureWithErrorThatMatches(containsString("j=0, i=1"));
+  }
+
+  private void inspectOutlines(Collection<ThrowBlockOutline> outlines) {
+    // Verify that we have a single outline with two users.
+    assertEquals(1, outlines.size());
+    ThrowBlockOutline outline = outlines.iterator().next();
+    assertEquals(2, outline.getNumberOfUsers());
+    assertEquals(4, outline.getProto().getArity());
+  }
+
+  private void inspectOutput(CodeInspector inspector) {
+    assertEquals(2, inspector.allClasses().size());
+
+    ClassSubject outlineClassSubject =
+        inspector.clazz(SyntheticItemsTestUtils.syntheticThrowBlockOutlineClass(Main.class, 0));
+    assertThat(outlineClassSubject, isPresent());
+    assertEquals(1, outlineClassSubject.allMethods().size());
+
+    // Validate that the outline uses StringBuilder.
+    MethodSubject outlineMethodSubject = outlineClassSubject.uniqueMethod();
+    assertTrue(
+        outlineMethodSubject
+            .streamInstructions()
+            .anyMatch(i -> i.isNewInstance("java.lang.StringBuilder")));
+
+    // Validate that main() no longer uses StringBuilder and that it calls the outline twice.
+    MethodSubject mainMethodSubject = inspector.clazz(Main.class).mainMethod();
+    assertThat(mainMethodSubject, isPresent());
+    assertTrue(
+        mainMethodSubject
+            .streamInstructions()
+            .noneMatch(i -> i.isNewInstance("java.lang.StringBuilder")));
+    assertTrue(mainMethodSubject.streamInstructions().anyMatch(i -> i.isConstString("i=")));
+    assertTrue(mainMethodSubject.streamInstructions().anyMatch(i -> i.isConstString("j=")));
+    assertTrue(mainMethodSubject.streamInstructions().anyMatch(i -> i.isConstString(", j=")));
+    assertTrue(mainMethodSubject.streamInstructions().anyMatch(i -> i.isConstString(", i=")));
+    assertEquals(
+        2,
+        mainMethodSubject
+            .streamInstructions()
+            .filter(isInvokeWithTarget(outlineMethodSubject))
+            .count());
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      int i = Integer.parseInt(args[0]);
+      int j = Integer.parseInt(args[1]);
+      if (i == 0) {
+        throw new IllegalArgumentException("i=" + i + ", j=" + j);
+      }
+      if (j == 0) {
+        throw new IllegalArgumentException("j=" + j + ", i=" + i);
+      }
+    }
+  }
+}