Simple class staticizer

Implementation of simple class staticizer, handles most of Kotlin
companion objects, simple cases of regular Kotlin objects as well as
regular singleton-like utility classes written in java and complying
with our requirements.

In general the optimization is performed in three phases:

  * Select potential candidates for staticizing
  * Check all code usages against criteria we expect
  * Staticize (change classes, fixup references)

I'll be sending a doc with more detailed description. See also
comments in the code for more details.

GmsCore:
  no visible compilation time impact
  36 classes staticized
  -4.3K

Tachiyomi:
  54 classes staticized
  -4.6K

Bug: 70156467
Change-Id: I45bf9ea6e709b58f72b2af86db61933dca659d0d
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 327d206..2a7b4c1 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -159,6 +159,7 @@
       options.enableMinification = false;
       options.enableInlining = false;
       options.enableClassInlining = false;
+      options.enableClassStaticizer = false;
       options.outline.enabled = false;
 
       DexApplication app = new ApplicationReader(inputApp, options, timing).read(executor);
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 8c6c02b..99b0a1e 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -237,6 +237,8 @@
     internal.enableInlining = false;
     assert internal.enableClassInlining;
     internal.enableClassInlining = false;
+    assert internal.enableClassStaticizer;
+    internal.enableClassStaticizer = false;
     assert internal.enableSwitchMapRemoval;
     internal.enableSwitchMapRemoval = false;
     assert internal.outline.enabled;
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index bca3e00..0f663c4 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -396,6 +396,7 @@
             new IRConverter(
                 appView.getAppInfo(), options, timing, printer, appView.getGraphLense());
         application = converter.optimize(application, executorService);
+        appView.setGraphLense(converter.getGraphLense());
       } finally {
         timing.end();
       }
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index f0ae9e0..4eb4b37 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -571,6 +571,7 @@
       // TODO(zerny): Should we support inlining in debug mode? b/62937285
       internal.enableInlining = false;
       internal.enableClassInlining = false;
+      internal.enableClassStaticizer = false;
       // TODO(zerny): Should we support outlining in debug mode? b/62937285
       internal.outline.enabled = false;
     }
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfo.java b/src/main/java/com/android/tools/r8/graph/AppInfo.java
index 0ef8a34..c29453a 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfo.java
@@ -528,7 +528,7 @@
     return false;
   }
 
