Enum unboxing: unbox simple enums

- disable enum unboxing when enums are stored into fields.

Bug: 147860220
Change-Id: I9d10bcce9babad4a06a4258f81c4565c1842f96f
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 4c4d2d5..3a4f267 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -660,6 +660,7 @@
             // assert Inliner.verifyNoMethodsInlinedDueToSingleCallSite(appView);
 
             assert appView.allMergedClasses().verifyAllSourcesPruned(appViewWithLiveness);
+            assert appView.validateUnboxedEnumsHaveBeenPruned();
 
             processWhyAreYouKeepingAndCheckDiscarded(
                 appView.rootSet(),
@@ -893,7 +894,7 @@
 
   private void computeKotlinInfoForProgramClasses(
       DexApplication application, AppView<?> appView, ExecutorService executorService)
-      throws ExecutionException{
+      throws ExecutionException {
     if (appView.options().kotlinOptimizationOptions().disableKotlinSpecificOptimizations) {
       return;
     }
@@ -906,8 +907,7 @@
           programClass.setKotlinInfo(kotlinInfo);
           KotlinMemberInfo.markKotlinMemberInfo(programClass, kotlinInfo, reporter);
         },
-        executorService
-    );
+        executorService);
   }
 
   private static boolean verifyNoJarApplicationReaders(List<DexProgramClass> classes) {
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index 1f24d35..8e0dfc4 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -24,6 +24,7 @@
 import com.android.tools.r8.utils.OptionalBool;
 import com.google.common.base.Predicates;
 import com.google.common.collect.ImmutableSet;
+import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.util.Map;
 import java.util.Set;
@@ -64,6 +65,7 @@
   private Set<DexMethod> unneededVisibilityBridgeMethods = ImmutableSet.of();
   private HorizontallyMergedLambdaClasses horizontallyMergedLambdaClasses;
   private VerticallyMergedClasses verticallyMergedClasses;
+  private Set<DexType> unboxedEnums = Collections.emptySet();
 
   private Map<DexClass, DexValueString> sourceDebugExtensions = new IdentityHashMap<>();
 
@@ -368,6 +370,20 @@
     this.verticallyMergedClasses = verticallyMergedClasses;
   }
 
+  public void setUnboxedEnums(Set<DexType> unboxedEnums) {
+    this.unboxedEnums = unboxedEnums;
+  }
+
+  public boolean validateUnboxedEnumsHaveBeenPruned() {
+    for (DexType unboxedEnum : unboxedEnums) {
+      assert definitionForProgramType(unboxedEnum) == null
+          : "Enum " + unboxedEnum + " has been unboxed but is still in the program.";
+      assert appInfo().withLiveness().wasPruned(unboxedEnum)
+          : "Enum " + unboxedEnum + " has been unboxed but was not pruned.";
+    }
+    return true;
+  }
+
   @SuppressWarnings("unchecked")
   public AppView<AppInfoWithClassHierarchy> withClassHierarchy() {
     return appInfo.hasClassHierarchy()
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index 08d4d61..c046e6a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -11,6 +11,7 @@
 import static com.android.tools.r8.ir.desugar.InterfaceMethodRewriter.EMULATE_LIBRARY_CLASS_NAME_SUFFIX;
 import static com.android.tools.r8.ir.desugar.LambdaRewriter.LAMBDA_CLASS_NAME_PREFIX;
 import static com.android.tools.r8.ir.desugar.LambdaRewriter.LAMBDA_GROUP_CLASS_NAME_PREFIX;
+import static com.android.tools.r8.ir.optimize.enums.EnumUnboxingRewriter.ENUM_UNBOXING_UTILITY_CLASS_NAME;
 
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.errors.Unreachable;
@@ -272,6 +273,7 @@
   public boolean isD8R8SynthesizedClassType() {
     String name = toSourceString();
     return name.contains(COMPANION_CLASS_NAME_SUFFIX)
+        || name.contains(ENUM_UNBOXING_UTILITY_CLASS_NAME)
         || name.contains(EMULATE_LIBRARY_CLASS_NAME_SUFFIX)
         || name.contains(DISPATCH_CLASS_NAME_SUFFIX)
         || name.contains(TYPE_WRAPPER_SUFFIX)
diff --git a/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java b/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java
index 3beb9ba..c128d5c 100644
--- a/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java
+++ b/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java
@@ -4,11 +4,15 @@
 
 package com.android.tools.r8.graph;
 
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.code.ConstInstruction;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.utils.BooleanUtils;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
 import it.unimi.dsi.fastutil.ints.IntBidirectionalIterator;
 import java.util.function.Consumer;
@@ -194,30 +198,169 @@
     }
   }
 
+  public static class RewrittenTypeInfo {
+
+    private final DexType oldType;
+    private final DexType newType;
+
+    public RewrittenTypeInfo(DexType oldType, DexType newType) {
+      this.oldType = oldType;
+      this.newType = newType;
+    }
+
+    public DexType getNewType() {
+      return newType;
+    }
+
+    public DexType getOldType() {
+      return oldType;
+    }
+
+    public boolean defaultValueHasChanged() {
+      if (newType.isPrimitiveType()) {
+        if (oldType.isPrimitiveType()) {
+          return ValueType.fromDexType(newType) != ValueType.fromDexType(oldType);
+        }
+        return true;
+      } else if (oldType.isPrimitiveType()) {
+        return true;
+      }
+      // All reference types uses null as default value.
+      assert newType.isReferenceType();
+      assert oldType.isReferenceType();
+      return false;
+    }
+
+    public TypeLatticeElement defaultValueLatticeElement(AppView<?> appView) {
+      if (newType.isPrimitiveType()) {
+        return TypeLatticeElement.fromDexType(newType, null, appView);
+      }
+      return TypeLatticeElement.getNull();
+    }
+  }
+
+  public static class RewrittenTypeArgumentInfoCollection {
+
+    private static final RewrittenTypeArgumentInfoCollection EMPTY =
+        new RewrittenTypeArgumentInfoCollection();
+    private final Int2ReferenceMap<RewrittenTypeInfo> rewrittenArgumentsInfo;
+
+    private RewrittenTypeArgumentInfoCollection() {
+      this.rewrittenArgumentsInfo = new Int2ReferenceOpenHashMap<>(0);
+    }
+
+    private RewrittenTypeArgumentInfoCollection(
+        Int2ReferenceMap<RewrittenTypeInfo> rewrittenArgumentsInfo) {
+      this.rewrittenArgumentsInfo = rewrittenArgumentsInfo;
+    }
+
+    public static RewrittenTypeArgumentInfoCollection empty() {
+      return EMPTY;
+    }
+
+    public boolean isEmpty() {
+      return rewrittenArgumentsInfo.isEmpty();
+    }
+
+    public RewrittenTypeInfo getArgumentRewrittenTypeInfo(int argIndex) {
+      return rewrittenArgumentsInfo.get(argIndex);
+    }
+
+    public boolean isArgumentRewrittenTypeInfo(int argIndex) {
+      return rewrittenArgumentsInfo.containsKey(argIndex);
+    }
+
+    public DexType[] rewriteParameters(DexEncodedMethod encodedMethod) {
+      DexType[] params = encodedMethod.method.proto.parameters.values;
+      if (isEmpty()) {
+        return params;
+      }
+      DexType[] newParams = new DexType[params.length];
+      int offset = encodedMethod.isStatic() ? 0 : 1;
+      for (int index = 0; index < params.length; index++) {
+        RewrittenTypeInfo argInfo = getArgumentRewrittenTypeInfo(index + offset);
+        if (argInfo != null) {
+          assert params[index] == argInfo.oldType;
+          newParams[index] = argInfo.newType;
+        } else {
+          newParams[index] = params[index];
+        }
+      }
+      return newParams;
+    }
+
+    public static Builder builder() {
+      return new Builder();
+    }
+
+    public static class Builder {
+
+      private Int2ReferenceMap<RewrittenTypeInfo> rewrittenArgumentsInfo;
+
+      public Builder rewriteArgument(int argIndex, DexType oldType, DexType newType) {
+        if (rewrittenArgumentsInfo == null) {
+          rewrittenArgumentsInfo = new Int2ReferenceOpenHashMap<>();
+        }
+        rewrittenArgumentsInfo.put(argIndex, new RewrittenTypeInfo(oldType, newType));
+        return this;
+      }
+
+      public RewrittenTypeArgumentInfoCollection build() {
+        if (rewrittenArgumentsInfo == null) {
+          return EMPTY;
+        }
+        assert !rewrittenArgumentsInfo.isEmpty();
+        return new RewrittenTypeArgumentInfoCollection(rewrittenArgumentsInfo);
+      }
+    }
+  }
+
   private static final RewrittenPrototypeDescription none = new RewrittenPrototypeDescription();
 
+  // TODO(b/149681096): Unify RewrittenPrototypeDescription.
   private final boolean hasBeenChangedToReturnVoid;
   private final boolean extraNullParameter;
