Implement the number unboxer optimization

- NumberUnboxerTreeFixer and NumberUnboxerRewriter

Bug: b/307872552
Change-Id: Id6c3a3b1035cd071ff7da80d0305330af19f4e97
diff --git a/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java b/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java
index 64f8399..eff9aa9 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/GraphLens.java
@@ -414,6 +414,10 @@
     return null;
   }
 
+  public boolean isNumberUnboxerLens() {
+    return false;
+  }
+
   public boolean isHorizontalClassMergerGraphLens() {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/lens/NestedGraphLensWithCustomLensCodeRewriter.java b/src/main/java/com/android/tools/r8/graph/lens/NestedGraphLensWithCustomLensCodeRewriter.java
index 84599f0..e64f101 100644
--- a/src/main/java/com/android/tools/r8/graph/lens/NestedGraphLensWithCustomLensCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/graph/lens/NestedGraphLensWithCustomLensCodeRewriter.java
@@ -21,8 +21,10 @@
       AppView<?> appView,
       BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap,
       BidirectionalManyToOneRepresentativeMap<DexMethod, DexMethod> methodMap,
-      BidirectionalManyToManyRepresentativeMap<DexType, DexType> typeMap) {
+      BidirectionalManyToManyRepresentativeMap<DexType, DexType> typeMap,
+      CustomLensCodeRewriter customLensCodeRewriter) {
     super(appView, fieldMap, methodMap, typeMap);
+    this.customLensCodeRewriter = customLensCodeRewriter;
   }
 
   public NestedGraphLensWithCustomLensCodeRewriter(
diff --git a/src/main/java/com/android/tools/r8/graph/proto/RewrittenPrototypeDescription.java b/src/main/java/com/android/tools/r8/graph/proto/RewrittenPrototypeDescription.java
index 23df11c..3f628b9 100644
--- a/src/main/java/com/android/tools/r8/graph/proto/RewrittenPrototypeDescription.java
+++ b/src/main/java/com/android/tools/r8/graph/proto/RewrittenPrototypeDescription.java
@@ -33,7 +33,7 @@
 
   private static final RewrittenPrototypeDescription NONE = new RewrittenPrototypeDescription();
 
-  private final List<ExtraParameter> extraParameters;
+  private final List<? extends ExtraParameter> extraParameters;
   private final ArgumentInfoCollection argumentInfoCollection;
   private final RewrittenTypeInfo rewrittenReturnInfo;
 
@@ -44,7 +44,7 @@
   }
 
   private RewrittenPrototypeDescription(
-      List<ExtraParameter> extraParameters,
+      List<? extends ExtraParameter> extraParameters,
       RewrittenTypeInfo rewrittenReturnInfo,
       ArgumentInfoCollection argumentsInfo) {
     assert argumentsInfo != null;
@@ -55,7 +55,7 @@
   }
 
   public static RewrittenPrototypeDescription create(
-      List<ExtraParameter> extraParameters,
+      List<? extends ExtraParameter> extraParameters,
       RewrittenTypeInfo rewrittenReturnInfo,
       ArgumentInfoCollection argumentsInfo) {
     return extraParameters.isEmpty() && rewrittenReturnInfo == null && argumentsInfo.isEmpty()
@@ -116,7 +116,7 @@
     return !extraParameters.isEmpty();
   }
 