-  private static boolean canTriggerStaticInitializer(DexClass clazz) {
+  public static boolean canTriggerStaticInitializer(DexClass clazz) {
     // Assume it *may* trigger if we didn't find the definition.
     return clazz == null || clazz.hasClassInitializer();
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexCode.java b/src/main/java/com/android/tools/r8/graph/DexCode.java
index 4a0a483..d0c40e1 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -62,6 +62,17 @@
     hashCode();  // Cache the hash code eagerly.
   }
 
+  public DexCode withoutThisParameter() {
+    // Note that we assume the original code has a register associated with 'this'
+    // argument of the (former) instance method. We also assume (but do not check)
+    // that 'this' register is never used, so when we decrease incoming register size
+    // by 1, it becomes just a regular register which is never used, and thus will be
+    // gone when we build an IR from this code. Rebuilding IR for methods 'staticized'
+    // this way is highly recommended to improve register allocation.
+    return new DexCode(registerSize, incomingRegisterSize - 1, outgoingRegisterSize,
+        instructions, tries, handlers, debugInfoWithoutFirstParameter(), highestSortingString);
+  }
+
   @Override
   public boolean isDexCode() {
     return true;
@@ -107,6 +118,19 @@
     return new DexDebugInfo(debugInfo.startLine, newParameters, debugInfo.events);
   }
 
+  public DexDebugInfo debugInfoWithoutFirstParameter() {
+    if (debugInfo == null) {
+      return null;
+    }
+    DexString[] parameters = debugInfo.parameters;
+    if(parameters.length == 0) {
+      return debugInfo;
+    }
+    DexString[] newParameters = new DexString[parameters.length - 1];
+    System.arraycopy(parameters, 1, newParameters, 0, parameters.length - 1);
+    return new DexDebugInfo(debugInfo.startLine, newParameters, debugInfo.events);
+  }
+
   public int codeSizeInBytes() {
     Instruction last = instructions[instructions.length - 1];
     return last.getOffset() + last.getSize();
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index cf1a105..e90839c 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -24,6 +24,7 @@
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.dex.JumboStringRewriter;
 import com.android.tools.r8.dex.MixedSectionCollection;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfo.ResolutionResult;
 import com.android.tools.r8.graph.ParameterUsagesInfo.ParameterUsage;
 import com.android.tools.r8.ir.code.IRCode;
@@ -541,6 +542,14 @@
     return builder.build();
   }
 
+  public DexEncodedMethod toStaticMethodWithoutThis() {
+    assert !accessFlags.isStatic();
+    Builder builder = builder(this);
+    builder.setStatic();
+    builder.withoutThisParameter();
+    return builder.build();
+  }
+
   /**
    * Rewrites the code in this method to have JumboString bytecode if required by mapping.
    * <p>
@@ -658,6 +667,7 @@
       forceInline = template.forceInline;
       useIdentifierNameString = template.useIdentifierNameString;
       checksNullReceiverBeforeAnySideEffect = template.checksNullReceiverBeforeAnySideEffect;
+      trivialInitializerInfo = template.trivialInitializerInfo;
     }
 
     public void setParameterUsages(ParameterUsagesInfo parametersUsages) {
@@ -928,6 +938,19 @@
       this.method = method;
     }
 
+    public void setStatic() {
+      this.accessFlags.setStatic();
+    }
+
+    public void withoutThisParameter() {
+      assert code != null;
+      if (code.isDexCode()) {
+        code = code.asDexCode().withoutThisParameter();
+      } else {
+        throw new Unreachable("Code " + code.getClass().getSimpleName() + " is not supported.");
+      }
+    }
+
     public void setCode(Code code) {
       this.code = code;
     }
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 02d2898..006da92 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -118,6 +118,10 @@
     return this == other || isStrictSubtypeOf(other, appInfo);
   }
 
+  public boolean hasSubtypes() {
+    return !directSubtypes.isEmpty();
+  }
+
   public boolean isStrictSubtypeOf(DexType other, AppInfo appInfo) {
     if (this == other) {
       return false;
diff --git a/src/main/java/com/android/tools/r8/ir/code/Goto.java b/src/main/java/com/android/tools/r8/ir/code/Goto.java
index cd5fcc8..80a76fe 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Goto.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Goto.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.utils.CfgPrinter;
 import java.util.List;
+import java.util.ListIterator;
 
 public class Goto extends JumpInstruction {
 
@@ -103,6 +104,18 @@
     // Nothing to do.
   }
 
+  public boolean isTrivialGotoToTheNextBlock(IRCode code) {
+    BasicBlock thisBlock = getBlock();
+    ListIterator<BasicBlock> blockIterator = code.blocks.listIterator();
+    while (blockIterator.hasNext()) {
+      BasicBlock block = blockIterator.next();
+      if (thisBlock == block) {
+        return blockIterator.hasNext() && blockIterator.next() == getTarget();
+      }
+    }
+    return false;
+  }
+
   @Override
   public void buildCf(CfBuilder builder) {
     builder.add(new CfGoto(builder.getLabel(getTarget())));
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index d9bf8d0..deae051 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -266,6 +266,14 @@
     block = null;
   }
 
+  public void removeOrReplaceByDebugLocalRead() {
+    getBlock().listIterator(this).removeOrReplaceByDebugLocalRead();
+  }
+
+  public void replace(Instruction newInstruction) {
+    getBlock().listIterator(this).replaceCurrentInstruction(newInstruction);
+  }
+
   /**
    * Returns true if the instruction is in the IR and therefore has a block.
    */
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 f523b46..af98f4b 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
@@ -50,6 +50,7 @@
 import com.android.tools.r8.ir.optimize.RedundantFieldLoadElimination;
 import com.android.tools.r8.ir.optimize.classinliner.ClassInliner;
 import com.android.tools.r8.ir.optimize.lambda.LambdaMerger;
+import com.android.tools.r8.ir.optimize.staticizer.ClassStaticizer;
 import com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator;
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
 import com.android.tools.r8.kotlin.KotlinInfo;
@@ -94,9 +95,10 @@
   private final InterfaceMethodRewriter interfaceMethodRewriter;
   private final LambdaMerger lambdaMerger;
   private final ClassInliner classInliner;
+  private final ClassStaticizer classStaticizer;
   private final InternalOptions options;
   private final CfgPrinter printer;
-  private final GraphLense graphLense;
+  private GraphLense graphLense;
   private final CodeRewriter codeRewriter;
   private final MemberValuePropagation memberValuePropagation;
   private final LensCodeRewriter lensCodeRewriter;
@@ -144,7 +146,7 @@
     if (enableWholeProgramOptimizations) {
       assert appInfo.hasLiveness();
       this.nonNullTracker = new NonNullTracker();
-      this.inliner = new Inliner(appInfo.withLiveness(), graphLense, options);
+      this.inliner = new Inliner(this, options);
       this.outliner = new Outliner(appInfo.withLiveness(), options);
       this.memberValuePropagation =
           options.enableValuePropagation ?
@@ -182,6 +184,17 @@
             ? new ClassInliner(
             appInfo.dexItemFactory, lambdaRewriter, options.classInliningInstructionLimit)
             : null;
+    this.classStaticizer = options.enableClassStaticizer && appInfo.hasLiveness()
+        ? new ClassStaticizer(appInfo.withLiveness(), this) : null;
+  }
+
+  public void setGraphLense(GraphLense graphLense) {
+    assert graphLense != null;
+    this.graphLense = graphLense;
+  }
+
+  public GraphLense getGraphLense() {
+    return graphLense;
   }
 
   /**
@@ -257,6 +270,18 @@
     }
   }
 
+  private void staticizeClasses(OptimizationFeedback feedback) {
+    if (classStaticizer != null) {
+      classStaticizer.staticizeCandidates(feedback);
+    }
+  }
+
+  private void collectStaticizerCandidates(DexApplication application) {
+    if (classStaticizer != null) {
+      classStaticizer.collectCandidates(application);
+    }
+  }
+
   private void desugarInterfaceMethods(
       Builder<?> builder, InterfaceMethodRewriter.Flavor includeAllResources) {
     if (interfaceMethodRewriter != null) {
@@ -408,6 +433,7 @@
       throws ExecutionException {
     removeLambdaDeserializationMethods();
     collectLambdaMergingCandidates(application);
+    collectStaticizerCandidates(application);
 
     // The process is in two phases.
     // 1) Subject all DexEncodedMethods to optimization (except outlining).
@@ -438,6 +464,8 @@
     Builder<?> builder = application.builder();
     builder.setHighestSortingString(highestSortingString);
 
+    staticizeClasses(directFeedback);
+
     // Second inlining pass for dealing with double inline callers.
     if (inliner != null) {
       // Use direct feedback still, since methods after inlining may
@@ -676,6 +704,11 @@
       }
     }
 
+    if (classStaticizer != null) {
+      classStaticizer.fixupMethodCode(method, code);
+      assert code.isConsistentSSA();
+    }
+
     if (identifierNameStringMarker != null) {
       identifierNameStringMarker.decoupleIdentifierNameStringsInMethod(method, code);
       assert code.isConsistentSSA();
@@ -802,6 +835,9 @@
       codeRewriter.identifyTrivialInitializer(method, code, feedback);
     }
     codeRewriter.identifyParameterUsages(method, code, feedback);
+    if (classStaticizer != null) {
+      classStaticizer.examineMethodCode(method, code);
+    }
 
     if (options.canHaveNumberConversionRegisterAllocationBug()) {
       codeRewriter.workaroundNumberConversionRegisterAllocationBug(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 d87a508..a8059bd 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
@@ -953,6 +953,14 @@
         continue;
       }
 
+      if (insn.isGoto()) {
+        // Trivial goto to the next block.
+        if (insn.asGoto().isTrivialGotoToTheNextBlock(code)) {
+          continue;
+        }
+        return null;
+      }
+
       // Other instructions make the instance initializer not eligible.
       return null;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index 1cbd2d9..04cd083 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -43,8 +43,8 @@
 public class Inliner {
   private static final int INITIAL_INLINING_INSTRUCTION_ALLOWANCE = 1500;
 
+  private final IRConverter converter;
   protected final AppInfoWithLiveness appInfo;
-  private final GraphLense graphLense;
   final InternalOptions options;
 
   // State for inlining methods which are known to be called twice.
@@ -55,12 +55,9 @@
 
   private final Set<DexMethod> blackList = Sets.newIdentityHashSet();
 
-  public Inliner(
-      AppInfoWithLiveness appInfo,
-      GraphLense graphLense,
-      InternalOptions options) {
-    this.appInfo = appInfo;
-    this.graphLense = graphLense;
+  public Inliner(IRConverter converter, InternalOptions options) {
+    this.converter = converter;
+    this.appInfo = converter.appInfo.withLiveness();
     this.options = options;
     fillInBlackList(appInfo);
   }
@@ -588,11 +585,11 @@
             }
             assert invokePosition.callerPosition == null
                 || invokePosition.getOutermostCaller().method
-                    == graphLense.getOriginalMethodSignature(method.method);
+                    == converter.getGraphLense().getOriginalMethodSignature(method.method);
 
             IRCode inlinee =
-                result.buildInliningIR(
-                    code.valueNumberGenerator, appInfo, graphLense, options, invokePosition);
+                result.buildInliningIR(code.valueNumberGenerator,
+                    appInfo, converter.getGraphLense(), options, invokePosition);
             if (inlinee != null) {
               // TODO(64432527): Get rid of this additional check by improved inlining.
               if (block.hasCatchHandlers() && inlinee.computeNormalExitBlocks().isEmpty()) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java
new file mode 100644
index 0000000..9b5af03
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java
@@ -0,0 +1,628 @@
+// Copyright (c) 2018, 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.staticizer;
+
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
+import com.android.tools.r8.ir.code.NewInstance;
+import com.android.tools.r8.ir.code.StaticGet;
+import com.android.tools.r8.ir.code.StaticPut;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.ir.conversion.OptimizationFeedback;
+import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiConsumer;
+
+public final class ClassStaticizer {
+  final AppInfoWithLiveness appInfo;
+  final DexItemFactory factory;
+  final IRConverter converter;
+
+  private enum Phase {
+    None, Examine, Fixup
+  }
+
+  private Phase phase = Phase.None;
+  private BiConsumer<DexEncodedMethod, IRCode> fixupStrategy = null;
+
+  // Represents a staticizing candidate with all information
+  // needed for staticizing.
+  final class CandidateInfo {
+    final DexProgramClass candidate;
+    final DexEncodedField singletonField;
+    final AtomicBoolean preserveRead = new AtomicBoolean(false);
+    // Number of singleton field writes.
+    final AtomicInteger fieldWrites = new AtomicInteger();
+    // Number of instances created.
+    final AtomicInteger instancesCreated = new AtomicInteger();
+    final Set<DexEncodedMethod> referencedFrom = Sets.newConcurrentHashSet();
+    final AtomicReference<DexEncodedMethod> constructor = new AtomicReference<>();
+
+    CandidateInfo(DexProgramClass candidate, DexEncodedField singletonField) {
+      assert candidate != null;
+      assert singletonField != null;
+      this.candidate = candidate;
+      this.singletonField = singletonField;
+
+      // register itself
+      candidates.put(candidate.type, this);
+    }
+
+    boolean isHostClassInitializer(DexEncodedMethod method) {
+      return factory.isClassConstructor(method.method) && method.method.holder == hostType();
+    }
+
+    DexType hostType() {
+      return singletonField.field.clazz;
+    }
+
+    DexClass hostClass() {
+      DexClass hostClass = appInfo.definitionFor(hostType());
+      assert hostClass != null;
+      return hostClass;
+    }
+
+    CandidateInfo invalidate() {
+      candidates.remove(candidate.type);
+      return null;
+    }
+  }
+
+  // The map storing all the potential candidates for staticizing.
+  final ConcurrentHashMap<DexType, CandidateInfo> candidates = new ConcurrentHashMap<>();
+
+  public ClassStaticizer(AppInfoWithLiveness appInfo, IRConverter converter) {
+    this.appInfo = appInfo;
+    this.factory = appInfo.dexItemFactory;
+    this.converter = converter;
+  }
+
+  // Before doing any usage-based analysis we collect a set of classes that can be
+  // candidates for staticizing. This analysis is very simple, but minimizes the
+  // set of eligible classes staticizer tracks and thus time and memory it needs.
+  public final void collectCandidates(DexApplication app) {
+    Set<DexType> notEligible = Sets.newIdentityHashSet();
+    Map<DexType, DexEncodedField> singletonFields = new HashMap<>();
+
+    app.classes().forEach(cls -> {
+      // We only consider classes eligible for staticizing if there is just
+      // one single static field in the whole app which has a type of this
+      // class. This field will be considered to be a candidate for a singleton
+      // field. The requirements for the initialization of this field will be
+      // checked later.
+      for (DexEncodedField field : cls.staticFields()) {
+        DexType type = field.field.type;
+        if (singletonFields.put(type, field) != null) {
+          // There is already candidate singleton field found.
+          notEligible.add(type);
+        }
+      }
+
+      // Don't allow fields with this candidate types.
+      for (DexEncodedField field : cls.instanceFields()) {
+        notEligible.add(field.field.type);
+      }
+
+      // Let's also assume no methods should take or return a
+      // value of this type.
+      for (DexEncodedMethod method : cls.methods()) {
+        DexProto proto = method.method.proto;
+        notEligible.add(proto.returnType);
+        notEligible.addAll(Arrays.asList(proto.parameters.values));
+      }
+
+      // High-level limitations on what classes we consider eligible.
+      if (cls.isInterface() || // Must not be an interface or an abstract class.
+          cls.accessFlags.isAbstract() ||
+          // Don't support candidates with instance fields
+          cls.instanceFields().length > 0 ||
+          // Only support classes directly extending java.lang.Object
+          cls.superType != factory.objectType ||
+          // Instead of requiring the class being final,
+          // just ensure it does not have subtypes
+          cls.type.hasSubtypes() ||
+          // Staticizing classes implementing interfaces is more
+          // difficult, so don't support it until we really need it.
+          !cls.interfaces.isEmpty()) {
+        notEligible.add(cls.type);
+      }
+    });
+
+    // Finalize the set of the candidates.
+    app.classes().forEach(cls -> {
+      DexType type = cls.type;
+      if (!notEligible.contains(type)) {
+        DexEncodedField field = singletonFields.get(type);
+        if (field != null && // Singleton field found
+            !field.accessFlags.isVolatile() && // Don't remove volatile fields.
+            !isPinned(cls, field)) { // Don't remove pinned objects.
+          assert field.accessFlags.isStatic();
+          // Note: we don't check that the field is final, since we will analyze
+          //       later how and where it is initialized.
+          new CandidateInfo(cls, field); // will self-register
+        }
+      }
+    });
+
+    // Next phase -- examine code for candidate usages
+    phase = Phase.Examine;
+  }
+
+  private boolean isPinned(DexClass clazz, DexEncodedField singletonField) {
+    if (appInfo.isPinned(clazz.type) || appInfo.isPinned(singletonField.field)) {
+      return true;
+    }
+    for (DexEncodedMethod method : clazz.methods()) {
+      if (!method.isStaticMethod() && appInfo.isPinned(method.method)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  // Check staticizing candidates' usages to ensure the candidate can be staticized.
+  //
+  // The criteria for type CANDIDATE to be eligible for staticizing fall into
+  // these categories:
+  //
+  //  * checking that there is only one instance of the class created, and it is created
+  //    inside the host class initializer, and it is guaranteed that nobody can access this
+  //    field before it is assigned.
+  //
+  //  * no other singleton field writes (except for those used to store the only candidate
+  //    class instance described above) are allowed.
+  //
+  //  * values read from singleton field should only be used for instance method calls.
+  //
+  // NOTE: there are more criteria eligible class needs to satisfy to be actually staticized,
+  // those will be checked later in staticizeCandidates().
+  //
+  // This method also collects all DexEncodedMethod instances that need to be rewritten if
+  // appropriate candidate is staticized. Essentially anything that references instance method
+  // or field defined in the class.
+  //
+  // NOTE: can be called concurrently.
+  public final void examineMethodCode(DexEncodedMethod method, IRCode code) {
+    if (phase != Phase.Examine) {
+      return;
+    }
+
+    Set<Instruction> alreadyProcessed = Sets.newIdentityHashSet();
+
+    CandidateInfo receiverClassCandidateInfo = candidates.get(method.method.holder);
+    Value receiverValue = code.getThis(); // NOTE: is null for static methods.
+    if (receiverClassCandidateInfo != null && receiverValue != null) {
+      // We are inside an instance method of candidate class (not an instance initializer
+      // which we will check later), check if all the references to 'this' are valid
+      // (the call will invalidate the candidate if some of them are not valid).
+      analyzeAllValueUsers(receiverClassCandidateInfo,
+          receiverValue, factory.isConstructor(method.method));
+
+      // If the candidate is still valid, ignore all instructions
+      // we treat as valid usages on receiver.
+      if (candidates.get(method.method.holder) != null) {
+        alreadyProcessed.addAll(receiverValue.uniqueUsers());
+      }
+    }
+
+    ListIterator<Instruction> iterator =
+        Lists.newArrayList(code.instructionIterator()).listIterator();
+    while (iterator.hasNext()) {
+      Instruction instruction = iterator.next();
+      if (alreadyProcessed.contains(instruction)) {
+        continue;
+      }
+
+      if (instruction.isNewInstance()) {
+        // Check the class being initialized against valid staticizing candidates.
+        NewInstance newInstance = instruction.asNewInstance();
+        CandidateInfo candidateInfo = processInstantiation(method, iterator, newInstance);
+        if (candidateInfo != null) {
+          // For host class initializers having eligible instantiation we also want to
+          // ensure that the rest of the initializer consist of code w/o side effects.
+          // This must guarantee that removing field access will not result in missing side
+          // effects, otherwise we can still staticize, but cannot remove singleton reads.
+          while (iterator.hasNext()) {
+            if (!isAllowedInHostClassInitializer(method.method.holder, iterator.next(), code)) {
+              candidateInfo.preserveRead.set(true);
+              iterator.previous();
+              break;
+            }
+            // Ignore just read instruction.
+          }
+          candidateInfo.referencedFrom.add(method);
+        }
+        continue;
+      }
+
+      if (instruction.isStaticPut()) {
+        // Check the field being written to: no writes to singleton fields are allowed
+        // except for those processed in processInstantiation(...).
+        DexType candidateType = instruction.asStaticPut().getField().type;
+        CandidateInfo candidateInfo = candidates.get(candidateType);
+        if (candidateInfo != null) {
+          candidateInfo.invalidate();
+        }
+        continue;
+      }
+
+      if (instruction.isStaticGet()) {
+        // Check the field being read: make sure all usages are valid.
+        CandidateInfo info = processStaticFieldRead(instruction.asStaticGet());
+        if (info != null) {
+          info.referencedFrom.add(method);
+          // If the candidate still valid, ignore all usages in further analysis.
+          Value value = instruction.outValue();
+          if (value != null) {
+            alreadyProcessed.addAll(value.uniqueUsers());
+          }
+        }
+        continue;
+      }
+
+      if (instruction.isInvokeMethodWithReceiver()) {
+        DexMethod invokedMethod = instruction.asInvokeMethodWithReceiver().getInvokedMethod();
+        CandidateInfo candidateInfo = candidates.get(invokedMethod.holder);
+        if (candidateInfo != null) {
+          // A call to instance method of the candidate class we don't know how to deal with.
+          candidateInfo.invalidate();
+        }
+        continue;
+      }
+
+      if (instruction.isInvokeCustom()) {
+        // Just invalidate any candidates referenced from non-static context.
+        CallSiteReferencesInvalidator invalidator = new CallSiteReferencesInvalidator();
+        invalidator.registerCallSite(instruction.asInvokeCustom().getCallSite());
+        continue;
+      }
+
+      if (instruction.isInstanceGet() || instruction.isInstancePut()) {
+        DexField fieldReferenced = instruction.asFieldInstruction().getField();
+        CandidateInfo candidateInfo = candidates.get(fieldReferenced.clazz);
+        if (candidateInfo != null) {
+          // Reads/writes to instance field of the candidate class are not supported.
+          candidateInfo.invalidate();
+        }
+        continue;
+      }
+    }
+  }
+
+  private boolean isAllowedInHostClassInitializer(
+      DexType host, Instruction insn, IRCode code) {
+    return (insn.isStaticPut() && insn.asStaticPut().getField().clazz == host) ||
+        insn.isConstNumber() ||
+        insn.isConstString() ||
+        (insn.isGoto() && insn.asGoto().isTrivialGotoToTheNextBlock(code)) ||
+        insn.isReturn();
+  }
+
+  private CandidateInfo processInstantiation(
+      DexEncodedMethod method, ListIterator<Instruction> iterator, NewInstance newInstance) {
+
+    DexType candidateType = newInstance.clazz;
+    CandidateInfo candidateInfo = candidates.get(candidateType);
+    if (candidateInfo == null) {
+      return null; // Not interested.
+    }
+
+    if (iterator.previousIndex() != 0) {
+      // Valid new instance must be the first instruction in the class initializer
+      return candidateInfo.invalidate();
+    }
+
+    if (!candidateInfo.isHostClassInitializer(method)) {
+      // A valid candidate must only have one instantiation which is
+      // done in the static initializer of the host class.
+      return candidateInfo.invalidate();
+    }
+
+    if (candidateInfo.instancesCreated.incrementAndGet() > 1) {
+      // Only one instance must be ever created.
+      return candidateInfo.invalidate();
+    }
+
+    Value candidateValue = newInstance.dest();
+    if (candidateValue == null) {
+      // Must be assigned to a singleton field.
+      return candidateInfo.invalidate();
+    }
+
+    if (candidateValue.numberOfPhiUsers() > 0) {
+      return candidateInfo.invalidate();
+    }
+
+    if (candidateValue.numberOfUsers() != 2) {
+      // We expect only two users for each instantiation: constructor call and
+      // static field write. We only check count here, since the exact instructions
+      // will be checked later.
+      return candidateInfo.invalidate();
+    }
+
+    // Check usages. Currently we only support the patterns like:
+    //
+    //     static constructor void <clinit>() {
+    //        new-instance v0, <candidate-type>
+    //  (opt) const/4 v1, #int 0 // (optional)
+    //        invoke-direct {v0, ...}, void <candidate-type>.<init>(...)
+    //        sput-object v0, <instance-field>
+    //        ...
+    //        ...
+    //
+    // In case we guarantee candidate constructor does not access <instance-field>
+    // directly or indirectly we can guarantee that all the potential reads get
+    // same non-null value.
+
+    // Skip potential constant instructions
+    while (iterator.hasNext() && isNonThrowingConstInstruction(iterator.next())) {
+      // Intentionally empty.
+    }
+    iterator.previous();
+
+    if (!iterator.hasNext()) {
+      return candidateInfo.invalidate();
+    }
+    if (!isValidInitCall(candidateInfo, iterator.next(), candidateValue, candidateType)) {
+      iterator.previous();
+      return candidateInfo.invalidate();
+    }
+
+    if (!iterator.hasNext()) {
+      return candidateInfo.invalidate();
+    }
+    if (!isValidStaticPut(candidateInfo, iterator.next())) {
+      iterator.previous();
+      return candidateInfo.invalidate();
+    }
+    if (candidateInfo.fieldWrites.incrementAndGet() > 1) {
+      return candidateInfo.invalidate();
+    }
+
+    return candidateInfo;
+  }
+
+  private boolean isNonThrowingConstInstruction(Instruction instruction) {
+    return instruction.isConstInstruction() && !instruction.instructionTypeCanThrow();
+  }
+
+  private boolean isValidInitCall(
+      CandidateInfo info, Instruction instruction, Value candidateValue, DexType candidateType) {
+    if (!instruction.isInvokeDirect()) {
+      return false;
+    }
+
+    // Check constructor.
+    InvokeDirect invoke = instruction.asInvokeDirect();
+    DexEncodedMethod methodInvoked = appInfo.lookupDirectTarget(invoke.getInvokedMethod());
+    List<Value> values = invoke.inValues();
+
+    if (values.lastIndexOf(candidateValue) != 0 ||
+        methodInvoked == null || methodInvoked.method.holder != candidateType) {
+      return false;
+    }
+
+    // Check arguments.
+    for (int i = 1; i < values.size(); i++) {
+      if (!values.get(i).definition.isConstInstruction()) {
+        return false;
+      }
+    }
+
+    DexEncodedMethod previous = info.constructor.getAndSet(methodInvoked);
+    assert previous == null;
+    return true;
+  }
+
+  private boolean isValidStaticPut(CandidateInfo info, Instruction instruction) {
+    if (!instruction.isStaticPut()) {
+      return false;
+    }
+    // Allow single assignment to a singleton field.
+    StaticPut staticPut = instruction.asStaticPut();
+    DexEncodedField fieldAccessed =
+        appInfo.lookupStaticTarget(staticPut.getField().clazz, staticPut.getField());
+    return fieldAccessed == info.singletonField;
+  }
+
+  // Static field get: can be a valid singleton field for a
+  // candidate in which case we should check if all the usages of the
+  // value read are eligible.
+  private CandidateInfo processStaticFieldRead(StaticGet staticGet) {
+    DexField field = staticGet.getField();
+    DexType candidateType = field.type;
+    CandidateInfo candidateInfo = candidates.get(candidateType);
+    if (candidateInfo == null) {
+      return null;
+    }
+
+    assert candidateInfo.singletonField == appInfo.lookupStaticTarget(field.clazz, field)
+        : "Added reference after collectCandidates(...)?";
+
+    Value singletonValue = staticGet.dest();
+    if (singletonValue != null) {
+      candidateInfo = analyzeAllValueUsers(candidateInfo, singletonValue, false);
+    }
+    return candidateInfo;
+  }
+
+  private CandidateInfo analyzeAllValueUsers(
+      CandidateInfo candidateInfo, Value value, boolean ignoreSuperClassInitInvoke) {
+    assert value != null;
+
+    if (value.numberOfPhiUsers() > 0) {
+      return candidateInfo.invalidate();
+    }
+
+    for (Instruction user : value.uniqueUsers()) {
+      if (user.isInvokeVirtual() || user.isInvokeDirect() /* private methods */) {
+        InvokeMethodWithReceiver invoke = user.asInvokeMethodWithReceiver();
+        DexMethod methodReferenced = invoke.getInvokedMethod();
+        if (factory.isConstructor(methodReferenced)) {
+          assert user.isInvokeDirect();
+          if (ignoreSuperClassInitInvoke &&
+              invoke.inValues().lastIndexOf(value) == 0 &&
+              methodReferenced == factory.objectMethods.constructor) {
+            // If we are inside candidate constructor and analyzing usages
+            // of the receiver, we want to ignore invocations of superclass
+            // constructor which will be removed after staticizing.
+            continue;
+          }
+          return candidateInfo.invalidate();
+        }
+        DexEncodedMethod methodInvoked = user.isInvokeDirect()
+            ? appInfo.lookupDirectTarget(methodReferenced)
+            : appInfo.lookupVirtualTarget(methodReferenced.holder, methodReferenced);
+        if (invoke.inValues().lastIndexOf(value) == 0 &&
+            methodInvoked != null && methodInvoked.method.holder == candidateInfo.candidate.type) {
+          continue;
+        }
+      }
+
+      // All other users are not allowed.
+      return candidateInfo.invalidate();
+    }
+
+    return candidateInfo;
+  }
+
+  // Perform staticizing candidates:
+  //
+  //  1. After filtering candidates based on usage, finalize the list of candidates by
+  //  filtering out candidates which don't satisfy the requirements:
+  //
+  //    * there must be one instance of the class
+  //    * constructor of the class used to create this instance must be a trivial one
+  //    * class initializer should only be present if candidate itself is own host
+  //    * no abstract or native instance methods
+  //
+  //  2. Rewrite instance methods of classes being staticized into static ones
+  //  3. Rewrite methods referencing staticized members, also remove instance creation
+  //
+  public final void staticizeCandidates(OptimizationFeedback feedback) {
+    phase = Phase.None; // We are done with processing/examining methods.
+    new StaticizingProcessor(this).run(feedback);
+  }
+
+  public final void fixupMethodCode(DexEncodedMethod method, IRCode code) {
+    if (phase == Phase.Fixup) {
+      assert fixupStrategy != null;
+      fixupStrategy.accept(method, code);
+    }
+  }
+
+  void setFixupStrategy(BiConsumer<DexEncodedMethod, IRCode> strategy) {
+    assert phase == Phase.None;
+    assert strategy != null;
+    phase = Phase.Fixup;
+    fixupStrategy = strategy;
+  }
+
+  void cleanFixupStrategy() {
+    assert phase == Phase.Fixup;
+    assert fixupStrategy != null;
+    phase = Phase.None;
+    fixupStrategy = null;
+  }
+
+  private class CallSiteReferencesInvalidator extends UseRegistry {
+    private boolean registerMethod(DexMethod method) {
+      registerTypeReference(method.holder);
+      registerProto(method.proto);
+      return true;
+    }
+
+    private boolean registerField(DexField field) {
+      registerTypeReference(field.clazz);
+      registerTypeReference(field.type);
+      return true;
+    }
+
+    @Override
+    public boolean registerInvokeVirtual(DexMethod method) {
+      return registerMethod(method);
+    }
+
+    @Override
+    public boolean registerInvokeDirect(DexMethod method) {
+      return registerMethod(method);
+    }
+
+    @Override
+    public boolean registerInvokeStatic(DexMethod method) {
+      return registerMethod(method);
+    }
+
+    @Override
+    public boolean registerInvokeInterface(DexMethod method) {
+      return registerMethod(method);
+    }
+
+    @Override
+    public boolean registerInvokeSuper(DexMethod method) {
+      return registerMethod(method);
+    }
+
+    @Override
+    public boolean registerInstanceFieldWrite(DexField field) {
+      return registerField(field);
+    }
+
+    @Override
+    public boolean registerInstanceFieldRead(DexField field) {
+      return registerField(field);
+    }
+
+    @Override
+    public boolean registerNewInstance(DexType type) {
+      return registerTypeReference(type);
+    }
+
+    @Override
+    public boolean registerStaticFieldRead(DexField field) {
+      return registerField(field);
+    }
+
+    @Override
+    public boolean registerStaticFieldWrite(DexField field) {
+      return registerField(field);
+    }
+
+    @Override
+    public boolean registerTypeReference(DexType type) {
+      CandidateInfo candidateInfo = candidates.get(type);
+      if (candidateInfo != null) {
+        candidateInfo.invalidate();
+      }
+      return true;
+    }
+  }
+}
+
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerGraphLense.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerGraphLense.java
new file mode 100644
index 0000000..4f6c33c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerGraphLense.java
@@ -0,0 +1,39 @@
+// Copyright (c) 2018, 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.staticizer;
+
+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.GraphLense;
+import com.android.tools.r8.graph.GraphLense.NestedGraphLense;
+import com.android.tools.r8.ir.code.Invoke.Type;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableMap;
+
+class ClassStaticizerGraphLense extends NestedGraphLense {
+  ClassStaticizerGraphLense(GraphLense previous, DexItemFactory factory,
+      BiMap<DexField, DexField> fieldMapping, BiMap<DexMethod, DexMethod> methodMapping) {
+    super(ImmutableMap.of(),
+        methodMapping,
+        fieldMapping,
+        fieldMapping.inverse(),
+        methodMapping.inverse(),
+        previous,
+        factory);
+  }
+
+  @Override
+  protected Type mapInvocationType(
+      DexMethod newMethod, DexMethod originalMethod,
+      DexEncodedMethod context, Type type) {
+    if (methodMap.get(originalMethod) == newMethod) {
+      assert type == Type.VIRTUAL || type == Type.DIRECT;
+      return Type.STATIC;
+    }
+    return super.mapInvocationType(newMethod, originalMethod, context, type);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
new file mode 100644
index 0000000..c5993dd
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
@@ -0,0 +1,444 @@
+// Copyright (c) 2018, 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.staticizer;
+
+import com.android.tools.r8.graph.DebugLocalInfo;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.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.InvokeMethodWithReceiver;
+import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.code.MemberType;
+import com.android.tools.r8.ir.code.StaticGet;
+import com.android.tools.r8.ir.code.StaticPut;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.CallSiteInformation;
+import com.android.tools.r8.ir.conversion.OptimizationFeedback;
+import com.android.tools.r8.ir.optimize.Outliner;
+import com.android.tools.r8.ir.optimize.staticizer.ClassStaticizer.CandidateInfo;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Streams;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+final class StaticizingProcessor {
+  private final ClassStaticizer classStaticizer;
+
+  private final Set<DexEncodedMethod> referencingExtraMethods = Sets.newIdentityHashSet();
+  private final Map<DexEncodedMethod, CandidateInfo> hostClassInits = new IdentityHashMap<>();
+  private final Set<DexEncodedMethod> methodsToBeStaticized = Sets.newIdentityHashSet();
+  private final Map<DexField, CandidateInfo> singletonFields = new IdentityHashMap<>();
+  private final Map<DexType, DexType> candidateToHostMapping = new IdentityHashMap<>();
+
+  StaticizingProcessor(ClassStaticizer classStaticizer) {
+    this.classStaticizer = classStaticizer;
+  }
+
+  final void run(OptimizationFeedback feedback) {
+    // Filter out candidates based on the information we collected
+    // while examining methods.
+    finalEligibilityCheck();
+
+    // Prepare interim data.
+    prepareCandidates();
+
+    // Process all host class initializers (only remove instantiations).
+    processMethods(hostClassInits.keySet().stream(), this::removeCandidateInstantiation, feedback);
+
+    // Process instance methods to be staticized (only remove references to 'this').
+    processMethods(methodsToBeStaticized.stream(), this::removeReferencesToThis, feedback);
+
+    // Convert instance methods into static methods with an extra parameter.
+    Set<DexEncodedMethod> staticizedMethods = staticizeMethodSymbols();
+
+    // Process all other methods that may reference singleton fields
+    // and call methods on them. (Note that we exclude the former instance methods,
+    // but include new static methods created as a result of staticizing.
+    Stream<DexEncodedMethod> methods = Streams.concat(
+        referencingExtraMethods.stream(),
+        staticizedMethods.stream(),
+        hostClassInits.keySet().stream());
+    processMethods(methods, this::rewriteReferences, feedback);
+  }
+
+  private void finalEligibilityCheck() {
+    Iterator<Entry<DexType, CandidateInfo>> it =
+        classStaticizer.candidates.entrySet().iterator();
+    while (it.hasNext()) {
+      Entry<DexType, CandidateInfo> entry = it.next();
+      DexType candidateType = entry.getKey();
+      CandidateInfo info = entry.getValue();
+      DexProgramClass candidateClass = info.candidate;
+      DexType candidateHostType = info.hostType();
+      DexEncodedMethod constructorUsed = info.constructor.get();
+
+      int instancesCreated = info.instancesCreated.get();
+      assert instancesCreated == info.fieldWrites.get();
+      assert instancesCreated <= 1;
+      assert (instancesCreated == 0) == (constructorUsed == null);
+
+      // CHECK: One instance, one singleton field, known constructor
+      if (instancesCreated == 0) {
+        // Give up on the candidate, if there are any reads from instance
+        // field the user should read null.
+        it.remove();
+        continue;
+      }
+
+      // CHECK: instance initializer used to create an instance is trivial.
+      // NOTE: Along with requirement that candidate does not have instance
+      // fields this should guarantee that the constructor is empty.
+      assert candidateClass.instanceFields().length == 0;
+      assert constructorUsed.isProcessed();
+      TrivialInitializer trivialInitializer =
+          constructorUsed.getOptimizationInfo().getTrivialInitializerInfo();
+      if (trivialInitializer == null) {
+        it.remove();
+        continue;
+      }
+
+      // CHECK: class initializer should only be present if candidate itself is its own host.
+      DexEncodedMethod classInitializer = candidateClass.getClassInitializer();
+      assert classInitializer != null || candidateType != candidateHostType;
+      if (classInitializer != null && candidateType != candidateHostType) {
+        it.remove();
+        continue;
+      }
+
+      // CHECK: no abstract or native instance methods.
+      if (Streams.stream(candidateClass.methods()).anyMatch(
+          method -> !method.isStaticMethod() &&
+              (method.accessFlags.isAbstract() || method.accessFlags.isNative()))) {
+        it.remove();
+        continue;
+      }
+    }
+  }
+
+  private void prepareCandidates() {
+    Set<DexEncodedMethod> removedInstanceMethods = Sets.newIdentityHashSet();
+
+    for (CandidateInfo candidate : classStaticizer.candidates.values()) {
+      DexProgramClass candidateClass = candidate.candidate;
+      // Host class initializer
+      DexClass hostClass = candidate.hostClass();
+      DexEncodedMethod hostClassInitializer = hostClass.getClassInitializer();
+      assert hostClassInitializer != null;
+      CandidateInfo previous = hostClassInits.put(hostClassInitializer, candidate);
+      assert previous == null;
+
+      // Collect instance methods to be staticized.
+      for (DexEncodedMethod method : candidateClass.methods()) {
+        if (!method.isStaticMethod()) {
+          removedInstanceMethods.add(method);
+          if (!factory().isConstructor(method.method)) {
+            methodsToBeStaticized.add(method);
+          }
+        }
+      }
+      singletonFields.put(candidate.singletonField.field, candidate);
+      referencingExtraMethods.addAll(candidate.referencedFrom);
+    }
+
+    referencingExtraMethods.removeAll(removedInstanceMethods);
+  }
+
+  private void processMethods(Stream<DexEncodedMethod> methods,
+      BiConsumer<DexEncodedMethod, IRCode> strategy, OptimizationFeedback feedback) {
+    classStaticizer.setFixupStrategy(strategy);
+    methods.sorted(DexEncodedMethod::slowCompare).forEach(
+        method -> classStaticizer.converter.processMethod(method, feedback,
+            x -> false, CallSiteInformation.empty(), Outliner::noProcessing));
+    classStaticizer.cleanFixupStrategy();
+  }
+
+  private void removeCandidateInstantiation(DexEncodedMethod method, IRCode code) {
+    CandidateInfo candidateInfo = hostClassInits.get(method);
+    assert candidateInfo != null;
+
+    // Find and remove instantiation and its users.
+    InstructionIterator iterator = code.instructionIterator();
+    while (iterator.hasNext()) {
+      Instruction instruction = iterator.next();
+      if (instruction.isNewInstance() &&
+          instruction.asNewInstance().clazz == candidateInfo.candidate.type) {
+        // Remove all usages
+        // NOTE: requiring (a) the instance initializer to be trivial, (b) not allowing
+        //       candidates with instance fields and (c) requiring candidate to directly
+        //       extend java.lang.Object guarantees that the constructor is actually
+        //       empty and does not need to be inlined.
+        assert candidateInfo.candidate.superType == factory().objectType;
+        assert candidateInfo.candidate.instanceFields().length == 0;
+
+        Value singletonValue = instruction.outValue();
+        assert singletonValue != null;
+        singletonValue.uniqueUsers().forEach(Instruction::removeOrReplaceByDebugLocalRead);
+        instruction.removeOrReplaceByDebugLocalRead();
+        return;
+      }
+    }
+
+    assert false : "Must always be able to find and remove the instantiation";
+  }
+
+  private void removeReferencesToThis(DexEncodedMethod method, IRCode code) {
+    fixupStaticizedValueUsers(code, code.getThis());
+  }
+
+  private void rewriteReferences(DexEncodedMethod method, IRCode code) {
+    // Process all singleton field reads and rewrite their users.
+    List<StaticGet> singletonFieldReads =
+        Streams.stream(code.instructionIterator())
+            .filter(Instruction::isStaticGet)
+            .map(Instruction::asStaticGet)
+            .filter(get -> singletonFields.containsKey(get.getField()))
+            .collect(Collectors.toList());
+
+    singletonFieldReads.forEach(read -> {
+      CandidateInfo candidateInfo = singletonFields.get(read.getField());
+      assert candidateInfo != null;
+      Value value = read.dest();
+      if (value != null) {
+        fixupStaticizedValueUsers(code, value);
+      }
+      if (!candidateInfo.preserveRead.get()) {
+        read.removeOrReplaceByDebugLocalRead();
+      }
+    });
+
+    if (!candidateToHostMapping.isEmpty()) {
+      remapMovedCandidates(code);
+    }
+  }
+
+  // Fixup value usages: rewrites all method calls so that they point to static methods.
+  private void fixupStaticizedValueUsers(IRCode code, Value thisValue) {
+    assert thisValue != null;
+    assert thisValue.numberOfPhiUsers() == 0;
+
+    for (Instruction user : thisValue.uniqueUsers()) {
+      assert user.isInvokeVirtual() || user.isInvokeDirect();
+      InvokeMethodWithReceiver invoke = user.asInvokeMethodWithReceiver();
+      Value newValue = null;
+      Value outValue = invoke.outValue();
+      if (outValue != null) {
+        newValue = code.createValue(outValue.outType());
+        DebugLocalInfo localInfo = outValue.getLocalInfo();
+        if (localInfo != null) {
+          newValue.setLocalInfo(localInfo);
+        }
+      }
+      List<Value> args = invoke.inValues();
+      invoke.replace(new InvokeStatic(
+          invoke.getInvokedMethod(), newValue, args.subList(1, args.size())));
+    }
+
+    assert thisValue.numberOfUsers() == 0;
+  }
+
+  private void remapMovedCandidates(IRCode code) {
+    InstructionIterator it = code.instructionIterator();
+    while (it.hasNext()) {
+      Instruction instruction = it.next();
+
+      if (instruction.isStaticGet()) {
+        StaticGet staticGet = instruction.asStaticGet();
+        DexField field = mapFieldIfMoved(staticGet.getField());
+        if (field != staticGet.getField()) {
+          Value outValue = staticGet.dest();
+          assert outValue != null;
+          it.replaceCurrentInstruction(
+              new StaticGet(
+                  MemberType.fromDexType(field.type),
+                  code.createValue(ValueType.fromDexType(field.type), outValue.getLocalInfo()),
+                  field
+              )
+          );
+        }
+        continue;
+      }
+
+      if (instruction.isStaticPut()) {
+        StaticPut staticPut = instruction.asStaticPut();
+        DexField field = mapFieldIfMoved(staticPut.getField());
+        if (field != staticPut.getField()) {
+          it.replaceCurrentInstruction(
+              new StaticPut(MemberType.fromDexType(field.type), staticPut.inValue(), field)
+          );
+        }
+        continue;
+      }
+
+      if (instruction.isInvokeStatic()) {
+        InvokeStatic invoke = instruction.asInvokeStatic();
+        DexMethod method = invoke.getInvokedMethod();
+        DexType hostType = candidateToHostMapping.get(method.holder);
+        if (hostType != null) {
+          DexMethod newMethod = factory().createMethod(hostType, method.proto, method.name);
+          Value outValue = invoke.outValue();
+          Value newOutValue = method.proto.returnType.isVoidType() ? null
+              : code.createValue(
+                  ValueType.fromDexType(method.proto.returnType),
+                  outValue == null ? null : outValue.getLocalInfo());
+          it.replaceCurrentInstruction(
+              new InvokeStatic(newMethod, newOutValue, invoke.inValues()));
+        }
+        continue;
+      }
+    }
+  }
+
+  private DexField mapFieldIfMoved(DexField field) {
+    DexType hostType = candidateToHostMapping.get(field.clazz);
+    if (hostType != null) {
+      field = factory().createField(hostType, field.type, field.name);
+    }
+    hostType = candidateToHostMapping.get(field.type);
+    if (hostType != null) {
+      field = factory().createField(field.clazz, hostType, field.name);
+    }
+    return field;
+  }
+
+  private Set<DexEncodedMethod> staticizeMethodSymbols() {
+    BiMap<DexMethod, DexMethod> methodMapping = HashBiMap.create();
+    BiMap<DexField, DexField> fieldMapping = HashBiMap.create();
+
+    Set<DexEncodedMethod> staticizedMethods = Sets.newIdentityHashSet();
+    for (CandidateInfo candidate : classStaticizer.candidates.values()) {
+      DexProgramClass candidateClass = candidate.candidate;
+
+      // Move instance methods into static ones.
+      List<DexEncodedMethod> newDirectMethods = new ArrayList<>();
+      for (DexEncodedMethod method : candidateClass.methods()) {
+        if (method.isStaticMethod()) {
+          newDirectMethods.add(method);
+        } else if (!factory().isConstructor(method.method)) {
+          DexEncodedMethod staticizedMethod = method.toStaticMethodWithoutThis();
+          newDirectMethods.add(staticizedMethod);
+          staticizedMethods.add(staticizedMethod);
+          methodMapping.put(method.method, staticizedMethod.method);
+        }
+      }
+      candidateClass.setVirtualMethods(DexEncodedMethod.EMPTY_ARRAY);
+      candidateClass.setDirectMethods(
+          newDirectMethods.toArray(new DexEncodedMethod[newDirectMethods.size()]));
+
+      // Consider moving static members from candidate into host.
+      DexType hostType = candidate.hostType();
+      if (candidateClass.type != hostType) {
+        DexClass hostClass = classStaticizer.appInfo.definitionFor(hostType);
+        assert hostClass != null;
+        if (!classMembersConflict(candidateClass, hostClass)) {
+          // Move all members of the candidate class into host class.
+          moveMembersIntoHost(staticizedMethods,
+              candidateClass, hostType, hostClass, methodMapping, fieldMapping);
+        }
+      }
+    }
+
+    if (!methodMapping.isEmpty() || fieldMapping.isEmpty()) {
+      classStaticizer.converter.setGraphLense(
+          new ClassStaticizerGraphLense(
+              classStaticizer.converter.getGraphLense(),
+              classStaticizer.factory,
+              fieldMapping,
+              methodMapping));
+    }
+    return staticizedMethods;
+  }
+
+  private boolean classMembersConflict(DexClass a, DexClass b) {
+    assert Streams.stream(a.methods()).allMatch(DexEncodedMethod::isStaticMethod);
+    assert a.instanceFields().length == 0;
+    return Stream.of(a.staticFields()).anyMatch(fld -> b.lookupField(fld.field) != null) ||
+        Streams.stream(a.methods()).anyMatch(method -> b.lookupMethod(method.method) != null);
+  }
+
+  private void moveMembersIntoHost(Set<DexEncodedMethod> staticizedMethods,
+      DexProgramClass candidateClass,
+      DexType hostType, DexClass hostClass,
+      BiMap<DexMethod, DexMethod> methodMapping,
+      BiMap<DexField, DexField> fieldMapping) {
+    candidateToHostMapping.put(candidateClass.type, hostType);
+
+    // Process static fields.
+    // Append fields first.
+    if (candidateClass.staticFields().length > 0) {
+      DexEncodedField[] oldFields = hostClass.staticFields();
+      DexEncodedField[] extraFields = candidateClass.staticFields();
+      DexEncodedField[] newFields = new DexEncodedField[oldFields.length + extraFields.length];
+      System.arraycopy(oldFields, 0, newFields, 0, oldFields.length);
+      System.arraycopy(extraFields, 0, newFields, oldFields.length, extraFields.length);
+      hostClass.setStaticFields(newFields);
+    }
+
+    // Fixup field types.
+    DexEncodedField[] staticFields = hostClass.staticFields();
+    for (int i = 0; i < staticFields.length; i++) {
+      DexEncodedField field = staticFields[i];
+      DexField newField = mapCandidateField(field.field, candidateClass.type, hostType);
+      if (newField != field.field) {
+        staticFields[i] = field.toTypeSubstitutedField(newField);
+        fieldMapping.put(field.field, newField);
+      }
+    }
+
+    // Process static methods.
+    if (candidateClass.directMethods().length > 0) {
+      DexEncodedMethod[] oldMethods = hostClass.directMethods();
+      DexEncodedMethod[] extraMethods = candidateClass.directMethods();
+      DexEncodedMethod[] newMethods = new DexEncodedMethod[oldMethods.length + extraMethods.length];
+      System.arraycopy(oldMethods, 0, newMethods, 0, oldMethods.length);
+      for (int i = 0; i < extraMethods.length; i++) {
+        DexEncodedMethod method = extraMethods[i];
+        DexEncodedMethod newMethod = method.toTypeSubstitutedMethod(
+            factory().createMethod(hostType, method.method.proto, method.method.name));
+        newMethods[oldMethods.length + i] = newMethod;
+        staticizedMethods.add(newMethod);
+        staticizedMethods.remove(method);
+        DexMethod originalMethod = methodMapping.inverse().get(method.method);
+        if (originalMethod == null) {
+          methodMapping.put(method.method, newMethod.method);
+        } else {
+          methodMapping.put(originalMethod, newMethod.method);
+        }
+      }
+      hostClass.setDirectMethods(newMethods);
+    }
+  }
+
+  private DexField mapCandidateField(DexField field, DexType candidateType, DexType hostType) {
+    return field.clazz != candidateType && field.type != candidateType ? field
+        : factory().createField(
+            field.clazz == candidateType ? hostType : field.clazz,
+            field.type == candidateType ? hostType : field.type,
+            field.name);
+  }
+
+  private DexItemFactory factory() {
+    return classStaticizer.factory;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index ae99990..d3c9e6d 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -84,6 +84,7 @@
       enableNonNullTracking = false;
       enableInlining = false;
       enableClassInlining = false;
+      enableClassStaticizer = false;
       enableSwitchMapRemoval = false;
       outline.enabled = false;
       enableValuePropagation = false;
@@ -102,6 +103,7 @@
   public boolean enableInlining =
       !Version.isDev() || System.getProperty("com.android.tools.r8.disableinlining") == null;
   public boolean enableClassInlining = true;
+  public boolean enableClassStaticizer = true;
   public int classInliningInstructionLimit = 50;
   public int inliningInstructionLimit = 5;
   public boolean enableSwitchMapRemoval = true;
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
new file mode 100644
index 0000000..9be118c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
@@ -0,0 +1,272 @@
+// Copyright (c) 2018, 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.staticizer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.VmTestRunner;
+import com.android.tools.r8.code.Instruction;
+import com.android.tools.r8.code.InvokeDirect;
+import com.android.tools.r8.code.InvokeStatic;
+import com.android.tools.r8.code.InvokeVirtual;
+import com.android.tools.r8.code.SgetObject;
+import com.android.tools.r8.code.SputObject;
+import com.android.tools.r8.graph.DexCode;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.optimize.staticizer.movetohost.CandidateConflictField;
+import com.android.tools.r8.ir.optimize.staticizer.movetohost.CandidateConflictMethod;
+import com.android.tools.r8.ir.optimize.staticizer.movetohost.CandidateOk;
+import com.android.tools.r8.ir.optimize.staticizer.movetohost.CandidateOkSideEffects;
+import com.android.tools.r8.ir.optimize.staticizer.movetohost.HostConflictField;
+import com.android.tools.r8.ir.optimize.staticizer.movetohost.HostConflictMethod;
+import com.android.tools.r8.ir.optimize.staticizer.movetohost.HostOk;
+import com.android.tools.r8.ir.optimize.staticizer.movetohost.HostOkSideEffects;
+import com.android.tools.r8.ir.optimize.staticizer.movetohost.MoveToHostTestClass;
+import com.android.tools.r8.ir.optimize.staticizer.trivial.Simple;
+import com.android.tools.r8.ir.optimize.staticizer.trivial.SimpleWithGetter;
+import com.android.tools.r8.ir.optimize.staticizer.trivial.SimpleWithParams;
+import com.android.tools.r8.ir.optimize.staticizer.trivial.SimpleWithSideEffects;
+import com.android.tools.r8.ir.optimize.staticizer.trivial.TrivialTestClass;
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Streams;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(VmTestRunner.class)
+public class ClassStaticizerTest extends TestBase {
+  @Test
+  public void testTrivial() throws Exception {
+    byte[][] classes = {
+        ToolHelper.getClassAsBytes(TrivialTestClass.class),
+        ToolHelper.getClassAsBytes(Simple.class),
+        ToolHelper.getClassAsBytes(SimpleWithSideEffects.class),
+        ToolHelper.getClassAsBytes(SimpleWithParams.class),
+        ToolHelper.getClassAsBytes(SimpleWithGetter.class),
+    };
+    AndroidApp app = runR8(buildAndroidApp(classes), TrivialTestClass.class);
+
+    String javaOutput = runOnJava(TrivialTestClass.class);
+    String artOutput = runOnArt(app, TrivialTestClass.class);
+    assertEquals(javaOutput, artOutput);
+
+    CodeInspector inspector = new CodeInspector(app);
+    ClassSubject clazz = inspector.clazz(TrivialTestClass.class);
+
+    assertEquals(
+        Lists.newArrayList(
+            "STATIC: String trivial.Simple.bar(String)",
+            "STATIC: String trivial.Simple.foo()",
+            "STATIC: String trivial.TrivialTestClass.next()"),
+        references(clazz, "testSimple", "void"));
+
+    assertTrue(instanceMethods(inspector.clazz(Simple.class)).isEmpty());
+
+    assertEquals(
+        Lists.newArrayList(
+            "STATIC: String trivial.SimpleWithParams.bar(String)",
+            "STATIC: String trivial.SimpleWithParams.foo()",
+            "STATIC: String trivial.TrivialTestClass.next()"),
+        references(clazz, "testSimpleWithParams", "void"));
+
+    assertTrue(instanceMethods(inspector.clazz(SimpleWithParams.class)).isEmpty());
+
+    assertEquals(
+        Lists.newArrayList(
+            "STATIC: String trivial.SimpleWithSideEffects.bar(String)",
+            "STATIC: String trivial.SimpleWithSideEffects.foo()",
+            "STATIC: String trivial.TrivialTestClass.next()",
+            "trivial.SimpleWithSideEffects trivial.SimpleWithSideEffects.INSTANCE",
+            "trivial.SimpleWithSideEffects trivial.SimpleWithSideEffects.INSTANCE"),
+        references(clazz, "testSimpleWithSideEffects", "void"));
+
+    assertTrue(instanceMethods(inspector.clazz(SimpleWithSideEffects.class)).isEmpty());
+
+    // TODO(b/111832046): add support for singleton instance getters.
+    assertEquals(
+        Lists.newArrayList(
+            "STATIC: String trivial.TrivialTestClass.next()",
+            "VIRTUAL: String trivial.SimpleWithGetter.bar(String)",
+            "VIRTUAL: String trivial.SimpleWithGetter.foo()",
+            "trivial.SimpleWithGetter trivial.SimpleWithGetter.INSTANCE",
+            "trivial.SimpleWithGetter trivial.SimpleWithGetter.INSTANCE"),
+        references(clazz, "testSimpleWithGetter", "void"));
+
+    assertFalse(instanceMethods(inspector.clazz(SimpleWithGetter.class)).isEmpty());
+  }
+
+  @Test
+  public void testMoveToHost() throws Exception {
+    byte[][] classes = {
+        ToolHelper.getClassAsBytes(MoveToHostTestClass.class),
+        ToolHelper.getClassAsBytes(HostOk.class),
+        ToolHelper.getClassAsBytes(CandidateOk.class),
+        ToolHelper.getClassAsBytes(HostOkSideEffects.class),
+        ToolHelper.getClassAsBytes(CandidateOkSideEffects.class),
+        ToolHelper.getClassAsBytes(HostConflictMethod.class),
+        ToolHelper.getClassAsBytes(CandidateConflictMethod.class),
+        ToolHelper.getClassAsBytes(HostConflictField.class),
+        ToolHelper.getClassAsBytes(CandidateConflictField.class),
+    };
+    AndroidApp app = runR8(buildAndroidApp(classes), MoveToHostTestClass.class);
+
+    String javaOutput = runOnJava(MoveToHostTestClass.class);
+    String artOutput = runOnArt(app, MoveToHostTestClass.class);
+    assertEquals(javaOutput, artOutput);
+
+    CodeInspector inspector = new CodeInspector(app);
+    ClassSubject clazz = inspector.clazz(MoveToHostTestClass.class);
+
+    assertEquals(
+        Lists.newArrayList(
+            "STATIC: String movetohost.HostOk.bar(String)",
+            "STATIC: String movetohost.HostOk.foo()",
+            "STATIC: String movetohost.MoveToHostTestClass.next()",
+            "STATIC: String movetohost.MoveToHostTestClass.next()",
+            "STATIC: void movetohost.HostOk.blah(String)"),
+        references(clazz, "testOk", "void"));
+
+    assertFalse(inspector.clazz(CandidateOk.class).isPresent());
+
+    assertEquals(
+        Lists.newArrayList(
+            "STATIC: String movetohost.HostOkSideEffects.bar(String)",
+            "STATIC: String movetohost.HostOkSideEffects.foo()",
+            "STATIC: String movetohost.MoveToHostTestClass.next()",
+            "movetohost.HostOkSideEffects movetohost.HostOkSideEffects.INSTANCE",
+            "movetohost.HostOkSideEffects movetohost.HostOkSideEffects.INSTANCE"),
+        references(clazz, "testOkSideEffects", "void"));
+
+    assertFalse(inspector.clazz(CandidateOkSideEffects.class).isPresent());
+
+    assertEquals(
+        Lists.newArrayList(
+            "DIRECT: void movetohost.HostConflictMethod.<init>()",
+            "STATIC: String movetohost.CandidateConflictMethod.bar(String)",
+            "STATIC: String movetohost.CandidateConflictMethod.foo()",
+            "STATIC: String movetohost.MoveToHostTestClass.next()",
+            "STATIC: String movetohost.MoveToHostTestClass.next()",
+            "VIRTUAL: String movetohost.HostConflictMethod.bar(String)"),
+        references(clazz, "testConflictMethod", "void"));
+
+    assertTrue(inspector.clazz(CandidateConflictMethod.class).isPresent());
+
+    assertEquals(
+        Lists.newArrayList(
+            "DIRECT: void movetohost.HostConflictField.<init>()",
+            "STATIC: String movetohost.CandidateConflictField.bar(String)",
+            "STATIC: String movetohost.CandidateConflictField.foo()",
+            "STATIC: String movetohost.MoveToHostTestClass.next()",
+            "String movetohost.CandidateConflictField.field"),
+        references(clazz, "testConflictField", "void"));
+
+    assertTrue(inspector.clazz(CandidateConflictMethod.class).isPresent());
+  }
+
+  private List<String> instanceMethods(ClassSubject clazz) {
+    assertNotNull(clazz);
+    assertTrue(clazz.isPresent());
+    return Streams.stream(clazz.getDexClass().methods())
+        .filter(method -> !method.isStaticMethod())
+        .map(method -> method.method.toSourceString())
+        .sorted()
+        .collect(Collectors.toList());
+  }
+
+  private List<String> references(
+      ClassSubject clazz, String methodName, String retValue, String... params) {
+    assertNotNull(clazz);
+    assertTrue(clazz.isPresent());
+
+    MethodSignature signature = new MethodSignature(methodName, retValue, params);
+    DexCode code = clazz.method(signature).getMethod().getCode().asDexCode();
+    return Streams.concat(
+        filterInstructionKind(code, SgetObject.class)
+            .map(Instruction::getField)
+            .filter(fld -> isTypeOfInterest(fld.clazz))
+            .map(DexField::toSourceString),
+        filterInstructionKind(code, SputObject.class)
+            .map(Instruction::getField)
+            .filter(fld -> isTypeOfInterest(fld.clazz))
+            .map(DexField::toSourceString),
+        filterInstructionKind(code, InvokeStatic.class)
+            .map(insn -> (InvokeStatic) insn)
+            .map(InvokeStatic::getMethod)
+            .filter(method -> isTypeOfInterest(method.holder))
+            .map(method -> "STATIC: " + method.toSourceString()),
+        filterInstructionKind(code, InvokeVirtual.class)
+            .map(insn -> (InvokeVirtual) insn)
+            .map(InvokeVirtual::getMethod)
+            .filter(method -> isTypeOfInterest(method.holder))
+            .map(method -> "VIRTUAL: " + method.toSourceString()),
+        filterInstructionKind(code, InvokeDirect.class)
+            .map(insn -> (InvokeDirect) insn)
+            .map(InvokeDirect::getMethod)
+            .filter(method -> isTypeOfInterest(method.holder))
+            .map(method -> "DIRECT: " + method.toSourceString()))
+        .map(txt -> txt.replace("java.lang.", ""))
+        .map(txt -> txt.replace("com.android.tools.r8.ir.optimize.staticizer.", ""))
+        .sorted()
+        .collect(Collectors.toList());
+  }
+
+  private boolean isTypeOfInterest(DexType type) {
+    return type.toSourceString().startsWith("com.android.tools.r8.ir.optimize.staticizer");
+  }
+
+  private AndroidApp runR8(AndroidApp app, Class mainClass) throws Exception {
+    AndroidApp compiled =
+        compileWithR8(app, getProguardConfig(mainClass.getCanonicalName()), this::configure);
+
+    // Materialize file for execution.
+    Path generatedDexFile = temp.getRoot().toPath().resolve("classes.jar");
+    compiled.writeToZip(generatedDexFile, OutputMode.DexIndexed);
+
+    // Run with ART.
+    String artOutput = ToolHelper.runArtNoVerificationErrors(
+        generatedDexFile.toString(), mainClass.getCanonicalName());
+
+    // Compare with Java.
+    ProcessResult javaResult = ToolHelper.runJava(
+        ToolHelper.getClassPathForTests(), mainClass.getCanonicalName());
+
+    if (javaResult.exitCode != 0) {
+      System.out.println(javaResult.stdout);
+      System.err.println(javaResult.stderr);
+      fail("JVM failed for: " + mainClass);
+    }
+    assertEquals("JVM and ART output differ", javaResult.stdout, artOutput);
+
+    return compiled;
+  }
+
+  private String getProguardConfig(String main) {
+    return keepMainProguardConfiguration(main)
+        + System.lineSeparator()
+        + "-dontobfuscate"
+        + System.lineSeparator()
+        + "-allowaccessmodification";
+  }
+
+  private void configure(InternalOptions options) {
+    options.enableClassInlining = false;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateConflictField.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateConflictField.java
new file mode 100644
index 0000000..f2108b0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateConflictField.java
@@ -0,0 +1,21 @@
+// Copyright (c) 2018, 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.staticizer.movetohost;
+
+public class CandidateConflictField {
+  public static String field;
+
+  public String foo() {
+    synchronized ("") {
+      return bar("CandidateConflictMethod::foo()");
+    }
+  }
+
+  public String bar(String other) {
+    synchronized ("") {
+      return "CandidateConflictMethod::bar(" + other + ")";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateConflictMethod.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateConflictMethod.java
new file mode 100644
index 0000000..40d0576
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateConflictMethod.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2018, 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.staticizer.movetohost;
+
+public class CandidateConflictMethod {
+  public String foo() {
+    synchronized ("") {
+      return bar("CandidateConflictMethod::foo()");
+    }
+  }
+
+  public String bar(String other) {
+    synchronized ("") {
+      return "CandidateConflictMethod::bar(" + other + ")";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOk.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOk.java
new file mode 100644
index 0000000..daf5647
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOk.java
@@ -0,0 +1,25 @@
+// Copyright (c) 2018, 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.staticizer.movetohost;
+
+public class CandidateOk {
+  public String foo() {
+    synchronized ("") {
+      return bar("CandidateOk::foo()");
+    }
+  }
+
+  public String bar(String other) {
+    synchronized ("") {
+      return "CandidateOk::bar(" + other + ")";
+    }
+  }
+
+  public void blah(String other) {
+    synchronized ("") {
+      System.out.println("CandidateOk::blah(" + other + ")");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOkSideEffects.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOkSideEffects.java
new file mode 100644
index 0000000..2077056
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOkSideEffects.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2018, 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.staticizer.movetohost;
+
+public class CandidateOkSideEffects {
+  public String foo() {
+    synchronized ("") {
+      return bar("CandidateOkSideEffects::foo()");
+    }
+  }
+
+  public String bar(String other) {
+    synchronized ("") {
+      return "CandidateOkSideEffects::bar(" + other + ")";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostConflictField.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostConflictField.java
new file mode 100644
index 0000000..7166b81
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostConflictField.java
@@ -0,0 +1,11 @@
+// Copyright (c) 2018, 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.staticizer.movetohost;
+
+public class HostConflictField {
+  static CandidateConflictField INSTANCE = new CandidateConflictField();
+
+  public String field;
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostConflictMethod.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostConflictMethod.java
new file mode 100644
index 0000000..ad86b35
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostConflictMethod.java
@@ -0,0 +1,15 @@
+// Copyright (c) 2018, 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.staticizer.movetohost;
+
+public class HostConflictMethod {
+  static CandidateConflictMethod INSTANCE = new CandidateConflictMethod();
+
+  public String bar(String other) {
+    synchronized ("") {
+      return "HostConflictMethod::bar(" + other + ")";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostOk.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostOk.java
new file mode 100644
index 0000000..8ecd7ac
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostOk.java
@@ -0,0 +1,9 @@
+// Copyright (c) 2018, 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.staticizer.movetohost;
+
+public class HostOk {
+  static CandidateOk INSTANCE = new CandidateOk();
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostOkSideEffects.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostOkSideEffects.java
new file mode 100644
index 0000000..1f6ec7c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostOkSideEffects.java
@@ -0,0 +1,13 @@
+// Copyright (c) 2018, 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.staticizer.movetohost;
+
+public class HostOkSideEffects {
+  static CandidateOkSideEffects INSTANCE = new CandidateOkSideEffects();
+
+  static {
+    System.out.println("Inside HostOkSideEffects::<clinit>()");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostTestClass.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostTestClass.java
new file mode 100644
index 0000000..3f31839
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostTestClass.java
@@ -0,0 +1,46 @@
+// Copyright (c) 2018, 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.staticizer.movetohost;
+
+public class MoveToHostTestClass {
+  private static int ID = 0;
+
+  private static String next() {
+    return Integer.toString(ID++);
+  }
+
+  public static void main(String[] args) {
+    MoveToHostTestClass test = new MoveToHostTestClass();
+    test.testOk();
+    test.testOkSideEffects();
+    test.testConflictMethod();
+    test.testConflictField();
+  }
+
+  private synchronized void testOk() {
+    System.out.println(HostOk.INSTANCE.foo());
+    System.out.println(HostOk.INSTANCE.bar(next()));
+    HostOk.INSTANCE.blah(next());
+  }
+
+  private synchronized void testOkSideEffects() {
+    System.out.println(HostOkSideEffects.INSTANCE.foo());
+    System.out.println(HostOkSideEffects.INSTANCE.bar(next()));
+  }
+
+  private synchronized void testConflictMethod() {
+    System.out.println(new HostConflictMethod().bar(next()));
+    System.out.println(HostConflictMethod.INSTANCE.foo());
+    System.out.println(HostConflictMethod.INSTANCE.bar(next()));
+  }
+
+  private synchronized void testConflictField() {
+    System.out.println(new HostConflictField().field);
+    System.out.println(CandidateConflictField.field);
+    System.out.println(HostConflictField.INSTANCE.foo());
+    System.out.println(HostConflictField.INSTANCE.bar(next()));
+  }
+}
+
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/Simple.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/Simple.java
new file mode 100644
index 0000000..8df079c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/Simple.java
@@ -0,0 +1,21 @@
+// Copyright (c) 2018, 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.staticizer.trivial;
+
+public class Simple {
+  static Simple INSTANCE = new Simple();
+
+  String foo() {
+    synchronized ("") {
+      return bar("Simple::foo()");
+    }
+  }
+
+  String bar(String other) {
+    synchronized ("") {
+      return "Simple::bar(" + other + ")";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithGetter.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithGetter.java
new file mode 100644
index 0000000..9ff20c3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithGetter.java
@@ -0,0 +1,25 @@
+// Copyright (c) 2018, 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.staticizer.trivial;
+
+public class SimpleWithGetter {
+  private static SimpleWithGetter INSTANCE = new SimpleWithGetter();
+
+  static SimpleWithGetter getInstance() {
+    return INSTANCE;
+  }
+
+  String foo() {
+    synchronized ("") {
+      return bar("Simple::foo()");
+    }
+  }
+
+  String bar(String other) {
+    synchronized ("") {
+      return "Simple::bar(" + other + ")";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithParams.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithParams.java
new file mode 100644
index 0000000..a71f1f5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithParams.java
@@ -0,0 +1,24 @@
+// Copyright (c) 2018, 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.staticizer.trivial;
+
+public class SimpleWithParams {
+  static SimpleWithParams INSTANCE = new SimpleWithParams(123);
+
+  SimpleWithParams(int i) {
+  }
+
+  String foo() {
+    synchronized ("") {
+      return bar("SimpleWithParams::foo()");
+    }
+  }
+
+  String bar(String other) {
+    synchronized ("") {
+      return "SimpleWithParams::bar(" + other + ")";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithSideEffects.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithSideEffects.java
new file mode 100644
index 0000000..5ba97dc
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithSideEffects.java
@@ -0,0 +1,25 @@
+// Copyright (c) 2018, 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.staticizer.trivial;
+
+public class SimpleWithSideEffects {
+  static SimpleWithSideEffects INSTANCE = new SimpleWithSideEffects();
+
+  static {
+    System.out.println("SimpleWithSideEffects::<clinit>()");
+  }
+
+  String foo() {
+    synchronized ("") {
+      return bar("SimpleWithSideEffects::foo()");
+    }
+  }
+
+  String bar(String other) {
+    synchronized ("") {
+      return "SimpleWithSideEffects::bar(" + other + ")";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/TrivialTestClass.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/TrivialTestClass.java
new file mode 100644
index 0000000..2dad273
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/TrivialTestClass.java
@@ -0,0 +1,42 @@
+// Copyright (c) 2018, 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.staticizer.trivial;
+
+public class TrivialTestClass {
+  private static int ID = 0;
+
+  private static String next() {
+    return Integer.toString(ID++);
+  }
+
+  public static void main(String[] args) {
+    TrivialTestClass test = new TrivialTestClass();
+    test.testSimple();
+    test.testSimpleWithSideEffects();
+    test.testSimpleWithParams();
+    test.testSimpleWithGetter();
+  }
+
+  private synchronized void testSimple() {
+    System.out.println(Simple.INSTANCE.foo());
+    System.out.println(Simple.INSTANCE.bar(next()));
+  }
+
+  private synchronized void testSimpleWithSideEffects() {
+    System.out.println(SimpleWithSideEffects.INSTANCE.foo());
+    System.out.println(SimpleWithSideEffects.INSTANCE.bar(next()));
+  }
+
+  private synchronized void testSimpleWithParams() {
+    System.out.println(SimpleWithParams.INSTANCE.foo());
+    System.out.println(SimpleWithParams.INSTANCE.bar(next()));
+  }
+
+  private synchronized void testSimpleWithGetter() {
+    System.out.println(SimpleWithGetter.getInstance().foo());
+    System.out.println(SimpleWithGetter.getInstance().bar(next()));
+  }
+}
+
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinClassStaticizerTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinClassStaticizerTest.java
new file mode 100644
index 0000000..c19b80e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinClassStaticizerTest.java
@@ -0,0 +1,72 @@
+// Copyright (c) 2018, 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.kotlin;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.Test;
+import org.junit.runners.Parameterized.Parameters;
+
+public class KotlinClassStaticizerTest extends AbstractR8KotlinTestBase {
+  @Parameters(name = "allowAccessModification: {0} target: {1}")
+  public static Collection<Object[]> data() {
+    ImmutableList.Builder<Object[]> builder = new ImmutableList.Builder<>();
+    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
+      builder.add(new Object[]{Boolean.TRUE, targetVersion});
+    }
+    return builder.build();
+  }
+
+  @Test
+  public void testCompanionAndRegularObjects() throws Exception {
+    final String mainClassName = "class_staticizer.MainKt";
+
+    // Without class staticizer.
+    runTest("class_staticizer", mainClassName, false, (app) -> {
+      CodeInspector inspector = new CodeInspector(app);
+      assertTrue(inspector.clazz("class_staticizer.Regular$Companion").isPresent());
+      assertTrue(inspector.clazz("class_staticizer.Derived$Companion").isPresent());
+
+      ClassSubject utilClass = inspector.clazz("class_staticizer.Util");
+      assertTrue(utilClass.isPresent());
+      AtomicInteger nonStaticMethodCount = new AtomicInteger();
+      utilClass.forAllMethods(method -> {
+        if (!method.isStatic()) {
+          nonStaticMethodCount.incrementAndGet();
+        }
+      });
+      assertEquals(4, nonStaticMethodCount.get());
+    });
+
+    // With class staticizer.
+    runTest("class_staticizer", mainClassName, true, (app) -> {
+      CodeInspector inspector = new CodeInspector(app);
+      assertFalse(inspector.clazz("class_staticizer.Regular$Companion").isPresent());
+      assertFalse(inspector.clazz("class_staticizer.Derived$Companion").isPresent());
+
+      ClassSubject utilClass = inspector.clazz("class_staticizer.Util");
+      assertTrue(utilClass.isPresent());
+      utilClass.forAllMethods(method -> assertTrue(method.isStatic()));
+    });
+  }
+
+  protected void runTest(String folder, String mainClass,
+      boolean enabled, AndroidAppInspector inspector) throws Exception {
+    runTest(
+        folder, mainClass, null,
+        options -> {
+          options.enableClassInlining = false;
+          options.enableClassStaticizer = enabled;
+        }, inspector);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
index 4b1b404..a3aa5e1 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
@@ -16,11 +16,13 @@
 import com.android.tools.r8.kotlin.TestKotlinClass.Visibility;
 import com.android.tools.r8.naming.MemberNaming;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.FieldSubject;
 import java.nio.file.Path;
 import java.util.Collections;
+import java.util.function.Consumer;
 import org.junit.Assert;
 import org.junit.Ignore;
 import org.junit.Test;
@@ -59,12 +61,15 @@
           .addProperty("property", JAVA_LANG_STRING, Visibility.PRIVATE)
           .addProperty("indirectPropertyGetter", JAVA_LANG_STRING, Visibility.PRIVATE);
 
+  private Consumer<InternalOptions> disableClassStaticizer =
+      opts -> opts.enableClassStaticizer = false;
+
   @Test
   public void testCompanionProperty_primitivePropertyIsAlwaysInlined() throws Exception {
     final TestKotlinCompanionClass testedClass = COMPANION_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionPropertiesKt",
         "companionProperties_usePrimitiveProp");
-    runTest(PROPERTIES_PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PROPERTIES_PACKAGE_NAME, mainClass, disableClassStaticizer, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector, testedClass.getOuterClassName());
       String propertyName = "primitiveProp";
@@ -93,7 +98,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionPropertiesKt",
         "companionProperties_usePrivateProp");
-    runTest(PROPERTIES_PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PROPERTIES_PACKAGE_NAME, mainClass, disableClassStaticizer, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector, testedClass.getOuterClassName());
       String propertyName = "privateProp";
@@ -123,7 +128,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionPropertiesKt",
         "companionProperties_useInternalProp");
-    runTest(PROPERTIES_PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PROPERTIES_PACKAGE_NAME, mainClass, disableClassStaticizer, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector, testedClass.getOuterClassName());
       String propertyName = "internalProp";
@@ -152,7 +157,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionPropertiesKt",
         "companionProperties_usePublicProp");
-    runTest(PROPERTIES_PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PROPERTIES_PACKAGE_NAME, mainClass, disableClassStaticizer, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector, testedClass.getOuterClassName());
       String propertyName = "publicProp";
@@ -181,7 +186,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_usePrivateLateInitProp");
-    runTest(PROPERTIES_PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PROPERTIES_PACKAGE_NAME, mainClass, disableClassStaticizer, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector, testedClass.getOuterClassName());
       String propertyName = "privateLateInitProp";
@@ -257,7 +262,7 @@
     TestKotlinCompanionClass testedClass = ACCESSOR_COMPANION_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("accessors.AccessorKt",
         "accessor_accessPropertyFromCompanionClass");
-    runTest("accessors", mainClass, (app) -> {
+    runTest("accessors", mainClass, disableClassStaticizer, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector, testedClass.getOuterClassName());
       ClassSubject companionClass = checkClassIsKept(codeInspector, testedClass.getClassName());
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
index 89ff81d..08fcebe 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
@@ -83,16 +83,17 @@
           .addProperty("internalLateInitProp", JAVA_LANG_STRING, Visibility.INTERNAL)
           .addProperty("publicLateInitProp", JAVA_LANG_STRING, Visibility.PUBLIC);
 
-  private Consumer<InternalOptions> disableClassInliningAndMerging = o -> {
+  private Consumer<InternalOptions> disableAggressiveClassOptimizations = o -> {
     o.enableClassInlining = false;
     o.enableClassMerging = false;
+    o.enableClassStaticizer = false;
   };
 
   @Test
   public void testMutableProperty_getterAndSetterAreRemoveIfNotUsed() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_noUseOfProperties");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -115,7 +116,7 @@
   public void testMutableProperty_privateIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_usePrivateProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -137,7 +138,7 @@
   public void testMutableProperty_protectedIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_useProtectedProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -160,7 +161,7 @@
   public void testMutableProperty_internalIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_useInternalProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -183,7 +184,7 @@
   public void testMutableProperty_publicIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_usePublicProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -206,7 +207,7 @@
   public void testMutableProperty_primitivePropertyIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_usePrimitiveProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -231,11 +232,12 @@
   public void testLateInitProperty_getterAndSetterAreRemoveIfNotUsed() throws Exception {
     String mainClass = addMainToClasspath("properties/LateInitPropertyKt",
         "lateInitProperty_noUseOfProperties");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
-      for (Entry<String, KotlinProperty> property : LATE_INIT_PROPERTY_CLASS.properties.entrySet()) {
+      for (Entry<String, KotlinProperty> property : LATE_INIT_PROPERTY_CLASS.properties
+          .entrySet()) {
         MethodSignature getter = LATE_INIT_PROPERTY_CLASS.getGetterForProperty(property.getKey());
         MethodSignature setter = LATE_INIT_PROPERTY_CLASS.getSetterForProperty(property.getKey());
         if (property.getValue().getVisibility() == Visibility.PRIVATE) {
@@ -255,7 +257,7 @@
   public void testLateInitProperty_privateIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/LateInitPropertyKt", "lateInitProperty_usePrivateLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -278,7 +280,7 @@
   public void testLateInitProperty_protectedIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/LateInitPropertyKt",
         "lateInitProperty_useProtectedLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -299,7 +301,7 @@
   public void testLateInitProperty_internalIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/LateInitPropertyKt", "lateInitProperty_useInternalLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -318,7 +320,7 @@
   public void testLateInitProperty_publicIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/LateInitPropertyKt", "lateInitProperty_usePublicLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -337,7 +339,7 @@
   public void testUserDefinedProperty_getterAndSetterAreRemoveIfNotUsed() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/UserDefinedPropertyKt", "userDefinedProperty_noUseOfProperties");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           USER_DEFINED_PROPERTY_CLASS.getClassName());
@@ -354,7 +356,7 @@
   public void testUserDefinedProperty_publicIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/UserDefinedPropertyKt", "userDefinedProperty_useProperties");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           USER_DEFINED_PROPERTY_CLASS.getClassName());
@@ -380,7 +382,7 @@
   public void testCompanionProperty_primitivePropertyCannotBeInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_usePrimitiveProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector,
           "properties.CompanionProperties");
@@ -411,7 +413,7 @@
   public void testCompanionProperty_privatePropertyIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_usePrivateProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector,
           "properties.CompanionProperties");
@@ -445,7 +447,7 @@
   public void testCompanionProperty_internalPropertyCannotBeInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_useInternalProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector,
           "properties.CompanionProperties");
@@ -476,7 +478,7 @@
   public void testCompanionProperty_publicPropertyCannotBeInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_usePublicProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector,
           "properties.CompanionProperties");
@@ -508,7 +510,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_usePrivateLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector, testedClass.getOuterClassName());
       ClassSubject companionClass = checkClassIsKept(codeInspector, testedClass.getClassName());
@@ -539,7 +541,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_useInternalLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector, testedClass.getOuterClassName());
       ClassSubject companionClass = checkClassIsKept(codeInspector, testedClass.getClassName());
@@ -563,7 +565,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_usePublicLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector, testedClass.getOuterClassName());
       ClassSubject companionClass = checkClassIsKept(codeInspector, testedClass.getClassName());
@@ -587,7 +589,7 @@
     final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_usePrimitiveProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "primitiveProp";
@@ -614,7 +616,7 @@
     final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_usePrivateProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "privateProp";
@@ -641,7 +643,7 @@
     final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_useInternalProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "internalProp";
@@ -668,7 +670,7 @@
     final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_usePublicProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "publicProp";
@@ -695,7 +697,7 @@
     final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_useLateInitPrivateProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "privateLateInitProp";
@@ -722,7 +724,7 @@
     final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_useLateInitInternalProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "internalLateInitProp";
@@ -745,7 +747,7 @@
     final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_useLateInitPublicProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "publicLateInitProp";
@@ -768,7 +770,7 @@
     final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_usePrimitiveProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "primitiveProp";
@@ -795,7 +797,7 @@
     final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_usePrivateProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "privateProp";
@@ -821,7 +823,7 @@
     final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_useInternalProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "internalProp";
@@ -847,7 +849,7 @@
     final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_usePublicProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "publicProp";
@@ -874,7 +876,7 @@
     final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_useLateInitPrivateProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject fileClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "privateLateInitProp";
@@ -900,7 +902,7 @@
     final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_useLateInitInternalProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "internalLateInitProp";
@@ -924,7 +926,7 @@
     final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_useLateInitPublicProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "publicLateInitProp";
diff --git a/src/test/kotlinR8TestResources/class_staticizer/main.kt b/src/test/kotlinR8TestResources/class_staticizer/main.kt
new file mode 100644
index 0000000..01915c4
--- /dev/null
+++ b/src/test/kotlinR8TestResources/class_staticizer/main.kt
@@ -0,0 +1,43 @@
+// Copyright (c) 2018, 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 class_staticizer
+
+private var COUNT = 0
+
+fun next() = "${COUNT++}".padStart(3, '0')
+
+fun main(args: Array<String>) {
+    println(Regular.foo)
+    println(Regular.bar)
+    println(Regular.blah(next()))
+    println(Derived.foo)
+    println(Derived.bar)
+    println(Derived.blah(next()))
+    println(Util.foo)
+    println(Util.bar)
+    println(Util.blah(next()))
+}
+
+open class Regular {
+    companion object {
+        var foo: String = "Regular::CC::foo[${next()}]"
+        var bar: String = blah(next())
+        fun blah(p: String) = "Regular::CC::blah($p)[${next()}]"
+    }
+}
+
+open class Derived : Regular() {
+    companion object {
+        var foo: String = "Derived::CC::foo[${next()}]"
+        var bar: String = blah(next())
+        fun blah(p: String) = "Derived::CC::blah($p)[${next()}]"
+    }
+}
+
+object Util {
+    var foo: String = "Util::foo[${next()}]"
+    var bar: String = Regular.blah(next()) + Derived.blah(next())
+    fun blah(p: String) = "Util::blah($p)[${next()}]"
+}