-  private final RemovedArgumentInfoCollection removedArgumentsInfo;
+  private final RemovedArgumentInfoCollection removedArgumentInfoCollection;
+  private final RewrittenTypeInfo rewrittenReturnInfo;
+  private final RewrittenTypeArgumentInfoCollection rewrittenTypeArgumentInfoCollection;
 
   private RewrittenPrototypeDescription() {
-    this(false, false, RemovedArgumentInfoCollection.empty());
+    this(
+        false,
+        false,
+        RemovedArgumentInfoCollection.empty(),
+        null,
+        RewrittenTypeArgumentInfoCollection.empty());
   }
 
   private RewrittenPrototypeDescription(
       boolean hasBeenChangedToReturnVoid,
       boolean extraNullParameter,
-      RemovedArgumentInfoCollection removedArgumentsInfo) {
+      RemovedArgumentInfoCollection removedArgumentsInfo,
+      RewrittenTypeInfo rewrittenReturnInfo,
+      RewrittenTypeArgumentInfoCollection rewrittenArgumentsInfo) {
     assert removedArgumentsInfo != null;
     this.extraNullParameter = extraNullParameter;
     this.hasBeenChangedToReturnVoid = hasBeenChangedToReturnVoid;
-    this.removedArgumentsInfo = removedArgumentsInfo;
+    this.removedArgumentInfoCollection = removedArgumentsInfo;
+    this.rewrittenReturnInfo = rewrittenReturnInfo;
+    this.rewrittenTypeArgumentInfoCollection = rewrittenArgumentsInfo;
   }
 
   public static RewrittenPrototypeDescription createForUninstantiatedTypes(
       boolean hasBeenChangedToReturnVoid, RemovedArgumentInfoCollection removedArgumentsInfo) {
     return new RewrittenPrototypeDescription(
-        hasBeenChangedToReturnVoid, false, removedArgumentsInfo);
+        hasBeenChangedToReturnVoid,
+        false,
+        removedArgumentsInfo,
+        null,
+        RewrittenTypeArgumentInfoCollection.empty());
+  }
+
+  public static RewrittenPrototypeDescription createForRewrittenTypes(
+      RewrittenTypeInfo returnInfo, RewrittenTypeArgumentInfoCollection rewrittenArgumentsInfo) {
+    return new RewrittenPrototypeDescription(
+        false, false, RemovedArgumentInfoCollection.empty(), returnInfo, rewrittenArgumentsInfo);
   }
 
   public static RewrittenPrototypeDescription none() {
@@ -227,7 +370,9 @@
   public boolean isEmpty() {
     return !extraNullParameter
         && !hasBeenChangedToReturnVoid
-        && !getRemovedArgumentInfoCollection().hasRemovedArguments();
+        && !removedArgumentInfoCollection.hasRemovedArguments()
+        && rewrittenReturnInfo == null
+        && rewrittenTypeArgumentInfoCollection.isEmpty();
   }
 
   public boolean hasExtraNullParameter() {
@@ -239,7 +384,19 @@
   }
 
   public RemovedArgumentInfoCollection getRemovedArgumentInfoCollection() {
-    return removedArgumentsInfo;
+    return removedArgumentInfoCollection;
+  }
+
+  public RewrittenTypeArgumentInfoCollection getRewrittenTypeArgumentInfoCollection() {
+    return rewrittenTypeArgumentInfoCollection;
+  }
+
+  public boolean hasRewrittenReturnInfo() {
+    return rewrittenReturnInfo != null;
+  }
+
+  public RewrittenTypeInfo getRewrittenReturnInfo() {
+    return rewrittenReturnInfo;
   }
 
   /**
@@ -258,32 +415,63 @@
     return instruction;
   }
 
+  @SuppressWarnings("ConstantConditions")
   public DexProto rewriteProto(DexEncodedMethod encodedMethod, DexItemFactory dexItemFactory) {
     if (isEmpty()) {
       return encodedMethod.method.proto;
     }
-    DexType newReturnType =
-        hasBeenChangedToReturnVoid
-            ? dexItemFactory.voidType
-            : encodedMethod.method.proto.returnType;
-    DexType[] newParameters = removedArgumentsInfo.rewriteParameters(encodedMethod);
-    return dexItemFactory.createProto(newReturnType, newParameters);
+    // TODO(b/149681096): Unify RewrittenPrototypeDescription, have a single variable for return.
+    if (rewrittenReturnInfo != null || !rewrittenTypeArgumentInfoCollection.isEmpty()) {
+      assert !hasBeenChangedToReturnVoid;
+      assert !removedArgumentInfoCollection.hasRemovedArguments();
+      DexType newReturnType =
+          rewrittenReturnInfo != null
+              ? rewrittenReturnInfo.newType
+              : encodedMethod.method.proto.returnType;
+      DexType[] newParameters =
+          rewrittenTypeArgumentInfoCollection.rewriteParameters(encodedMethod);
+      return dexItemFactory.createProto(newReturnType, newParameters);
+    } else {
+      assert rewrittenReturnInfo == null;
+      assert rewrittenTypeArgumentInfoCollection.isEmpty();
+      DexType newReturnType =
+          hasBeenChangedToReturnVoid
+              ? dexItemFactory.voidType
+              : encodedMethod.method.proto.returnType;
+      DexType[] newParameters = removedArgumentInfoCollection.rewriteParameters(encodedMethod);
+      return dexItemFactory.createProto(newReturnType, newParameters);
+    }
   }
 
   public RewrittenPrototypeDescription withConstantReturn() {
+    assert rewrittenReturnInfo == null;
     return !hasBeenChangedToReturnVoid
-        ? new RewrittenPrototypeDescription(true, extraNullParameter, removedArgumentsInfo)
+        ? new RewrittenPrototypeDescription(
+            true,
+            extraNullParameter,
+            removedArgumentInfoCollection,
+            rewrittenReturnInfo,
+            rewrittenTypeArgumentInfoCollection)
         : this;
   }
 
   public RewrittenPrototypeDescription withRemovedArguments(RemovedArgumentInfoCollection other) {
     return new RewrittenPrototypeDescription(
-        hasBeenChangedToReturnVoid, extraNullParameter, removedArgumentsInfo.combine(other));
+        hasBeenChangedToReturnVoid,
+        extraNullParameter,
+        removedArgumentInfoCollection.combine(other),
+        rewrittenReturnInfo,
+        rewrittenTypeArgumentInfoCollection);
   }
 
   public RewrittenPrototypeDescription withExtraNullParameter() {
     return !extraNullParameter
-        ? new RewrittenPrototypeDescription(hasBeenChangedToReturnVoid, true, removedArgumentsInfo)
+        ? new RewrittenPrototypeDescription(
+            hasBeenChangedToReturnVoid,
+            true,
+            removedArgumentInfoCollection,
+            rewrittenReturnInfo,
+            rewrittenTypeArgumentInfoCollection)
         : this;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeLatticeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeLatticeElement.java
index eff9681..0499222 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeLatticeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeLatticeElement.java
@@ -145,9 +145,12 @@
   }
 
   @Override
-  public ClassTypeLatticeElement fixupClassTypeReferences(
+  public TypeLatticeElement fixupClassTypeReferences(
       Function<DexType, DexType> mapping, AppView<? extends AppInfoWithSubtyping> appView) {
     DexType mappedType = mapping.apply(type);
+    if (mappedType.isPrimitiveType()) {
+      return PrimitiveTypeLatticeElement.fromDexType(mappedType, false);
+    }
     if (mappedType != type) {
       return create(mappedType, nullability, appView);
     }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstancePut.java b/src/main/java/com/android/tools/r8/ir/code/InstancePut.java
index 32484c3..cd71750 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstancePut.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstancePut.java
@@ -34,9 +34,23 @@
 public class InstancePut extends FieldInstruction {
 
   public InstancePut(DexField field, Value object, Value value) {
+    this(field, object, value, false);
+  }
+
+  // During structural changes, IRCode is not valid from IR building until the point where
+  // several passes, such as the lens code rewriter, has been run. At this point, it can happen,
+  // for example in the context of enum unboxing, that some InstancePut have temporarily
+  // a primitive type as the object. Skip assertions in this case.
+  public static InstancePut createPotentiallyInvalid(DexField field, Value object, Value value) {
+    return new InstancePut(field, object, value, true);
+  }
+
+  private InstancePut(DexField field, Value object, Value value, boolean skipAssertion) {
     super(field, null, Arrays.asList(object, value));
-    assert object().verifyCompatible(ValueType.OBJECT);
-    assert value().verifyCompatible(ValueType.fromDexType(field.type));
+    if (!skipAssertion) {
+      assert object().verifyCompatible(ValueType.OBJECT);
+      assert value().verifyCompatible(ValueType.fromDexType(field.type));
+    }
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
index 8f6ea1e..1f1d2e5 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
@@ -385,7 +385,7 @@
     inPrelude = true;
     state.buildPrelude(canonicalPositions.getPreamblePosition());
     setLocalVariableLists();
-    builder.buildArgumentsWithUnusedArgumentStubs(0, method, state::write);
+    builder.buildArgumentsWithRewrittenPrototypeChanges(0, method, state::write);
     // Add debug information for all locals at the initial label.
     Int2ReferenceMap<DebugLocalInfo> locals = getLocalVariables(0).locals;
     if (!locals.isEmpty()) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
index 2436ed1..f19ab4d 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
@@ -137,7 +137,7 @@
     if (code.incomingRegisterSize == 0) {
       return;
     }
-    builder.buildArgumentsWithUnusedArgumentStubs(
+    builder.buildArgumentsWithRewrittenPrototypeChanges(
         code.registerSize - code.incomingRegisterSize,
         method,
         DexSourceCode::doNothingWriteConsumer);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
index 0e24fe9..6e5a0a4 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -36,6 +36,8 @@
 import com.android.tools.r8.graph.RewrittenPrototypeDescription;
 import com.android.tools.r8.graph.RewrittenPrototypeDescription.RemovedArgumentInfo;
 import com.android.tools.r8.graph.RewrittenPrototypeDescription.RemovedArgumentInfoCollection;
+import com.android.tools.r8.graph.RewrittenPrototypeDescription.RewrittenTypeArgumentInfoCollection;
+import com.android.tools.r8.graph.RewrittenPrototypeDescription.RewrittenTypeInfo;
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.PrimitiveTypeLatticeElement;
 import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
@@ -192,6 +194,7 @@
   }
 
   private static class MoveExceptionWorklistItem extends WorklistItem {
+
     private final DexType guard;
     private final int sourceOffset;
     private final int targetOffset;
@@ -206,6 +209,7 @@
   }
 
   private static class SplitBlockWorklistItem extends WorklistItem {
+
     private final int sourceOffset;
     private final int targetOffset;
     private final Position position;
@@ -224,9 +228,8 @@
   }
 
   /**
-   * Representation of lists of values that can be used as keys in maps. A list of
-   * values is equal to another list of values if it contains exactly the same values
-   * in the same order.
+   * Representation of lists of values that can be used as keys in maps. A list of values is equal
+   * to another list of values if it contains exactly the same values in the same order.
    */
   private static class ValueList {
 
@@ -267,6 +270,7 @@
   }
 
   public static class BlockInfo {
+
     BasicBlock block = new BasicBlock();
     IntSet normalPredecessors = new IntArraySet();
     IntSet normalSuccessors = new IntArraySet();
@@ -423,11 +427,10 @@
   // then the IR does not necessarily contain a const-string instruction).
   private final IRMetadata metadata = new IRMetadata();
 
-  public static IRBuilder create(DexEncodedMethod method,
-      AppView<?> appView,
-      SourceCode source,
-      Origin origin) {
-    return new IRBuilder(method,
+  public static IRBuilder create(
+      DexEncodedMethod method, AppView<?> appView, SourceCode source, Origin origin) {
+    return new IRBuilder(
+        method,
         appView,
         source,
         origin,
@@ -435,29 +438,25 @@
         new ValueNumberGenerator());
   }
 
-  public static IRBuilder createForInlining(DexEncodedMethod method,
+  public static IRBuilder createForInlining(
+      DexEncodedMethod method,
       AppView<?> appView,
       SourceCode source,
       Origin origin,
       MethodProcessor processor,
       ValueNumberGenerator valueNumberGenerator) {
-    RewrittenPrototypeDescription protoChanges = processor.shouldApplyCodeRewritings(method) ?
-        lookupPrototypeChanges(appView, method.method) :
-        RewrittenPrototypeDescription.none();
-    return new IRBuilder(method,
-        appView,
-        source,
-        origin,
-        protoChanges,
-        valueNumberGenerator);
+    RewrittenPrototypeDescription protoChanges =
+        processor.shouldApplyCodeRewritings(method)
+            ? lookupPrototypeChanges(appView, method.method)
+            : RewrittenPrototypeDescription.none();
+    return new IRBuilder(method, appView, source, origin, protoChanges, valueNumberGenerator);
   }
 
-  private static RewrittenPrototypeDescription lookupPrototypeChanges(AppView<?> appView,
-      DexMethod method) {
-    RewrittenPrototypeDescription prototypeChanges = appView.graphLense()
-        .lookupPrototypeChanges(method);
-    if (Log.ENABLED
-        && prototypeChanges.getRemovedArgumentInfoCollection().hasRemovedArguments()) {
+  private static RewrittenPrototypeDescription lookupPrototypeChanges(
+      AppView<?> appView, DexMethod method) {
+    RewrittenPrototypeDescription prototypeChanges =
+        appView.graphLense().lookupPrototypeChanges(method);
+    if (Log.ENABLED && prototypeChanges.getRemovedArgumentInfoCollection().hasRemovedArguments()) {
       Log.info(
           IRBuilder.class,
           "Removed "
@@ -521,10 +520,13 @@
     currentBlock = block;
   }
 
-  public void buildArgumentsWithUnusedArgumentStubs(
+  public void buildArgumentsWithRewrittenPrototypeChanges(
       int register, DexEncodedMethod method, BiConsumer<Integer, DexType> writeCallback) {
     RemovedArgumentInfoCollection removedArgumentsInfo =
         prototypeChanges.getRemovedArgumentInfoCollection();
+    RewrittenTypeArgumentInfoCollection rewrittenArgumentsInfo =
+        prototypeChanges.getRewrittenTypeArgumentInfoCollection();
+    assert !removedArgumentsInfo.hasRemovedArguments() || rewrittenArgumentsInfo.isEmpty();
 
     // Fill in the Argument instructions (incomingRegisterSize last registers) in the argument
     // block.
@@ -553,10 +555,23 @@
                 argumentInfo.getType(), Nullability.maybeNull(), appView);
         addConstantOrUnusedArgument(register, argumentInfo);
       } else {
-        DexType dexType = method.method.proto.parameters.values[usedArgumentIndex++];
-        writeCallback.accept(register, dexType);
-        type = TypeLatticeElement.fromDexType(dexType, Nullability.maybeNull(), appView);
-        if (dexType.isBooleanType()) {
+        DexType argType;
+        if (rewrittenArgumentsInfo.isArgumentRewrittenTypeInfo(argumentIndex)) {
+          RewrittenTypeInfo argumentRewrittenTypeInfo =
+              rewrittenArgumentsInfo.getArgumentRewrittenTypeInfo(argumentIndex);
+          assert method.method.proto.parameters.values[usedArgumentIndex]
+              == argumentRewrittenTypeInfo.getNewType();
+          // The old type is used to prevent that a changed value from reference to primitive
+          // type breaks IR building. Rewriting from the old to the new type will be done in the
+          // IRConverter (typically through the lensCodeRewriter).
+          argType = argumentRewrittenTypeInfo.getOldType();
+        } else {
+          argType = method.method.proto.parameters.values[usedArgumentIndex];
+        }
+        usedArgumentIndex++;
+        writeCallback.accept(register, argType);
+        type = TypeLatticeElement.fromDexType(argType, Nullability.maybeNull(), appView);
+        if (argType.isBooleanType()) {
           addBooleanNonThisArgument(register);
         } else {
           addNonThisArgument(register, type);
@@ -572,7 +587,6 @@
    * Build the high-level IR in SSA form.
    *
    * @param context Under what context this IRCode is built. Either the current method or caller.
-   *
    * @return The list of basic blocks. First block is the main entry.
    */
   public IRCode build(DexEncodedMethod context) {
@@ -935,14 +949,14 @@
   }
 
   public void addNonThisArgument(int register, TypeLatticeElement typeLattice) {
-      DebugLocalInfo local = getOutgoingLocal(register);
-      Value value = writeRegister(register, typeLattice, ThrowingInfo.NO_THROW, local);
+    DebugLocalInfo local = getOutgoingLocal(register);
+    Value value = writeRegister(register, typeLattice, ThrowingInfo.NO_THROW, local);
     addNonThisArgument(new Argument(value, currentBlock.size(), false));
   }
 
   public void addBooleanNonThisArgument(int register) {
-      DebugLocalInfo local = getOutgoingLocal(register);
-      Value value = writeRegister(register, getInt(), ThrowingInfo.NO_THROW, local);
+    DebugLocalInfo local = getOutgoingLocal(register);
+    Value value = writeRegister(register, getInt(), ThrowingInfo.NO_THROW, local);
     addNonThisArgument(new Argument(value, currentBlock.size(), true));
   }
 
@@ -1765,7 +1779,10 @@
       addReturn();
     } else {
       ValueTypeConstraint returnTypeConstraint =
-          ValueTypeConstraint.fromDexType(method.method.proto.returnType);
+          prototypeChanges.hasRewrittenReturnInfo()
+              ? ValueTypeConstraint.fromDexType(
+                  prototypeChanges.getRewrittenReturnInfo().getOldType())
+              : ValueTypeConstraint.fromDexType(method.method.proto.returnType);
       Value in = readRegister(value, returnTypeConstraint);
       addReturn(new Return(in));
     }
@@ -2567,10 +2584,10 @@
   }
 
   /**
-   * Change to control-flow graph to avoid repeated phi operands when all the same values
-   * flow in from multiple predecessors.
+   * Change to control-flow graph to avoid repeated phi operands when all the same values flow in
+   * from multiple predecessors.
    *
-   * <p> As an example:
+   * <p>As an example:
    *
    * <pre>
    *
@@ -2582,7 +2599,7 @@
    *                  v3 = phi(v1, v1, v2)
    * </pre>
    *
-   * <p> Is rewritten to:
+   * <p>Is rewritten to:
    *
    * <pre>
    *              b1          b2         b3
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 94e5cf4..98c1915 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -451,6 +451,13 @@
     backportedMethodRewriter.synthesizeUtilityClasses(builder, executorService);
   }
 
+  private void synthesizeEnumUnboxingUtilityClass(
+      Builder<?> builder, ExecutorService executorService) throws ExecutionException {
+    if (enumUnboxer != null) {
+      enumUnboxer.synthesizeUtilityClass(builder, this, executorService);
+    }
+  }
+
   private void processCovariantReturnTypeAnnotations(Builder<?> builder) {
     if (covariantReturnTypeAnnotationTransformer != null) {
       covariantReturnTypeAnnotationTransformer.process(builder);
@@ -674,16 +681,12 @@
     // All the code has been processed so the rewriting required by the lenses is done everywhere,
     // we clear lens code rewriting so that the lens rewriter can be re-executed in phase 2 if new
     // lenses with code rewriting are added.
-    graphLenseForIR = appView.clearCodeRewritings();
+    appView.clearCodeRewritings();
 
     if (libraryMethodOverrideAnalysis != null) {
       libraryMethodOverrideAnalysis.finish();
     }
 
-    if (enumUnboxer != null) {
-      enumUnboxer.finishEnumAnalysis();
-    }
-
     // Post processing:
     //   1) Second pass for methods whose collected call site information become more precise.
     //   2) Second inlining pass for dealing with double inline callers.
@@ -694,8 +697,12 @@
     if (inliner != null) {
       postMethodProcessorBuilder.put(inliner);
     }
+    if (enumUnboxer != null) {
+      enumUnboxer.finishAnalysis();
+      enumUnboxer.unboxEnums(postMethodProcessorBuilder);
+    }
     timing.begin("IR conversion phase 2");
-    assert graphLenseForIR == appView.graphLense();
+    graphLenseForIR = appView.graphLense();
     PostMethodProcessor postMethodProcessor =
         postMethodProcessorBuilder.build(appView.withLiveness(), executorService, timing);
     if (postMethodProcessor != null) {
@@ -732,10 +739,11 @@
     desugarInterfaceMethods(builder, IncludeAllResources, executorService);
     feedback.updateVisibleOptimizationInfo();
 
-    printPhase("Twr close resource utility class synthesis");
+    printPhase("Utility classes synthesis");
     synthesizeTwrCloseResourceUtilityClass(builder, executorService);
     synthesizeJava8UtilityClass(builder, executorService);
     handleSynthesizedClassMapping(builder);
+    synthesizeEnumUnboxingUtilityClass(builder, executorService);
 
     printPhase("Lambda merging finalization");
     // TODO(b/127694949): Adapt to PostOptimization.
@@ -1133,6 +1141,10 @@
       timing.end();
     }
 
+    if (enumUnboxer != null && methodProcessor.isPost()) {
+      enumUnboxer.rewriteCode(code);
+    }
+
     if (method.isProcessed()) {
       assert !appView.enableWholeProgramOptimizations()
           || !appView.appInfo().withLiveness().neverReprocess.contains(method.method);
@@ -1431,10 +1443,6 @@
 
     previous = printMethod(code, "IR after interface method rewriting (SSA)", previous);
 
-    if (enumUnboxer != null && methodProcessor.isPost()) {
-      enumUnboxer.unboxEnums(code);
-    }
-
     // This pass has to be after interfaceMethodRewriter and BackportedMethodRewriter.
     if (desugaredLibraryAPIConverter != null
         && (!appView.enableWholeProgramOptimizations() || methodProcessor.isPrimary())) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
index fc50632..1cfb92b 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
@@ -29,6 +29,8 @@
 import com.android.tools.r8.graph.GraphLense.GraphLenseLookupResult;
 import com.android.tools.r8.graph.RewrittenPrototypeDescription;
 import com.android.tools.r8.graph.RewrittenPrototypeDescription.RemovedArgumentInfoCollection;
+import com.android.tools.r8.graph.RewrittenPrototypeDescription.RewrittenTypeArgumentInfoCollection;
+import com.android.tools.r8.graph.RewrittenPrototypeDescription.RewrittenTypeInfo;
 import com.android.tools.r8.graph.UseRegistry.MethodHandleUse;
 import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
 import com.android.tools.r8.ir.analysis.type.DestructivePhiTypeUpdater;
@@ -170,6 +172,39 @@
           if (actualTarget != invokedMethod || invoke.getType() != actualInvokeType) {
             RewrittenPrototypeDescription prototypeChanges =
                 graphLense.lookupPrototypeChanges(actualTarget);
+
+            // TODO(b/149681096): Unify RewrittenPrototypeDescription and merge rewritten type and
+            // removedArgument rewriting.
+            // When converting types the default value may change (for example default value of
+            // a reference type is null while default value of int is 0).
+            List<Value> newInValues;
+            RewrittenTypeArgumentInfoCollection rewrittenTypeArgumentInfoCollection =
+                prototypeChanges.getRewrittenTypeArgumentInfoCollection();
+            if (rewrittenTypeArgumentInfoCollection.isEmpty()) {
+              newInValues = invoke.inValues();
+            } else {
+              newInValues = new ArrayList<>(actualTarget.proto.parameters.size());
+              for (int i = 0; i < invoke.inValues().size(); i++) {
+                RewrittenTypeInfo argInfo =
+                    rewrittenTypeArgumentInfoCollection.getArgumentRewrittenTypeInfo(i);
+                Value value = invoke.inValues().get(i);
+                if (argInfo != null
+                    && argInfo.defaultValueHasChanged()
+                    && value.isConstNumber()
+                    && value.definition.asConstNumber().isZero()) {
+                  iterator.previous();
+                  // TODO(b/150188380): Add API to insert a const instruction with a type lattice.
+                  Value rewrittenDefaultValue =
+                      iterator.insertConstIntInstruction(code, appView.options(), 0);
+                  iterator.next();
+                  rewrittenDefaultValue.setTypeLattice(argInfo.defaultValueLatticeElement(appView));
+                  newInValues.add(rewrittenDefaultValue);
+                } else {
+                  newInValues.add(invoke.inValues().get(i));
+                }
+              }
+            }
+
             RemovedArgumentInfoCollection removedArgumentsInfo =
                 prototypeChanges.getRemovedArgumentInfoCollection();
 
@@ -192,7 +227,6 @@
             Value newOutValue =
                 prototypeChanges.hasBeenChangedToReturnVoid() ? null : makeOutValue(invoke, code);
 
-            List<Value> newInValues;
             if (removedArgumentsInfo.hasRemovedArguments()) {
               if (Log.ENABLED) {
                 Log.info(
@@ -204,14 +238,13 @@
                         + " arguments removed");
               }
               // Remove removed arguments from the invoke.
-              newInValues = new ArrayList<>(actualTarget.proto.parameters.size());
-              for (int i = 0; i < invoke.inValues().size(); i++) {
+              List<Value> tempNewInValues = new ArrayList<>(actualTarget.proto.parameters.size());
+              for (int i = 0; i < newInValues.size(); i++) {
                 if (!removedArgumentsInfo.isArgumentRemoved(i)) {
-                  newInValues.add(invoke.inValues().get(i));
+                  tempNewInValues.add(newInValues.get(i));
                 }
               }
-            } else {
-              newInValues = invoke.inValues();
+              newInValues = tempNewInValues;
             }
 
             if (prototypeChanges.hasExtraNullParameter()) {
@@ -288,7 +321,8 @@
                 new InvokeStatic(replacementMethod, null, current.inValues()));
           } else if (actualField != field) {
             InstancePut newInstancePut =
-                new InstancePut(actualField, instancePut.object(), instancePut.value());
+                InstancePut.createPotentiallyInvalid(
+                    actualField, instancePut.object(), instancePut.value());
             iterator.replaceCurrentInstruction(newInstancePut);
           }
         } else if (current.isStaticGet()) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java b/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java
index 529bdc7..6052427 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java
@@ -20,6 +20,7 @@
 import java.util.ArrayDeque;
 import java.util.Collection;
 import java.util.Deque;
+import java.util.IdentityHashMap;
 import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Objects;
@@ -28,7 +29,7 @@
 import java.util.concurrent.ExecutorService;
 import java.util.stream.Collectors;
 
-class PostMethodProcessor implements MethodProcessor {
+public class PostMethodProcessor implements MethodProcessor {
 
   private final AppView<AppInfoWithLiveness> appView;
   private final Map<DexEncodedMethod, Collection<CodeOptimization>> methodsMap;
@@ -56,7 +57,8 @@
     return !processed.contains(method);
   }
 
-  static class Builder {
+  public static class Builder {
+
     private final Collection<CodeOptimization> defaultCodeOptimizations;
     private final Map<DexEncodedMethod, Collection<CodeOptimization>> methodsMap =
         Maps.newIdentityHashMap();
@@ -85,7 +87,7 @@
       put(methodsToRevisit, defaultCodeOptimizations);
     }
 
-    void put(PostOptimization postOptimization) {
+    public void put(PostOptimization postOptimization) {
       Collection<CodeOptimization> codeOptimizations =
           postOptimization.codeOptimizationsForPostProcessing();
       if (codeOptimizations == null) {
@@ -94,6 +96,20 @@
       put(postOptimization.methodsToRevisit(), codeOptimizations);
     }
 
+    // Some optimizations may change methods, creating new instances of the encoded methods with a
+    // new signature. The compiler needs to update the set of methods that must be reprocessed
+    // according to the graph lens.
+    public void mapDexEncodedMethods(AppView<?> appView) {
+      Map<DexEncodedMethod, Collection<CodeOptimization>> newMethodsMap = new IdentityHashMap<>();
+      methodsMap.forEach(
+          (dexEncodedMethod, optimizations) -> {
+            newMethodsMap.put(
+                appView.graphLense().mapDexEncodedMethod(dexEncodedMethod, appView), optimizations);
+          });
+      methodsMap.clear();
+      methodsMap.putAll(newMethodsMap);
+    }
+
     PostMethodProcessor build(
         AppView<AppInfoWithLiveness> appView, ExecutorService executorService, Timing timing)
         throws ExecutionException {
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
index f6693eb..403f2f0 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
@@ -1051,8 +1051,7 @@
 
   private boolean shouldIgnoreFromReports(DexType missing) {
     return appView.rewritePrefix.hasRewrittenType(missing, appView)
-        || DesugaredLibraryWrapperSynthesizer.isSynthesizedWrapper(missing)
-        || DesugaredLibraryAPIConverter.isVivifiedType(missing)
+        || missing.isD8R8SynthesizedClassType()
         || isCompanionClassType(missing)
         || emulatedInterfaces.containsValue(missing)
         || options.desugaredLibraryConfiguration.getCustomConversions().containsValue(missing);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
index b3a25d0..43c57fc 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
@@ -5,41 +5,68 @@
 package com.android.tools.r8.ir.optimize.enums;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClass.FieldSetter;
+import com.android.tools.r8.graph.DexClass.MethodSetter;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 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.GraphLense;
+import com.android.tools.r8.graph.GraphLense.NestedGraphLense;
+import com.android.tools.r8.graph.RewrittenPrototypeDescription;
+import com.android.tools.r8.graph.RewrittenPrototypeDescription.RewrittenTypeArgumentInfoCollection;
+import com.android.tools.r8.graph.RewrittenPrototypeDescription.RewrittenTypeInfo;
 import com.android.tools.r8.ir.analysis.type.ArrayTypeLatticeElement;
 import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.FieldInstruction;
+import com.android.tools.r8.ir.code.ConstClass;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.CodeOptimization;
+import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.ir.conversion.PostMethodProcessor;
+import com.android.tools.r8.ir.conversion.PostOptimization;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.AppInfoWithLiveness.EnumValueInfo;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringDiagnostic;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
 
-public class EnumUnboxer {
+public class EnumUnboxer implements PostOptimization {
 
   private final AppView<AppInfoWithLiveness> appView;
-  private final Set<DexType> enumsToUnbox;
+  private final DexItemFactory factory;
+  // Map the enum candidates with their dependencies, i.e., the methods to reprocess for the given
+  // enum if the optimization eventually decides to unbox it.
+  private final Map<DexType, Set<DexEncodedMethod>> enumsUnboxingCandidates;
+
+  private EnumUnboxingRewriter enumUnboxerRewriter;
 
   private final boolean debugLogEnabled;
   private final Map<DexType, Reason> debugLogs;
-  private final DexItemFactory factory;
 
   public EnumUnboxer(AppView<AppInfoWithLiveness> appView) {
     this.appView = appView;
@@ -51,12 +78,7 @@
       debugLogEnabled = false;
       debugLogs = null;
     }
-    enumsToUnbox = new EnumUnboxingCandidateAnalysis(appView, this).findCandidates();
-  }
-
-  public void unboxEnums(IRCode code) {
-    // TODO(b/147860220): To implement.
-    // Do not forget static get, which is implicitly valid (no inValue).
+    enumsUnboxingCandidates = new EnumUnboxingCandidateAnalysis(appView, this).findCandidates();
   }
 
   public void analyzeEnums(IRCode code) {
@@ -72,7 +94,7 @@
   private void markEnumAsUnboxable(Reason reason, DexProgramClass enumClass) {
     assert enumClass.isEnum();
     reportFailure(enumClass.type, reason);
-    enumsToUnbox.remove(enumClass.type);
+    enumsUnboxingCandidates.remove(enumClass.type);
   }
 
   private DexProgramClass getEnumUnboxingCandidateOrNull(TypeLatticeElement lattice) {
@@ -92,26 +114,96 @@
   }
 
   private DexProgramClass getEnumUnboxingCandidateOrNull(DexType anyType) {
-    if (!enumsToUnbox.contains(anyType)) {
+    if (!enumsUnboxingCandidates.containsKey(anyType)) {
       return null;
     }
     return appView.definitionForProgramType(anyType);
   }
 
   private void analyzeEnumsInMethod(IRCode code) {
+    Set<DexType> eligibleEnums = Sets.newIdentityHashSet();
     for (BasicBlock block : code.blocks) {
       for (Instruction instruction : block.getInstructions()) {
         Value outValue = instruction.outValue();
-        DexProgramClass enumClass =
-            outValue == null ? null : getEnumUnboxingCandidateOrNull(outValue.getTypeLattice());
-        if (enumClass != null) {
-          validateEnumUsages(code, outValue.uniqueUsers(), outValue.uniquePhiUsers(), enumClass);
+        if (outValue != null) {
+          DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(outValue.getTypeLattice());
+          if (enumClass != null) {
+            Reason reason =
+                validateEnumUsages(
+                    code, outValue.uniqueUsers(), outValue.uniquePhiUsers(), enumClass);
+            if (reason == Reason.ELIGIBLE) {
+              eligibleEnums.add(enumClass.type);
+            }
+          }
+          if (outValue.getTypeLattice().isNullType()) {
+            addNullDependencies(outValue.uniqueUsers(), eligibleEnums);
+          }
+        }
+        // If we have a ConstClass referencing directly an enum, it cannot be unboxed, except if
+        // the constClass is in an enum valueOf method (in this case the valueOf method will be
+        // removed or the enum will be marked as non unboxable).
+        if (instruction.isConstClass()) {
+          ConstClass constClass = instruction.asConstClass();
+          if (enumsUnboxingCandidates.containsKey(constClass.getValue())) {
+            DexMethod context = code.method.method;
+            DexClass dexClass = appView.definitionFor(context.holder);
+            if (dexClass != null
+                && dexClass.isEnum()
+                && factory.enumMethods.isValueOfMethod(context, dexClass)) {
+              continue;
+            }
+            markEnumAsUnboxable(
+                Reason.CONST_CLASS, appView.definitionForProgramType(constClass.getValue()));
+          }
         }
       }
       for (Phi phi : block.getPhis()) {
         DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(phi.getTypeLattice());
         if (enumClass != null) {
-          validateEnumUsages(code, phi.uniqueUsers(), phi.uniquePhiUsers(), enumClass);
+          Reason reason =
+              validateEnumUsages(code, phi.uniqueUsers(), phi.uniquePhiUsers(), enumClass);
+          if (reason == Reason.ELIGIBLE) {
+            eligibleEnums.add(enumClass.type);
+          }
+        }
+        if (phi.getTypeLattice().isNullType()) {
+          addNullDependencies(phi.uniqueUsers(), eligibleEnums);
+        }
+      }
+    }
+    if (!eligibleEnums.isEmpty()) {
+      for (DexType eligibleEnum : eligibleEnums) {
+        Set<DexEncodedMethod> dependencies = enumsUnboxingCandidates.get(eligibleEnum);
+        // If dependencies is null, it means the enum is not eligible (It has been marked as
+        // unboxable by this thread or another one), so we do not need to record dependencies.
+        if (dependencies != null) {
+          dependencies.add(code.method);
+        }
+      }
+    }
+  }
+
+  private void addNullDependencies(Set<Instruction> uses, Set<DexType> eligibleEnums) {
+    for (Instruction use : uses) {
+      if (use.isInvokeMethod()) {
+        InvokeMethod invokeMethod = use.asInvokeMethod();
+        DexMethod invokedMethod = invokeMethod.getInvokedMethod();
+        for (DexType paramType : invokedMethod.proto.parameters.values) {
+          if (enumsUnboxingCandidates.containsKey(paramType)) {
+            eligibleEnums.add(paramType);
+          }
+        }
+        if (invokeMethod.isInvokeMethodWithReceiver()) {
+          DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(invokedMethod.holder);
+          if (enumClass != null) {
+            markEnumAsUnboxable(Reason.ENUM_METHOD_CALLED_WITH_NULL_RECEIVER, enumClass);
+          }
+        }
+      }
+      if (use.isFieldPut()) {
+        DexType type = use.asFieldInstruction().getField().type;
+        if (enumsUnboxingCandidates.containsKey(type)) {
+          eligibleEnums.add(type);
         }
       }
     }
@@ -137,8 +229,22 @@
     return Reason.ELIGIBLE;
   }
 
-  public void finishEnumAnalysis() {
-    for (DexType toUnbox : enumsToUnbox) {
+  public void unboxEnums(PostMethodProcessor.Builder postBuilder) {
+    // At this point the enumsToUnbox are no longer candidates, they will all be unboxed.
+    if (enumsUnboxingCandidates.isEmpty()) {
+      return;
+    }
+    ImmutableSet<DexType> enumsToUnbox = ImmutableSet.copyOf(this.enumsUnboxingCandidates.keySet());
+    appView.setUnboxedEnums(enumsToUnbox);
+    GraphLense enumUnboxingLens = new TreeFixer(enumsToUnbox).fixupTypeReferences();
+    appView.setGraphLense(enumUnboxingLens);
+    enumUnboxerRewriter = new EnumUnboxingRewriter(appView, enumsToUnbox);
+    postBuilder.put(this);
+    postBuilder.mapDexEncodedMethods(appView);
+  }
+
+  public void finishAnalysis() {
+    for (DexType toUnbox : enumsUnboxingCandidates.keySet()) {
       DexProgramClass enumClass = appView.definitionForProgramType(toUnbox);
       assert enumClass != null;
 
@@ -235,27 +341,28 @@
       return Reason.ELIGIBLE;
     }
 
+    // TODO(b/147860220): Re-enable enum unboxing with fields of enum types.
     // A field put is valid only if the field is not on an enum, and the field type and the valuePut
     // have identical enum type.
-    if (instruction.isFieldPut()) {
-      FieldInstruction fieldInstruction = instruction.asFieldInstruction();
-      DexEncodedField field = appView.appInfo().resolveField(fieldInstruction.getField());
-      if (field == null) {
-        return Reason.INVALID_FIELD_PUT;
-      }
-      DexProgramClass dexClass = appView.definitionForProgramType(field.field.holder);
-      if (dexClass == null) {
-        return Reason.INVALID_FIELD_PUT;
-      }
-      if (dexClass.isEnum()) {
-        return Reason.FIELD_PUT_ON_ENUM;
-      }
-      // The put value has to be of the field type.
-      if (field.field.type != enumClass.type) {
-        return Reason.TYPE_MISSMATCH_FIELD_PUT;
-      }
-      return Reason.ELIGIBLE;
-    }
+    // if (instruction.isFieldPut()) {
+    //   FieldInstruction fieldInstruction = instruction.asFieldInstruction();
+    //   DexEncodedField field = appView.appInfo().resolveField(fieldInstruction.getField());
+    //   if (field == null) {
+    //     return Reason.INVALID_FIELD_PUT;
+    //   }
+    //   DexProgramClass dexClass = appView.definitionForProgramType(field.field.holder);
+    //   if (dexClass == null) {
+    //     return Reason.INVALID_FIELD_PUT;
+    //   }
+    //   if (dexClass.isEnum()) {
+    //     return Reason.FIELD_PUT_ON_ENUM;
+    //   }
+    //   // The put value has to be of the field type.
+    //   if (field.field.type != enumClass.type) {
+    //     return Reason.TYPE_MISSMATCH_FIELD_PUT;
+    //   }
+    //   return Reason.ELIGIBLE;
+    // }
 
     // An If using enum as inValue is valid if it matches e == null
     // or e == X with X of same enum type as e. Ex: if (e == MyEnum.A).
@@ -301,9 +408,9 @@
     reporter.info(
         new StringDiagnostic(
             "Unboxed enums (Unboxing succeeded "
-                + enumsToUnbox.size()
+                + enumsUnboxingCandidates.size()
                 + "): "
-                + Arrays.toString(enumsToUnbox.toArray())));
+                + Arrays.toString(enumsUnboxingCandidates.keySet().toArray())));
     StringBuilder sb = new StringBuilder();
     sb.append("Boxed enums (Unboxing failed ").append(debugLogs.size()).append("):\n");
     for (DexType enumType : debugLogs.keySet()) {
@@ -322,6 +429,36 @@
     }
   }
 
+  public void rewriteCode(IRCode code) {
+    if (enumUnboxerRewriter != null) {
+      enumUnboxerRewriter.rewriteCode(code);
+    }
+  }
+
+  public void synthesizeUtilityClass(
+      DexApplication.Builder<?> appBuilder, IRConverter converter, ExecutorService executorService)
+      throws ExecutionException {
+    if (enumUnboxerRewriter != null) {
+      enumUnboxerRewriter.synthesizeEnumUnboxingUtilityClass(
+          appBuilder, converter, executorService);
+    }
+  }
+
+  @Override
+  public Set<DexEncodedMethod> methodsToRevisit() {
+    Set<DexEncodedMethod> toReprocess = Sets.newIdentityHashSet();
+    for (Set<DexEncodedMethod> methods : enumsUnboxingCandidates.values()) {
+      toReprocess.addAll(methods);
+    }
+    return toReprocess;
+  }
+
+  @Override
+  public Collection<CodeOptimization> codeOptimizationsForPostProcessing() {
+    // Answers null so default optimization setup is performed.
+    return null;
+  }
+
   public enum Reason {
     ELIGIBLE,
     SUBTYPES,
@@ -330,6 +467,7 @@
     UNEXPECTED_STATIC_FIELD,
     VIRTUAL_METHOD,
     UNEXPECTED_DIRECT_METHOD,
+    CONST_CLASS,
     INVALID_PHI,
     NO_INIT,
     INVALID_INIT,
@@ -348,6 +486,196 @@
     FIELD_PUT_ON_ENUM,
     TYPE_MISSMATCH_FIELD_PUT,
     INVALID_IF_TYPES,
+    ENUM_METHOD_CALLED_WITH_NULL_RECEIVER,
     OTHER_UNSUPPORTED_INSTRUCTION;
   }
+
+  private class TreeFixer {
+
+    private final EnumUnboxingLens.Builder lensBuilder = EnumUnboxingLens.builder();
+    private final Set<DexType> enumsToUnbox;
+
+    private TreeFixer(Set<DexType> enumsToUnbox) {
+      this.enumsToUnbox = enumsToUnbox;
+    }
+
+    private GraphLense fixupTypeReferences() {
+      // Fix all methods and fields using enums to unbox.
+      for (DexProgramClass clazz : appView.appInfo().classes()) {
+        if (enumsToUnbox.contains(clazz.type)) {
+          assert clazz.instanceFields().size() == 0;
+          clearEnumtoUnboxMethods(clazz);
+        } else {
+          fixupMethods(clazz.directMethods(), clazz::setDirectMethod);
+          fixupMethods(clazz.virtualMethods(), clazz::setVirtualMethod);
+          fixupFields(clazz.staticFields(), clazz::setStaticField);
+          fixupFields(clazz.instanceFields(), clazz::setInstanceField);
+        }
+      }
+      for (DexType toUnbox : enumsToUnbox) {
+        lensBuilder.map(toUnbox, factory.intType);
+      }
+      return lensBuilder.build(factory, appView.graphLense());
+    }
+
+    private void clearEnumtoUnboxMethods(DexProgramClass clazz) {
+      // The compiler may have references to the enum methods, but such methods will be removed
+      // and they cannot be reprocessed since their rewriting through the lensCodeRewriter/
+      // enumUnboxerRewriter will generate invalid code.
+      // To work around this problem we clear such methods, i.e., we replace the code object by
+      // an empty throwing code object, so reprocessing won't take time and will be valid.
+      for (DexEncodedMethod method : clazz.methods()) {
+        method.setCode(
+            appView.options().isGeneratingClassFiles()
+                ? method.buildEmptyThrowingCfCode()
+                : method.buildEmptyThrowingDexCode(),
+            appView);
+      }
+    }
+
+    private void fixupMethods(List<DexEncodedMethod> methods, MethodSetter setter) {
+      if (methods == null) {
+        return;
+      }
+      for (int i = 0; i < methods.size(); i++) {
+        DexEncodedMethod encodedMethod = methods.get(i);
+        DexMethod method = encodedMethod.method;
+        DexMethod newMethod = fixupMethod(method);
+        if (newMethod != method) {
+          lensBuilder.move(method, newMethod, encodedMethod.isStatic());
+          setter.setMethod(i, encodedMethod.toTypeSubstitutedMethod(newMethod));
+        }
+      }
+    }
+
+    private void fixupFields(List<DexEncodedField> fields, FieldSetter setter) {
+      if (fields == null) {
+        return;
+      }
+      for (int i = 0; i < fields.size(); i++) {
+        DexEncodedField encodedField = fields.get(i);
+        DexField field = encodedField.field;
+        DexType newType = fixupType(field.type);
+        if (newType != field.type) {
+          DexField newField = factory.createField(field.holder, newType, field.name);
+          lensBuilder.move(field, newField);
+          setter.setField(i, encodedField.toTypeSubstitutedField(newField));
+        }
+      }
+    }
+
+    private DexMethod fixupMethod(DexMethod method) {
+      return factory.createMethod(method.holder, fixupProto(method.proto), method.name);
+    }
+
+    private DexProto fixupProto(DexProto proto) {
+      DexType returnType = fixupType(proto.returnType);
+      DexType[] arguments = fixupTypes(proto.parameters.values);
+      return factory.createProto(returnType, arguments);
+    }
+
+    private DexType fixupType(DexType type) {
+      if (type.isArrayType()) {
+        DexType base = type.toBaseType(factory);
+        DexType fixed = fixupType(base);
+        if (base == fixed) {
+          return type;
+        }
+        return type.replaceBaseType(fixed, factory);
+      }
+      if (type.isClassType() && enumsToUnbox.contains(type)) {
+        DexType intType = factory.intType;
+        lensBuilder.map(type, intType);
+        return intType;
+      }
+      return type;
+    }
+
+    private DexType[] fixupTypes(DexType[] types) {
+      DexType[] result = new DexType[types.length];
+      for (int i = 0; i < result.length; i++) {
+        result[i] = fixupType(types[i]);
+      }
+      return result;
+    }
+  }
+
+  private static class EnumUnboxingLens extends NestedGraphLense {
+
+    private final Map<DexMethod, RewrittenPrototypeDescription> prototypeChanges;
+
+    EnumUnboxingLens(
+        Map<DexType, DexType> typeMap,
+        Map<DexMethod, DexMethod> methodMap,
+        Map<DexField, DexField> fieldMap,
+        BiMap<DexField, DexField> originalFieldSignatures,
+        BiMap<DexMethod, DexMethod> originalMethodSignatures,
+        GraphLense previousLense,
+        DexItemFactory dexItemFactory,
+        Map<DexMethod, RewrittenPrototypeDescription> prototypeChanges) {
+      super(
+          typeMap,
+          methodMap,
+          fieldMap,
+          originalFieldSignatures,
+          originalMethodSignatures,
+          previousLense,
+          dexItemFactory);
+      this.prototypeChanges = prototypeChanges;
+    }
+
+    @Override
+    public RewrittenPrototypeDescription lookupPrototypeChanges(DexMethod method) {
+      // During the second IR processing enum unboxing is the only optimization rewriting
+      // prototype description, if this does not hold, remove the assertion and merge
+      // the two prototype changes.
+      assert previousLense.lookupPrototypeChanges(method).isEmpty();
+      return prototypeChanges.getOrDefault(method, RewrittenPrototypeDescription.none());
+    }
+
+    public static Builder builder() {
+      return new Builder();
+    }
+
+    private static class Builder extends NestedGraphLense.Builder {
+
+      private Map<DexMethod, RewrittenPrototypeDescription> prototypeChanges =
+          new IdentityHashMap<>();
+
+      public void move(DexMethod from, DexMethod to, boolean isStatic) {
+        super.move(from, to);
+        int offset = BooleanUtils.intValue(!isStatic);
+        RewrittenTypeArgumentInfoCollection.Builder builder =
+            RewrittenTypeArgumentInfoCollection.builder();
+        for (int i = 0; i < from.proto.parameters.size(); i++) {
+          DexType fromType = from.proto.parameters.values[i];
+          DexType toType = to.proto.parameters.values[i];
+          if (fromType != toType) {
+            builder.rewriteArgument(i + offset, fromType, toType);
+          }
+        }
+        RewrittenTypeInfo returnInfo =
+            from.proto.returnType == to.proto.returnType
+                ? null
+                : new RewrittenTypeInfo(from.proto.returnType, to.proto.returnType);
+        prototypeChanges.put(
+            to, RewrittenPrototypeDescription.createForRewrittenTypes(returnInfo, builder.build()));
+      }
+
+      public GraphLense build(DexItemFactory dexItemFactory, GraphLense previousLense) {
+        if (typeMap.isEmpty() && methodMap.isEmpty() && fieldMap.isEmpty()) {
+          return previousLense;
+        }
+        return new EnumUnboxingLens(
+            typeMap,
+            methodMap,
+            fieldMap,
+            originalFieldSignatures,
+            originalMethodSignatures,
+            previousLense,
+            dexItemFactory,
+            ImmutableMap.copyOf(prototypeChanges));
+      }
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
index 7b207c6..53cbcc0 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
@@ -12,7 +12,9 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.optimize.enums.EnumUnboxer.Reason;
 import com.google.common.collect.Sets;
+import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 class EnumUnboxingCandidateAnalysis {
 
@@ -26,11 +28,11 @@
     factory = appView.dexItemFactory();
   }
 
-  Set<DexType> findCandidates() {
-    Set<DexType> enums = Sets.newConcurrentHashSet();
+  Map<DexType, Set<DexEncodedMethod>> findCandidates() {
+    Map<DexType, Set<DexEncodedMethod>> enums = new ConcurrentHashMap<>();
     for (DexProgramClass clazz : appView.appInfo().classes()) {
       if (isEnumUnboxingCandidate(clazz)) {
-        enums.add(clazz.type);
+        enums.put(clazz.type, Sets.newConcurrentHashSet());
       }
     }
     return enums;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
new file mode 100644
index 0000000..9888aec
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
@@ -0,0 +1,218 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.enums;
+
+import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.ClassAccessFlags;
+import com.android.tools.r8.graph.DexAnnotationSet;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ParameterAnnotationsList;
+import com.android.tools.r8.ir.analysis.type.DestructivePhiTypeUpdater;
+import com.android.tools.r8.ir.analysis.type.PrimitiveTypeLatticeElement;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
+import com.android.tools.r8.ir.code.ConstNumber;
+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.InvokeMethodWithReceiver;
+import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.code.NumericType;
+import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.code.StaticGet;
+import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.origin.SynthesizedOrigin;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.AppInfoWithLiveness.EnumValueInfo;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+public class EnumUnboxingRewriter {
+
+  public static final String ENUM_UNBOXING_UTILITY_CLASS_NAME = "$r8$EnumUnboxingUtility";
+  private static final int REQUIRED_CLASS_FILE_VERSION = 52;
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final DexItemFactory factory;
+  private final Set<DexType> enumsToUnbox;
+
+  private final DexType utilityClassType;
+  private final DexMethod ordinalUtilityMethod;
+
+  private boolean requiresOrdinalUtilityMethod = false;
+
+  EnumUnboxingRewriter(AppView<AppInfoWithLiveness> appView, Set<DexType> enumsToUnbox) {
+    this.appView = appView;
+    this.factory = appView.dexItemFactory();
+    this.enumsToUnbox = enumsToUnbox;
+
+    this.utilityClassType = factory.createType("L" + ENUM_UNBOXING_UTILITY_CLASS_NAME + ";");
+    this.ordinalUtilityMethod =
+        factory.createMethod(
+            utilityClassType,
+            factory.createProto(factory.intType, factory.intType),
+            "$enumboxing$ordinal");
+  }
+
+  void rewriteCode(IRCode code) {
+    // We should not process the enum methods, they will be removed and they may contain invalid
+    // rewriting rules.
+    if (enumsToUnbox.isEmpty()) {
+      return;
+    }
+    Set<Phi> affectedPhis = Sets.newIdentityHashSet();
+    InstructionListIterator iterator = code.instructionListIterator();
+    while (iterator.hasNext()) {
+      Instruction instruction = iterator.next();
+      // Rewrites specific enum methods, such as ordinal, into their corresponding enum unboxed
+      // counterpart.
+      if (instruction.isInvokeMethodWithReceiver()) {
+        InvokeMethodWithReceiver invokeMethod = instruction.asInvokeMethodWithReceiver();
+        DexMethod invokedMethod = invokeMethod.getInvokedMethod();
+        if (invokedMethod == factory.enumMethods.ordinal
+            && invokeMethod.getReceiver().getTypeLattice().isInt()) {
+          instruction =
+              new InvokeStatic(
+                  ordinalUtilityMethod, invokeMethod.outValue(), invokeMethod.inValues());
+          iterator.replaceCurrentInstruction(instruction);
+          requiresOrdinalUtilityMethod = true;
+        }
+        // TODO(b/147860220): rewrite also other enum methods.
+      }
+      // Rewrites direct access to enum values into the corresponding int, $VALUES is not
+      // supported.
+      if (instruction.isStaticGet()) {
+        StaticGet staticGet = instruction.asStaticGet();
+        DexType holder = staticGet.getField().holder;
+        if (enumsToUnbox.contains(holder)) {
+          if (staticGet.outValue() == null) {
+            iterator.removeInstructionIgnoreOutValue();
+            continue;
+          }
+          Map<DexField, EnumValueInfo> enumValueInfoMapFor =
+              appView.appInfo().withLiveness().getEnumValueInfoMapFor(holder);
+          assert enumValueInfoMapFor != null;
+          // Replace by ordinal + 1 for null check (null is 0).
+          EnumValueInfo enumValueInfo = enumValueInfoMapFor.get(staticGet.getField());
+          assert enumValueInfo != null
+              : "Invalid read to " + staticGet.getField().name + ", error during enum analysis";
+          instruction = new ConstNumber(staticGet.outValue(), enumValueInfo.ordinal + 1);
+          staticGet
+              .outValue()
+              .setTypeLattice(PrimitiveTypeLatticeElement.fromNumericType(NumericType.INT));
+          iterator.replaceCurrentInstruction(instruction);
+          affectedPhis.addAll(staticGet.outValue().uniquePhiUsers());
+        }
+      }
+      assert validateEnumToUnboxRemoved(instruction);
+    }
+    if (!affectedPhis.isEmpty()) {
+      new DestructivePhiTypeUpdater(appView).recomputeAndPropagateTypes(code, affectedPhis);
+      assert code.verifyTypes(appView);
+    }
+    assert code.isConsistentSSA();
+  }
+
+  private boolean validateEnumToUnboxRemoved(Instruction instruction) {
+    if (instruction.outValue() == null) {
+      return true;
+    }
+    TypeLatticeElement typeLattice = instruction.outValue().getTypeLattice();
+    assert !typeLattice.isClassType()
+        || !enumsToUnbox.contains(typeLattice.asClassTypeLatticeElement().getClassType());
+    if (typeLattice.isArrayType()) {
+      TypeLatticeElement arrayBaseTypeLattice =
+          typeLattice.asArrayTypeLatticeElement().getArrayBaseTypeLattice();
+      assert !arrayBaseTypeLattice.isClassType()
+          || !enumsToUnbox.contains(
+              arrayBaseTypeLattice.asClassTypeLatticeElement().getClassType());
+    }
+    return true;
+  }
+
+  // TODO(b/150172351): Synthesize the utility class upfront in the enqueuer.
+  void synthesizeEnumUnboxingUtilityClass(
+      DexApplication.Builder<?> appBuilder, IRConverter converter, ExecutorService executorService)
+      throws ExecutionException {
+    // Synthesize a class which holds various utility methods that may be called from the IR
+    // rewriting. If any of these methods are not used, they will be removed by the Enqueuer.
+    List<DexEncodedMethod> requiredMethods = new ArrayList<>();
+    if (requiresOrdinalUtilityMethod) {
+      requiredMethods.add(synthesizeOrdinalMethod());
+    }
+    // TODO(b/147860220): synthesize also other enum methods.
+    if (requiredMethods.isEmpty()) {
+      return;
+    }
+    DexProgramClass utilityClass =
+        new DexProgramClass(
+            utilityClassType,
+            null,
+            new SynthesizedOrigin("EnumUnboxing ", getClass()),
+            ClassAccessFlags.fromSharedAccessFlags(Constants.ACC_PUBLIC | Constants.ACC_SYNTHETIC),
+            factory.objectType,
+            DexTypeList.empty(),
+            factory.createString("enumunboxing"),
+            null,
+            Collections.emptyList(),
+            null,
+            Collections.emptyList(),
+            DexAnnotationSet.empty(),
+            DexEncodedField.EMPTY_ARRAY,
+            DexEncodedField.EMPTY_ARRAY,
+            // All synthesized methods are static in this case.
+            requiredMethods.toArray(DexEncodedMethod.EMPTY_ARRAY),
+            DexEncodedMethod.EMPTY_ARRAY,
+            factory.getSkipNameValidationForTesting(),
+            DexProgramClass::checksumFromType);
+    appBuilder.addSynthesizedClass(utilityClass, utilityClassInMainDexList());
+    appView.appInfo().addSynthesizedClass(utilityClass);
+    converter.optimizeSynthesizedClass(utilityClass, executorService);
+  }
+
+  // TODO(b/150178516): Add a test for this case.
+  private boolean utilityClassInMainDexList() {
+    for (DexType toUnbox : enumsToUnbox) {
+      if (appView.appInfo().isInMainDexList(toUnbox)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private DexEncodedMethod synthesizeOrdinalMethod() {
+    CfCode cfCode =
+        EnumUnboxingCfMethods.EnumUnboxingMethods_ordinal(appView.options(), ordinalUtilityMethod);
+    return new DexEncodedMethod(
+        ordinalUtilityMethod,
+        synthesizedMethodAccessFlags(),
+        DexAnnotationSet.empty(),
+        ParameterAnnotationsList.empty(),
+        cfCode,
+        REQUIRED_CLASS_FILE_VERSION,
+        true);
+  }
+
+  private MethodAccessFlags synthesizedMethodAccessFlags() {
+    return MethodAccessFlags.fromSharedAccessFlags(
+        Constants.ACC_PUBLIC | Constants.ACC_SYNTHETIC | Constants.ACC_STATIC, false);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java
index 91aa098..33c7b2a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java
@@ -31,12 +31,20 @@
 
   public void fixupClassTypeReferences(
       Function<DexType, DexType> mapping, AppView<? extends AppInfoWithSubtyping> appView) {
-    if (dynamicLowerBoundType != null) {
-      dynamicLowerBoundType = dynamicLowerBoundType.fixupClassTypeReferences(mapping, appView);
-    }
     if (dynamicUpperBoundType != null) {
       dynamicUpperBoundType = dynamicUpperBoundType.fixupClassTypeReferences(mapping, appView);
     }
+    if (dynamicLowerBoundType != null) {
+      TypeLatticeElement dynamicLowerBoundType =
+          this.dynamicLowerBoundType.fixupClassTypeReferences(mapping, appView);
+      if (dynamicLowerBoundType.isClassType()) {
+        this.dynamicLowerBoundType = dynamicLowerBoundType.asClassTypeLatticeElement();
+      } else {
+        assert dynamicLowerBoundType.isPrimitive();
+        this.dynamicLowerBoundType = null;
+        this.dynamicUpperBoundType = null;
+      }
+    }
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/UpdatableMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/UpdatableMethodOptimizationInfo.java
index d2e78f4..af494c8 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/UpdatableMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/UpdatableMethodOptimizationInfo.java
@@ -147,8 +147,16 @@
           returnsObjectWithUpperBoundType.fixupClassTypeReferences(mapping, appView);
     }
     if (returnsObjectWithLowerBoundType != null) {
-      returnsObjectWithLowerBoundType =
-          returnsObjectWithLowerBoundType.fixupClassTypeReferences(mapping, appView);
+      TypeLatticeElement returnsObjectWithLowerBoundType =
+          this.returnsObjectWithLowerBoundType.fixupClassTypeReferences(mapping, appView);
+      if (returnsObjectWithLowerBoundType.isClassType()) {
+        this.returnsObjectWithLowerBoundType =
+            returnsObjectWithLowerBoundType.asClassTypeLatticeElement();
+      } else {
+        assert returnsObjectWithLowerBoundType.isPrimitive();
+        this.returnsObjectWithUpperBoundType = DefaultMethodOptimizationInfo.UNKNOWN_TYPE;
+        this.returnsObjectWithLowerBoundType = DefaultMethodOptimizationInfo.UNKNOWN_CLASS_TYPE;
+      }
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/SyntheticSourceCode.java b/src/main/java/com/android/tools/r8/ir/synthetic/SyntheticSourceCode.java
index ed47e54..c7d94d4 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/SyntheticSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/SyntheticSourceCode.java
@@ -156,7 +156,7 @@
 
   @Override
   public final void buildPrelude(IRBuilder builder) {
-    builder.buildArgumentsWithUnusedArgumentStubs(
+    builder.buildArgumentsWithRewrittenPrototypeChanges(
         0, builder.getMethod(), DexSourceCode::doNothingWriteConsumer);
   }
 
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/FieldPutEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/FieldPutEnumUnboxingTest.java
index 129230a..f67cd54 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/FieldPutEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/FieldPutEnumUnboxingTest.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.TestParameters;
 import java.util.List;
+import org.junit.Assume;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -41,6 +42,8 @@
 
   @Test
   public void testEnumUnboxing() throws Exception {
+    // TODO(b/147860220): Fix fields of type enums.
+    Assume.assumeTrue("Fix fields", false);
     R8TestCompileResult compile =
         testForR8(parameters.getBackend())
             .addInnerClasses(FieldPutEnumUnboxingTest.class)