-  public List<ExtraParameter> getExtraParameters() {
+  public List<? extends ExtraParameter> getExtraParameters() {
     return extraParameters;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
index 3588346..0f6fb1f 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
@@ -87,6 +87,13 @@
     Instruction previous = previous();
     assert previous == instruction;
   }
+
+  default void addBeforeAndPositionAfterNewInstruction(Instruction instruction) {
+    previous();
+    add(instruction);
+    next();
+  }
+
   /** See {@link #replaceCurrentInstruction(Instruction, Set)}. */
   default void replaceCurrentInstruction(Instruction newInstruction) {
     replaceCurrentInstruction(newInstruction, null);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxer.java b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxer.java
index dbfb37a..b360857 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxer.java
@@ -34,7 +34,8 @@
   public abstract void unboxNumbers(
       PostMethodProcessor.Builder postMethodProcessorBuilder,
       Timing timing,
-      ExecutorService executorService);
+      ExecutorService executorService)
+      throws ExecutionException;
 
   public abstract void onMethodPruned(ProgramMethod method);
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerBoxingStatusResolution.java b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerBoxingStatusResolution.java
index 2f0a128..4a9cbfd 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerBoxingStatusResolution.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerBoxingStatusResolution.java
@@ -7,12 +7,12 @@
 import static com.android.tools.r8.ir.optimize.numberunboxer.NumberUnboxerBoxingStatusResolution.MethodBoxingStatusResult.BoxingStatusResult.NO_UNBOX;
 import static com.android.tools.r8.ir.optimize.numberunboxer.NumberUnboxerBoxingStatusResolution.MethodBoxingStatusResult.BoxingStatusResult.TO_PROCESS;
 import static com.android.tools.r8.ir.optimize.numberunboxer.NumberUnboxerBoxingStatusResolution.MethodBoxingStatusResult.BoxingStatusResult.UNBOX;
-import static com.android.tools.r8.utils.ListUtils.*;
 
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.ir.optimize.numberunboxer.NumberUnboxerBoxingStatusResolution.MethodBoxingStatusResult.BoxingStatusResult;
 import com.android.tools.r8.ir.optimize.numberunboxer.TransitiveDependency.MethodArg;
 import com.android.tools.r8.ir.optimize.numberunboxer.TransitiveDependency.MethodRet;
+import com.android.tools.r8.utils.ArrayUtils;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.WorkList;
 import java.util.Arrays;
@@ -72,6 +72,20 @@
     public BoxingStatusResult[] getArgs() {
       return args;
     }
+
+    public boolean isNoneUnboxable() {
+      return ret == NO_UNBOX && ArrayUtils.all(args, NO_UNBOX);
+    }
+
+    public boolean shouldUnboxArg(int i) {
+      assert args[i] != TO_PROCESS;
+      return args[i] == UNBOX;
+    }
+
+    public boolean shouldUnboxRet() {
+      assert ret != TO_PROCESS;
+      return ret == UNBOX;
+    }
   }
 
   void markNoneUnboxable(DexMethod method) {
@@ -135,9 +149,14 @@
       }
     }
     assert allProcessed();
+    clearNoneUnboxable();
     return boxingStatusResultMap;
   }
 
