Implement first version of proto lite shrinker.

Bug:
Change-Id: Ia77e68cfd634f38954cdcbe1537fd86937ff9720
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index fd5c29f..50f79dc 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -17,6 +17,8 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLense;
 import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.ir.optimize.EnumOrdinalMapCollector;
+import com.android.tools.r8.ir.optimize.SwitchMapCollector;
 import com.android.tools.r8.naming.Minifier;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.optimize.BridgeMethodAnalysis;
@@ -36,6 +38,7 @@
 import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
 import com.android.tools.r8.shaking.SimpleClassMerger;
 import com.android.tools.r8.shaking.TreePruner;
+import com.android.tools.r8.shaking.protolite.ProtoLiteExtension;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.CfgPrinter;
 import com.android.tools.r8.utils.FileUtils;
@@ -241,6 +244,7 @@
         }
         rootSet = new RootSetBuilder(application, appInfo, options.keepRules).run(executorService);
         Enqueuer enqueuer = new Enqueuer(appInfo);
+        enqueuer.addExtension(new ProtoLiteExtension(appInfo));
         appInfo = enqueuer.traceApplication(rootSet, timing);
         if (options.printSeeds) {
           ByteArrayOutputStream bytes = new ByteArrayOutputStream();
@@ -270,8 +274,7 @@
 
       GraphLense graphLense = GraphLense.getIdentityLense();
 
-      if (appInfo.withLiveness() != null) {
-        // No-op until class merger is added.
+      if (appInfo.hasLiveness()) {
         graphLense = new MemberRebindingAnalysis(appInfo.withLiveness(), graphLense).run();
         // Class merging requires inlining.
         if (!options.skipClassMerging && options.inlineAccessors) {
@@ -282,6 +285,9 @@
         }
         appInfo = appInfo.withLiveness().prunedCopyFrom(application);
         appInfo = appInfo.withLiveness().rewrittenWithLense(graphLense);
+        // Collect switch maps and ordinals maps.
+        new SwitchMapCollector(appInfo.withLiveness(), options).run();
+        new EnumOrdinalMapCollector(appInfo.withLiveness(), options).run();
       }
 
       graphLense = new BridgeMethodAnalysis(graphLense, appInfo.withSubtyping()).run();
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 1f45dec..8d3d937 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -87,6 +87,7 @@
       minimalMainDex = value;
       return self();
     }
+
     /**
      * Add proguard configuration file resources.
      */
diff --git a/src/main/java/com/android/tools/r8/graph/DelegatingUseRegistry.java b/src/main/java/com/android/tools/r8/graph/DelegatingUseRegistry.java
new file mode 100644
index 0000000..0e28d3d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/DelegatingUseRegistry.java
@@ -0,0 +1,68 @@
+// Copyright (c) 2017, 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.graph;
+
+public class DelegatingUseRegistry extends UseRegistry {
+    private final UseRegistry delegate;
+
+    public DelegatingUseRegistry(UseRegistry delegate) {
+      this.delegate = delegate;
+    }
+
+    @Override
+    public boolean registerInvokeVirtual(DexMethod method) {
+      return delegate.registerInvokeVirtual(method);
+    }
+
+    @Override
+    public boolean registerInvokeDirect(DexMethod method) {
+      return delegate.registerInvokeDirect(method);
+    }
+
+    @Override
+    public boolean registerInvokeStatic(DexMethod method) {
+      return delegate.registerInvokeStatic(method);
+    }
+
+    @Override
+    public boolean registerInvokeInterface(DexMethod method) {
+      return delegate.registerInvokeInterface(method);
+    }
+
+    @Override
+    public boolean registerInvokeSuper(DexMethod method) {
+      return delegate.registerInvokeSuper(method);
+    }
+
+    @Override
+    public boolean registerInstanceFieldWrite(DexField field) {
+      return delegate.registerInstanceFieldWrite(field);
+    }
+
+    @Override
+    public boolean registerInstanceFieldRead(DexField field) {
+      return delegate.registerInstanceFieldRead(field);
+    }
+
+    @Override
+    public boolean registerNewInstance(DexType type) {
+      return delegate.registerNewInstance(type);
+    }
+
+    @Override
+    public boolean registerStaticFieldRead(DexField field) {
+      return delegate.registerStaticFieldRead(field);
+    }
+
+    @Override
+    public boolean registerStaticFieldWrite(DexField field) {
+      return delegate.registerStaticFieldWrite(field);
+    }
+
+    @Override
+    public boolean registerTypeReference(DexType type) {
+      return delegate.registerTypeReference(type);
+    }
+
+}
diff --git a/src/main/java/com/android/tools/r8/graph/DexString.java b/src/main/java/com/android/tools/r8/graph/DexString.java
index 25416d9..e6914ec 100644
--- a/src/main/java/com/android/tools/r8/graph/DexString.java
+++ b/src/main/java/com/android/tools/r8/graph/DexString.java
@@ -321,4 +321,28 @@
     builder.append("]");
     return builder.toString();
   }
+
+  public boolean beginsWith(DexString prefix) {
+    if (content.length < prefix.content.length) {
+      return false;
+    }
+    for (int i = 0; i < prefix.content.length - 1; i++) {
+      if (content[i] != prefix.content[i]) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  public boolean endsWith(DexString suffix) {
+    if (content.length < suffix.content.length) {
+      return false;
+    }
+    for (int i = content.length - suffix.content.length, j = 0; i < content.length; i++, j++) {
+      if (content[i] != suffix.content[j]) {
+        return false;
+      }
+    }
+    return true;
+  }
 }
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 ac7d9ee..e06c0ad 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -139,6 +139,11 @@
 
   private boolean isSubtypeOfClass(DexType other, AppInfo appInfo) {
     DexType self = this;
+    if (other.hierarchyLevel == UNKNOWN_LEVEL) {
+      // We have no definition for this class, hence it is not part of the
+      // hierarchy.
+      return false;
+    }
     while (other.hierarchyLevel < self.hierarchyLevel) {
       DexClass holder = appInfo.definitionFor(self);
       assert holder != null && !holder.isInterface();
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
index 5c9d171..5f1b33f 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -44,6 +44,12 @@
     return localsAtEntry;
   }
 
+  public void replaceLastInstruction(Instruction instruction) {
+    InstructionListIterator iterator = listIterator(getInstructions().size());
+    iterator.previous();
+    iterator.replaceCurrentInstruction(instruction);
+  }
+
   public enum ThrowingInfo {
     NO_THROW, CAN_THROW
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/DominatorTree.java b/src/main/java/com/android/tools/r8/ir/code/DominatorTree.java
index b5a7b75..9f28eea 100644
--- a/src/main/java/com/android/tools/r8/ir/code/DominatorTree.java
+++ b/src/main/java/com/android/tools/r8/ir/code/DominatorTree.java
@@ -87,6 +87,29 @@
     return dominator;
   }
 
+  /**
+   * Returns an iterator over all blocks dominated by dominator, including dominator itself.
+   */
+  public Iterable<BasicBlock> dominatedBlocks(BasicBlock domintator) {
+    return () -> new Iterator<BasicBlock>() {
+      private int current = domintator.getNumber();
+
+      @Override
+      public boolean hasNext() {
+        return dominatedBy(sorted[current], domintator);
+      }
+
+      @Override
+      public BasicBlock next() {
+        if (!hasNext()) {
+          return null;
+        } else {
+          return sorted[current++];
+        }
+      }
+    };
+  }
+
   public BasicBlock[] getSortedBlocks() {
     return sorted;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
index 7c37649..824d75c 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -390,4 +390,9 @@
   public final int getHighestBlockNumber() {
     return blocks.stream().max(Comparator.comparingInt(BasicBlock::getNumber)).get().getNumber();
   }
+
+  public Instruction createConstNull(Instruction from) {
+    Value newValue = createValue(from.outType());
+    return new ConstNumber(ConstType.fromMoveType(from.outType()), newValue, 0);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionIterator.java b/src/main/java/com/android/tools/r8/ir/code/InstructionIterator.java
index 997b092..2cb8185 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstructionIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionIterator.java
@@ -4,9 +4,7 @@
 
 package com.android.tools.r8.ir.code;
 
-import java.util.Iterator;
-
-public interface InstructionIterator extends Iterator<Instruction> {
+public interface InstructionIterator extends NextUntilIterator<Instruction> {
   /**
    * Replace the current instruction (aka the {@link Instruction} returned by the previous call to
    * {@link #next} with the passed in <code>newInstruction</code>.
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 bb88b64..431d338 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
@@ -10,7 +10,8 @@
 import java.util.ListIterator;
 import java.util.function.Predicate;
 
-public interface InstructionListIterator extends ListIterator<Instruction> {
+public interface InstructionListIterator extends ListIterator<Instruction>,
+    NextUntilIterator<Instruction> {
 
   /**
    * Peek the previous instruction.
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java b/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
index cdc8644..6ccf1cc 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
@@ -34,6 +34,9 @@
     return this;
   }
 
+  public Value getReceiver() {
+    return inValues.get(0);
+  }
 
   @Override
   public final InlineAction computeInlining(InliningOracle decider) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/NextUntilIterator.java b/src/main/java/com/android/tools/r8/ir/code/NextUntilIterator.java
new file mode 100644
index 0000000..d15376e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/code/NextUntilIterator.java
@@ -0,0 +1,26 @@
+// Copyright (c) 2017, 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.code;
+
+import java.util.Iterator;
+import java.util.function.Predicate;
+
+public interface NextUntilIterator<T> extends Iterator<T> {
+
+  /**
+   * Continue to call {@link #next} while {@code predicate} tests {@code false}.
+   *
+   * @returns the item that matched the predicate or {@code null} if all items fail
+   * the predicate test
+   */
+  default T nextUntil(Predicate<T> predicate) {
+    while (hasNext()) {
+      T item = next();
+      if (predicate.test(item)) {
+        return item;
+      }
+    }
+    return null;
+  }
+}
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 e6b592d..8e613c0 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
@@ -32,6 +32,7 @@
 import com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator;
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
 import com.android.tools.r8.logging.Log;
+import com.android.tools.r8.shaking.protolite.ProtoLitePruner;
 import com.android.tools.r8.utils.CfgPrinter;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
@@ -64,9 +65,10 @@
   private final MemberValuePropagation memberValuePropagation;
   private final LensCodeRewriter lensCodeRewriter;
   private final Inliner inliner;
+  private final ProtoLitePruner protoLiteRewriter;
   private CallGraph callGraph;
-  private OptimizationFeedback ignoreOptimizationFeedback = new OptimizationFeedbackIgnore();
 
+  private OptimizationFeedback ignoreOptimizationFeedback = new OptimizationFeedbackIgnore();
   private DexString highestSortingString;
 
   private IRConverter(
@@ -94,16 +96,22 @@
         (enableDesugaring && enableInterfaceMethodDesugaring())
             ? new InterfaceMethodRewriter(this) : null;
     if (enableWholeProgramOptimizations) {
-      assert appInfo.withSubtyping() != null;
+      assert appInfo.hasSubtyping();
       this.inliner = new Inliner(appInfo.withSubtyping(), graphLense, options);
       this.outliner = new Outliner(appInfo, options);
       this.memberValuePropagation = new MemberValuePropagation(appInfo);
       this.lensCodeRewriter = new LensCodeRewriter(graphLense, appInfo.withSubtyping());
+      if (appInfo.hasLiveness()) {
+        this.protoLiteRewriter = new ProtoLitePruner(appInfo.withLiveness());
+      } else {
+        this.protoLiteRewriter = null;
+      }
     } else {
       this.inliner = null;
       this.outliner = null;
       this.memberValuePropagation = null;
       this.lensCodeRewriter = null;
+      this.protoLiteRewriter = null;
     }
   }
 
@@ -427,15 +435,20 @@
     printC1VisualizerHeader(method);
     printMethod(code, "Initial IR (SSA)");
 
-    if (lensCodeRewriter != null) {
-      lensCodeRewriter.rewrite(code, method);
-    } else {
-      assert graphLense.isIdentityLense();
+    if (!method.isProcessed()) {
+      if (protoLiteRewriter != null && protoLiteRewriter.appliesTo(method)) {
+        protoLiteRewriter.rewriteProtoLiteSpecialMethod(code, method);
+      }
+      if (lensCodeRewriter != null) {
+        lensCodeRewriter.rewrite(code, method);
+      } else {
+        assert graphLense.isIdentityLense();
+      }
     }
     if (memberValuePropagation != null) {
       memberValuePropagation.rewriteWithConstantValues(code);
     }
-    if (options.removeSwitchMaps) {
+    if (options.removeSwitchMaps && appInfo.hasLiveness()) {
       // TODO(zerny): Should we support removeSwitchMaps in debug mode? b/62936642
       assert !options.debug;
       codeRewriter.removeSwitchMaps(code);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index 14cd563..b1cdcbc 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -9,12 +9,10 @@
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexClass;
 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.DexProto;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.ir.code.ArrayGet;
 import com.android.tools.r8.ir.code.ArrayPut;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.Binop;
@@ -32,7 +30,6 @@
 import com.android.tools.r8.ir.code.InstructionIterator;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Invoke;
-import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.MemberType;
@@ -47,6 +44,7 @@
 import com.android.tools.r8.ir.code.Switch;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.OptimizationFeedback;
+import com.android.tools.r8.ir.optimize.SwitchUtils.EnumSwitchInfo;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.LongInterval;
 import com.google.common.base.Equivalence;
@@ -56,12 +54,8 @@
 import com.google.common.collect.ListMultimap;
 import it.unimi.dsi.fastutil.ints.Int2IntArrayMap;
 import it.unimi.dsi.fastutil.ints.Int2IntMap;
-import it.unimi.dsi.fastutil.ints.Int2ReferenceArrayMap;
-import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import it.unimi.dsi.fastutil.ints.IntArrayList;
 import it.unimi.dsi.fastutil.ints.IntList;
-import it.unimi.dsi.fastutil.objects.Reference2IntArrayMap;
-import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.HashMap;
@@ -371,8 +365,6 @@
    *   ...
    * }
    * </pre></blockquote>
-   * See {@link #extractIndexMapFrom} and {@link #extractOrdinalsMapFor} for
-   * details of the companion class and ordinals computation.
    */
   public void removeSwitchMaps(IRCode code) {
     for (BasicBlock block : code.blocks) {
@@ -382,68 +374,38 @@
         // Pattern match a switch on a switch map as input.
         if (insn.isSwitch()) {
           Switch switchInsn = insn.asSwitch();
-          Instruction input = switchInsn.inValues().get(0).definition;
-          if (input == null || !input.isArrayGet()) {
-            continue;
-          }
-          ArrayGet arrayGet = input.asArrayGet();
-          Instruction index = arrayGet.index().definition;
-          if (index == null || !index.isInvokeVirtual()) {
-            continue;
-          }
-          InvokeVirtual ordinalInvoke = index.asInvokeVirtual();
-          DexMethod ordinalMethod = ordinalInvoke.getInvokedMethod();
-          DexClass enumClass = appInfo.definitionFor(ordinalMethod.holder);
-          if (enumClass == null
-              || (!enumClass.accessFlags.isEnum() && enumClass.type != dexItemFactory.enumType)
-              || ordinalMethod.name != dexItemFactory.ordinalMethodName
-              || ordinalMethod.proto.returnType != dexItemFactory.intType
-              || !ordinalMethod.proto.parameters.isEmpty()) {
-            continue;
-          }
-          Instruction array = arrayGet.array().definition;
-          if (array == null || !array.isStaticGet()) {
-            continue;
-          }
-          StaticGet staticGet = array.asStaticGet();
-          if (staticGet.getField().name.toSourceString().startsWith("$SwitchMap$")) {
-            Int2ReferenceMap<DexField> indexMap = extractIndexMapFrom(staticGet.getField());
-            if (indexMap == null || indexMap.isEmpty()) {
-              continue;
+          EnumSwitchInfo info = SwitchUtils
+              .analyzeSwitchOverEnum(switchInsn, appInfo.withLiveness());
+          if (info != null) {
+            Int2IntMap targetMap = new Int2IntArrayMap();
+            IntList keys = new IntArrayList(switchInsn.numberOfKeys());
+            for (int i = 0; i < switchInsn.numberOfKeys(); i++) {
+              assert switchInsn.targetBlockIndices()[i] != switchInsn.getFallthroughBlockIndex();
+              int key = info.ordinalsMap.getInt(info.indexMap.get(switchInsn.getKey(i)));
+              keys.add(key);
+              targetMap.put(key, switchInsn.targetBlockIndices()[i]);
             }
-            // Due to member rebinding, only the fields are certain to provide the actual enums
-            // class.
-            DexType switchMapHolder = indexMap.values().iterator().next().getHolder();
-            Reference2IntMap<DexField> ordinalsMap = extractOrdinalsMapFor(switchMapHolder);
-            if (ordinalsMap != null) {
-              Int2IntMap targetMap = new Int2IntArrayMap();
-              IntList keys = new IntArrayList(switchInsn.numberOfKeys());
-              for (int i = 0; i < switchInsn.numberOfKeys(); i++) {
-                assert switchInsn.targetBlockIndices()[i] != switchInsn.getFallthroughBlockIndex();
-                int key = ordinalsMap.getInt(indexMap.get(switchInsn.getKey(i)));
-                keys.add(key);
-                targetMap.put(key, switchInsn.targetBlockIndices()[i]);
-              }
-              keys.sort(Comparator.naturalOrder());
-              int[] targets = new int[keys.size()];
-              for (int i = 0; i < keys.size(); i++) {
-                targets[i] = targetMap.get(keys.getInt(i));
-              }
+            keys.sort(Comparator.naturalOrder());
+            int[] targets = new int[keys.size()];
+            for (int i = 0; i < keys.size(); i++) {
+              targets[i] = targetMap.get(keys.getInt(i));
+            }
 
-              Switch newSwitch = new Switch(ordinalInvoke.outValue(), keys.toIntArray(),
-                  targets, switchInsn.getFallthroughBlockIndex());
-              // Replace the switch itself.
-              it.replaceCurrentInstruction(newSwitch);
-              // If the original input to the switch is now unused, remove it too. It is not dead
-              // as it might have side-effects but we ignore these here.
-              if (arrayGet.outValue().numberOfUsers() == 0) {
-                arrayGet.inValues().forEach(v -> v.removeUser(arrayGet));
-                arrayGet.getBlock().removeInstruction(arrayGet);
-              }
-              if (staticGet.outValue().numberOfUsers() == 0) {
-                assert staticGet.inValues().isEmpty();
-                staticGet.getBlock().removeInstruction(staticGet);
-              }
+            Switch newSwitch = new Switch(info.ordinalInvoke.outValue(), keys.toIntArray(),
+                targets, switchInsn.getFallthroughBlockIndex());
+            // Replace the switch itself.
+            it.replaceCurrentInstruction(newSwitch);
+            // If the original input to the switch is now unused, remove it too. It is not dead
+            // as it might have side-effects but we ignore these here.
+            Instruction arrayGet = info.arrayGet;
+            if (arrayGet.outValue().numberOfUsers() == 0) {
+              arrayGet.inValues().forEach(v -> v.removeUser(arrayGet));
+              arrayGet.getBlock().removeInstruction(arrayGet);
+            }
+            Instruction staticGet = info.staticGet;
+            if (staticGet.outValue().numberOfUsers() == 0) {
+              assert staticGet.inValues().isEmpty();
+              staticGet.getBlock().removeInstruction(staticGet);
             }
           }
         }
@@ -451,156 +413,6 @@
     }
   }
 
-
-  /**
-   * Extracts the mapping from ordinal values to switch case constants.
-   * <p>
-   * This is done by pattern-matching on the class initializer of the synthetic switch map class.
-   * For a switch
-   *
-   * <blockquote><pre>
-   * switch (day) {
-   *   case WEDNESDAY:
-   *   case FRIDAY:
-   *     System.out.println("3 or 5");
-   *     break;
-   *   case SUNDAY:
-   *     System.out.println("7");
-   *     break;
-   *   default:
-   *     System.out.println("other");
-   * }
-   * </pre></blockquote>
-   *
-   * the generated companing class initializer will have the form
-   *
-   * <blockquote><pre>
-   * class Switches$1 {
-   *   static {
-   *   $SwitchMap$switchmaps$Days[Days.WEDNESDAY.ordinal()] = 1;
-   *   $SwitchMap$switchmaps$Days[Days.FRIDAY.ordinal()] = 2;
-   *   $SwitchMap$switchmaps$Days[Days.SUNDAY.ordinal()] = 3;
-   * }
-   * </pre></blockquote>
-   *
-   * Note that one map per class is generated, so the map might contain additional entries as used
-   * by other switches in the class.
-   */
-  private Int2ReferenceMap<DexField> extractIndexMapFrom(DexField field) {
-    DexClass clazz = appInfo.definitionFor(field.getHolder());
-    if (!clazz.accessFlags.isSynthetic()) {
-      return null;
-    }
-    DexEncodedMethod initializer = clazz.getClassInitializer();
-    if (initializer == null || initializer.getCode() == null) {
-      return null;
-    }
-    IRCode code = initializer.getCode().buildIR(initializer, new InternalOptions());
-    Int2ReferenceMap<DexField> switchMap = new Int2ReferenceArrayMap<>();
-    for (BasicBlock block : code.blocks) {
-      InstructionListIterator it = block.listIterator();
-      Instruction insn = it.nextUntil(i -> i.isStaticGet() && i.asStaticGet().getField() == field);
-      if (insn == null) {
-        continue;
-      }
-      for (Instruction use : insn.outValue().uniqueUsers()) {
-        if (use.isArrayPut()) {
-          Instruction index = use.asArrayPut().source().definition;
-          if (index == null || !index.isConstNumber()) {
-            return null;
-          }
-          int integerIndex = index.asConstNumber().getIntValue();
-          Instruction value = use.asArrayPut().index().definition;
-          if (value == null || !value.isInvokeVirtual()) {
-            return null;
-          }
-          InvokeVirtual invoke = value.asInvokeVirtual();
-          DexClass holder = appInfo.definitionFor(invoke.getInvokedMethod().holder);
-          if (holder == null ||
-              (!holder.accessFlags.isEnum() && holder.type != dexItemFactory.enumType)) {
-            return null;
-          }
-          Instruction enumGet = invoke.arguments().get(0).definition;
-          if (enumGet == null || !enumGet.isStaticGet()) {
-            return null;
-          }
-          DexField enumField = enumGet.asStaticGet().getField();
-          if (!appInfo.definitionFor(enumField.getHolder()).accessFlags.isEnum()) {
-            return null;
-          }
-          if (switchMap.put(integerIndex, enumField) != null) {
-            return null;
-          }
-        } else {
-          return null;
-        }
-      }
-    }
-    return switchMap;
-  }
-
-  /**
-   * Extracts the ordinal values for an Enum class from the classes static initializer.
-   * <p>
-   * An Enum class has a field for each value. In the class initializer, each field is initialized
-   * to a singleton object that represents the value. This code matches on the corresponding call
-   * to the constructor (instance initializer) and extracts the value of the second argument, which
-   * is the ordinal.
-   */
-  private Reference2IntMap<DexField> extractOrdinalsMapFor(DexType enumClass) {
-    DexClass clazz = appInfo.definitionFor(enumClass);
-    if (clazz == null || clazz.isLibraryClass()) {
-      // We have to keep binary compatibility in tact for libraries.
-      return null;
-    }
-    DexEncodedMethod initializer = clazz.getClassInitializer();
-    if (!clazz.accessFlags.isEnum() || initializer == null || initializer.getCode() == null) {
-      return null;
-    }
-    if (initializer.getCode().isDexCode()) {
-      // If the initializer have been optimized we cannot extract the ordinals map reliably.
-      return null;
-    }
-    IRCode code = initializer.getCode().buildIR(initializer, new InternalOptions());
-    Reference2IntMap<DexField> ordinalsMap = new Reference2IntArrayMap<>();
-    ordinalsMap.defaultReturnValue(-1);
-    InstructionIterator it = code.instructionIterator();
-    while (it.hasNext()) {
-      Instruction insn = it.next();
-      if (!insn.isStaticPut()) {
-        continue;
-      }
-      StaticPut staticPut = insn.asStaticPut();
-      if (staticPut.getField().type != enumClass) {
-        continue;
-      }
-      Instruction newInstance = staticPut.inValue().definition;
-      if (newInstance == null || !newInstance.isNewInstance()) {
-        continue;
-      }
-      Instruction ordinal = null;
-      for (Instruction ctorCall : newInstance.outValue().uniqueUsers()) {
-        if (!ctorCall.isInvokeDirect()) {
-          continue;
-        }
-        InvokeDirect invoke = ctorCall.asInvokeDirect();
-        if (!dexItemFactory.isConstructor(invoke.getInvokedMethod())
-            || invoke.arguments().size() < 3) {
-          continue;
-        }
-        ordinal = invoke.arguments().get(2).definition;
-        break;
-      }
-      if (ordinal == null || !ordinal.isConstNumber()) {
-        return null;
-      }
-      if (ordinalsMap.put(staticPut.getField(), ordinal.asConstNumber().getIntValue()) != -1) {
-        return null;
-      }
-    }
-    return ordinalsMap;
-  }
-
   /**
    * Rewrite all branch targets to the destination of trivial goto chains when possible.
    * Does not rewrite fallthrough targets as that would require block reordering and the
@@ -1287,7 +1099,7 @@
           }
         }
         assert theIf == block.exit();
-        replaceLastInstruction(block, new Goto());
+        block.replaceLastInstruction(new Goto());
         assert block.exit().isGoto();
         assert block.exit().asGoto().getTarget() == target;
       }
@@ -1310,7 +1122,7 @@
         int left = leftValue.getConstInstruction().asConstNumber().getIntValue();
         if (left == 0) {
           If ifz = new If(theIf.getType().forSwappedOperands(), rightValue);
-          replaceLastInstruction(block, ifz);
+          block.replaceLastInstruction(ifz);
           assert block.exit() == ifz;
         }
       } else {
@@ -1318,19 +1130,13 @@
         int right = rightValue.getConstInstruction().asConstNumber().getIntValue();
         if (right == 0) {
           If ifz = new If(theIf.getType(), leftValue);
-          replaceLastInstruction(block, ifz);
+          block.replaceLastInstruction(ifz);
           assert block.exit() == ifz;
         }
       }
     }
   }
 
-  private void replaceLastInstruction(BasicBlock block, Instruction instruction) {
-    InstructionListIterator iterator = block.listIterator(block.getInstructions().size());
-    iterator.previous();
-    iterator.replaceCurrentInstruction(instruction);
-  }
-
   public void rewriteLongCompareAndRequireNonNull(IRCode code, InternalOptions options) {
     if (options.canUseLongCompareAndObjectsNonNull()) {
       return;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/EnumOrdinalMapCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/EnumOrdinalMapCollector.java
new file mode 100644
index 0000000..b2ebecc
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/EnumOrdinalMapCollector.java
@@ -0,0 +1,102 @@
+// Copyright (c) 2017, 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;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionIterator;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.ir.code.StaticPut;
+import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
+import com.android.tools.r8.utils.InternalOptions;
+import it.unimi.dsi.fastutil.objects.Reference2IntArrayMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+/**
+ * Extracts the ordinal values for all Enum classes from their static initializer.
+ * <p>
+ * An Enum class has a field for each value. In the class initializer, each field is initialized
+ * to a singleton object that represents the value. This code matches on the corresponding call
+ * to the constructor (instance initializer) and extracts the value of the second argument, which
+ * is the ordinal.
+ */
+public class EnumOrdinalMapCollector {
+
+  private final AppInfoWithLiveness appInfo;
+  private final InternalOptions options;
+
+  private final Map<DexType, Reference2IntMap<DexField>> ordinalsMaps = new IdentityHashMap<>();
+
+  public EnumOrdinalMapCollector(AppInfoWithLiveness appInfo, InternalOptions options) {
+    this.appInfo = appInfo;
+    this.options = options;
+  }
+
+  public static Reference2IntMap<DexField> getOrdinalsMapFor(DexType enumClass,
+      AppInfoWithLiveness appInfo) {
+    Map<DexType, Reference2IntMap<DexField>> ordinalsMaps = appInfo
+        .getExtension(EnumOrdinalMapCollector.class, Collections.emptyMap());
+    return ordinalsMaps.get(enumClass);
+  }
+
+  public void run() {
+    appInfo.classes().forEach(this::processClasses);
+    if (!ordinalsMaps.isEmpty()) {
+      appInfo.setExtension(EnumOrdinalMapCollector.class, ordinalsMaps);
+    }
+  }
+
+  private void processClasses(DexProgramClass clazz) {
+    // Enum classes are flagged as such. Also, for library classes, the ordinals are not known.
+    if (!clazz.accessFlags.isEnum() || clazz.isLibraryClass() || !clazz.hasClassInitializer()) {
+      return;
+    }
+    DexEncodedMethod initializer = clazz.getClassInitializer();
+    IRCode code = initializer.getCode().buildIR(initializer, options);
+    Reference2IntMap<DexField> ordinalsMap = new Reference2IntArrayMap<>();
+    ordinalsMap.defaultReturnValue(-1);
+    InstructionIterator it = code.instructionIterator();
+    while (it.hasNext()) {
+      Instruction insn = it.next();
+      if (!insn.isStaticPut()) {
+        continue;
+      }
+      StaticPut staticPut = insn.asStaticPut();
+      if (staticPut.getField().type != clazz.type) {
+        continue;
+      }
+      Instruction newInstance = staticPut.inValue().definition;
+      if (newInstance == null || !newInstance.isNewInstance()) {
+        continue;
+      }
+      Instruction ordinal = null;
+      for (Instruction ctorCall : newInstance.outValue().uniqueUsers()) {
+        if (!ctorCall.isInvokeDirect()) {
+          continue;
+        }
+        InvokeDirect invoke = ctorCall.asInvokeDirect();
+        if (!appInfo.dexItemFactory.isConstructor(invoke.getInvokedMethod())
+            || invoke.arguments().size() < 3) {
+          continue;
+        }
+        ordinal = invoke.arguments().get(2).definition;
+        break;
+      }
+      if (ordinal == null || !ordinal.isConstNumber()) {
+        return;
+      }
+      if (ordinalsMap.put(staticPut.getField(), ordinal.asConstNumber().getIntValue()) != -1) {
+        return;
+      }
+    }
+    ordinalsMaps.put(clazz.type, ordinalsMap);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/SwitchMapCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/SwitchMapCollector.java
new file mode 100644
index 0000000..44f0759
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/SwitchMapCollector.java
@@ -0,0 +1,155 @@
+// Copyright (c) 2017, 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;
+
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionIterator;
+import com.android.tools.r8.ir.code.InvokeVirtual;
+import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
+import com.android.tools.r8.utils.InternalOptions;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceArrayMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * Extracts the mapping from ordinal values to switch case constants.
+ * <p>
+ * This is done by pattern-matching on the class initializer of the synthetic switch map class.
+ * For a switch
+ *
+ * <blockquote><pre>
+ * switch (day) {
+ *   case WEDNESDAY:
+ *   case FRIDAY:
+ *     System.out.println("3 or 5");
+ *     break;
+ *   case SUNDAY:
+ *     System.out.println("7");
+ *     break;
+ *   default:
+ *     System.out.println("other");
+ * }
+ * </pre></blockquote>
+ *
+ * the generated companing class initializer will have the form
+ *
+ * <blockquote><pre>
+ * class Switches$1 {
+ *   static {
+ *   $SwitchMap$switchmaps$Days[Days.WEDNESDAY.ordinal()] = 1;
+ *   $SwitchMap$switchmaps$Days[Days.FRIDAY.ordinal()] = 2;
+ *   $SwitchMap$switchmaps$Days[Days.SUNDAY.ordinal()] = 3;
+ * }
+ * </pre></blockquote>
+ *
+ * Note that one map per class is generated, so the map might contain additional entries as used
+ * by other switches in the class.
+ */
+public class SwitchMapCollector {
+
+  private final AppInfoWithLiveness appInfo;
+  private final InternalOptions options;
+  private final DexString switchMapPrefix;
+  private final DexType intArrayType;
+
+  private final Map<DexField, Int2ReferenceMap<DexField>> switchMaps = new IdentityHashMap<>();
+
+  public SwitchMapCollector(AppInfoWithLiveness appInfo, InternalOptions options) {
+    this.appInfo = appInfo;
+    this.options = options;
+    switchMapPrefix = appInfo.dexItemFactory.createString("$SwitchMap$");
+    intArrayType = appInfo.dexItemFactory.createType("[I");
+  }
+
+  public void run() {
+    appInfo.classes().forEach(this::processClasses);
+    if (!switchMaps.isEmpty()) {
+      appInfo.setExtension(SwitchMapCollector.class, switchMaps);
+    }
+  }
+
+  public static Int2ReferenceMap<DexField> getSwitchMapFor(DexField field,
+      AppInfoWithLiveness appInfo) {
+    Map<DexField, Int2ReferenceMap<DexField>> switchMaps = appInfo
+        .getExtension(SwitchMapCollector.class, Collections.emptyMap());
+    return switchMaps.get(field);
+  }
+
+  private void processClasses(DexProgramClass clazz) {
+    // Switchmap classes are synthetic and have a class initializer.
+    if (!clazz.accessFlags.isSynthetic() && !clazz.hasClassInitializer()) {
+      return;
+    }
+    List<DexEncodedField> switchMapFields = Arrays.stream(clazz.staticFields())
+        .filter(this::maybeIsSwitchMap).collect(Collectors.toList());
+    if (!switchMapFields.isEmpty()) {
+      IRCode initializer = clazz.getClassInitializer().buildIR(options);
+      switchMapFields.forEach(field -> extractSwitchMap(field, initializer));
+    }
+  }
+
+  private void extractSwitchMap(DexEncodedField encodedField, IRCode initializer) {
+    DexField field = encodedField.field;
+    Int2ReferenceMap<DexField> switchMap = new Int2ReferenceArrayMap<>();
+    InstructionIterator it = initializer.instructionIterator();
+    Instruction insn;
+    Predicate<Instruction> predicate = i -> i.isStaticGet() && i.asStaticGet().getField() == field;
+    while ((insn = it.nextUntil(predicate)) != null) {
+      for (Instruction use : insn.outValue().uniqueUsers()) {
+        if (use.isArrayPut()) {
+          Instruction index = use.asArrayPut().source().definition;
+          if (index == null || !index.isConstNumber()) {
+            return;
+          }
+          int integerIndex = index.asConstNumber().getIntValue();
+          Instruction value = use.asArrayPut().index().definition;
+          if (value == null || !value.isInvokeVirtual()) {
+            return;
+          }
+          InvokeVirtual invoke = value.asInvokeVirtual();
+          DexClass holder = appInfo.definitionFor(invoke.getInvokedMethod().holder);
+          if (holder == null ||
+              (!holder.accessFlags.isEnum() && holder.type != appInfo.dexItemFactory.enumType)) {
+            return;
+          }
+          Instruction enumGet = invoke.arguments().get(0).definition;
+          if (enumGet == null || !enumGet.isStaticGet()) {
+            return;
+          }
+          DexField enumField = enumGet.asStaticGet().getField();
+          if (!appInfo.definitionFor(enumField.getHolder()).accessFlags.isEnum()) {
+            return;
+          }
+          if (switchMap.put(integerIndex, enumField) != null) {
+            return;
+          }
+        } else {
+          return;
+        }
+      }
+    }
+    switchMaps.put(field, switchMap);
+  }
+
+  private boolean maybeIsSwitchMap(DexEncodedField dexEncodedField) {
+    // We are looking for synthetic fields of type int[].
+    DexField field = dexEncodedField.field;
+    return dexEncodedField.accessFlags.isSynthetic()
+        && field.name.beginsWith(switchMapPrefix)
+        && field.type == intArrayType;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/SwitchUtils.java b/src/main/java/com/android/tools/r8/ir/optimize/SwitchUtils.java
new file mode 100644
index 0000000..0c33561
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/SwitchUtils.java
@@ -0,0 +1,102 @@
+// Copyright (c) 2017, 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;
+
+import com.android.tools.r8.graph.DexClass;
+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.DexType;
+import com.android.tools.r8.ir.code.ArrayGet;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeVirtual;
+import com.android.tools.r8.ir.code.StaticGet;
+import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+
+public class SwitchUtils {
+
+  public static final class EnumSwitchInfo {
+    public final DexType enumClass;
+    public final Instruction ordinalInvoke;
+    public final Instruction arrayGet;
+    public final Instruction staticGet;
+    public final Int2ReferenceMap<DexField> indexMap;
+    public final Reference2IntMap ordinalsMap;
+
+    private EnumSwitchInfo(DexType enumClass,
+        Instruction ordinalInvoke,
+        Instruction arrayGet, Instruction staticGet,
+        Int2ReferenceMap<DexField> indexMap,
+        Reference2IntMap ordinalsMap) {
+      this.enumClass = enumClass;
+      this.ordinalInvoke = ordinalInvoke;
+      this.arrayGet = arrayGet;
+      this.staticGet = staticGet;
+      this.indexMap = indexMap;
+      this.ordinalsMap = ordinalsMap;
+    }
+  }
+
+  /**
+   * Looks for a switch statement over the enum companion class of the form
+   *
+   * <blockquote><pre>
+   * switch(CompanionClass.$switchmap$field[enumValue.ordinal()]) {
+   *   ...
+   * }
+   * </pre></blockquote>
+   *
+   * and extracts the components and the index and ordinal maps. See
+   * {@link EnumOrdinalMapCollector} and
+   * {@link SwitchMapCollector} for details.
+   */
+  public static EnumSwitchInfo analyzeSwitchOverEnum(Instruction switchInsn,
+      AppInfoWithLiveness appInfo) {
+    Instruction input = switchInsn.inValues().get(0).definition;
+    if (input == null || !input.isArrayGet()) {
+      return null;
+    }
+    ArrayGet arrayGet = input.asArrayGet();
+    Instruction index = arrayGet.index().definition;
+    if (index == null || !index.isInvokeVirtual()) {
+      return null;
+    }
+    InvokeVirtual ordinalInvoke = index.asInvokeVirtual();
+    DexMethod ordinalMethod = ordinalInvoke.getInvokedMethod();
+    DexClass enumClass = appInfo.definitionFor(ordinalMethod.holder);
+    DexItemFactory dexItemFactory = appInfo.dexItemFactory;
+    // After member rebinding, enumClass will be the actual java.lang.Enum class.
+    if (enumClass == null
+        || (!enumClass.accessFlags.isEnum() && enumClass.type != dexItemFactory.enumType)
+        || ordinalMethod.name != dexItemFactory.ordinalMethodName
+        || ordinalMethod.proto.returnType != dexItemFactory.intType
+        || !ordinalMethod.proto.parameters.isEmpty()) {
+      return null;
+    }
+    Instruction array = arrayGet.array().definition;
+    if (array == null || !array.isStaticGet()) {
+      return null;
+    }
+    StaticGet staticGet = array.asStaticGet();
+    Int2ReferenceMap<DexField> indexMap
+        = SwitchMapCollector.getSwitchMapFor(staticGet.getField(), appInfo);
+    if (indexMap == null || indexMap.isEmpty()) {
+      return null;
+    }
+    // Due to member rebinding, only the fields are certain to provide the actual enums
+    // class.
+    DexType enumTyoe = indexMap.values().iterator().next().getHolder();
+    Reference2IntMap ordinalsMap
+        = EnumOrdinalMapCollector.getOrdinalsMapFor(enumTyoe, appInfo);
+    if (ordinalsMap == null) {
+      return null;
+    }
+    return new EnumSwitchInfo(enumTyoe, ordinalInvoke, arrayGet, staticGet, indexMap,
+        ordinalsMap);
+  }
+
+
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index b8fa85a..d25c4bc 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -37,12 +37,14 @@
 import com.google.common.collect.Sets;
 import com.google.common.collect.Sets.SetView;
 import java.util.ArrayDeque;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Deque;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Queue;
@@ -75,6 +77,9 @@
   private Map<DexType, Set<DexField>> staticFieldsRead = Maps.newIdentityHashMap();
   private Map<DexType, Set<DexField>> staticFieldsWritten = Maps.newIdentityHashMap();
 
+  private final List<SemanticsProvider> extensions = new ArrayList<>();
+  private final Map<Class, Object> extensionsState = new HashMap<>();
+
   /**
    * This map keeps a view of all virtual methods that are reachable from virtual invokes. A method
    * is reachable even if no live subtypes exist, so this is not sufficient for inclusion in the
@@ -152,6 +157,10 @@
     this.appInfo = appInfo;
   }
 
+  public void addExtension(SemanticsProvider extension) {
+    extensions.add(extension);
+  }
+
   private void enqueueRootItems(Map<DexItem, ProguardKeepRule> items) {
     workList.addAll(
         items.entrySet().stream().map(Action::forRootItem).collect(Collectors.toList()));
@@ -852,7 +861,21 @@
       for (DexAnnotationSet parameterAnnotation : method.parameterAnnotations.values) {
         processAnnotations(parameterAnnotation.annotations);
       }
-      method.registerReachableDefinitions(new UseRegistry(method));
+      boolean processed = false;
+      if (!extensions.isEmpty()) {
+        for (SemanticsProvider extension : extensions) {
+          if (extension.appliesTo(method)) {
+            assert extensions.stream().filter(e -> e.appliesTo(method)).count() == 1;
+            extensionsState.put(extension.getClass(),
+                extension.processMethod(method, new UseRegistry(method),
+                    extensionsState.get(extension.getClass())));
+            processed = true;
+          }
+        }
+      }
+      if (!processed) {
+        method.registerReachableDefinitions(new UseRegistry(method));
+      }
       // Add all dependent members to the workqueue.
       enqueueRootItems(rootSet.getDependentItems(method));
     }
@@ -1007,7 +1030,7 @@
      * Set of fields that belong to live classes and can be reached by invokes. These need to be
      * kept.
      */
-    final Set<DexField> liveFields;
+    public final Set<DexField> liveFields;
     /**
      * Set of all fields which may be touched by a get operation. This is actual field definitions.
      */
@@ -1060,6 +1083,10 @@
      * All items with assumevalues rule.
      */
     public final Map<DexItem, ProguardMemberRule> assumedValues;
+    /**
+     * Map from the class of an extension to the state it produced.
+     */
+    public final Map<Class, Object> extensions;
 
     private AppInfoWithLiveness(AppInfoWithSubtyping appInfo, Enqueuer enqueuer) {
       super(appInfo);
@@ -1081,6 +1108,7 @@
       this.staticInvokes = joinInvokedMethods(enqueuer.staticInvokes);
       this.noSideEffects = enqueuer.rootSet.noSideEffects;
       this.assumedValues = enqueuer.rootSet.assumedValues;
+      this.extensions = enqueuer.extensionsState;
       assert Sets.intersection(instanceFieldReads, staticFieldReads).size() == 0;
       assert Sets.intersection(instanceFieldWrites, staticFieldWrites).size() == 0;
     }
@@ -1106,6 +1134,7 @@
       this.superInvokes = previous.superInvokes;
       this.directInvokes = previous.directInvokes;
       this.staticInvokes = previous.staticInvokes;
+      this.extensions = previous.extensions;
       assert Sets.intersection(instanceFieldReads, staticFieldReads).size() == 0;
       assert Sets.intersection(instanceFieldWrites, staticFieldWrites).size() == 0;
     }
@@ -1131,6 +1160,7 @@
       this.superInvokes = rewriteItems(previous.superInvokes, lense::lookupMethod);
       this.directInvokes = rewriteItems(previous.directInvokes, lense::lookupMethod);
       this.staticInvokes = rewriteItems(previous.staticInvokes, lense::lookupMethod);
+      this.extensions = previous.extensions;
       assert Sets.intersection(instanceFieldReads, staticFieldReads).size() == 0;
       assert Sets.intersection(instanceFieldWrites, staticFieldWrites).size() == 0;
     }
@@ -1159,6 +1189,20 @@
       return builder.build();
     }
 
+    @SuppressWarnings("unchecked")
+    public <T> T getExtension(Class extension, T defaultValue) {
+      if (extensions.containsKey(extension)) {
+        return (T) extensions.get(extension);
+      } else {
+        return defaultValue;
+      }
+    }
+
+    public <T> void setExtension(Class extension, T value) {
+      assert !extensions.containsKey(extension);
+      extensions.put(extension, value);
+    }
+
     @Override
     public boolean hasLiveness() {
       return true;
@@ -1299,4 +1343,12 @@
       return false;
     }
   }
+
+  public interface SemanticsProvider {
+
+    boolean appliesTo(DexEncodedMethod method);
+
+    Object processMethod(DexEncodedMethod method,
+        com.android.tools.r8.graph.UseRegistry useRegistry, Object state);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/shaking/protolite/ProtoLiteBase.java b/src/main/java/com/android/tools/r8/shaking/protolite/ProtoLiteBase.java
new file mode 100644
index 0000000..c2264f2
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/protolite/ProtoLiteBase.java
@@ -0,0 +1,98 @@
+// Copyright (c) 2017, 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.shaking.protolite;
+
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.DexClass;
+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.DexString;
+import com.android.tools.r8.graph.DexType;
+
+/**
+ * Contains common definitions used by the {@link ProtoLiteExtension} for tree shaking and the
+ * corresponding {@link ProtoLitePruner} code rewriting.
+ */
+abstract class ProtoLiteBase {
+
+  static final int GETTER_NAME_PREFIX_LENGTH = 3;
+  static final int COUNT_POSTFIX_LENGTH = 5;
+
+  final AppInfoWithSubtyping appInfo;
+
+  final DexType messageType;
+  final DexString dynamicMethodName;
+  final DexString writeToMethodName;
+  final DexString getSerializedSizeMethodName;
+  final DexString constructorMethodName;
+  final DexString setterNamePrefix;
+  final DexString getterNamePrefix;
+  final DexString bitFieldPrefix;
+  final DexString underscore;
+
+  ProtoLiteBase(AppInfoWithSubtyping appInfo) {
+    this.appInfo = appInfo;
+    DexItemFactory factory = appInfo.dexItemFactory;
+    this.messageType = factory.createType("Lcom/google/protobuf/GeneratedMessageLite;");
+    this.dynamicMethodName = factory.createString("dynamicMethod");
+    this.writeToMethodName = factory.createString("writeTo");
+    this.getSerializedSizeMethodName = factory.createString("getSerializedSize");
+    this.constructorMethodName = factory.constructorMethodName;
+    this.setterNamePrefix = factory.createString("set");
+    this.getterNamePrefix = factory.createString("get");
+    this.bitFieldPrefix = factory.createString("bitField");
+    this.underscore = factory.createString("_");
+    assert getterNamePrefix.size == GETTER_NAME_PREFIX_LENGTH;
+  }
+
+  /**
+   * Returns true of the given method is a setter on a message class that does need processing
+   * by this phase of proto lite shaking.
+   * <p>
+   * False positives are ok.
+   */
+  abstract boolean isSetterThatNeedsProcessing(DexEncodedMethod method);
+
+  DexField getterToField(DexMethod getter) {
+    return getterToField(getter, 0);
+  }
+
+  DexField getterToField(DexMethod getter, int postfixLength) {
+    String getterName = getter.name.toString();
+    assert getterName.length() > GETTER_NAME_PREFIX_LENGTH + postfixLength;
+    String fieldName = Character.toLowerCase(getterName.charAt(GETTER_NAME_PREFIX_LENGTH))
+        + getterName.substring(GETTER_NAME_PREFIX_LENGTH + 1, getterName.length() - postfixLength)
+        + "_";
+    DexItemFactory factory = appInfo.dexItemFactory;
+    return factory
+        .createField(getter.holder, getter.proto.returnType, factory.createString(fieldName));
+  }
+
+  boolean hasSingleIntArgument(DexMethod method) {
+    return method.getArity() == 1
+        && method.proto.parameters.values[0] == appInfo.dexItemFactory.intType;
+  }
+
+  public boolean appliesTo(DexEncodedMethod method) {
+    if (!method.method.holder.isSubtypeOf(messageType, appInfo)) {
+      return false;
+    }
+    DexClass clazz = appInfo.definitionFor(method.method.holder);
+    // We only care for the actual leaf classes that implement a specific proto.
+    if (!clazz.accessFlags.isFinal()) {
+      return false;
+    }
+    DexString methodName = method.method.name;
+    // We could be even more precise here and check for the signature. However, there is only
+    // one of each method generated.
+    return methodName == dynamicMethodName
+        || methodName == writeToMethodName
+        || methodName == getSerializedSizeMethodName
+        || methodName == constructorMethodName
+        || isSetterThatNeedsProcessing(method);
+  }
+
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/protolite/ProtoLiteExtension.java b/src/main/java/com/android/tools/r8/shaking/protolite/ProtoLiteExtension.java
new file mode 100644
index 0000000..b217194
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/protolite/ProtoLiteExtension.java
@@ -0,0 +1,208 @@
+// Copyright (c) 2017, 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.shaking.protolite;
+
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.DelegatingUseRegistry;
+import com.android.tools.r8.graph.DexClass;
+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.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.shaking.Enqueuer.SemanticsProvider;
+import com.android.tools.r8.utils.MethodJavaSignatureEquivalence;
+import com.google.common.base.Equivalence;
+import com.google.common.base.Equivalence.Wrapper;
+import com.google.common.collect.Sets;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Extends the tree shaker with special treatment for ProtoLite generated classes.
+ * <p>
+ * The goal is to remove unused fields from proto classes. To achieve this, we filter the
+ * dependency processing of certain methods of ProtoLite classes. Read/write references to
+ * fields or corresponding getters are ignored. If the fields or getters are not used otherwise
+ * in the program, they will be pruned even though they are referenced from the processed
+ * ProtoLite class.
+ * <p>
+ * The companion code rewriter in {@link ProtoLitePruner} then fixes up the code and removes
+ * references to these dead fields and getters in a semantics preserving way. For proto2, the
+ * fields are turned into unknown fields and hence are preserved over the wire. For proto3, the
+ * fields are removed and, as with unknown fields in proto3, their data is lost on retransmission.
+ * <p>
+ * We have to process the following three methods specially:
+ * <dl>
+ *   <dt>dynamicMethod</dt>
+ *   <dd>implements most proto operations, like merging, comparing, etc</dd>
+ *   <dt>writeTo</dt>
+ *   <dd>performs the actual write operations</dd>
+ *   <dt>getSerializedSize</dt>
+ *   <dd>implements computing the serialized size of a proto, very similar to writeTo</dd>
+ * </dl>
+ * As they access all fields of a proto, regardless of whether they are used, we have to mask
+ * their accesses to ensure actually dead fields are not made live.
+ * <p>
+ * We also have to treat setters specially. While their code does not need to be rewritten, we
+ * need to ensure that the fields they write are actually kept and marked as live. We achieve
+ * this by also marking them read.
+ */
+public class ProtoLiteExtension extends ProtoLiteBase implements SemanticsProvider {
+
+  private final Equivalence<DexMethod> equivalence = MethodJavaSignatureEquivalence.get();
+  /**
+   * Set of all methods directly defined on the GeneratedMessageLite class. Used to filter
+   * getters from other methods that begin with get.
+   */
+  private final Set<Wrapper<DexMethod>> methodsOnMessageType;
+
+  private final DexString caseGetterSuffix;
+  private final DexString caseFieldSuffix;
+
+  public ProtoLiteExtension(AppInfoWithSubtyping appInfo) {
+    super(appInfo);
+    DexItemFactory factory = appInfo.dexItemFactory;
+    this.methodsOnMessageType = computeMethodsOnMessageType();
+    this.caseGetterSuffix = factory.createString("Case");
+    this.caseFieldSuffix = factory.createString("Case_");
+  }
+
+  private Set<Wrapper<DexMethod>> computeMethodsOnMessageType() {
+    DexClass messageClass = appInfo.definitionFor(messageType);
+    if (messageClass == null) {
+      return Collections.emptySet();
+    }
+    Set<Wrapper<DexMethod>> superMethods = new HashSet<>();
+    messageClass.forEachMethod(method -> superMethods.add(equivalence.wrap(method.method)));
+    return superMethods;
+  }
+
+  boolean isSetterThatNeedsProcessing(DexEncodedMethod method) {
+    return method.accessFlags.isPrivate()
+        && method.method.name.beginsWith(setterNamePrefix)
+        && !methodsOnMessageType.contains(equivalence.wrap(method.method));
+  }
+
+  private boolean isGetter(DexMethod method, DexType instanceType) {
+    return method.holder == instanceType
+        && (method.proto.parameters.isEmpty() || hasSingleIntArgument(method))
+        && method.name.beginsWith(getterNamePrefix)
+        && !method.name.endsWith(caseGetterSuffix)
+        && !methodsOnMessageType.contains(equivalence.wrap(method));
+  }
+
+  private boolean isProtoField(DexField field, DexType instanceType) {
+    if (field.getHolder() != instanceType) {
+      return false;
+    }
+    // All instance fields that end with _ are proto fields. For proto2, there are also the
+    // bitField<n>_ fields that are used to store presence information. We process those normally.
+    // Likewise, the XXXCase_ fields for oneOfs.
+    DexString name = field.name;
+    return name.endsWith(underscore)
+        && !name.beginsWith(bitFieldPrefix)
+        && !name.endsWith(caseFieldSuffix);
+  }
+
+  @Override
+  @SuppressWarnings("unchecked")
+  public Object processMethod(DexEncodedMethod method, UseRegistry registry, Object state) {
+    return processMethod(method, registry, (Set<DexField>) state);
+  }
+
+  private Set<DexField> processMethod(DexEncodedMethod method, UseRegistry registry,
+      Set<DexField> state) {
+    if (state == null) {
+      state = Sets.newIdentityHashSet();
+    }
+    if (isSetterThatNeedsProcessing(method)) {
+      // If a field is accessed by a live setter, the field is live as it has to be written to the
+      // serialized stream for this proto. As we mask all reads in the writing code and normally
+      // remove fields that are only written but never read, we have to mark fields used in setters
+      // as read and written.
+      method.registerReachableDefinitions(
+          new FieldWriteImpliesReadUseRegistry(registry, method.method.holder));
+    } else {
+      // Filter all getters and field accesses in these methods. We do not want fields to become
+      // live just due to being referenced in a special method. The pruning phase will remove
+      // all references to dead fields in the code later.
+      method.registerReachableDefinitions(new FilteringUseRegistry(registry, method.method.holder,
+          state));
+    }
+    return state;
+  }
+
+  private class FieldWriteImpliesReadUseRegistry extends DelegatingUseRegistry {
+
+    private final DexType instanceType;
+
+    FieldWriteImpliesReadUseRegistry(UseRegistry delegate,
+        DexType instanceType) {
+      super(delegate);
+      this.instanceType = instanceType;
+    }
+
+    @Override
+    public boolean registerInstanceFieldWrite(DexField field) {
+      if (isProtoField(field, instanceType)) {
+        super.registerInstanceFieldRead(field);
+      }
+      return super.registerInstanceFieldWrite(field);
+    }
+  }
+
+  private class FilteringUseRegistry extends DelegatingUseRegistry {
+
+    private final DexType instanceType;
+    private final Set<DexField> registerField;
+
+    private FilteringUseRegistry(UseRegistry delegate,
+        DexType instanceType, Set<DexField> registerField) {
+      super(delegate);
+      this.instanceType = instanceType;
+      this.registerField = registerField;
+    }
+
+    @Override
+    public boolean registerInstanceFieldWrite(DexField field) {
+      if (isProtoField(field, instanceType)) {
+        registerField.add(field);
+        return false;
+      }
+      return super.registerInstanceFieldWrite(field);
+    }
+
+    @Override
+    public boolean registerInstanceFieldRead(DexField field) {
+      if (isProtoField(field, instanceType)) {
+        registerField.add(field);
+        return false;
+      }
+      return super.registerInstanceFieldRead(field);
+    }
+
+    @Override
+    public boolean registerInvokeVirtual(DexMethod method) {
+      if (isGetter(method, instanceType)) {
+        // Try whether this is a getXXX method.
+        DexField field = getterToField(method);
+        if (isProtoField(field, instanceType)) {
+          registerField.add(field);
+          return false;
+        }
+        // Try whether this is a getXXXCount method.
+        field = getterToField(method, COUNT_POSTFIX_LENGTH);
+        if (isProtoField(field, instanceType)) {
+          registerField.add(field);
+          return false;
+        }
+      }
+      return super.registerInvokeVirtual(method);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/protolite/ProtoLitePruner.java b/src/main/java/com/android/tools/r8/shaking/protolite/ProtoLitePruner.java
new file mode 100644
index 0000000..8b0ac9e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/protolite/ProtoLitePruner.java
@@ -0,0 +1,739 @@
+// Copyright (c) 2017, 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.shaking.protolite;
+
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.errors.Unreachable;
+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.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.DominatorTree;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstanceGet;
+import com.android.tools.r8.ir.code.InstancePut;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionIterator;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeInterface;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
+import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.code.MemberType;
+import com.android.tools.r8.ir.code.MoveType;
+import com.android.tools.r8.ir.code.Switch;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.SwitchUtils;
+import com.android.tools.r8.ir.optimize.SwitchUtils.EnumSwitchInfo;
+import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.function.BiPredicate;
+
+/**
+ * Implements the second phase of proto lite tree shaking.
+ * <p>
+ * In the pruner pass, we remove all references to the now dead fields from the methods that
+ * were treated specially during tree shaking using the {@link ProtoLiteExtension}.
+ * <p>
+ * For proto2 code, we aim to keep presence information for fields alive and move the values of
+ * dead fields to the unknown fields storage. For proto3, as it has no concept of passing on
+ * unknown fields, dead fields are completely removed. In particular, reading a proto and writing
+ * it again might loose data.
+ */
+public class ProtoLitePruner extends ProtoLiteBase {
+
+  private final AppInfoWithLiveness appInfo;
+
+  private final DexType visitorType;
+
+  private final DexType methodEnumType;
+  private final DexType codedOutputStreamType;
+  private final DexType protobufListType;
+  private final DexType listType;
+
+  private final DexString visitTag;
+  private final DexString mergeTag;
+  private final DexString isInitializedTag;
+  private final DexString makeImmutabkeTag;
+  private final DexString computeMethodPrefix;
+  private final DexString writeMethodPrefix;
+  private final DexString isInitializedMethodName;
+
+  private final DexString makeImmutableMethodName;
+  private final DexString sizeMethodName;
+  private final Set<DexField> seenFields;
+  private final DexMethod sizeMethod;
+
+  public ProtoLitePruner(AppInfoWithLiveness appInfo) {
+    super(appInfo);
+    this.appInfo = appInfo;
+    DexItemFactory factory = appInfo.dexItemFactory;
+    this.visitorType = factory.createType("Lcom/google/protobuf/GeneratedMessageLite$Visitor;");
+    this.methodEnumType = factory
+        .createType("Lcom/google/protobuf/GeneratedMessageLite$MethodToInvoke;");
+    this.codedOutputStreamType = factory.createType("Lcom/google/protobuf/CodedOutputStream;");
+    this.protobufListType = factory.createType("Lcom/google/protobuf/Internal$ProtobufList;");
+    this.listType = factory.createType("Ljava/util/List;");
+    this.visitTag = factory.createString("VISIT");
+    this.mergeTag = factory.createString("MERGE_FROM_STREAM");
+    this.isInitializedTag = factory.createString("IS_INITIALIZED");
+    this.makeImmutabkeTag = factory.createString("MAKE_IMMUTABLE");
+    this.computeMethodPrefix = factory.createString("compute");
+    this.writeMethodPrefix = factory.createString("write");
+    this.sizeMethodName = factory.createString("size");
+    this.isInitializedMethodName = factory.createString("isInitialized");
+    this.makeImmutableMethodName = factory.createString("makeImmutable");
+    this.sizeMethod = factory.createMethod(listType,
+        factory.createProto(factory.intType), sizeMethodName);
+
+    seenFields = appInfo.withLiveness()
+        .getExtension(ProtoLiteExtension.class, Collections.emptySet());
+  }
+
+  private boolean isPresenceField(DexField field, DexType instanceType) {
+    if (field.getHolder() != instanceType) {
+      return false;
+    }
+    // Proto2 uses fields named bitField<n>_ fields to store presence information.
+    String fieldName = field.name.toString();
+    return fieldName.endsWith("_")
+        && fieldName.startsWith("bitField");
+  }
+
+  boolean isSetterThatNeedsProcessing(DexEncodedMethod method) {
+    // The pruner does not need to process setters, so this method always returns false.
+    return false;
+  }
+
+  private boolean isGetter(DexMethod method) {
+    return isGetterHelper(method, 0);
+  }
+
+  private boolean isCountGetter(DexMethod method) {
+    return isGetterHelper(method, COUNT_POSTFIX_LENGTH);
+  }
+
+  private boolean isGetterHelper(DexMethod method, int postFixLength) {
+    if (!method.name.beginsWith(getterNamePrefix)
+        || !(method.proto.parameters.isEmpty() || hasSingleIntArgument(method))
+        || (method.holder == messageType)
+        || !method.holder.isSubtypeOf(messageType, appInfo)
+        || method.name.size <= GETTER_NAME_PREFIX_LENGTH + postFixLength) {
+      return false;
+    }
+    DexField correspondingField = getterToField(method, postFixLength);
+    return seenFields.contains(correspondingField);
+  }
+
+  private boolean isDefinedAsNull(Value value) {
+    return value.definition != null && value.definition.isConstNumber()
+        && value.definition.asConstNumber().isZero();
+  }
+
+  private boolean isComputeSizeMethod(DexMethod invokedMethod) {
+    return invokedMethod.holder == codedOutputStreamType
+        && invokedMethod.name.beginsWith(computeMethodPrefix);
+  }
+
+  private boolean isWriteMethod(DexMethod invokedMethod) {
+    return invokedMethod.holder == codedOutputStreamType
+        && invokedMethod.name.beginsWith(writeMethodPrefix);
+  }
+
+  private boolean isProtoField(DexField field) {
+    return seenFields.contains(field);
+  }
+
+  public void rewriteProtoLiteSpecialMethod(IRCode code, DexEncodedMethod method) {
+    DexString methodName = method.method.name;
+    if (methodName == dynamicMethodName) {
+      rewriteDynamicMethod(code, method);
+    } else if ((methodName == writeToMethodName) || (methodName == getSerializedSizeMethodName)) {
+      rewriteSizeOrWriteMethod(code);
+    } else if (methodName == constructorMethodName) {
+      rewriteConstructor(code);
+    } else {
+      throw new Unreachable();
+    }
+  }
+
+  /**
+   * For protos with repeated fields, the constructor may contain field initialization code like
+   *
+   * <pre>
+   * private Repeated() {
+   *  repeated_ = com.google.protobuf.GeneratedMessageLite.emptyProtobufList();
+   *  other_ = emptyBooleanList();
+   *  sub_ = emptyProtobufList();
+   * }
+   * </pre>
+   *
+   * which this rewriting removes.
+   */
+  private void rewriteConstructor(IRCode code) {
+    boolean wasRewritten;
+    do {
+      wasRewritten = false;
+      InstructionIterator it = code.instructionIterator();
+      while (it.hasNext()) {
+        Instruction insn = it.next();
+        if (insn.isInstancePut() && isDeadProtoField(insn.asInstancePut().getField())) {
+          // Remove initializations of dead fields.
+          it.remove();
+          wasRewritten = true;
+        } else if (insn.isInvokeStatic()) {
+          // Remove now unneeded constructor calls.
+          InvokeStatic invokeStatic = insn.asInvokeStatic();
+          DexMethod invokedMethod = invokeStatic.getInvokedMethod();
+          if ((invokeStatic.outValue().numberOfAllUsers() == 0)
+              && invokedMethod.proto.returnType.isSubtypeOf(protobufListType, appInfo)) {
+            it.remove();
+          }
+        }
+      }
+    } while (wasRewritten);
+  }
+
+
+  /**
+   * The writeTo and getSerializedSize methods of a generated proto access all fields. We have to
+   * remove the accesses to dead fields. The actual code of these methods varies to quite some
+   * degree depending on the types of the fields. For example
+   *
+   * <pre>
+   * public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io.IOException {
+   *   if (((bitField0_ & 0x00000001) == 0x00000001)) {
+   *     output.writeInt32(1, id_);
+   *   }
+   *   if (((bitField0_ & 0x00000002) == 0x00000002)) {
+   *     output.writeMessage(2, getInner());
+   *   }
+   *   for (int i = 0; i < repeated_.size(); i++) {
+   *     output.writeString(3, repeated_.get(i));
+   *   }
+   *   for (int i = 0; i < other_.size(); i++) {
+   *     output.writeBool(4, other_.getBoolean(i));
+   *   }
+   *   for (int i = 0; i < sub_.size(); i++) {
+   *     output.writeMessage(5, sub_.get(i));
+   *   }
+   *   unknownFields.writeTo(output);
+   * }
+   * </pre>
+   *
+   * We look for direct field accesses (id_, repeated_, etc. above) and getters (getInner) and
+   * rewrite those to null/0. We also rewrite all uses of the results, like the size() and
+   * write methods above.
+   */
+  private void rewriteSizeOrWriteMethod(IRCode code) {
+    boolean wasRewritten;
+    do {
+      wasRewritten = false;
+      InstructionIterator it = code.instructionIterator();
+      while (it.hasNext()) {
+        Instruction insn = it.next();
+        if (insn.isInstanceGet()) {
+          DexField field = insn.asInstanceGet().getField();
+          if (isDeadProtoField(field)) {
+            // Rewrite deads field access to corresponding 0.
+            it.replaceCurrentInstruction(code.createConstNull(insn.asInstanceGet()));
+            wasRewritten = true;
+          }
+        } else if (insn.isInvokeMethodWithReceiver()) {
+          InvokeMethodWithReceiver invokeMethod = insn.asInvokeMethodWithReceiver();
+          DexMethod invokedMethod = invokeMethod.getInvokedMethod();
+          if (isDeadProtoGetter(invokedMethod)) {
+            // Rewrite dead getters.
+            it.replaceCurrentInstruction(code.createConstNull(invokeMethod));
+            wasRewritten = true;
+          } else if (invokedMethod.name == sizeMethodName
+              && invokedMethod.holder.isSubtypeOf(listType, appInfo)) {
+            Value receiver = invokeMethod.getReceiver();
+            if (isDefinedAsNull(receiver)) {
+              // Rewrite size() methods with null receiver.
+              it.replaceCurrentInstruction(code.createConstNull(invokeMethod));
+            }
+          } else if (invokedMethod.name == getterNamePrefix
+              && invokedMethod.holder.isSubtypeOf(listType, appInfo)) {
+            Value receiver = invokeMethod.getReceiver();
+            if (isDefinedAsNull(receiver)) {
+              // Rewrite get(x) methods with null receiver.
+              it.replaceCurrentInstruction(code.createConstNull(invokeMethod));
+              wasRewritten = true;
+            }
+          } else if (isWriteMethod(invokedMethod)) {
+            Value lastArg = Iterables.getLast(invokeMethod.inValues());
+            if (isDefinedAsNull(lastArg)) {
+              it.remove();
+            }
+          }
+        } else if (insn.isInvokeMethod()) {
+          InvokeMethod invokeMethod = insn.asInvokeMethod();
+          DexMethod invokedMethod = invokeMethod.getInvokedMethod();
+          if (isComputeSizeMethod(invokedMethod)) {
+            Value lastArg = Iterables.getLast(invokeMethod.inValues());
+            if (isDefinedAsNull(lastArg)) {
+              // This is a computeSize method on a constant null. The field was dead.
+              assert (invokeMethod.outValue() != null);
+              it.replaceCurrentInstruction(code.createConstNull(invokeMethod));
+              wasRewritten = true;
+            }
+          }
+        }
+      }
+    } while (wasRewritten);
+  }
+
+  /**
+   * The dyanmicMethod code is actually a collection of various methods contained in one.
+   * It uses a switch statement over an enum to identify which actual operation is performed.
+   * We need to rewrite all cases that might access dead fields. These are
+   * <dl>
+   * <dt>IS_INITIALIZED</dt>
+   * <dd>See {@link #rewriteIsInitializedCase}.</dd>
+   * <dt>MAKE_IMMUTABLE</dt>
+   * <dd>See {@link #rewriteMakeImmutableCase}.</dd>
+   * <dt>VISIT</dt>
+   * <dd>See {@link #rewriteVisitCase}.</dd>
+   * <dt>MERGE_FROM_STREAM</dt>
+   * <dd>See {@link #rewriteMergeCase}.</dd>
+   * </dl>
+   */
+  private void rewriteDynamicMethod(IRCode code, DexEncodedMethod method) {
+    // This method contains a switch and we are interested in some of the cases only.
+    InstructionIterator iterator = code.instructionIterator();
+    Instruction matchingInstr = iterator.nextUntil(Instruction::isSwitch);
+    if (matchingInstr == null) {
+      throw new CompilationError("dynamicMethod in protoLite without switch.");
+    }
+    Switch switchInstr = matchingInstr.asSwitch();
+    EnumSwitchInfo info = SwitchUtils.analyzeSwitchOverEnum(switchInstr, appInfo);
+    if (info == null || info.enumClass != methodEnumType) {
+      throw new CompilationError("Malformed switch in dynamicMethod of proto lite.");
+    }
+    BasicBlock initializedCase = null;
+    BasicBlock visitCase = null;
+    BasicBlock mergeCase = null;
+    BasicBlock makeImmutableCase = null;
+    for (int keyIdx = 0; keyIdx < switchInstr.numberOfKeys(); keyIdx++) {
+      int key = switchInstr.getKey(keyIdx);
+      DexField label = info.indexMap.get(key);
+      assert label != null;
+      if (label.name == visitTag) {
+        assert visitCase == null;
+        visitCase = switchInstr.targetBlock(keyIdx);
+      } else if (label.name == mergeTag) {
+        assert mergeCase == null;
+        mergeCase = switchInstr.targetBlock(keyIdx);
+      } else if (label.name == isInitializedTag) {
+        assert initializedCase == null;
+        initializedCase = switchInstr.targetBlock(keyIdx);
+      } else if (label.name == makeImmutabkeTag) {
+        assert makeImmutableCase == null;
+        makeImmutableCase = switchInstr.targetBlock(keyIdx);
+      }
+    }
+    DexType instanceType = method.method.getHolder();
+    rewriteIsInitializedCase(initializedCase, instanceType, code);
+    assert code.isConsistentSSA();
+    rewriteMakeImmutableCase(makeImmutableCase, code);
+    assert code.isConsistentSSA();
+    rewriteVisitCase(visitCase, code);
+    assert code.isConsistentSSA();
+    rewriteMergeCase(mergeCase, instanceType, code);
+    assert code.isConsistentSSA();
+  }
+
+  /**
+   * In the presence of repeated fields, the MAKE_IMMUTABLE case will contain code of the form
+   *
+   * <pre>
+   * case MAKE_IMMUTABLE: {
+   *   repeated_.makeImmutable();
+   *   other_.makeImmutable();
+   *   sub_.makeImmutable();
+   *   return null;
+   * }
+   * </pre>
+   *
+   * For dead fields, we remove the field access and the call to makeImmutable.
+   */
+  private void rewriteMakeImmutableCase(BasicBlock switchCase, IRCode code) {
+    DominatorTree dom = new DominatorTree(code);
+    boolean wasRewritten;
+    do {
+      wasRewritten = false;
+      for (BasicBlock current : dom.dominatedBlocks(switchCase)) {
+        InstructionIterator it = current.iterator();
+        while (it.hasNext()) {
+          Instruction insn = it.next();
+          if (insn.isInstanceGet() && isDeadProtoField(insn.asInstanceGet().getField())) {
+            it.replaceCurrentInstruction(code.createConstNull(insn));
+            wasRewritten = true;
+          } else if (insn.isInvokeMethodWithReceiver()) {
+            InvokeMethodWithReceiver invokeMethod = insn.asInvokeMethodWithReceiver();
+            DexMethod invokedMethod = invokeMethod.getInvokedMethod();
+            if (isDeadProtoGetter(invokedMethod)) {
+              it.replaceCurrentInstruction(code.createConstNull(invokeMethod));
+              wasRewritten = true;
+            } else if (invokedMethod.name == makeImmutableMethodName
+                && invokedMethod.getHolder().isSubtypeOf(protobufListType, appInfo)) {
+              Value receiver = invokeMethod.getReceiver();
+              if (isDefinedAsNull(receiver)) {
+                it.remove();
+              }
+            }
+          }
+        }
+      }
+    } while (wasRewritten);
+  }
+
+  /**
+   * The IS_INITIALIZED case also has a high degree of variability depending on the type of fields.
+   * Common code looks as follows
+   *
+   * <pre>
+   * case IS_INITIALIZED: {
+   *   byte isInitialized = memoizedIsInitialized;
+   *   if (isInitialized == 1) return DEFAULT_INSTANCE;
+   *   if (isInitialized == 0) return null;
+   *
+   *   boolean shouldMemoize = ((Boolean) arg0).booleanValue();
+   *   if (!hasId()) {
+   *     if (shouldMemoize) {
+   *       memoizedIsInitialized = 0;
+   *     }
+   *     return null;
+   *   }
+   *   for (int i = 0; i < getSubCount(); i++) {
+   *     if (!getSub(i).isInitialized()) {
+   *       if (shouldMemoize) {
+   *         memoizedIsInitialized = 0;
+   *       }
+   *       return null;
+   *     }
+   *   }
+   *   if (shouldMemoize) memoizedIsInitialized = 1;
+   *   return DEFAULT_INSTANCE;
+   * }
+   * </pre>
+   *
+   * We remove all the accesses of dead fields and getters. We also replace secondary invokes
+   * (like getSubCount or isInitialized) with conservative default values (e.g. 0, true).
+   * <p>
+   * We also rewrite getXXXCount methods on live fields, as accesses to those methods was filtered
+   * during tree shaking. In place of those methods, we inline their definition.
+   */
+  private void rewriteIsInitializedCase(BasicBlock switchCase, DexType instanceType,
+      IRCode code) {
+    DominatorTree dom = new DominatorTree(code);
+    boolean wasRewritten;
+    do {
+      wasRewritten = false;
+      for (BasicBlock current : dom.dominatedBlocks(switchCase)) {
+        InstructionIterator it = current.iterator();
+        while (it.hasNext()) {
+          Instruction insn = it.next();
+          if (insn.isInvokeMethodWithReceiver()) {
+            InvokeMethodWithReceiver invokeMethod = insn.asInvokeMethodWithReceiver();
+            DexMethod invokedMethod = invokeMethod.getInvokedMethod();
+            if (isDeadProtoGetter(invokedMethod)) {
+              it.replaceCurrentInstruction(code.createConstNull(invokeMethod));
+              wasRewritten = true;
+            } else if (invokedMethod.name == isInitializedMethodName
+                && invokedMethod.getHolder().isSubtypeOf(messageType, appInfo)) {
+              Value receiver = invokeMethod.getReceiver();
+              if (isDefinedAsNull(receiver)) {
+                // We cannot compute initialization state for nested messages and repeated
+                // messages that have been removed or moved to unknown fields. Just return
+                // true.
+                it.replaceCurrentInstruction(code.createTrue());
+                wasRewritten = true;
+              }
+            } else if (isCountGetter(invokedMethod)) {
+              // We have to rewrite these as a precaution, as they might be dead due to
+              // tree shaking ignoring them.
+              DexField field = getterToField(invokedMethod, 5);
+              if (appInfo.liveFields.contains(field)) {
+                // Effectively inline the code that is normally inside these methods.
+                Value thisReference = invokeMethod.getReceiver();
+                Value newResult = code.createValue(MoveType.SINGLE);
+                invokeMethod.outValue().replaceUsers(newResult);
+                Value theList = code.createValue(MoveType.OBJECT);
+                it.replaceCurrentInstruction(
+                    new InstanceGet(MemberType.OBJECT, theList, thisReference, field));
+                it.add(new InvokeInterface(sizeMethod, newResult, Collections.emptyList()));
+              } else {
+                // The field is dead, so its count is always 0.
+                it.replaceCurrentInstruction(code.createConstNull(invokeMethod));
+              }
+            }
+          }
+        }
+      }
+    } while (wasRewritten);
+  }
+
+  private InstancePut findProtoFieldWrite(BasicBlock block, DexType instanceType,
+      BiPredicate<DexField, DexType> filter, DominatorTree dom) {
+    for (BasicBlock current : dom.dominatedBlocks(block)) {
+      InstructionIterator insns = current.iterator();
+      InstancePut instancePut = (InstancePut) insns.nextUntil(Instruction::isInstancePut);
+      if (instancePut != null && filter.test(instancePut.getField(), instanceType)) {
+        return instancePut;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * For the merge case, we typically see code that contains a switch over the field tags. The inner
+   * switch has the form
+   *
+   * <pre>
+   * switch (tag) {
+   *   case 0:
+   *     done = true;
+   *     break;
+   *   default: {
+   *     if (!parseUnknownField(tag, input)) {
+   *       done = true;
+   *     }
+   *     break;
+   *   }
+   *   case 8: {
+   *     bitField0_ |= 0x00000001;
+   *     id_ = input.readInt32();
+   *     break;
+   *   }
+   *   case 18: {
+   *     nestedproto2.GeneratedNestedProto.NestedOne.Builder subBuilder = null;
+   *     if (((bitField0_ & 0x00000002) == 0x00000002)) {
+   *       subBuilder = inner_.toBuilder();
+   *     }
+   *     inner_ = input.readMessage(nestedproto2.GeneratedNestedProto.NestedOne.parser(),
+   *         extensionRegistry);
+   *     if (subBuilder != null) {
+   *       subBuilder.mergeFrom(inner_);
+   *       inner_ = subBuilder.buildPartial();
+   *     }
+   *     bitField0_ |= 0x00000002;
+   *     break;
+   *   }
+   *   case 24: {
+   *     if (!other_.isModifiable()) {
+   *       other_ =
+   *           com.google.protobuf.GeneratedMessageLite.mutableCopy(other_);
+   *     }
+   *     other_.addBoolean(input.readBool());
+   *     break;
+   *   }
+   *   case 26: {
+   *     int length = input.readRawVarint32();
+   *     int limit = input.pushLimit(length);
+   *     if (!other_.isModifiable() && input.getBytesUntilLimit() > 0) {
+   *       final int currentSize = other_.size();
+   *       other_ = other_.mutableCopyWithCapacity(
+   *       currentSize + (length/1));
+   *     }
+   *     while (input.getBytesUntilLimit() > 0) {
+   *       other_.addBoolean(input.readBool());
+   *     }
+   *     input.popLimit(limit);
+   *     break;
+   *   }
+   * }
+   * </pre>
+   *
+   * The general approach here is to identify the field that is processed by a case and, if
+   * the field is dead, remove the entire case.
+   * <p>
+   * We slightly complicate the rewriting by also checking whether the block computes a
+   * presence bitfield (bitField0_ above). If so, we move that computation to a new block that
+   * continues to the default case. This ensures that presence is recorded correctly, yet the
+   * field is moved to the unknownFields collection, if such exists.
+   */
+  private void rewriteMergeCase(BasicBlock caseBlock, DexType instanceType,
+      IRCode code) {
+    // We are looking for a switch statement over the input tag. Just traverse all blocks until
+    // we find it.
+    List<BasicBlock> deadBlocks = new ArrayList<>();
+    DominatorTree dom = new DominatorTree(code);
+    for (BasicBlock current : dom.dominatedBlocks(caseBlock)) {
+      InstructionIterator it = current.iterator();
+      Switch switchInstr;
+      if ((switchInstr = (Switch) it.nextUntil(Instruction::isSwitch)) != null) {
+        int nextBlock = code.getHighestBlockNumber() + 1;
+        IntList liveKeys = new IntArrayList(switchInstr.numberOfKeys());
+        List<BasicBlock> liveBlocks = new ArrayList<>(switchInstr.numberOfKeys());
+        boolean needsCleanup = false;
+        // Filter out all the cases that contain writes to dead fields.
+        for (int keyIdx = 0; keyIdx < switchInstr.numberOfKeys(); keyIdx++) {
+          BasicBlock targetBlock = switchInstr.targetBlock(keyIdx);
+          InstancePut instancePut =
+              findProtoFieldWrite(targetBlock, instanceType, (field, holder) -> isProtoField(field),
+                  dom);
+          if (instancePut == null
+              || appInfo.withLiveness().liveFields.contains(instancePut.getField())) {
+            // This is a live case. Keep it.
+            liveKeys.add(switchInstr.getKey(keyIdx));
+            liveBlocks.add(targetBlock);
+          } else {
+            // We cannot just remove this entire switch case if there is some computation here
+            // for whether the field is present. We check this by searching for a write to
+            // the bitField<xxx>_ fields. If such write exists, we move the corresponding
+            // instructions to the first block in the switch.
+            //TODO(herhut): Only do this if the written field has a live hasMethod.
+            InstancePut bitFieldUpdate = findProtoFieldWrite(targetBlock, instanceType,
+                this::isPresenceField, dom);
+            if (bitFieldUpdate != null) {
+              BasicBlock newBlock = BasicBlock.createGotoBlock(nextBlock++);
+              newBlock.link(switchInstr.fallthroughBlock());
+              // Copy over the computation of the field;
+              moveInstructionTo(newBlock.listIterator(), bitFieldUpdate, dom, targetBlock);
+              switchInstr.getBlock().link(newBlock);
+              liveKeys.add(switchInstr.getKey(keyIdx));
+              liveBlocks.add(newBlock);
+              code.blocks.add(newBlock);
+            }
+            needsCleanup = true;
+          }
+        }
+        if (needsCleanup) {
+          DominatorTree updatedTree = new DominatorTree(code);
+          BasicBlock fallThrough = switchInstr.fallthroughBlock();
+          List<BasicBlock> successors = ImmutableList.copyOf(current.getNormalSucessors());
+          for (BasicBlock successor : successors) {
+            if (successor != fallThrough && !liveBlocks.contains(successor)) {
+              deadBlocks.addAll(current.unlink(successor, updatedTree));
+            }
+          }
+          int[] blockIndices = new int[liveBlocks.size()];
+          for (int i = 0; i < liveBlocks.size(); i++) {
+            blockIndices[i] = current.getSuccessors().indexOf(liveBlocks.get(i));
+          }
+          Switch newSwitch = new Switch(switchInstr.inValues().get(0), liveKeys.toIntArray(),
+              blockIndices, current.getSuccessors().indexOf(fallThrough));
+          it.replaceCurrentInstruction(newSwitch);
+        }
+        break;
+      }
+    }
+    code.removeBlocks(deadBlocks);
+  }
+
+  //TODO(herhut): This should really be a copy with a value substitution map.
+  private void moveInstructionTo(InstructionListIterator iterator, Instruction insn,
+      DominatorTree dom,
+      BasicBlock dominator) {
+    for (Value value : insn.inValues()) {
+      Instruction input = value.definition;
+      // We do not support phis.
+      assert input != null;
+      if (dom.dominatedBy(input.getBlock(), dominator)) {
+        // And no shared instructions.
+        assert input.outValue().numberOfUsers() == 1;
+        moveInstructionTo(iterator, input, dom, dominator);
+      }
+    }
+    insn.getBlock().removeInstruction(insn);
+    iterator.add(insn);
+  }
+
+  private boolean isDeadProtoField(DexField field) {
+    return isProtoField(field) && !appInfo.liveFields.contains(field);
+  }
+
+  private boolean isDeadProtoGetter(DexMethod method) {
+    return isGetter(method) && isDeadProtoField(getterToField(method));
+  }
+
+  private boolean isVisitOfDeadField(Instruction instruction) {
+    if (!instruction.isInvokeMethod()) {
+      return false;
+    }
+    InvokeMethod invokeMethod = instruction.asInvokeMethod();
+    if (invokeMethod.getInvokedMethod().getHolder() == visitorType
+        && invokeMethod.getInvokedMethod().getArity() >= 2) {
+      Instruction secondArg = invokeMethod.inValues().get(2).definition;
+      return secondArg.isConstNumber();
+    }
+    return false;
+  }
+
+  /**
+   * The visit case has typically the form
+   *
+   * <pre>
+   * case VISIT: {
+   *   Visitor visitor = (Visitor) arg0;
+   *   repeatedproto.GeneratedRepeatedProto.Repeated other =
+   *       (repeatedproto.GeneratedRepeatedProto.Repeated) arg1;
+   *   id_ = visitor.visitInt(
+   *       hasId(), id_,
+   *       other.hasId(), other.id_);
+   *   repeated_= visitor.visitList(repeated_, other.repeated_);
+   *   inner_ = visitor.visitMessage(inner_, other.inner_);
+   *   if (visitor == com.google.protobuf.GeneratedMessageLite.MergeFromVisitor.INSTANCE) {
+   *     bitField0_ |= other.bitField0_;
+   *   }
+   *   return this;
+   * }
+   * </pre>
+   *
+   * We remove all writes and reads to dead fields and correspondign secondary instructions, like
+   * the visitXXX methods.
+   * <p>
+   * Note that the invoked hasMethods are benign, as the only access the bitFieldXXX_ fields, which
+   * we currently do not remove. Inlining will likely remove the methods.
+   */
+  private void rewriteVisitCase(BasicBlock switchCase, IRCode code) {
+    DominatorTree dom = new DominatorTree(code);
+    boolean wasRewritten;
+    do {
+      wasRewritten = false;
+      for (BasicBlock target : dom.dominatedBlocks(switchCase)) {
+        InstructionIterator it = target.iterator();
+        while (it.hasNext()) {
+          Instruction insn = it.next();
+          if (insn.isInstanceGet()) {
+            InstanceGet instanceGet = insn.asInstanceGet();
+            if (isDeadProtoField(instanceGet.getField())) {
+              it.replaceCurrentInstruction(code.createConstNull(instanceGet));
+              wasRewritten = true;
+            }
+          } else if (insn.isInstancePut()) {
+            if (isDeadProtoField(insn.asInstancePut().getField())) {
+              it.remove();
+            }
+          } else if (isVisitOfDeadField(insn)) {
+            it.replaceCurrentInstruction(code.createConstNull(insn));
+          } else if (insn.isCheckCast()) {
+            // The call to visitXXX is a generic method invoke, so it will be followed by a check
+            // cast to fix up the type. As the result is no longer needed once we are done, we can
+            // remove the cast. This removes a potential last reference to an inner message class.
+            // TODO(herhut): We should have a generic dead cast removal.
+            Value inValue = insn.inValues().get(0);
+            if (isDefinedAsNull(inValue)) {
+              insn.outValue().replaceUsers(inValue);
+              it.remove();
+            }
+          }
+
+        }
+      }
+    } while (wasRewritten);
+  }
+}
diff --git a/src/test/examples/enumproto/Enumproto.java b/src/test/examples/enumproto/Enumproto.java
new file mode 100644
index 0000000..9176378
--- /dev/null
+++ b/src/test/examples/enumproto/Enumproto.java
@@ -0,0 +1,66 @@
+// Copyright (c) 2017, 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 enumproto;
+
+
+import enumproto.GeneratedEnumProto.Enum;
+import enumproto.three.GeneratedEnumProto.EnumThree;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+public class Enumproto {
+
+  private static final byte[] WITH_ALL_FIELDS = new byte[]{6, 8, 42, 16, 2, 24, 3};
+  private static final byte[] WITH_DEFAULT_FOR_ENUM = new byte[]{2, 8, 42};
+
+
+  public static void main(String... args) throws IOException {
+    readProtoAndPrintDaEnum(WITH_ALL_FIELDS);
+    readProtoAndPrintDaEnum(WITH_DEFAULT_FOR_ENUM);
+    readProtoThreeAndPrintDaEnum(WITH_ALL_FIELDS);
+    readProtoThreeAndPrintDaEnum(WITH_DEFAULT_FOR_ENUM);
+    roundTrip(WITH_ALL_FIELDS);
+    roundTrip(WITH_DEFAULT_FOR_ENUM);
+    roundTripThree(WITH_ALL_FIELDS);
+    roundTripThree(WITH_DEFAULT_FOR_ENUM);
+  }
+
+  private static void readProtoAndPrintDaEnum(byte[] bytes) throws IOException {
+    ByteArrayInputStream input = new ByteArrayInputStream(bytes);
+    Enum.Builder builder = Enum.newBuilder();
+    builder.mergeDelimitedFrom(input);
+    Enum buffer = builder.build();
+    System.out.println(buffer.getEnum());
+  }
+
+  private static void readProtoThreeAndPrintDaEnum(byte[] bytes) throws IOException {
+    ByteArrayInputStream input = new ByteArrayInputStream(bytes);
+    EnumThree.Builder builder = EnumThree.newBuilder();
+    builder.mergeDelimitedFrom(input);
+    EnumThree buffer = builder.build();
+    System.out.println(buffer.getEnum());
+  }
+
+  private static void roundTrip(byte[] bytes) throws IOException {
+    ByteArrayInputStream input = new ByteArrayInputStream(bytes);
+    Enum.Builder builder = Enum.newBuilder();
+    builder.mergeDelimitedFrom(input);
+    Enum buffer = builder.build();
+    ByteArrayOutputStream output = new ByteArrayOutputStream();
+    buffer.writeDelimitedTo(output);
+    readProtoAndPrintDaEnum(output.toByteArray());
+  }
+
+  private static void roundTripThree(byte[] bytes) throws IOException {
+    ByteArrayInputStream input = new ByteArrayInputStream(bytes);
+    EnumThree.Builder builder = EnumThree.newBuilder();
+    builder.mergeDelimitedFrom(input);
+    EnumThree buffer = builder.build();
+    ByteArrayOutputStream output = new ByteArrayOutputStream();
+    buffer.writeDelimitedTo(output);
+    readProtoThreeAndPrintDaEnum(output.toByteArray());
+  }
+
+}
diff --git a/src/test/examples/enumproto/enum.proto b/src/test/examples/enumproto/enum.proto
new file mode 100644
index 0000000..0fa5695
--- /dev/null
+++ b/src/test/examples/enumproto/enum.proto
@@ -0,0 +1,25 @@
+// Copyright (c) 2017, 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.
+syntax = "proto2";
+package enumproto;
+
+option java_outer_classname = "GeneratedEnumProto";
+
+message Enum {
+  required int32 id = 1;
+  enum DaEnum {
+    UNKOWN = 0;
+    KNOWN = 1;
+    BELIEF = 2;
+  }
+  optional DaEnum enum = 2;
+  enum OtherEnum {
+    BLACK = 0;
+    RED = 1;
+    GREEN = 2;
+    OKALALALA = 3;
+  }
+  optional OtherEnum other = 3;
+}
+
diff --git a/src/test/examples/enumproto/enum_three.proto b/src/test/examples/enumproto/enum_three.proto
new file mode 100644
index 0000000..a2731fb
--- /dev/null
+++ b/src/test/examples/enumproto/enum_three.proto
@@ -0,0 +1,25 @@
+// Copyright (c) 2016, 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.
+syntax = "proto3";
+package enumproto.three;
+
+option java_outer_classname = "GeneratedEnumProto";
+
+message EnumThree {
+  int32 id = 1;
+  enum DaEnum {
+    UNKOWN = 0;
+    KNOWN = 1;
+    BELIEF = 2;
+  }
+  DaEnum enum = 2;
+  enum OtherEnum {
+    BLACK = 0;
+    RED = 1;
+    GREEN = 2;
+    OKALALALA = 3;
+  }
+  OtherEnum other = 3;
+}
+
diff --git a/src/test/examples/enumproto/keep-rules.txt b/src/test/examples/enumproto/keep-rules.txt
new file mode 100644
index 0000000..088f88f
--- /dev/null
+++ b/src/test/examples/enumproto/keep-rules.txt
@@ -0,0 +1,12 @@
+# Copyright (c) 2017, 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.
+
+# Keep the application entry point. Get rid of everything that is not
+# reachable from there.
+-keep public class enumproto.Enumproto {
+  public static void main(...);
+}
+
+# allow access modification to enable minification
+-allowaccessmodification
diff --git a/src/test/examples/nestedproto1/Nestedproto.java b/src/test/examples/nestedproto1/Nestedproto.java
new file mode 100644
index 0000000..fe1a535
--- /dev/null
+++ b/src/test/examples/nestedproto1/Nestedproto.java
@@ -0,0 +1,31 @@
+// Copyright (c) 2017, 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 nestedproto1;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import nestedproto1.GeneratedNestedProto.Outer;
+
+public class Nestedproto {
+
+  private static final byte[] NESTED_MESSAGE_WITH_BOTH = new byte[] {25, 8, 42, 18, 12, 8, 1, 18, 8,
+      105, 110, 110, 101, 114, 79, 110, 101, 26, 7, 8, 2, 21, 0, 0, -10, 66};
+
+  private static final byte[] NESTED_MESSAGE_WITH_ONE = new byte[]{16, 8, 42, 18, 12, 8, 1, 18, 8,
+      105,
+      110, 110, 101, 114, 79, 110, 101};
+
+  public static void main(String... args) throws IOException {
+    testWith(NESTED_MESSAGE_WITH_BOTH);
+    testWith(NESTED_MESSAGE_WITH_ONE);
+  }
+
+  public static void testWith(byte[] data) throws IOException {
+    ByteArrayInputStream input = new ByteArrayInputStream(data);
+    Outer.Builder builder = Outer.newBuilder();
+    builder.mergeDelimitedFrom(input);
+    Outer outer = builder.build();
+    System.out.println(outer.getInner().getOther());
+  }
+}
diff --git a/src/test/examples/nestedproto1/keep-rules.txt b/src/test/examples/nestedproto1/keep-rules.txt
new file mode 100644
index 0000000..1c47672
--- /dev/null
+++ b/src/test/examples/nestedproto1/keep-rules.txt
@@ -0,0 +1,12 @@
+# Copyright (c) 2017, 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.
+
+# Keep the application entry point. Get rid of everything that is not
+# reachable from there.
+-keep public class nestedproto1.Nestedproto {
+  public static void main(...);
+}
+
+# allow access modification to enable minification
+-allowaccessmodification
diff --git a/src/test/examples/nestedproto1/nested.proto b/src/test/examples/nestedproto1/nested.proto
new file mode 100644
index 0000000..0d05749
--- /dev/null
+++ b/src/test/examples/nestedproto1/nested.proto
@@ -0,0 +1,24 @@
+// Copyright (c) 2016, 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.
+syntax = "proto2";
+package nestedproto1;
+
+option java_outer_classname = "GeneratedNestedProto";
+
+message NestedOne {
+  required int32 id = 1;
+  optional string other = 2;
+}
+
+message NestedTwo {
+  required int32 id = 1;
+  optional float other = 2;
+}
+
+message Outer {
+  required int32 id = 1;
+  required NestedOne inner = 2;
+  optional NestedTwo inner2 = 3;
+}
+
diff --git a/src/test/examples/nestedproto2/Nestedproto.java b/src/test/examples/nestedproto2/Nestedproto.java
new file mode 100644
index 0000000..59217de
--- /dev/null
+++ b/src/test/examples/nestedproto2/Nestedproto.java
@@ -0,0 +1,33 @@
+// Copyright (c) 2017, 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 nestedproto2;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import nestedproto2.GeneratedNestedProto.Outer;
+
+public class Nestedproto {
+
+  private static final byte[] NESTED_MESSAGE_WITH_BOTH = new byte[]{25, 8, 42, 18, 12, 8, 1, 18, 8,
+      105, 110, 110, 101, 114, 79, 110, 101, 26, 7, 8, 2, 21, 0, 0, -10, 66};
+
+  private static final byte[] NESTED_MESSAGE_WITH_ONE = new byte[]{16, 8, 42, 18, 12, 8, 1, 18, 8,
+      105,
+      110, 110, 101, 114, 79, 110, 101};
+
+  // Test that all fields remain when roundtripping with removed fields.
+  public static void main(String... args) throws IOException {
+    testWith(NESTED_MESSAGE_WITH_BOTH);
+    testWith(NESTED_MESSAGE_WITH_ONE);
+  }
+
+  private static void testWith(byte[] data) throws IOException {
+    ByteArrayInputStream input = new ByteArrayInputStream(data);
+    Outer.Builder builder = Outer.newBuilder();
+    builder.mergeDelimitedFrom(input);
+    builder.setId(1982);
+    Outer outer = builder.build();
+    outer.writeTo(System.out);
+  }
+}
diff --git a/src/test/examples/nestedproto2/keep-rules.txt b/src/test/examples/nestedproto2/keep-rules.txt
new file mode 100644
index 0000000..87c9218
--- /dev/null
+++ b/src/test/examples/nestedproto2/keep-rules.txt
@@ -0,0 +1,12 @@
+# Copyright (c) 2017, 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.
+
+# Keep the application entry point. Get rid of everything that is not
+# reachable from there.
+-keep public class nestedproto2.Nestedproto {
+  public static void main(...);
+}
+
+# allow access modification to enable minification
+-allowaccessmodification
diff --git a/src/test/examples/nestedproto2/nested.proto b/src/test/examples/nestedproto2/nested.proto
new file mode 100644
index 0000000..ac56f40
--- /dev/null
+++ b/src/test/examples/nestedproto2/nested.proto
@@ -0,0 +1,24 @@
+// Copyright (c) 2016, 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.
+syntax = "proto2";
+package nestedproto2;
+
+option java_outer_classname = "GeneratedNestedProto";
+
+message NestedOne {
+  required int32 id = 1;
+  optional string other = 2;
+}
+
+message NestedTwo {
+  required int32 id = 1;
+  optional float other = 2;
+}
+
+message Outer {
+  required int32 id = 1;
+  required NestedOne inner = 2;
+  optional NestedTwo inner2 = 3;
+}
+
diff --git a/src/test/examples/oneofproto/Oneofproto.java b/src/test/examples/oneofproto/Oneofproto.java
new file mode 100644
index 0000000..261705f
--- /dev/null
+++ b/src/test/examples/oneofproto/Oneofproto.java
@@ -0,0 +1,38 @@
+// Copyright (c) 2017, 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 oneofproto;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import oneofproto.GeneratedOneOfProto.Oneof;
+
+public class Oneofproto {
+
+  private static final byte[] WITH_BOOL_FIELD = new byte[]{4, 8, 42, 24, 1};
+  private static final byte[] WITH_FLOAT_FIELD = new byte[]{7, 8, 42, 21, 0, 0, -10, 66};
+  private static final byte[] WITH_STRING_FIELD = new byte[]{9, 8, 42, 34, 5, 104, 101, 108, 108,
+      111};
+  private static final byte[] WITH_NO_FIELD = new byte[]{2, 8, 42};
+
+
+  public static void main(String... args) throws IOException {
+    roundTrip(WITH_BOOL_FIELD);
+    roundTrip(WITH_FLOAT_FIELD);
+    roundTrip(WITH_STRING_FIELD);
+    roundTrip(WITH_NO_FIELD);
+  }
+
+  private static void roundTrip(byte[] data) throws IOException {
+    ByteArrayInputStream input = new ByteArrayInputStream(data);
+    Oneof.Builder builder = Oneof.newBuilder();
+    builder.mergeDelimitedFrom(input);
+    Oneof oneof = builder.build();
+    ByteArrayOutputStream output = new ByteArrayOutputStream();
+    oneof.writeDelimitedTo(output);
+    System.out.println(Arrays.toString(output.toByteArray()));
+  }
+
+}
diff --git a/src/test/examples/oneofproto/keep-rules.txt b/src/test/examples/oneofproto/keep-rules.txt
new file mode 100644
index 0000000..70f00f1
--- /dev/null
+++ b/src/test/examples/oneofproto/keep-rules.txt
@@ -0,0 +1,12 @@
+# Copyright (c) 2017, 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.
+
+# Keep the application entry point. Get rid of everything that is not
+# reachable from there.
+-keep public class oneofproto.Oneofproto {
+  public static void main(...);
+}
+
+# allow access modification to enable minification
+-allowaccessmodification
diff --git a/src/test/examples/oneofproto/oneof.proto b/src/test/examples/oneofproto/oneof.proto
new file mode 100644
index 0000000..c5a67a1
--- /dev/null
+++ b/src/test/examples/oneofproto/oneof.proto
@@ -0,0 +1,17 @@
+// Copyright (c) 2016, 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.
+syntax = "proto2";
+package oneofproto;
+
+option java_outer_classname = "GeneratedOneOfProto";
+
+message Oneof {
+  required int32 id = 1;
+  oneof otherfields {
+    float floatField = 2;
+    bool boolField = 3;
+    string stringField = 4;
+  }
+}
+
diff --git a/src/test/examples/protowithexts/withexts.proto b/src/test/examples/protowithexts/withexts.proto
new file mode 100644
index 0000000..d384173
--- /dev/null
+++ b/src/test/examples/protowithexts/withexts.proto
@@ -0,0 +1,19 @@
+// Copyright (c) 2016, 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.
+syntax = "proto2";
+package protowithexts;
+
+option java_outer_classname = "GeneratedProtoWithExts";
+
+message Simple {
+  required int32 id = 1;
+
+  optional int32 other = 2;
+
+  extensions 10 to 19;
+}
+
+extend Simple {
+  optional string extra = 10;
+}
diff --git a/src/test/examples/repeatedproto/Repeatedproto.java b/src/test/examples/repeatedproto/Repeatedproto.java
new file mode 100644
index 0000000..280070d
--- /dev/null
+++ b/src/test/examples/repeatedproto/Repeatedproto.java
@@ -0,0 +1,22 @@
+// Copyright (c) 2017, 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 repeatedproto;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import repeatedproto.GeneratedRepeatedProto.Repeated;
+
+public class Repeatedproto {
+
+  private static final byte[] WITH_ALL_FIELDS = new byte[]{29, 8, 123, 18, 3, 111, 110, 101, 18, 3,
+      116, 119, 111, 18, 5, 116, 104, 114, 101, 101, 24, 1, 34, 2, 8, 42, 34, 2, 8, 42};
+
+  public static void main(String... args) throws IOException {
+    ByteArrayInputStream input = new ByteArrayInputStream(WITH_ALL_FIELDS);
+    Repeated.Builder builder = Repeated.newBuilder();
+    builder.mergeDelimitedFrom(input);
+    Repeated repeated = builder.build();
+    System.out.println(repeated.getRepeatedList());
+  }
+}
diff --git a/src/test/examples/repeatedproto/keep-rules.txt b/src/test/examples/repeatedproto/keep-rules.txt
new file mode 100644
index 0000000..3d02352
--- /dev/null
+++ b/src/test/examples/repeatedproto/keep-rules.txt
@@ -0,0 +1,12 @@
+# Copyright (c) 2017, 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.
+
+# Keep the application entry point. Get rid of everything that is not
+# reachable from there.
+-keep public class repeatedproto.Repeatedproto {
+  public static void main(...);
+}
+
+# allow access modification to enable minification
+-allowaccessmodification
diff --git a/src/test/examples/repeatedproto/repeated.proto b/src/test/examples/repeatedproto/repeated.proto
new file mode 100644
index 0000000..7414e6f
--- /dev/null
+++ b/src/test/examples/repeatedproto/repeated.proto
@@ -0,0 +1,20 @@
+// Copyright (c) 2016, 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.
+syntax = "proto2";
+package repeatedproto;
+
+option java_outer_classname = "GeneratedRepeatedProto";
+
+message Repeated {
+  required int32 id = 1;
+  repeated string repeated = 2;
+  repeated bool other = 3;
+
+  message Sub {
+    required int32 value = 1;
+  }
+
+  repeated Sub sub = 4;
+}
+
diff --git a/src/test/examples/repeatedproto/repeated_three.proto b/src/test/examples/repeatedproto/repeated_three.proto
new file mode 100644
index 0000000..7da7881
--- /dev/null
+++ b/src/test/examples/repeatedproto/repeated_three.proto
@@ -0,0 +1,15 @@
+// Copyright (c) 2016, 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.
+syntax = "proto3";
+package repeatedproto.three;
+
+option java_outer_classname = "GeneratedRepeatedProto";
+
+
+message RepeatedThree {
+  int32 id = 1;
+  repeated string repeated = 2;
+  repeated bool other = 3;
+}
+
diff --git a/src/test/examples/simpleproto1/Simpleproto.java b/src/test/examples/simpleproto1/Simpleproto.java
new file mode 100644
index 0000000..d07ce8d
--- /dev/null
+++ b/src/test/examples/simpleproto1/Simpleproto.java
@@ -0,0 +1,56 @@
+// Copyright (c) 2017, 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 simpleproto1;
+
+import com.google.protobuf.UninitializedMessageException;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import simpleproto1.GeneratedSimpleProto.Simple;
+
+public class Simpleproto {
+
+  private static final byte[] WITH_REQUIRED_FIELDS = new byte[]{7, 8, 42, 21, 0, 0, -10, 66};
+  private static final byte[] WITH_MISSING_FIELD = new byte[]{2, 8, 42};
+
+
+  public static void main(String... args) throws IOException {
+    readProtoWithAllReqFields();
+    partialBuildFails();
+    partialReadFails();
+  }
+
+  private static void partialBuildFails() {
+    Simple.Builder builder = Simple.newBuilder();
+    builder.setId(32);
+    try {
+      builder.build();
+    } catch (UninitializedMessageException e) {
+      System.out.println("got exception");
+    }
+  }
+
+  private static void partialReadFails() throws IOException {
+    ByteArrayInputStream input = new ByteArrayInputStream(WITH_MISSING_FIELD);
+    Simple.Builder builder = Simple.newBuilder();
+    builder.mergeDelimitedFrom(input);
+    try {
+      builder.build();
+    } catch (UninitializedMessageException e) {
+      System.out.println("got exception");
+    }
+  }
+
+  private static void readProtoWithAllReqFields() throws IOException {
+    ByteArrayInputStream input = new ByteArrayInputStream(WITH_REQUIRED_FIELDS);
+    Simple.Builder builder = Simple.newBuilder();
+    builder.mergeDelimitedFrom(input);
+    Simple simple = builder.build();
+    ByteArrayOutputStream output = new ByteArrayOutputStream(WITH_REQUIRED_FIELDS.length);
+    simple.writeDelimitedTo(output);
+    System.out.println(Arrays.toString(output.toByteArray()));
+    System.out.println(Arrays.equals(WITH_REQUIRED_FIELDS, output.toByteArray()));
+  }
+}
diff --git a/src/test/examples/simpleproto1/keep-rules.txt b/src/test/examples/simpleproto1/keep-rules.txt
new file mode 100644
index 0000000..3c3c33f
--- /dev/null
+++ b/src/test/examples/simpleproto1/keep-rules.txt
@@ -0,0 +1,12 @@
+# Copyright (c) 2017, 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.
+
+# Keep the application entry point. Get rid of everything that is not
+# reachable from there.
+-keep public class simpleproto1.Simpleproto {
+  public static void main(...);
+}
+
+# allow access modification to enable minification
+-allowaccessmodification
diff --git a/src/test/examples/simpleproto1/simple.proto b/src/test/examples/simpleproto1/simple.proto
new file mode 100644
index 0000000..f4e1be4
--- /dev/null
+++ b/src/test/examples/simpleproto1/simple.proto
@@ -0,0 +1,14 @@
+// Copyright (c) 2016, 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.
+syntax = "proto2";
+package simpleproto1;
+
+option java_outer_classname = "GeneratedSimpleProto";
+
+message Simple {
+  required int32 id = 1;
+  required float unusedRequired = 2;
+  optional bool other = 3;
+}
+
diff --git a/src/test/examples/simpleproto2/Simpleproto.java b/src/test/examples/simpleproto2/Simpleproto.java
new file mode 100644
index 0000000..f333d72
--- /dev/null
+++ b/src/test/examples/simpleproto2/Simpleproto.java
@@ -0,0 +1,30 @@
+// Copyright (c) 2017, 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 simpleproto2;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import simpleproto2.GeneratedSimpleProto.Simple;
+
+/**
+ * A class that only uses a has method but otherwise ignores the value of a field.
+ */
+public class Simpleproto {
+
+  private static final byte[] WITHOUT_HASME_FIELD = new byte[]{2, 8, 42};
+  private static final byte[] WITH_HASME_FIELD = new byte[]{7, 8, 42, 21, 0, 0, -10, 66};
+
+  public static void main(String... args) throws IOException {
+    testHasWorks(WITHOUT_HASME_FIELD, false);
+    testHasWorks(WITH_HASME_FIELD, true);
+  }
+
+  private static void testHasWorks(byte[] msg, boolean expected) throws IOException {
+    ByteArrayInputStream input = new ByteArrayInputStream(msg);
+    Simple.Builder builder = Simple.newBuilder();
+    builder.mergeDelimitedFrom(input);
+    Simple simple = builder.build();
+    System.out.println("Expected " + expected + " and got " + simple.hasHasMe());
+  }
+}
diff --git a/src/test/examples/simpleproto2/keep-rules.txt b/src/test/examples/simpleproto2/keep-rules.txt
new file mode 100644
index 0000000..8f9c93e
--- /dev/null
+++ b/src/test/examples/simpleproto2/keep-rules.txt
@@ -0,0 +1,12 @@
+# Copyright (c) 2017, 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.
+
+# Keep the application entry point. Get rid of everything that is not
+# reachable from there.
+-keep public class simpleproto2.Simpleproto {
+  public static void main(...);
+}
+
+# allow access modification to enable minification
+-allowaccessmodification
diff --git a/src/test/examples/simpleproto2/simple.proto b/src/test/examples/simpleproto2/simple.proto
new file mode 100644
index 0000000..b9173e9
--- /dev/null
+++ b/src/test/examples/simpleproto2/simple.proto
@@ -0,0 +1,14 @@
+// Copyright (c) 2016, 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.
+syntax = "proto2";
+package simpleproto2;
+
+option java_outer_classname = "GeneratedSimpleProto";
+
+message Simple {
+  required int32 id = 1;
+  optional float hasMe = 2;
+  optional int32 other = 3;
+}
+
diff --git a/src/test/examples/simpleproto3/Simpleproto.java b/src/test/examples/simpleproto3/Simpleproto.java
new file mode 100644
index 0000000..2cdbae8
--- /dev/null
+++ b/src/test/examples/simpleproto3/Simpleproto.java
@@ -0,0 +1,65 @@
+// Copyright (c) 2017, 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 simpleproto3;
+
+import com.google.protobuf.UninitializedMessageException;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import simpleproto3.GeneratedSimpleProto.Simple;
+
+public class Simpleproto {
+
+  private static final byte[] WITH_REQUIRED_FIELDS = new byte[]{7, 8, 42, 21, 0, 0, -10, 66};
+  private static final byte[] WITH_MISSING_FIELD = new byte[]{2, 8, 42};
+
+
+  public static void main(String... args) throws IOException {
+    readProtoWithAllReqFields();
+    partialBuildFails();
+    partialReadFails();
+  }
+
+  private static void partialBuildFails() {
+    Simple.Builder builder = Simple.newBuilder();
+    builder.setId(32);
+    try {
+      builder.build();
+    } catch (UninitializedMessageException e) {
+      System.out.println("got exception");
+    }
+  }
+
+  private static void partialReadFails() throws IOException {
+    ByteArrayInputStream input = new ByteArrayInputStream(WITH_MISSING_FIELD);
+    Simple.Builder builder = Simple.newBuilder();
+    builder.mergeDelimitedFrom(input);
+    try {
+      builder.build();
+    } catch (UninitializedMessageException e) {
+      System.out.println("got exception");
+    }
+  }
+
+  private static void readProtoWithAllReqFields() throws IOException {
+    ByteArrayInputStream input = new ByteArrayInputStream(WITH_REQUIRED_FIELDS);
+    Simple.Builder builder = Simple.newBuilder();
+    builder.mergeDelimitedFrom(input);
+    Simple simple = builder.build();
+    ByteArrayOutputStream output = new ByteArrayOutputStream(WITH_REQUIRED_FIELDS.length);
+    simple.writeDelimitedTo(output);
+    System.out.println(isContained(WITH_REQUIRED_FIELDS, output.toByteArray()));
+  }
+
+  // After shaking, the serialized proto will no longer contain fields that are not referenced.
+  private static boolean isContained(byte[] fullBytes, byte[] reducedBytes) {
+    int j = 1;
+    for (int i = 1; i < fullBytes.length && j < reducedBytes.length; i++) {
+      if (fullBytes[i] == reducedBytes[j]) {
+        j++;
+      }
+    }
+    return j == reducedBytes.length;
+  }
+}
diff --git a/src/test/examples/simpleproto3/keep-rules.txt b/src/test/examples/simpleproto3/keep-rules.txt
new file mode 100644
index 0000000..186e9f8
--- /dev/null
+++ b/src/test/examples/simpleproto3/keep-rules.txt
@@ -0,0 +1,12 @@
+# Copyright (c) 2017, 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.
+
+# Keep the application entry point. Get rid of everything that is not
+# reachable from there.
+-keep public class simpleproto3.Simpleproto {
+  public static void main(...);
+}
+
+# allow access modification to enable minification
+-allowaccessmodification
diff --git a/src/test/examples/simpleproto3/simple.proto b/src/test/examples/simpleproto3/simple.proto
new file mode 100644
index 0000000..87512d1
--- /dev/null
+++ b/src/test/examples/simpleproto3/simple.proto
@@ -0,0 +1,14 @@
+// Copyright (c) 2016, 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.
+syntax = "proto3";
+package simpleproto3;
+
+option java_outer_classname = "GeneratedSimpleProto";
+
+message Simple {
+  int32 id = 1;
+  float unusedRequired = 2;
+  bool other = 3;
+}
+
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10DeployJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10DeployJarVerificationTest.java
index afa437e..ca713fd 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10DeployJarVerificationTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10DeployJarVerificationTest.java
@@ -9,27 +9,22 @@
 import com.android.tools.r8.R8RunArtTestsTest.CompilerUnderTest;
 import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.InternalOptions;
 import java.io.IOException;
 import java.util.concurrent.ExecutionException;
 import org.junit.Test;
 
 public class R8GMSCoreV10DeployJarVerificationTest extends GMSCoreDeployJarVerificationTest {
 
-  private void configureDeterministic(InternalOptions options) {
-    options.removeSwitchMaps = false;
-  }
-
   @Test
   public void buildFromDeployJar()
       // TODO(tamaskenez): set hasReference = true when we have the noshrink file for V10
       throws ExecutionException, IOException, ProguardRuleParserException, CompilationException {
     AndroidApp app1 = buildFromDeployJar(
         CompilerUnderTest.R8, CompilationMode.RELEASE,
-        GMSCoreCompilationTestBase.GMSCORE_V10_DIR, false, this::configureDeterministic);
+        GMSCoreCompilationTestBase.GMSCORE_V10_DIR, false);
     AndroidApp app2 = buildFromDeployJar(
         CompilerUnderTest.R8, CompilationMode.RELEASE,
-        GMSCoreCompilationTestBase.GMSCORE_V10_DIR, false, this::configureDeterministic);
+        GMSCoreCompilationTestBase.GMSCORE_V10_DIR, false);
 
     // Verify that the result of the two compilations was the same.
     assertIdenticalApplications(app1, app2);
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10TreeShakeJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10TreeShakeJarVerificationTest.java
index 2640f13..766de9d 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10TreeShakeJarVerificationTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10TreeShakeJarVerificationTest.java
@@ -20,7 +20,6 @@
 
   private void configureDeterministic(InternalOptions options) {
     options.skipMinification = true;
-    options.removeSwitchMaps = false;
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/rewrite/switchmaps/RewriteSwitchMapsTest.java b/src/test/java/com/android/tools/r8/rewrite/switchmaps/RewriteSwitchMapsTest.java
index 646a481..4e8881a 100644
--- a/src/test/java/com/android/tools/r8/rewrite/switchmaps/RewriteSwitchMapsTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/switchmaps/RewriteSwitchMapsTest.java
@@ -13,7 +13,6 @@
 import java.nio.file.Paths;
 import java.util.concurrent.ExecutionException;
 import org.junit.Assert;
-import org.junit.Ignore;
 import org.junit.Test;
 
 public class RewriteSwitchMapsTest extends TestBase {
@@ -24,8 +23,6 @@
       "-keep class switchmaps.Switches { public static void main(...); } " +
           "-dontobfuscate";
 
-  // TODO(sgjesse): Re-enable this when the switch-map extraction has been fixed.
-  @Ignore
   @Test
   public void checkSwitchMapsRemoved()
       throws IOException, ProguardRuleParserException, ExecutionException, CompilationException {
diff --git a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
index 3435577..99be74a 100644
--- a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
@@ -372,6 +372,80 @@
         subclass.method("double", "anotherMethod", ImmutableList.of("double")).isPresent());
   }
 
+  private static void simpleproto1UnusedFieldIsGone(DexInspector inspector) {
+    ClassSubject protoClass = inspector.clazz("simpleproto1.GeneratedSimpleProto$Simple");
+    Assert.assertTrue(protoClass.isPresent());
+    Assert.assertFalse(protoClass.field("boolean", "other_").isPresent());
+  }
+
+  private static void simpleproto2UnusedFieldsAreGone(DexInspector inspector) {
+    ClassSubject protoClass = inspector.clazz("simpleproto2.GeneratedSimpleProto$Simple");
+    Assert.assertTrue(protoClass.isPresent());
+    Assert.assertFalse(protoClass.field("int", "id_").isPresent());
+    Assert.assertFalse(protoClass.field("float", "hasMe_").isPresent());
+    Assert.assertFalse(protoClass.field("int", "other_").isPresent());
+  }
+
+  private static void nestedproto1UnusedFieldsAreGone(DexInspector inspector) {
+    ClassSubject protoClass = inspector.clazz("nestedproto1.GeneratedNestedProto$Outer");
+    Assert.assertTrue(protoClass.isPresent());
+    Assert.assertFalse(protoClass.field("int", "id_").isPresent());
+    Assert.assertTrue(
+        protoClass.field("nestedproto1.GeneratedNestedProto$NestedOne", "inner_").isPresent());
+    Assert.assertFalse(
+        protoClass.field("nestedproto1.GeneratedNestedProto$NestedTwo", "inner2_").isPresent());
+    ClassSubject nestedOne = inspector.clazz("nestedproto1.GeneratedNestedProto$NestedOne");
+    Assert.assertTrue(nestedOne.isPresent());
+    Assert.assertTrue(nestedOne.field("java.lang.String", "other_").isPresent());
+    Assert.assertFalse(nestedOne.field("int", "id_").isPresent());
+    Assert.assertFalse(inspector.clazz("nestedproto1.GeneratedNestedProto$NestedTwo").isPresent());
+  }
+
+  private static void nestedproto2UnusedFieldsAreGone(DexInspector inspector) {
+    ClassSubject protoClass = inspector.clazz("nestedproto2.GeneratedNestedProto$Outer");
+    Assert.assertTrue(protoClass.isPresent());
+    Assert.assertTrue(protoClass.field("int", "id_").isPresent());
+    Assert.assertFalse(
+        protoClass.field("nestedproto2.GeneratedNestedProto$NestedOne", "inner_").isPresent());
+    Assert.assertFalse(
+        protoClass.field("nestedproto2.GeneratedNestedProto$NestedTwo", "inner2_").isPresent());
+    Assert.assertFalse(inspector.clazz("nestedproto2.GeneratedNestedProto$NestedOne").isPresent());
+    Assert.assertFalse(inspector.clazz("nestedproto2.GeneratedNestedProto$NestedTwo").isPresent());
+  }
+
+
+  private static void enumprotoUnusedFieldsAreGone(DexInspector inspector) {
+    ClassSubject protoClass = inspector.clazz("enumproto.GeneratedEnumProto$Enum");
+    Assert.assertTrue(protoClass.isPresent());
+    Assert.assertFalse(protoClass.field("int", "id_").isPresent());
+    Assert.assertTrue(protoClass.field("int", "enum_").isPresent());
+    Assert.assertFalse(protoClass.field("int", "other_").isPresent());
+    ClassSubject protoThreeClass = inspector.clazz("enumproto.three.GeneratedEnumProto$EnumThree");
+    Assert.assertTrue(protoThreeClass.isPresent());
+    Assert.assertFalse(protoThreeClass.field("int", "id_").isPresent());
+    Assert.assertTrue(protoThreeClass.field("int", "enum_").isPresent());
+    Assert.assertFalse(protoThreeClass.field("int", "other_").isPresent());
+  }
+
+  private static void repeatedUnusedFieldsAreGone(DexInspector inspector) {
+    ClassSubject protoClass = inspector.clazz("repeatedproto.GeneratedRepeatedProto$Repeated");
+    Assert.assertTrue(protoClass.isPresent());
+    Assert.assertFalse(protoClass.field("int", "id_").isPresent());
+    Assert.assertTrue(
+        protoClass.field("com.google.protobuf.Internal$ProtobufList", "repeated_").isPresent());
+    Assert.assertFalse(
+        protoClass.field("com.google.protobuf.Internal$ProtobufList", "sub_").isPresent());
+    Assert.assertFalse(
+        protoClass.field("com.google.protobuf.Internal$BooleanList", "other_").isPresent());
+  }
+
+  private static void oneofprotoUnusedFieldsAreGone(DexInspector inspector) {
+    ClassSubject protoClass = inspector.clazz("oneofproto.GeneratedOneOfProto$Oneof");
+    Assert.assertTrue(protoClass.isPresent());
+    Assert.assertFalse(protoClass.field("int", "id_").isPresent());
+    Assert.assertFalse(protoClass.field("Object", "otherfields_").isPresent());
+  }
+
   private static List<String> names =
       ImmutableList.of("pqr", "vw$", "abc", "def", "stu", "ghi", "jkl", "ea", "xyz_", "mno");
 
@@ -557,7 +631,15 @@
             "assumevalues5",
             "annotationremoval",
             "memberrebinding2",
-            "memberrebinding3");
+            "memberrebinding3",
+            "simpleproto1",
+            "simpleproto2",
+            "simpleproto3",
+            "nestedproto1",
+            "nestedproto2",
+            "enumproto",
+            "repeatedproto",
+            "oneofproto");
 
     // Keys can be the name of the test or the name of the test followed by a colon and the name
     // of the keep file.
@@ -614,6 +696,20 @@
     inspections
         .put("annotationremoval:keep-rules-keep-innerannotation.txt",
             TreeShakingTest::annotationRemovalHasAllInnerClassAnnotations);
+    inspections
+        .put("simpleproto1:keep-rules.txt", TreeShakingTest::simpleproto1UnusedFieldIsGone);
+    inspections
+        .put("simpleproto2:keep-rules.txt", TreeShakingTest::simpleproto2UnusedFieldsAreGone);
+    inspections
+        .put("nestedproto1:keep-rules.txt", TreeShakingTest::nestedproto1UnusedFieldsAreGone);
+    inspections
+        .put("nestedproto2:keep-rules.txt", TreeShakingTest::nestedproto2UnusedFieldsAreGone);
+    inspections
+        .put("enumproto:keep-rules.txt", TreeShakingTest::enumprotoUnusedFieldsAreGone);
+    inspections
+        .put("repeatedproto:keep-rules.txt", TreeShakingTest::repeatedUnusedFieldsAreGone);
+    inspections
+        .put("oneofproto:keep-rules.txt", TreeShakingTest::oneofprotoUnusedFieldsAreGone);
 
     // Keys can be the name of the test or the name of the test followed by a colon and the name
     // of the keep file.
@@ -779,7 +875,7 @@
           Collections.singletonList(generated.toString()), mainClass, extraArtArgs, null);
       outputComparator.accept(output1, output2);
     } else {
-      ToolHelper.checkArtOutputIdentical(Collections.singletonList(originalDex),
+      String output = ToolHelper.checkArtOutputIdentical(Collections.singletonList(originalDex),
           Collections.singletonList(generated.toString()), mainClass,
           extraArtArgs, null);
     }