+  private void clearNoneUnboxable() {
+    boxingStatusResultMap.values().removeIf(MethodBoxingStatusResult::isNoneUnboxable);
+  }
+
   private boolean allProcessed() {
     boxingStatusResultMap.forEach(
         (k, v) -> {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerImpl.java
index cf8f2ec..c6b0d70 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerImpl.java
@@ -40,6 +40,8 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
 
 public class NumberUnboxerImpl extends NumberUnboxer {
 
@@ -301,17 +303,26 @@
   public void unboxNumbers(
       PostMethodProcessor.Builder postMethodProcessorBuilder,
       Timing timing,
-      ExecutorService executorService) {
-
-    Map<DexMethod, MethodBoxingStatusResult> result =
+      ExecutorService executorService)
+      throws ExecutionException {
+    Map<DexMethod, MethodBoxingStatusResult> unboxingResult =
         new NumberUnboxerBoxingStatusResolution().resolve(methodBoxingStatus);
+    if (unboxingResult.isEmpty()) {
+      return;
+    }
+
+    NumberUnboxerLens numberUnboxerLens =
+        new NumberUnboxerTreeFixer(appView, unboxingResult).fixupTree(executorService, timing);
+    appView.rewriteWithLens(numberUnboxerLens, executorService, timing);
+
+    enqueueMethodsForReprocessing(postMethodProcessorBuilder);
 
     // TODO(b/307872552): The result encodes for each method which return value and parameter of
     //  each method should be unboxed. We need here to implement the treefixer using it, and set up
     //  correctly the reprocessing with a code rewriter similar to the enum unboxing code rewriter.
     //  We should implement the optimization, so far, we just print out the result.
     StringBuilder stringBuilder = new StringBuilder();
-    result.forEach(
+    unboxingResult.forEach(
         (k, v) -> {
           if (v.getRet() == UNBOX) {
             stringBuilder
@@ -333,6 +344,19 @@
     appView.reporter().warning(stringBuilder.toString());
   }
 
+  private void enqueueMethodsForReprocessing(
+      PostMethodProcessor.Builder postMethodProcessorBuilder) {
+    postMethodProcessorBuilder.rewrittenWithLens(appView);
+
+    // TODO(b/307872552): Implement the reprocessing enqueuer so that only relevant methods are
+    //  reprocessed. For testing we temporarily reprocess all.
+    postMethodProcessorBuilder.addAll(
+        appView.appInfo().classes().stream()
+            .flatMap(c -> StreamSupport.stream(c.programMethods().spliterator(), false))
+            .collect(Collectors.toList()),
+        appView.graphLens());
+  }
+
   @Override
   public void onMethodPruned(ProgramMethod method) {
     // TODO(b/307872552): Should we do something about this? We might need to change the
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerLens.java b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerLens.java
new file mode 100644
index 0000000..eeb1768
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerLens.java
@@ -0,0 +1,108 @@
+// Copyright (c) 2023, 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.numberunboxer;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.lens.NestedGraphLensWithCustomLensCodeRewriter;
+import com.android.tools.r8.graph.proto.ArgumentInfoCollection;
+import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
+import com.android.tools.r8.graph.proto.RewrittenTypeInfo;
+import com.android.tools.r8.ir.conversion.ExtraUnusedNullParameter;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalOneToOneMap;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class NumberUnboxerLens extends NestedGraphLensWithCustomLensCodeRewriter {
+  private final Map<DexMethod, RewrittenPrototypeDescription> prototypeChangesPerMethod;
+
+  NumberUnboxerLens(
+      AppView<AppInfoWithLiveness> appView,
+      BidirectionalManyToOneRepresentativeMap<DexMethod, DexMethod> renamedSignatures,
+      Map<DexMethod, RewrittenPrototypeDescription> prototypeChangesPerMethod,
+      NumberUnboxerRewriter numberUnboxerRewriter) {
+    super(appView, EMPTY_FIELD_MAP, renamedSignatures, EMPTY_TYPE_MAP, numberUnboxerRewriter);
+    this.prototypeChangesPerMethod = prototypeChangesPerMethod;
+  }
+
+  @Override
+  protected RewrittenPrototypeDescription internalDescribePrototypeChanges(
+      RewrittenPrototypeDescription prototypeChanges, DexMethod method) {
+    RewrittenPrototypeDescription enumUnboxingPrototypeChanges =
+        prototypeChangesPerMethod.getOrDefault(method, RewrittenPrototypeDescription.none());
+    return prototypeChanges.combine(enumUnboxingPrototypeChanges);
+  }
+
+  @Override
+  public boolean isNumberUnboxerLens() {
+    return true;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+
+    private final Map<DexMethod, RewrittenPrototypeDescription> prototypeChangesPerMethod =
+        new IdentityHashMap<>();
+    private final MutableBidirectionalOneToOneMap<DexMethod, DexMethod> newMethodSignatures =
+        new BidirectionalOneToOneHashMap<>();
+
+    public RewrittenPrototypeDescription move(DexEncodedMethod fromEncoded, DexMethod to) {
+      DexMethod from = fromEncoded.getReference();
+      assert !from.isIdenticalTo(to);
+      List<ExtraUnusedNullParameter> extraUnusedNullParameters =
+          ExtraUnusedNullParameter.computeExtraUnusedNullParameters(from, to);
+      RewrittenPrototypeDescription prototypeChanges =
+          computePrototypeChanges(from, to, fromEncoded.isStatic(), extraUnusedNullParameters);
+      synchronized (this) {
+        newMethodSignatures.put(from, to);
+        prototypeChangesPerMethod.put(to, prototypeChanges);
+      }
+      return prototypeChanges;
+    }
+
+    private RewrittenPrototypeDescription computePrototypeChanges(
+        DexMethod from,
+        DexMethod to,
+        boolean staticMethod,
+        List<ExtraUnusedNullParameter> extraUnusedNullParameters) {
+      assert from.getArity() + extraUnusedNullParameters.size() == to.getArity();
+      ArgumentInfoCollection.Builder builder =
+          ArgumentInfoCollection.builder()
+              .setArgumentInfosSize(from.getNumberOfArguments(staticMethod));
+      for (int i = 0; i < from.getParameters().size(); i++) {
+        DexType fromType = from.getParameter(i);
+        DexType toType = to.getParameter(i);
+        if (!fromType.isIdenticalTo(toType)) {
+          builder.addArgumentInfo(
+              i, RewrittenTypeInfo.builder().setOldType(fromType).setNewType(toType).build());
+        }
+      }
+      RewrittenTypeInfo returnInfo =
+          from.getReturnType().isIdenticalTo(to.getReturnType())
+              ? null
+              : RewrittenTypeInfo.builder()
+                  .setOldType(from.getReturnType())
+                  .setNewType(to.getReturnType())
+                  .build();
+      return RewrittenPrototypeDescription.create(
+          extraUnusedNullParameters, returnInfo, builder.build());
+    }
+
+    public NumberUnboxerLens build(
+        AppView<AppInfoWithLiveness> appView, NumberUnboxerRewriter numberUnboxerRewriter) {
+      return new NumberUnboxerLens(
+          appView, newMethodSignatures, prototypeChangesPerMethod, numberUnboxerRewriter);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerRewriter.java
new file mode 100644
index 0000000..83b1c96
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerRewriter.java
@@ -0,0 +1,189 @@
+// Copyright (c) 2023, 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.numberunboxer;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.lens.NonIdentityGraphLens;
+import com.android.tools.r8.graph.proto.ArgumentInfo;
+import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
+import com.android.tools.r8.graph.proto.RewrittenTypeInfo;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.code.InvokeVirtual;
+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.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.optimize.CustomLensCodeRewriter;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+public class NumberUnboxerRewriter implements CustomLensCodeRewriter {
+
+  private final AppView<AppInfoWithLiveness> appView;
+
+  public NumberUnboxerRewriter(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  @Override
+  public Set<Phi> rewriteCode(
+      IRCode code,
+      MethodProcessor methodProcessor,
+      RewrittenPrototypeDescription prototypeChanges,
+      NonIdentityGraphLens graphLens) {
+    assert graphLens.isNumberUnboxerLens();
+    Set<Phi> affectedPhis = Sets.newIdentityHashSet();
+    rewriteArgs(code, prototypeChanges, affectedPhis);
+    InstructionListIterator iterator = code.instructionListIterator();
+    while (iterator.hasNext()) {
+      Instruction next = iterator.next();
+      if (next.isInvokeMethod()) {
+        InvokeMethod invokeMethod = next.asInvokeMethod();
+        // TODO(b/314117865): This is assuming that there are no non-rebound method references.
+        DexMethod rewrittenMethod =
+            graphLens
+                .lookupMethod(
+                    invokeMethod.getInvokedMethod(),
+                    code.context().getReference(),
+                    invokeMethod.getType(),
+                    graphLens.getPrevious())
+                .getReference();
+        assert rewrittenMethod != null;
+        RewrittenPrototypeDescription rewrittenPrototypeDescription =
+            graphLens.lookupPrototypeChangesForMethodDefinition(
+                rewrittenMethod, graphLens.getPrevious());
+        if (!rewrittenPrototypeDescription.isEmpty()) {
+          unboxInvokeValues(
+              code, iterator, invokeMethod, rewrittenPrototypeDescription, affectedPhis);
+        }
+      } else if (next.isReturn() && next.asReturn().hasReturnValue()) {
+        unboxReturnIfNeeded(code, iterator, next.asReturn(), prototypeChanges);
+      }
+    }
+    return affectedPhis;
+  }
+
+  private void unboxInvokeValues(
+      IRCode code,
+      InstructionListIterator iterator,
+      InvokeMethod invokeMethod,
+      RewrittenPrototypeDescription prototypeChanges,
+      Set<Phi> affectedPhis) {
+    assert prototypeChanges.getArgumentInfoCollection().numberOfRemovedArguments() == 0;
+    for (int inValueIndex = 0; inValueIndex < invokeMethod.inValues().size(); inValueIndex++) {
+      ArgumentInfo argumentInfo =
+          prototypeChanges.getArgumentInfoCollection().getArgumentInfo(inValueIndex);
+      if (argumentInfo.isRewrittenTypeInfo()) {
+        Value invokeArg = invokeMethod.getArgument(inValueIndex);
+        InvokeVirtual unboxOperation =
+            computeUnboxInvokeIfNeeded(
+                code, invokeArg, argumentInfo.asRewrittenTypeInfo(), invokeMethod.getPosition());
+        if (unboxOperation != null) {
+          iterator.addBeforeAndPositionAfterNewInstruction(unboxOperation);
+          invokeMethod.replaceValue(inValueIndex, unboxOperation.outValue());
+        }
+      }
+    }
+    if (invokeMethod.hasOutValue()) {
+      InvokeStatic boxOperation =
+          computeBoxInvokeIfNeeded(
+              code,
+              invokeMethod.outValue(),
+              prototypeChanges.getRewrittenReturnInfo(),
+              invokeMethod.getPosition());
+      if (boxOperation != null) {
+        iterator.add(boxOperation);
+        affectedPhis.addAll(boxOperation.outValue().uniquePhiUsers());
+      }
+    }
+  }
+
+  private void unboxReturnIfNeeded(
+      IRCode code,
+      InstructionListIterator iterator,
+      Return ret,
+      RewrittenPrototypeDescription prototypeChanges) {
+    InvokeVirtual unbox =
+        computeUnboxInvokeIfNeeded(
+            code, ret.returnValue(), prototypeChanges.getRewrittenReturnInfo(), ret.getPosition());
+    if (unbox != null) {
+      iterator.addBeforeAndPositionAfterNewInstruction(unbox);
+      ret.replaceValue(ret.returnValue(), unbox.outValue());
+    }
+  }
+
+  private InvokeVirtual computeUnboxInvokeIfNeeded(
+      IRCode code, Value input, RewrittenTypeInfo rewrittenTypeInfo, Position pos) {
+    if (rewrittenTypeInfo == null) {
+      return null;
+    }
+    assert rewrittenTypeInfo.getOldType().isReferenceType();
+    assert rewrittenTypeInfo.getNewType().isPrimitiveType();
+    assert appView.dexItemFactory().primitiveToBoxed.containsValue(rewrittenTypeInfo.getOldType());
+    return InvokeVirtual.builder()
+        .setMethod(appView.dexItemFactory().getUnboxPrimitiveMethod(rewrittenTypeInfo.getNewType()))
+        .setFreshOutValue(
+            code.valueNumberGenerator, rewrittenTypeInfo.getNewType().toTypeElement(appView))
+        .setArguments(ImmutableList.of(input))
+        .setPosition(pos)
+        .build();
+  }
+
+  private InvokeStatic computeBoxInvokeIfNeeded(
+      IRCode code, Value output, RewrittenTypeInfo rewrittenTypeInfo, Position pos) {
+    if (rewrittenTypeInfo == null) {
+      return null;
+    }
+    assert rewrittenTypeInfo.getOldType().isReferenceType();
+    assert rewrittenTypeInfo.getNewType().isPrimitiveType();
+    assert appView.dexItemFactory().primitiveToBoxed.containsValue(rewrittenTypeInfo.getOldType());
+    Value outValue = code.createValue(rewrittenTypeInfo.getOldType().toTypeElement(appView));
+    output.replaceUsers(outValue);
+    return InvokeStatic.builder()
+        .setOutValue(outValue)
+        .setMethod(appView.dexItemFactory().getBoxPrimitiveMethod(rewrittenTypeInfo.getNewType()))
+        .setSingleArgument(output)
+        .setPosition(pos)
+        .build();
+  }
+
+  private void rewriteArgs(
+      IRCode code, RewrittenPrototypeDescription prototypeChanges, Set<Phi> affectedPhis) {
+    List<InvokeStatic> boxingOperations = new ArrayList<>();
+    InstructionListIterator iterator = code.entryBlock().listIterator(code);
+    Position pos = iterator.peekNext().getPosition();
+    assert prototypeChanges.getArgumentInfoCollection().numberOfRemovedArguments() == 0;
+    int originalNumberOfArguments = code.getNumberOfArguments();
+    for (int argumentIndex = 0; argumentIndex < originalNumberOfArguments; argumentIndex++) {
+      ArgumentInfo argumentInfo =
+          prototypeChanges.getArgumentInfoCollection().getArgumentInfo(argumentIndex);
+      Instruction next = iterator.next();
+      assert next.isArgument();
+      if (argumentInfo.isRewrittenTypeInfo()) {
+        InvokeStatic boxOperation =
+            computeBoxInvokeIfNeeded(
+                code, next.outValue(), argumentInfo.asRewrittenTypeInfo(), pos);
+        if (boxOperation != null) {
+          boxingOperations.add(boxOperation);
+        }
+      }
+    }
+    assert !iterator.peekNext().isArgument();
+    for (InvokeStatic boxingOp : boxingOperations) {
+      iterator.add(boxingOp);
+      affectedPhis.addAll(boxingOp.outValue().uniquePhiUsers());
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerTreeFixer.java b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerTreeFixer.java
new file mode 100644
index 0000000..166cccd
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/numberunboxer/NumberUnboxerTreeFixer.java
@@ -0,0 +1,111 @@
+// Copyright (c) 2023, 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.numberunboxer;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.fixup.ConcurrentMethodFixup;
+import com.android.tools.r8.graph.fixup.ConcurrentMethodFixup.ProgramClassFixer;
+import com.android.tools.r8.graph.fixup.MethodNamingUtility;
+import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
+import com.android.tools.r8.ir.optimize.numberunboxer.NumberUnboxerBoxingStatusResolution.MethodBoxingStatusResult;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.ArrayUtils;
+import com.android.tools.r8.utils.OptionalBool;
+import com.android.tools.r8.utils.Timing;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+public class NumberUnboxerTreeFixer implements ProgramClassFixer {
+
+  private final Map<DexMethod, MethodBoxingStatusResult> unboxingResult;
+  private final AppView<AppInfoWithLiveness> appView;
+
+  private final NumberUnboxerLens.Builder lensBuilder = NumberUnboxerLens.builder();
+
+  public NumberUnboxerTreeFixer(
+      AppView<AppInfoWithLiveness> appView,
+      Map<DexMethod, MethodBoxingStatusResult> unboxingResult) {
+    this.unboxingResult = unboxingResult;
+    this.appView = appView;
+  }
+
+  public NumberUnboxerLens fixupTree(ExecutorService executorService, Timing timing)
+      throws ExecutionException {
+    // Take strongly connected components, for each merge data from call-sites and methods and
+    // unbox.
+    new ConcurrentMethodFixup(appView, this)
+        .fixupClassesConcurrentlyByConnectedProgramComponents(timing, executorService);
+    return lensBuilder.build(appView, new NumberUnboxerRewriter(appView));
+  }
+
+  @Override
+  public void fixupProgramClass(DexProgramClass clazz, MethodNamingUtility utility) {
+    clazz.getMethodCollection().replaceMethods(m -> fixupEncodedMethod(m, utility));
+  }
+
+  @Override
+  public boolean shouldReserveAsIfPinned(ProgramMethod method) {
+    // We don't reprocess dependencies of unchanged methods so we have to maintain them
+    // with the same signature.
+    return !unboxingResult.containsKey(method.getReference());
+  }
+
+  private DexEncodedMethod fixupEncodedMethod(
+      DexEncodedMethod method, MethodNamingUtility utility) {
+    if (!unboxingResult.containsKey(method.getReference())) {
+      assert method
+          .getReference()
+          .isIdenticalTo(
+              utility.nextUniqueMethod(
+                  method, method.getProto(), appView.dexItemFactory().shortType));
+      return method;
+    }
+    MethodBoxingStatusResult methodBoxingStatus = unboxingResult.get(method.getReference());
+    assert !methodBoxingStatus.isNoneUnboxable();
+    DexProto newProto = fixupProto(method.getProto(), methodBoxingStatus);
+    DexMethod newMethod =
+        utility.nextUniqueMethod(method, newProto, appView.dexItemFactory().shortType);
+
+    RewrittenPrototypeDescription prototypeChanges = lensBuilder.move(method, newMethod);
+    return method.toTypeSubstitutedMethodAsInlining(
+        newMethod,
+        appView.dexItemFactory(),
+        builder ->
+            builder
+                .fixupOptimizationInfo(
+                    appView, prototypeChanges.createMethodOptimizationInfoFixer())
+                .setCompilationState(method.getCompilationState())
+                .setIsLibraryMethodOverrideIf(
+                    method.isNonPrivateVirtualMethod(), OptionalBool.FALSE));
+  }
+
+  private DexType fixupType(DexType type, boolean unbox) {
+    if (!unbox) {
+      return type;
+    }
+    DexType newType = appView.dexItemFactory().primitiveToBoxed.inverse().get(type);
+    assert newType != null;
+    return newType;
+  }
+
+  private DexProto fixupProto(DexProto proto, MethodBoxingStatusResult methodBoxingStatus) {
+    DexType[] argTypes = proto.getParameters().values;
+    DexType[] newArgTypes =
+        ArrayUtils.initialize(
+            new DexType[argTypes.length],
+            i -> fixupType(argTypes[i], methodBoxingStatus.shouldUnboxArg(i)));
+    DexType newReturnType = fixupType(proto.getReturnType(), methodBoxingStatus.shouldUnboxRet());
+    DexProto newProto = appView.dexItemFactory().createProto(newReturnType, newArgTypes);
+    assert newProto.isNotIdenticalTo(proto);
+    return newProto;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/numberunboxing/SimpleNumberUnboxingTest.java b/src/test/java/com/android/tools/r8/numberunboxing/SimpleNumberUnboxingTest.java
index 755096b..c14645d 100644
--- a/src/test/java/com/android/tools/r8/numberunboxing/SimpleNumberUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/numberunboxing/SimpleNumberUnboxingTest.java
@@ -4,10 +4,17 @@
 
 package com.android.tools.r8.numberunboxing;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+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.Objects;
 import org.hamcrest.CoreMatchers;
 import org.junit.Test;
@@ -39,6 +46,7 @@
         .setMinApi(parameters)
         .allowDiagnosticWarningMessages()
         .compile()
+        .inspect(this::assertUnboxing)
         .assertWarningMessageThatMatches(
             CoreMatchers.containsString(
                 "Unboxing of arg 0 of void"
@@ -67,6 +75,33 @@
         .assertSuccessWithOutputLines("32", "33", "42", "43", "51", "52", "2");
   }
 
+  private void assertFirstParameterUnboxed(ClassSubject mainClass, String methodName) {
+    MethodSubject methodSubject = mainClass.uniqueMethodWithOriginalName(methodName);
+    assertThat(methodSubject, isPresent());
+    assertEquals("java.lang.Integer", methodSubject.getOriginalSignature().parameters[0]);
+    assertEquals("int", methodSubject.getFinalSignature().asMethodSignature().parameters[0]);
+  }
+
+  private void assertReturnUnboxed(ClassSubject mainClass, String methodName) {
+    MethodSubject methodSubject = mainClass.uniqueMethodWithOriginalName(methodName);
+    assertThat(methodSubject, isPresent());
+    assertEquals("java.lang.Integer", methodSubject.getOriginalSignature().type);
+    assertEquals("int", methodSubject.getFinalSignature().asMethodSignature().type);
+  }
+
+  private void assertUnboxing(CodeInspector codeInspector) {
+    ClassSubject mainClass = codeInspector.clazz(Main.class);
+    assertThat(mainClass, isPresent());
+
+    assertFirstParameterUnboxed(mainClass, "print");
+    assertFirstParameterUnboxed(mainClass, "forwardToPrint2");
+    assertFirstParameterUnboxed(mainClass, "directPrintUnbox");
+    assertFirstParameterUnboxed(mainClass, "forwardToPrint");
+
+    assertReturnUnboxed(mainClass, "get");
+    assertReturnUnboxed(mainClass, "forwardGet");
+  }
+
   static class Main {
 
     public static void main(String[] args) {