Kotlin-style lambda class merging, phase 1.

The first part of class merging for Kotlin k-style lambdas, includes
basic working functionality and leaves out a few improvements which
will be worked on separately.

Since Kotlin lambda classes are generated based on a common template
many of them have same structure and differ only by code of main
lambda method. In this CL we combine lambda with same structure
classes into groups, assign each lambda in a group a unique id, and
then use same lambda group class to represent all lambdas inside the
group dispatching the calls by lambda id when necessary.

Grouping criteria:
  - lambda implement same functional interface
  - lambda has the same "capture signature"
  - lambda has same "main method"
  - lambda are located inside same package (unless access is relaxed)
  - lambda have same generic signature (if present)
  - lambda have same InnerClasses/EnclosingMethod attribute (if present)

Since the implementation is based on several assumptions about lambda
class struture, we exclude lambdas failing these assumptions from processing.
We also exclude lambda classes having unexpected references/usages.

Lambda groups consisting from single lambdas are not processed, such
lambda classes are kept as-is.

Note that the efficiency of merging depends on options. For example if
EnclosingMethod is kept, we only merge lambdas inside one method. OTOH,
if access relaxation is enabled and signature/inner/enclosing attributes
are removed, we end up with as many groups as <functional-interface X
captures X main method>. (And main methods are actually expected to
always be inlined and removed, we’ll address this in follow up CL.)

Current implementation is only enabled if tree shaking is enabled since
we rely on it to remove unused lambda classes. This may be revised later.

Bug:72860066

Change-Id: Ib5dd1af6ca13c74fd513714ed5a8d1f3c7fbea0b
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 2678c1f..394805d 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -347,6 +347,7 @@
     internal.enableValuePropagation = false;
 
     internal.enableDesugaring = getEnableDesugaring();
+    internal.enableLambdaMerging = false;
     return internal;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 906c8d8..59c2639 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -14,12 +14,14 @@
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.ClassAndMemberPublicizer;
 import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLense;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.optimize.EnumOrdinalMapCollector;
 import com.android.tools.r8.ir.optimize.SwitchMapCollector;
 import com.android.tools.r8.jar.CfApplicationWriter;
+import com.android.tools.r8.kotlin.Kotlin;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.Minifier;
 import com.android.tools.r8.naming.NamingLens;
@@ -52,6 +54,7 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
 import com.android.tools.r8.utils.LineNumberOptimizer;
+import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
@@ -270,6 +273,10 @@
           }
         }
 
+        // Compute kotlin info before setting the roots and before
+        // kotlin metadata annotation is removed.
+        computeKotlinInfoForProgramClasses(application, appInfo);
+
         final ProguardConfiguration.Builder compatibility =
             ProguardConfiguration.builder(application.dexItemFactory, options.reporter);
 
@@ -480,6 +487,15 @@
     }
   }
 
+  private void computeKotlinInfoForProgramClasses(
+      DexApplication application, AppInfoWithSubtyping appInfo) {
+    Kotlin kotlin = appInfo.dexItemFactory.kotlin;
+    Reporter reporter = options.reporter;
+    for (DexProgramClass programClass : application.classes()) {
+      programClass.setKotlinInfo(kotlin.getKotlinInfo(programClass, reporter));
+    }
+  }
+
   static void unwrapExecutionException(ExecutionException executionException)
       throws CompilationException {
     Throwable cause = executionException.getCause();
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 9a61a9f..4615fa5 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -643,6 +643,10 @@
     internal.enableMinification = getEnableMinification();
     assert internal.enableTreeShaking;
     internal.enableTreeShaking = getEnableTreeShaking();
+    // In current implementation we only enable lambda merger if the tree
+    // shaking is enabled. This is caused by the fact that we rely on tree
+    // shaking for removing the lambda classes which should be revised later.
+    internal.enableLambdaMerging = getEnableTreeShaking();
     assert !internal.ignoreMissingClasses;
     internal.ignoreMissingClasses = proguardConfiguration.isIgnoreWarnings()
         // TODO(70706667): We probably only want this in Proguard compatibility mode.
diff --git a/src/main/java/com/android/tools/r8/code/Format22c.java b/src/main/java/com/android/tools/r8/code/Format22c.java
index 8254f91..6f8d6c6 100644
--- a/src/main/java/com/android/tools/r8/code/Format22c.java
+++ b/src/main/java/com/android/tools/r8/code/Format22c.java
@@ -11,7 +11,7 @@
 import java.nio.ShortBuffer;
 import java.util.function.BiPredicate;
 
-abstract class Format22c extends Base2Format {
+public abstract class Format22c extends Base2Format {
 
   public final byte A;
   public final byte B;
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
index 28a7fbb..6a63a8e 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -199,7 +199,7 @@
         for (ProgramResource input : dexSources) {
           DexReader dexReader = new DexReader(input);
           computedMinApiLevel = verifyOrComputeMinApiLevel(computedMinApiLevel, dexReader);
-          dexParsers.add(new DexParser(dexReader, classKind, itemFactory));
+          dexParsers.add(new DexParser(dexReader, classKind, itemFactory, options.reporter));
         }
         options.minApiLevel = computedMinApiLevel;
         for (DexParser dexParser : dexParsers) {
diff --git a/src/main/java/com/android/tools/r8/dex/DexParser.java b/src/main/java/com/android/tools/r8/dex/DexParser.java
index 9aa7eed..0ccd724 100644
--- a/src/main/java/com/android/tools/r8/dex/DexParser.java
+++ b/src/main/java/com/android/tools/r8/dex/DexParser.java
@@ -8,6 +8,7 @@
 import static com.android.tools.r8.utils.EncodedValueUtils.parseSigned;
 import static com.android.tools.r8.utils.EncodedValueUtils.parseUnsigned;
 
+import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.ProgramResource.Kind;
 import com.android.tools.r8.code.Instruction;
 import com.android.tools.r8.code.InstructionFactory;
@@ -58,6 +59,7 @@
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.DefaultDiagnosticsHandler;
 import com.android.tools.r8.utils.Pair;
 import com.google.common.io.ByteStreams;
 import it.unimi.dsi.fastutil.ints.Int2IntArrayMap;
@@ -84,6 +86,7 @@
   private final DexSection[] dexSections;
   private int[] stringIDs;
   private final ClassKind classKind;
+  private final DiagnosticsHandler reporter;
 
   public static DexSection[] parseMapFrom(Path file) throws IOException {
     return parseMapFrom(Files.newInputStream(file), new PathOrigin(file));
@@ -94,7 +97,8 @@
   }
 
   private static DexSection[] parseMapFrom(DexReader dexReader) {
-    DexParser dexParser = new DexParser(dexReader, ClassKind.PROGRAM, new DexItemFactory());
+    DexParser dexParser = new DexParser(dexReader,
+        ClassKind.PROGRAM, new DexItemFactory(), new DefaultDiagnosticsHandler());
     return dexParser.dexSections;
   }
 
@@ -119,7 +123,8 @@
   // Factory to canonicalize certain dexitems.
   private final DexItemFactory dexItemFactory;
 
-  public DexParser(DexReader dexReader, ClassKind classKind, DexItemFactory dexItemFactory) {
+  public DexParser(DexReader dexReader,
+      ClassKind classKind, DexItemFactory dexItemFactory, DiagnosticsHandler reporter) {
     assert dexReader.getOrigin() != null;
     this.origin = dexReader.getOrigin();
     this.dexReader = dexReader;
@@ -128,6 +133,7 @@
     dexSections = parseMap();
     parseStringIDs();
     this.classKind = classKind;
+    this.reporter = reporter;
   }
 
   private void ensureCodesInited() {
diff --git a/src/main/java/com/android/tools/r8/graph/DexAnnotation.java b/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
index 2e0b6ff..06e72a3 100644
--- a/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
+++ b/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
@@ -202,6 +202,15 @@
         compressSignature(signature, factory));
   }
 
+  public static String getSignature(DexAnnotation signatureAnnotation) {
+    DexValueArray elements = (DexValueArray) signatureAnnotation.annotation.elements[0].value;
+    StringBuilder signature = new StringBuilder();
+    for (DexValue element : elements.getValues()) {
+      signature.append(((DexValueString) element).value.toString());
+    }
+    return signature.toString();
+  }
+
   public static DexAnnotation createThrowsAnnotation(DexValue[] exceptions,
       DexItemFactory factory) {
     return createSystemValueAnnotation(factory.annotationThrows, factory,
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index 2295c16..42acb7c 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.dex.MixedSectionCollection;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.kotlin.KotlinInfo;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.ThrowingConsumer;
 import com.google.common.base.MoreObjects;
@@ -262,6 +263,10 @@
     return false;
   }
 
+  public DexClasspathClass asClasspathClass() {
+    return null;
+  }
+
   public boolean isLibraryClass() {
     return false;
   }
@@ -347,4 +352,11 @@
   public void clearInnerClasses() {
     innerClasses.clear();
   }
+
+  /** Returns kotlin class info if the class is synthesized by kotlin compiler. */
+  public abstract KotlinInfo getKotlinInfo();
+
+  public final boolean hasKotlinInfo() {
+    return getKotlinInfo() != null;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexClasspathClass.java b/src/main/java/com/android/tools/r8/graph/DexClasspathClass.java
index 76d08d5..6148b00 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClasspathClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClasspathClass.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.dex.MixedSectionCollection;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.kotlin.KotlinInfo;
 import com.android.tools.r8.origin.Origin;
 import java.util.List;
 import java.util.function.Supplier;
@@ -68,6 +69,16 @@
   }
 
   @Override
+  public DexClasspathClass asClasspathClass() {
+    return this;
+  }
+
+  @Override
+  public KotlinInfo getKotlinInfo() {
+    throw new Unreachable("Kotlin into n classpath class is not supported yet.");
+  }
+
+  @Override
   public DexClasspathClass get() {
     return this;
   }
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 0bd48bd..b7f1802 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -168,24 +168,29 @@
 
   public boolean isInliningCandidate(DexEncodedMethod container, Reason inliningReason,
       AppInfoWithSubtyping appInfo) {
+    return isInliningCandidate(container.method.getHolder(), inliningReason, appInfo);
+  }
+
+  public boolean isInliningCandidate(DexType containerType, Reason inliningReason,
+      AppInfoWithSubtyping appInfo) {
     if (isClassInitializer()) {
       // This will probably never happen but never inline a class initializer.
       return false;
     }
     if (inliningReason == Reason.FORCE) {
       // Make sure we would be able to inline this normally.
-      assert isInliningCandidate(container, Reason.SIMPLE, appInfo);
+      assert isInliningCandidate(containerType, Reason.SIMPLE, appInfo);
       return true;
     }
     switch (compilationState) {
       case PROCESSED_INLINING_CANDIDATE_ANY:
         return true;
       case PROCESSED_INLINING_CANDIDATE_SUBCLASS:
-        return container.method.getHolder().isSubtypeOf(method.getHolder(), appInfo);
+        return containerType.isSubtypeOf(method.getHolder(), appInfo);
       case PROCESSED_INLINING_CANDIDATE_SAME_PACKAGE:
-        return container.method.getHolder().isSamePackage(method.getHolder());
+        return containerType.isSamePackage(method.getHolder());
       case PROCESSED_INLINING_CANDIDATE_SAME_CLASS:
-        return container.method.getHolder() == method.getHolder();
+        return containerType == method.getHolder();
       default:
         return false;
     }
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index b1a5781..3439ea6 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.graph.DexDebugEvent.SetPrologueEnd;
 import com.android.tools.r8.graph.DexMethodHandle.MethodHandleType;
 import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.kotlin.Kotlin;
 import com.android.tools.r8.naming.NamingLens;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
@@ -72,6 +73,10 @@
               nullValueType, nullValueType,
               unknownTypeName, unknownTypeName));
 
+  public DexItemFactory() {
+    this.kotlin = new Kotlin(this);
+  }
+
   public static boolean isInternalSentinel(DexItem item) {
     return internalSentinels.containsKey(item);
   }
@@ -194,7 +199,7 @@
   public final LongMethods longMethods = new LongMethods();
   public final ThrowableMethods throwableMethods = new ThrowableMethods();
   public final ClassMethods classMethods = new ClassMethods();
-  public final Kotlin kotlin = new Kotlin();
+  public final Kotlin kotlin;
 
   // Dex system annotations.
   // See https://source.android.com/devices/tech/dalvik/dex-format.html#system-annotation
@@ -335,24 +340,6 @@
     }
   }
 
-  public class Kotlin {
-    private Kotlin() {
-    }
-
-    public final Intrinsics intrinsics = new Intrinsics();
-
-    // kotlin.jvm.internal.Intrinsics class
-    public class Intrinsics {
-      private Intrinsics() {
-      }
-
-      public final DexType type = createType(createString("Lkotlin/jvm/internal/Intrinsics;"));
-      public final DexMethod throwParameterIsNullException =
-          createMethod(type, createProto(voidType, stringType), "throwParameterIsNullException");
-      public final DexMethod throwNpe = createMethod(type, createProto(voidType), "throwNpe");
-    }
-  }
-
   private static <T extends DexItem> T canonicalize(ConcurrentHashMap<T, T> map, T item) {
     assert item != null;
     assert !DexItemFactory.isInternalSentinel(item);
diff --git a/src/main/java/com/android/tools/r8/graph/DexLibraryClass.java b/src/main/java/com/android/tools/r8/graph/DexLibraryClass.java
index 946cd90..4700553 100644
--- a/src/main/java/com/android/tools/r8/graph/DexLibraryClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexLibraryClass.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.dex.MixedSectionCollection;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.kotlin.KotlinInfo;
 import com.android.tools.r8.origin.Origin;
 import java.util.List;
 import java.util.function.Supplier;
@@ -83,6 +84,11 @@
   }
 
   @Override
+  public KotlinInfo getKotlinInfo() {
+    return null;
+  }
+
+  @Override
   public DexLibraryClass get() {
     return this;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
index 37f774d..ffff10f 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.ProgramResource.Kind;
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.dex.MixedSectionCollection;
+import com.android.tools.r8.kotlin.KotlinInfo;
 import com.android.tools.r8.origin.Origin;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -27,6 +28,7 @@
   private DexEncodedArray staticValues = SENTINEL_NOT_YET_COMPUTED;
   private final Collection<DexProgramClass> synthesizedFrom;
   private int classFileVersion = -1;
+  private KotlinInfo kotlinInfo = null;
 
   public DexProgramClass(
       DexType type,
@@ -199,6 +201,16 @@
     return this;
   }
 
+  @Override
+  public KotlinInfo getKotlinInfo() {
+    return kotlinInfo;
+  }
+
+  public void setKotlinInfo(KotlinInfo kotlinInfo) {
+    assert this.kotlinInfo == null || kotlinInfo == null;
+    this.kotlinInfo = kotlinInfo;
+  }
+
   public boolean hasMethodsOrFields() {
     int numberOfFields = staticFields().length + instanceFields().length;
     int numberOfMethods = directMethods().length + virtualMethods().length;
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 fdc07a7..cc1f8e7 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -76,7 +76,7 @@
     }
   }
 
-  synchronized void addDirectSubtype(DexType type) {
+  public synchronized void addDirectSubtype(DexType type) {
     assert hierarchyLevel != UNKNOWN_LEVEL;
     ensureDirectSubTypeSet();
     directSubtypes.add(type);
diff --git a/src/main/java/com/android/tools/r8/graph/EnclosingMethodAttribute.java b/src/main/java/com/android/tools/r8/graph/EnclosingMethodAttribute.java
index d743f6c..ce741a3 100644
--- a/src/main/java/com/android/tools/r8/graph/EnclosingMethodAttribute.java
+++ b/src/main/java/com/android/tools/r8/graph/EnclosingMethodAttribute.java
@@ -12,7 +12,7 @@
  * declared in the context of a constructor or method, in which case the method field will be
  * non-null. Otherwise, i.e., if the class is declared in an initializer, method is null.
  */
-public class EnclosingMethodAttribute {
+public final class EnclosingMethodAttribute {
 
   // Enclosing class of the inner class.
   // Null if the inner class is declared inside of a method or constructor.
@@ -48,4 +48,17 @@
   public DexType getEnclosingClass() {
     return enclosingClass;
   }
+
+  @Override
+  public int hashCode() {
+    assert (enclosingClass == null) != (enclosingMethod == null);
+    return System.identityHashCode(enclosingClass) + System.identityHashCode(enclosingMethod);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    return obj instanceof EnclosingMethodAttribute &&
+        enclosingClass == ((EnclosingMethodAttribute) obj).enclosingClass &&
+        enclosingMethod == ((EnclosingMethodAttribute) obj).enclosingMethod;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
index 26aa423..945da10 100644
--- a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
@@ -252,10 +252,8 @@
               instanceFields.toArray(new DexEncodedField[instanceFields.size()]),
               directMethods.toArray(new DexEncodedMethod[directMethods.size()]),
               virtualMethods.toArray(new DexEncodedMethod[virtualMethods.size()]));
-      if (classKind == ClassKind.PROGRAM) {
-        context.owner = clazz.asProgramClass();
-      }
       if (clazz.isProgramClass()) {
+        context.owner = clazz.asProgramClass();
         clazz.asProgramClass().setClassFileVersion(version);
       }
       classConsumer.accept(clazz);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
index 6364c82..76a1a43 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -1846,7 +1846,7 @@
     targetInfo.block.incrementUnfilledPredecessorCount();
   }
 
-  void ensureNormalSuccessorBlock(int sourceOffset, int targetOffset) {
+  public void ensureNormalSuccessorBlock(int sourceOffset, int targetOffset) {
     ensureSuccessorBlock(sourceOffset, targetOffset, true);
   }
 
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 9a3a218..42b06ef 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
@@ -39,6 +39,7 @@
 import com.android.tools.r8.ir.optimize.NonNullTracker;
 import com.android.tools.r8.ir.optimize.Outliner;
 import com.android.tools.r8.ir.optimize.PeepholeOptimizer;
+import com.android.tools.r8.ir.optimize.lambda.LambdaMerger;
 import com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator;
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
 import com.android.tools.r8.logging.Log;
@@ -78,6 +79,7 @@
   private final StringConcatRewriter stringConcatRewriter;
   private final LambdaRewriter lambdaRewriter;
   private final InterfaceMethodRewriter interfaceMethodRewriter;
+  private final LambdaMerger lambdaMerger;
   private final InternalOptions options;
   private final CfgPrinter printer;
   private final GraphLense graphLense;
@@ -114,6 +116,8 @@
     this.interfaceMethodRewriter =
         (options.enableDesugaring && enableInterfaceMethodDesugaring())
             ? new InterfaceMethodRewriter(this, options) : null;
+    this.lambdaMerger = options.enableLambdaMerging
+        ? new LambdaMerger(appInfo.dexItemFactory, options.reporter) : null;
     if (enableWholeProgramOptimizations) {
       assert appInfo.hasLiveness();
       this.nonNullTracker = new NonNullTracker();
@@ -360,6 +364,7 @@
       ExecutorService executorService)
       throws ExecutionException, ApiLevelException {
     removeLambdaDeserializationMethods();
+    collectLambdaMergingCandidates(application);
 
     // The process is in two phases.
     // 1) Subject all DexEncodedMethods to optimization (except outlining).
@@ -397,6 +402,7 @@
     desugarInterfaceMethods(builder, IncludeAllResources);
 
     handleSynthesizedClassMapping(builder);
+    finalizeLambdaMerging(application, directFeedback, builder, executorService);
 
     if (outliner != null) {
       timing.begin("IR conversion phase 2");
@@ -433,6 +439,21 @@
     return builder.build();
   }
 
+  private void collectLambdaMergingCandidates(DexApplication application) {
+    if (lambdaMerger != null) {
+      lambdaMerger.collectGroupCandidates(application, appInfo.withLiveness(), options);
+    }
+  }
+
+  private void finalizeLambdaMerging(DexApplication application,
+      OptimizationFeedback directFeedback, Builder<?> builder, ExecutorService executorService)
+      throws ExecutionException, ApiLevelException {
+    if (lambdaMerger != null) {
+      lambdaMerger.applyLambdaClassMapping(
+          application, this, directFeedback, builder, executorService);
+    }
+  }
+
   private void clearDexMethodCompilationState() {
     appInfo.classes().forEach(this::clearDexMethodCompilationState);
   }
@@ -488,8 +509,13 @@
   }
 
   public void optimizeSynthesizedClass(DexProgramClass clazz) throws ApiLevelException {
-    // Process the generated class, but don't apply any outlining.
-    clazz.forEachMethodThrowing(this::optimizeSynthesizedMethod);
+    try {
+      codeRewriter.enterCachedClass(clazz);
+      // Process the generated class, but don't apply any outlining.
+      clazz.forEachMethodThrowing(this::optimizeSynthesizedMethod);
+    } finally {
+      codeRewriter.leaveCachedClass(clazz);
+    }
   }
 
   public void optimizeSynthesizedMethod(DexEncodedMethod method) throws ApiLevelException {
@@ -644,6 +670,10 @@
       interfaceMethodRewriter.rewriteMethodReferences(method, code);
       assert code.isConsistentSSA();
     }
+    if (lambdaMerger != null) {
+      lambdaMerger.processMethodCode(method, code);
+      assert code.isConsistentSSA();
+    }
 
     if (options.outline.enabled) {
       outlineHandler.accept(code, method);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/SynthesizedLambdaSourceCode.java b/src/main/java/com/android/tools/r8/ir/desugar/SynthesizedLambdaSourceCode.java
index bd38e1a..cf647a9 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/SynthesizedLambdaSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/SynthesizedLambdaSourceCode.java
@@ -8,10 +8,10 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexTypeList;
-import com.android.tools.r8.ir.synthetic.SingleBlockSourceCode;
+import com.android.tools.r8.ir.synthetic.SyntheticSourceCode;
 
 // Represents source code of synthesized lambda class methods.
-abstract class SynthesizedLambdaSourceCode extends SingleBlockSourceCode {
+abstract class SynthesizedLambdaSourceCode extends SyntheticSourceCode {
 
   final DexMethod currentMethod;
   final LambdaClass lambda;
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 3dc38c6..a6b29e8 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
@@ -14,6 +14,7 @@
 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.DexValue.DexValueBoolean;
@@ -117,6 +118,12 @@
   private final Set<DexMethod> libraryMethodsReturningReceiver;
   private final InternalOptions options;
 
+  // For some optimizations, e.g. optimizing synthetic classes, we may need to resolve
+  // the current class being optimized. Since all methods of this class are optimized
+  // together and are not concurrent to other optimizations, we just store current
+  // synthetic class.
+  private DexProgramClass cachedClass = null;
+
   public CodeRewriter(
       AppInfo appInfo, Set<DexMethod> libraryMethodsReturningReceiver, InternalOptions options) {
     this.appInfo = appInfo;
@@ -1026,13 +1033,8 @@
     if (code.computeNormalExitBlocks().isEmpty()) {
       return;
     }
-    DexClass clazz = appInfo.definitionFor(method.method.getHolder());
-    if (clazz == null) {
-      // TODO(67672280): Synthesized lambda classes are also optimized. However, they are not
-      // added to the AppInfo.
-      assert method.accessFlags.isSynthetic();
-      return;
-    }
+    DexClass clazz = definitionFor(method.method.getHolder());
+    assert clazz != null;
     DominatorTree dominatorTree = new DominatorTree(code);
     Set<StaticPut> puts = Sets.newIdentityHashSet();
     Map<DexField, StaticPut> dominatingPuts = Maps.newIdentityHashMap();
@@ -1167,6 +1169,23 @@
     }
   }
 
+  private DexClass definitionFor(DexType type) {
+    if (cachedClass != null && cachedClass.type == type) {
+      return cachedClass;
+    }
+    return appInfo.definitionFor(type);
+  }
+
+  public void enterCachedClass(DexProgramClass clazz) {
+    assert cachedClass == null;
+    cachedClass = clazz;
+  }
+
+  public void leaveCachedClass(DexProgramClass clazz) {
+    assert cachedClass == clazz;
+    cachedClass = null;
+  }
+
   public void removeCasts(IRCode code, TypeEnvironment typeEnvironment) {
     InstructionIterator it = code.instructionIterator();
     boolean needToRemoveTrivialPhis = false;
@@ -2390,7 +2409,7 @@
       if (type == dexItemFactory.throwableType) {
         return true;
       }
-      DexClass dexClass = appInfo.definitionFor(type);
+      DexClass dexClass = definitionFor(type);
       if (dexClass == null) {
         throw new CompilationError("Class or interface " + type.toSourceString() +
             " required for desugaring of try-with-resources is not found.");
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/CaptureSignature.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/CaptureSignature.java
new file mode 100644
index 0000000..ede37b1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/CaptureSignature.java
@@ -0,0 +1,159 @@
+// 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.lambda;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
+import it.unimi.dsi.fastutil.ints.IntLists;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Comparator;
+import java.util.function.IntFunction;
+
+// While mapping fields representing lambda captures we rearrange fields to make sure
+// lambdas having different order of the fields still can be merged. We also store
+// all captures of reference types in fields of java.lang.Objects class.
+//
+// This allows us to use same lambda groups class for these two different lambdas:
+//
+//      Lambda Group Class         Lambda$1               Lambda$2
+//          Object $c0        int      foo -> $c1     char foo -> $c2
+//          int    $c1        String[] bar -> $c0     int  bar -> $c1
+//          char   $c2        char     baz -> $c2     List baz -> $c0
+//
+// Capture signature is represented by a string with sorted shorties of field
+// types, such as "CIL" for both Lambda$1 and Lambda$2 in the above example.
+//
+public final class CaptureSignature {
+  private static final IntList EMPTY_LIST = IntLists.EMPTY_LIST;
+  private static final IntList SINGLE_LIST = IntLists.singleton(0);
+
+  private CaptureSignature() {
+  }
+
+  // Returns an array representing mapping of fields of the normalized capture
+  // into fields of original capture such that:
+  //
+  //   mapping = getReverseCaptureMapping(...)
+  //   <original-capture-index> = mapping[<normalized-capture-index>]
+  public static IntList getReverseCaptureMapping(DexType[] types) {
+    if (types.length == 0) {
+      return EMPTY_LIST;
+    }
+
+    if (types.length == 1) {
+      return SINGLE_LIST;
+    }
+
+    IntList result = new IntArrayList(types.length);
+    for (int i = 0; i < types.length; i++) {
+      result.add(i);
+    }
+    // Sort the indices by shorties (sorting is stable).
+    result.sort(Comparator.comparingInt(i -> types[i].toShorty()));
+    assert verifyMapping(result);
+    return result;
+  }
+
+  // Given a capture signature and an index returns the type of the field.
+  public static DexType fieldType(DexItemFactory factory, String capture, int index) {
+    switch (capture.charAt(index)) {
+      case 'L':
+        return factory.objectType;
+      case 'Z':
+        return factory.booleanType;
+      case 'B':
+        return factory.byteType;
+      case 'S':
+        return factory.shortType;
+      case 'C':
+        return factory.charType;
+      case 'I':
+        return factory.intType;
+      case 'F':
+        return factory.floatType;
+      case 'J':
+        return factory.longType;
+      case 'D':
+        return factory.doubleType;
+      default:
+        throw new Unreachable("Invalid capture character: " + capture.charAt(index));
+    }
+  }
+
+  private static String getCaptureSignature(int size, IntFunction<DexType> type) {
+    if (size == 0) {
+      return "";
+    }
+    if (size == 1) {
+      return Character.toString(type.apply(0).toShorty());
+    }
+
+    char[] chars = new char[size];
+    for (int i = 0; i < size; i++) {
+      chars[i] = type.apply(i).toShorty();
+    }
+    Arrays.sort(chars);
+    return new String(chars);
+  }
+
+  // Compute capture signature based on lambda class capture fields.
+  public static String getCaptureSignature(DexEncodedField[] fields) {
+    return getCaptureSignature(fields.length, i -> fields[i].field.type);
+  }
+
+  // Compute capture signature based on type list.
+  public static String getCaptureSignature(DexTypeList types) {
+    return getCaptureSignature(types.values.length, i -> types.values[i]);
+  }
+
+  // Having a list of fields of lambda captured values, maps one of them into
+  // an index of the appropriate field in normalized capture signature.
+  public static int mapFieldIntoCaptureIndex(
+      String capture, DexEncodedField[] lambdaFields, DexField fieldToMap) {
+    char fieldKind = fieldToMap.type.toShorty();
+    int numberOfSameCaptureKind = 0;
+    int result = -1;
+
+    for (DexEncodedField encodedField : lambdaFields) {
+      if (encodedField.field == fieldToMap) {
+        result = numberOfSameCaptureKind;
+        break;
+      }
+      if (encodedField.field.type.toShorty() == fieldKind) {
+        numberOfSameCaptureKind++;
+      }
+    }
+
+    assert result >= 0 : "Field was not found in the lambda class.";
+
+    for (int index = 0; index < capture.length(); index++) {
+      if (capture.charAt(index) == fieldKind) {
+        if (result == 0) {
+          return index;
+        }
+        result--;
+      }
+    }
+
+    throw new Unreachable("Were not able to map lambda field into capture index");
+  }
+
+  private static boolean verifyMapping(IntList mapping) {
+    BitSet bits = new BitSet();
+    for (Integer i : mapping) {
+      assert i >= 0 && i < mapping.size();
+      assert !bits.get(i);
+      bits.set(i);
+    }
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/CodeProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/CodeProcessor.java
new file mode 100644
index 0000000..daa7811
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/CodeProcessor.java
@@ -0,0 +1,350 @@
+// 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.lambda;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.CheckCast;
+import com.android.tools.r8.ir.code.ConstClass;
+import com.android.tools.r8.ir.code.ConstMethodHandle;
+import com.android.tools.r8.ir.code.ConstMethodType;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstanceGet;
+import com.android.tools.r8.ir.code.InstancePut;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.Invoke;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.NewArrayEmpty;
+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.kotlin.Kotlin;
+import java.util.ListIterator;
+import java.util.function.Function;
+
+// Performs processing of the method code (all methods) needed by lambda rewriter.
+//
+// Functionality can be modified by strategy (specific to lambda group) and lambda
+// type visitor.
+//
+// In general it is used to
+//   (a) track the code for illegal lambda type usages inside the code, and
+//   (b) patching valid lambda type references to point to lambda group classes instead.
+//
+// This class is also used inside particular strategies as a context of the instruction
+// being checked or patched, it provides access to code, block and instruction iterators.
+public abstract class CodeProcessor {
+  // Strategy (specific to lambda group) for detecting valid references to the
+  // lambda classes (of this group) and patching them with group class references.
+  public interface Strategy {
+    LambdaGroup group();
+
+    boolean isValidStaticFieldWrite(CodeProcessor context, DexField field);
+
+    boolean isValidStaticFieldRead(CodeProcessor context, DexField field);
+
+    boolean isValidInstanceFieldWrite(CodeProcessor context, DexField field);
+
+    boolean isValidInstanceFieldRead(CodeProcessor context, DexField field);
+
+    boolean isValidInvoke(CodeProcessor context, InvokeMethod invoke);
+
+    boolean isValidNewInstance(CodeProcessor context, NewInstance invoke);
+
+    void patch(CodeProcessor context, NewInstance newInstance);
+
+    void patch(CodeProcessor context, InvokeMethod invoke);
+
+    void patch(CodeProcessor context, InstancePut instancePut);
+
+    void patch(CodeProcessor context, InstanceGet instanceGet);
+
+    void patch(CodeProcessor context, StaticPut staticPut);
+
+    void patch(CodeProcessor context, StaticGet staticGet);
+  }
+
+  // No-op strategy.
+  public static final Strategy NoOp = new Strategy() {
+    @Override
+    public LambdaGroup group() {
+      return null;
+    }
+
+    @Override
+    public boolean isValidInstanceFieldWrite(CodeProcessor context, DexField field) {
+      return false;
+    }
+
+    @Override
+    public boolean isValidInstanceFieldRead(CodeProcessor context, DexField field) {
+      return false;
+    }
+
+    @Override
+    public boolean isValidStaticFieldWrite(CodeProcessor context, DexField field) {
+      return false;
+    }
+
+    @Override
+    public boolean isValidStaticFieldRead(CodeProcessor context, DexField field) {
+      return false;
+    }
+
+    @Override
+    public boolean isValidInvoke(CodeProcessor context, InvokeMethod invoke) {
+      return false;
+    }
+
+    @Override
+    public boolean isValidNewInstance(CodeProcessor context, NewInstance invoke) {
+      return false;
+    }
+
+    @Override
+    public void patch(CodeProcessor context, NewInstance newInstance) {
+      throw new Unreachable();
+    }
+
+    @Override
+    public void patch(CodeProcessor context, InvokeMethod invoke) {
+      throw new Unreachable();
+    }
+
+    @Override
+    public void patch(CodeProcessor context, InstancePut instancePut) {
+      throw new Unreachable();
+    }
+
+    @Override
+    public void patch(CodeProcessor context, InstanceGet instanceGet) {
+      throw new Unreachable();
+    }
+
+    @Override
+    public void patch(CodeProcessor context, StaticPut staticPut) {
+      throw new Unreachable();
+    }
+
+    @Override
+    public void patch(CodeProcessor context, StaticGet staticGet) {
+      throw new Unreachable();
+    }
+  };
+
+  public final DexItemFactory factory;
+  public final Kotlin kotlin;
+
+  // Defines a factory providing a strategy for a lambda type, returns
+  // NoOp strategy if the type is not a lambda.
+  private final Function<DexType, Strategy> strategyProvider;
+
+  // Visitor for lambda type references seen in unexpected places. Either
+  // invalidates the lambda or asserts depending on the processing phase.
+  private final LambdaTypeVisitor lambdaChecker;
+
+  // Specify the context of the current instruction: method/code/blocks/instructions.
+  public final DexEncodedMethod method;
+  public final IRCode code;
+  public final ListIterator<BasicBlock> blocks;
+  private InstructionListIterator instructions;
+
+  CodeProcessor(DexItemFactory factory,
+      Function<DexType, Strategy> strategyProvider,
+      LambdaTypeVisitor lambdaChecker,
+      DexEncodedMethod method, IRCode code) {
+
+    this.strategyProvider = strategyProvider;
+    this.factory = factory;
+    this.kotlin = factory.kotlin;
+    this.lambdaChecker = lambdaChecker;
+    this.method = method;
+    this.code = code;
+    this.blocks = code.listIterator();
+  }
+
+  public final InstructionListIterator instructions() {
+    assert instructions != null;
+    return instructions;
+  }
+
+  final void processCode() {
+    while (blocks.hasNext()) {
+      BasicBlock block = blocks.next();
+      instructions = block.listIterator();
+      while (instructions.hasNext()) {
+        onInstruction(instructions.next());
+      }
+    }
+  }
+
+  private void onInstruction(Instruction instruction) {
+    if (instruction.isInvoke()) {
+      handle(instruction.asInvoke());
+    } else if (instruction.isNewInstance()) {
+      handle(instruction.asNewInstance());
+    } else if (instruction.isCheckCast()) {
+      handle(instruction.asCheckCast());
+    } else if (instruction.isNewArrayEmpty()) {
+      handle(instruction.asNewArrayEmpty());
+    } else if (instruction.isConstClass()) {
+      handle(instruction.asConstClass());
+    } else if (instruction.isConstMethodType()) {
+      handle(instruction.asConstMethodType());
+    } else if (instruction.isConstMethodHandle()) {
+      handle(instruction.asConstMethodHandle());
+    } else if (instruction.isInstanceGet()) {
+      handle(instruction.asInstanceGet());
+    } else if (instruction.isInstancePut()) {
+      handle(instruction.asInstancePut());
+    } else if (instruction.isStaticGet()) {
+      handle(instruction.asStaticGet());
+    } else if (instruction.isStaticPut()) {
+      handle(instruction.asStaticPut());
+    }
+  }
+
+  private void handle(Invoke invoke) {
+    if (invoke.isInvokeNewArray()) {
+      lambdaChecker.accept(invoke.asInvokeNewArray().getReturnType());
+      return;
+    }
+    if (invoke.isInvokeMultiNewArray()) {
+      lambdaChecker.accept(invoke.asInvokeMultiNewArray().getReturnType());
+      return;
+    }
+    if (invoke.isInvokeCustom()) {
+      lambdaChecker.accept(invoke.asInvokeCustom().getCallSite());
+      return;
+    }
+
+    InvokeMethod invokeMethod = invoke.asInvokeMethod();
+    Strategy strategy = strategyProvider.apply(invokeMethod.getInvokedMethod().holder);
+    if (strategy.isValidInvoke(this, invokeMethod)) {
+      // Invalidate signature, there still should not be lambda references.
+      lambdaChecker.accept(invokeMethod.getInvokedMethod().proto);
+      // Only rewrite references to lambda classes if we are outside the class.
+      if (invokeMethod.getInvokedMethod().holder != this.method.method.holder) {
+        process(strategy, invokeMethod);
+      }
+      return;
+    }
+
+    // For the rest invalidate any references.
+    if (invoke.isInvokePolymorphic()) {
+      lambdaChecker.accept(invoke.asInvokePolymorphic().getProto());
+    }
+    lambdaChecker.accept(invokeMethod.getInvokedMethod(), null);
+  }
+
+  private void handle(NewInstance newInstance) {
+    Strategy strategy = strategyProvider.apply(newInstance.clazz);
+    if (strategy.isValidNewInstance(this, newInstance)) {
+      // Only rewrite references to lambda classes if we are outside the class.
+      if (newInstance.clazz != this.method.method.holder) {
+        process(strategy, newInstance);
+      }
+    }
+  }
+
+  private void handle(CheckCast checkCast) {
+    lambdaChecker.accept(checkCast.getType());
+  }
+
+  private void handle(NewArrayEmpty newArrayEmpty) {
+    lambdaChecker.accept(newArrayEmpty.type);
+  }
+
+  private void handle(ConstClass constClass) {
+    lambdaChecker.accept(constClass.getValue());
+  }
+
+  private void handle(ConstMethodType constMethodType) {
+    lambdaChecker.accept(constMethodType.getValue());
+  }
+
+  private void handle(ConstMethodHandle constMethodHandle) {
+    lambdaChecker.accept(constMethodHandle.getValue());
+  }
+
+  private void handle(InstanceGet instanceGet) {
+    DexField field = instanceGet.getField();
+    Strategy strategy = strategyProvider.apply(field.clazz);
+    if (strategy.isValidInstanceFieldRead(this, field)) {
+      if (field.clazz != this.method.method.holder) {
+        // Only rewrite references to lambda classes if we are outside the class.
+        process(strategy, instanceGet);
+      }
+    } else {
+      lambdaChecker.accept(field.type);
+    }
+
+    // We avoid fields with type being lambda class, it is possible for
+    // a lambda to capture another lambda, but we don't support it for now.
+    lambdaChecker.accept(field.type);
+  }
+
+  private void handle(InstancePut instancePut) {
+    DexField field = instancePut.getField();
+    Strategy strategy = strategyProvider.apply(field.clazz);
+    if (strategy.isValidInstanceFieldWrite(this, field)) {
+      if (field.clazz != this.method.method.holder) {
+        // Only rewrite references to lambda classes if we are outside the class.
+        process(strategy, instancePut);
+      }
+    } else {
+      lambdaChecker.accept(field.type);
+    }
+
+    // We avoid fields with type being lambda class, it is possible for
+    // a lambda to capture another lambda, but we don't support it for now.
+    lambdaChecker.accept(field.type);
+  }
+
+  private void handle(StaticGet staticGet) {
+    DexField field = staticGet.getField();
+    Strategy strategy = strategyProvider.apply(field.clazz);
+    if (strategy.isValidStaticFieldRead(this, field)) {
+      if (field.clazz != this.method.method.holder) {
+        // Only rewrite references to lambda classes if we are outside the class.
+        process(strategy, staticGet);
+      }
+    } else {
+      lambdaChecker.accept(field.type);
+      lambdaChecker.accept(field.clazz);
+    }
+  }
+
+  private void handle(StaticPut staticPut) {
+    DexField field = staticPut.getField();
+    Strategy strategy = strategyProvider.apply(field.clazz);
+    if (strategy.isValidStaticFieldWrite(this, field)) {
+      if (field.clazz != this.method.method.holder) {
+        // Only rewrite references to lambda classes if we are outside the class.
+        process(strategy, staticPut);
+      }
+    } else {
+      lambdaChecker.accept(field.type);
+      lambdaChecker.accept(field.clazz);
+    }
+  }
+
+  abstract void process(Strategy strategy, InvokeMethod invokeMethod);
+
+  abstract void process(Strategy strategy, NewInstance newInstance);
+
+  abstract void process(Strategy strategy, InstancePut instancePut);
+
+  abstract void process(Strategy strategy, InstanceGet instanceGet);
+
+  abstract void process(Strategy strategy, StaticPut staticPut);
+
+  abstract void process(Strategy strategy, StaticGet staticGet);
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaGroup.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaGroup.java
new file mode 100644
index 0000000..fea83eb
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaGroup.java
@@ -0,0 +1,176 @@
+// 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.lambda;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.optimize.lambda.CodeProcessor.Strategy;
+import com.android.tools.r8.kotlin.Kotlin;
+import com.android.tools.r8.utils.ThrowingConsumer;
+import com.google.common.collect.Lists;
+import com.google.common.io.BaseEncoding;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+// Represents a group of lambda classes which potentially can be represented
+// by the same lambda _group_ class. Each lambda class inside the group is
+// assigned an integer id.
+public abstract class LambdaGroup {
+  public final LambdaGroupId id;
+
+  // Lambda group class name. Is intended to be stable and uniques.
+  // In current implementation is generated in following way:
+  //       <optional-package>.-$$LambdaGroup$<HASH>
+  // with HASH generated based on names of the lambda class names
+  // of lambdas included in the group.
+  private DexType classType;
+
+  // Maps lambda classes belonging to the group into the index inside the
+  // group. Note usage of linked hash map to keep insertion ordering stable.
+  //
+  // WARNING: access to this map is NOT synchronized and must be performed in
+  //          thread-safe context.
+  private final Map<DexType, LambdaInfo> lambdas = new LinkedHashMap<>();
+
+  public static class LambdaInfo {
+    public int id;
+    public final DexProgramClass clazz;
+
+    LambdaInfo(int id, DexProgramClass clazz) {
+      this.id = id;
+      this.clazz = clazz;
+    }
+  }
+
+  public LambdaGroup(LambdaGroupId id) {
+    this.id = id;
+  }
+
+  public final DexType getGroupClassType() {
+    assert classType != null;
+    return classType;
+  }
+
+  public final List<LambdaInfo> lambdas() {
+    return Lists.newArrayList(lambdas.values());
+  }
+
+  public final boolean shouldAddToMainDex(AppInfo appInfo) {
+    // We add the group class to main index if any of the
+    // lambda classes it replaces is added to main index.
+    for (DexType type : lambdas.keySet()) {
+      if (appInfo.isInMainDexList(type)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public final boolean containsLambda(DexType lambda) {
+    return lambdas.containsKey(lambda);
+  }
+
+  public final int lambdaId(DexType lambda) {
+    assert lambdas.containsKey(lambda);
+    return lambdas.get(lambda).id;
+  }
+
+  protected final DexEncodedField[] lambdaCaptureFields(DexType lambda) {
+    assert lambdas.containsKey(lambda);
+    return lambdas.get(lambda).clazz.instanceFields();
+  }
+
+  // Contains less than 2 elements?
+  final boolean isTrivial() {
+    return lambdas.size() < 2;
+  }
+
+  final void add(DexProgramClass lambda) {
+    assert !lambdas.containsKey(lambda.type);
+    lambdas.put(lambda.type, new LambdaInfo(lambdas.size(), lambda));
+  }
+
+  final void remove(DexType lambda) {
+    assert lambdas.containsKey(lambda);
+    lambdas.remove(lambda);
+  }
+
+  final void compact() {
+    int lastUsed = -1;
+    int lastSeen = -1;
+    for (Entry<DexType, LambdaInfo> entry : lambdas.entrySet()) {
+      Integer index = entry.getValue().id;
+      assert lastUsed <= lastSeen && lastSeen < index;
+      lastUsed++;
+      lastSeen = index;
+      if (lastUsed < index) {
+        entry.getValue().id = lastUsed;
+      }
+    }
+  }
+
+  public abstract Strategy getCodeStrategy();
+
+  public abstract ThrowingConsumer<DexClass, LambdaStructureError> lambdaClassValidator(
+      Kotlin kotlin, AppInfoWithSubtyping appInfo);
+
+  // Package for a lambda group class to be created in.
+  protected abstract String getTypePackage();
+
+  public final DexProgramClass synthesizeClass(DexItemFactory factory) {
+    assert classType == null;
+    List<LambdaInfo> lambdas = Lists.newArrayList(this.lambdas.values());
+    classType = factory.createType(
+        "L" + getTypePackage() + "-$$LambdaGroup$" + createHash(lambdas) + ";");
+    // We need to register new subtype manually  the newly introduced type
+    // does not have 'hierarchyLevel' set, but it is actually needed during
+    // synthetic class methods processing.
+    factory.kotlin.functional.lambdaType.addDirectSubtype(classType);
+    return getBuilder(factory).synthesizeClass();
+  }
+
+  protected abstract LambdaGroupClassBuilder getBuilder(DexItemFactory factory);
+
+  private String createHash(List<LambdaInfo> lambdas) {
+    try {
+      ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+      ObjectOutputStream out = new ObjectOutputStream(bytes);
+
+      // We will generate SHA-1 hash of the list of lambda classes represented in the group.
+      for (LambdaInfo lambda : lambdas) {
+        DexString descriptor = lambda.clazz.type.descriptor;
+        out.writeInt(descriptor.size); // To avoid same-prefix problem
+        out.write(descriptor.content);
+      }
+      out.close();
+
+      MessageDigest digest = MessageDigest.getInstance("SHA-1");
+      digest.update(bytes.toByteArray());
+      return BaseEncoding.base64Url().omitPadding().encode(digest.digest());
+    } catch (NoSuchAlgorithmException | IOException ex) {
+      throw new Unreachable("Cannot get SHA-1 message digest");
+    }
+  }
+
+  public static class LambdaStructureError extends Exception {
+    public LambdaStructureError(String cause) {
+      super(cause);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaGroupClassBuilder.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaGroupClassBuilder.java
new file mode 100644
index 0000000..5c591ab
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaGroupClassBuilder.java
@@ -0,0 +1,73 @@
+// 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.lambda;
+
+import com.android.tools.r8.graph.ClassAccessFlags;
+import com.android.tools.r8.graph.DexAnnotationSet;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
+import com.android.tools.r8.graph.EnclosingMethodAttribute;
+import com.android.tools.r8.graph.InnerClassAttribute;
+import com.android.tools.r8.ir.optimize.lambda.LambdaGroup.LambdaInfo;
+import com.android.tools.r8.origin.SynthesizedOrigin;
+import java.util.List;
+
+// Encapsulates lambda group class building logic and separates
+// it from the rest of lambda group functionality.
+public abstract class LambdaGroupClassBuilder<T extends LambdaGroup> {
+  protected final T group;
+  protected final DexItemFactory factory;
+  protected final String origin;
+  protected final List<LambdaInfo> lambdas;
+
+  protected LambdaGroupClassBuilder(T group, DexItemFactory factory, String origin) {
+    this.group = group;
+    this.factory = factory;
+    this.origin = origin;
+    this.lambdas = group.lambdas();
+  }
+
+  public final DexProgramClass synthesizeClass() {
+    return new DexProgramClass(
+        group.getGroupClassType(),
+        null,
+        new SynthesizedOrigin(origin, getClass()),
+        buildAccessFlags(),
+        getSuperClassType(),
+        buildInterfaces(),
+        factory.createString(origin),
+        buildEnclosingMethodAttribute(),
+        buildInnerClasses(),
+        buildAnnotations(),
+        buildStaticFields(),
+        buildInstanceFields(),
+        buildDirectMethods(),
+        buildVirtualMethods());
+  }
+
+  protected abstract DexType getSuperClassType();
+
+  protected abstract ClassAccessFlags buildAccessFlags();
+
+  protected abstract EnclosingMethodAttribute buildEnclosingMethodAttribute();
+
+  protected abstract List<InnerClassAttribute> buildInnerClasses();
+
+  protected abstract DexAnnotationSet buildAnnotations();
+
+  protected abstract DexEncodedMethod[] buildVirtualMethods();
+
+  protected abstract DexEncodedMethod[] buildDirectMethods();
+
+  protected abstract DexEncodedField[] buildInstanceFields();
+
+  protected abstract DexEncodedField[] buildStaticFields();
+
+  protected abstract DexTypeList buildInterfaces();
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaGroupId.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaGroupId.java
new file mode 100644
index 0000000..2c31541
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaGroupId.java
@@ -0,0 +1,20 @@
+// 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.lambda;
+
+// Represents a lambda group identifier uniquely identifying the groups
+// of potentially mergeable lambdas.
+//
+// Implements hashCode/equals in a way that guarantees that if two lambda
+// classes has equal ids they belong to the same lambda group.
+public interface LambdaGroupId {
+  LambdaGroup createGroup();
+
+  @Override
+  int hashCode();
+
+  @Override
+  boolean equals(Object obj);
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java
new file mode 100644
index 0000000..11976b6
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java
@@ -0,0 +1,414 @@
+// 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.lambda;
+
+import com.android.tools.r8.ApiLevelException;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexApplication.Builder;
+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.DexItemFactory;
+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.InstanceGet;
+import com.android.tools.r8.ir.code.InstancePut;
+import com.android.tools.r8.ir.code.InvokeMethod;
+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.conversion.CallSiteInformation;
+import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.ir.conversion.OptimizationFeedback;
+import com.android.tools.r8.ir.optimize.Outliner;
+import com.android.tools.r8.ir.optimize.lambda.CodeProcessor.Strategy;
+import com.android.tools.r8.ir.optimize.lambda.LambdaGroup.LambdaInfo;
+import com.android.tools.r8.ir.optimize.lambda.LambdaGroup.LambdaStructureError;
+import com.android.tools.r8.ir.optimize.lambda.kstyle.KStyleLambdaGroupIdFactory;
+import com.android.tools.r8.kotlin.Kotlin;
+import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.StringDiagnostic;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.ThrowingConsumer;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.function.BiFunction;
+import java.util.stream.Collectors;
+
+// Merging lambda classes into single lambda group classes. There are three flavors
+// of lambdas we are dealing with:
+//   (a) lambda classes synthesized in desugaring, handles java lambdas
+//   (b) k-style lambda classes synthesized by kotlin compiler
+//   (c) j-style lambda classes synthesized by kotlin compiler
+//
+// Lambda merging is potentially applicable to all three of them, but
+// current implementation only deals with k-style lambdas.
+//
+// In general we merge lambdas in 5 phases:
+//   1. collect all lambdas and compute group candidates. we do it synchronously
+//      and ensure that the order of lambda groups and lambdas inside each group
+//      is stable.
+//   2. analyze usages of lambdas and exclude lambdas with unexpected usage
+//      NOTE: currently we consider *all* usages outside the code invalid
+//      so we only need to patch method code when replacing the lambda class.
+//   3. exclude (invalidate) all lambda classes with usages we don't understand
+//      or support, compact the remaining lambda groups, remove trivial groups
+//      with less that 2 lambdas.
+//   4. replace lambda valid/supported class constructions with references to
+//      lambda group classes.
+//   5. synthesize group lambda classes.
+//
+public final class LambdaMerger {
+  // Maps lambda into a group, only contains lambdas we decided to merge.
+  // NOTE: needs synchronization.
+  private final Map<DexType, LambdaGroup> lambdas = new IdentityHashMap<>();
+  // We use linked map to ensure stable ordering of the groups
+  // when they are processed sequentially.
+  // NOTE: needs synchronization.
+  private final Map<LambdaGroupId, LambdaGroup> groups = new LinkedHashMap<>();
+
+  // Since invalidating lambdas may happen concurrently we don't remove invalidated lambdas
+  // from groups (and `lambdas`) right away since the ordering may be important. Instead we
+  // collect invalidated lambdas and remove them from groups after analysis is done.
+  private final Set<DexType> invalidatedLambdas = Sets.newConcurrentHashSet();
+
+  // Methods which need to be patched to reference lambda group classes instead of the
+  // original lambda classes. The number of methods is expected to be small since there
+  // is a 1:1 relation between lambda and method it is defined in (unless such a method
+  // was inlined by either kotlinc or r8).
+  //
+  // Note that we don't track precisely lambda -> method mapping, so it may happen that
+  // we mark a method for further processing, and then invalidate the only lambda referenced
+  // from it. In this case we will reprocess method that does not need patching, but it
+  // should not be happening very frequently and we ignore possible overhead.
+  private final Set<DexEncodedMethod> methodsToReprocess = Sets.newIdentityHashSet();
+
+  private final DexItemFactory factory;
+  private final Kotlin kotlin;
+  private final DiagnosticsHandler reporter;
+
+  private BiFunction<DexEncodedMethod, IRCode, CodeProcessor> strategyFactory = null;
+
+  // Lambda visitor invalidating lambdas it sees.
+  private final LambdaTypeVisitor lambdaInvalidator;
+  // Lambda visitor throwing Unreachable on each lambdas it sees.
+  private final LambdaTypeVisitor lambdaChecker;
+
+  public LambdaMerger(DexItemFactory factory, DiagnosticsHandler reporter) {
+    this.factory = factory;
+    this.kotlin = factory.kotlin;
+    this.reporter = reporter;
+
+    this.lambdaInvalidator = new LambdaTypeVisitor(factory, this::isMergeableLambda,
+        this::invalidateLambda);
+    this.lambdaChecker = new LambdaTypeVisitor(factory, this::isMergeableLambda,
+        type -> {
+          throw new Unreachable("Unexpected lambda " + type.toSourceString());
+        });
+  }
+
+  private void invalidateLambda(DexType lambda) {
+    invalidatedLambdas.add(lambda);
+  }
+
+  private synchronized boolean isMergeableLambda(DexType lambda) {
+    return lambdas.containsKey(lambda);
+  }
+
+  private synchronized LambdaGroup getLambdaGroup(DexType lambda) {
+    return lambdas.get(lambda);
+  }
+
+  private synchronized void queueForProcessing(DexEncodedMethod method) {
+    methodsToReprocess.add(method);
+  }
+
+  // Collect all group candidates and assign unique lambda ids inside each group.
+  // We do this before methods are being processed to guarantee stable order of
+  // lambdas inside each group.
+  public final void collectGroupCandidates(
+      DexApplication app, AppInfoWithLiveness infoWithLiveness, InternalOptions options) {
+    assert infoWithLiveness != null;
+    // Collect lambda groups.
+    app.classes().stream()
+        .filter(cls -> !infoWithLiveness.isPinned(cls.type))
+        .filter(cls -> cls.hasKotlinInfo() &&
+            cls.getKotlinInfo().isSyntheticClass() &&
+            cls.getKotlinInfo().asSyntheticClass().isKotlinStyleLambda())
+        .sorted((a, b) -> a.type.slowCompareTo(b.type)) // Ensure stable ordering.
+        .forEachOrdered(lambda -> {
+          try {
+            LambdaGroupId id = KStyleLambdaGroupIdFactory.create(kotlin, lambda, options);
+            LambdaGroup group = groups.computeIfAbsent(id, LambdaGroupId::createGroup);
+            group.add(lambda);
+            lambdas.put(lambda.type, group);
+          } catch (LambdaStructureError error) {
+            reporter.warning(
+                new StringDiagnostic("Invalid Kotlin-style lambda [" +
+                    lambda.type.toSourceString() + "]: " + error.getMessage()));
+          }
+        });
+
+    // Remove trivial groups.
+    removeTrivialLambdaGroups();
+
+    assert strategyFactory == null;
+    strategyFactory = AnalysisStrategy::new;
+  }
+
+  // Is called by IRConverter::rewriteCode, performs different actions
+  // depending on phase:
+  //   - in ANALYZE phase just analyzes invalid usages of lambda classes
+  //     inside the method code, invalidated such lambda classes,
+  //     collects methods that need to be patched.
+  //   - in APPLY phase patches the code to use lambda group classes, also
+  //     asserts that there are no more invalid lambda class references.
+  public final void processMethodCode(DexEncodedMethod method, IRCode code) {
+    if (strategyFactory != null) {
+      strategyFactory.apply(method, code).processCode();
+    }
+  }
+
+  public final void applyLambdaClassMapping(DexApplication app,
+      IRConverter converter, OptimizationFeedback feedback, Builder<?> builder,
+      ExecutorService executorService) throws ExecutionException, ApiLevelException {
+    if (lambdas.isEmpty()) {
+      return;
+    }
+
+    // Analyse references from program classes. We assume that this optimization
+    // is only used for full program analysis and there are no classpath classes.
+    analyzeReferencesInProgramClasses(app, executorService);
+
+    // Analyse more complex aspects of lambda classes including method code.
+    assert converter.appInfo.hasSubtyping();
+    analyzeLambdaClassesStructure(converter.appInfo.withSubtyping(), executorService);
+
+    // Remove invalidated lambdas, compact groups to ensure
+    // sequential lambda ids, create group lambda classes.
+    Map<LambdaGroup, DexProgramClass> lambdaGroupsClasses = finalizeLambdaGroups();
+
+    // Switch to APPLY strategy.
+    this.strategyFactory = ApplyStrategy::new;
+
+    // Add synthesized lambda group classes to the builder.
+    for (Entry<LambdaGroup, DexProgramClass> entry : lambdaGroupsClasses.entrySet()) {
+      converter.optimizeSynthesizedClass(entry.getValue());
+      builder.addSynthesizedClass(entry.getValue(),
+          entry.getKey().shouldAddToMainDex(converter.appInfo));
+    }
+
+    // Rewrite lambda class references into lambda group class
+    // references inside methods from the processing queue.
+    rewriteLambdaReferences(converter, feedback);
+    this.strategyFactory = null;
+  }
+
+  private void analyzeReferencesInProgramClasses(
+      DexApplication app, ExecutorService service) throws ExecutionException {
+    List<Future<?>> futures = new ArrayList<>();
+    for (DexProgramClass clazz : app.classes()) {
+      futures.add(service.submit(() -> analyzeClass(clazz)));
+    }
+    ThreadUtils.awaitFutures(futures);
+  }
+
+  private void analyzeLambdaClassesStructure(
+      AppInfoWithSubtyping appInfo, ExecutorService service) throws ExecutionException {
+    List<Future<?>> futures = new ArrayList<>();
+    for (LambdaGroup group : groups.values()) {
+      ThrowingConsumer<DexClass, LambdaStructureError> validator =
+          group.lambdaClassValidator(kotlin, appInfo);
+      for (LambdaInfo lambda : group.lambdas()) {
+        futures.add(service.submit(() -> {
+          try {
+            validator.accept(lambda.clazz);
+          } catch (LambdaStructureError error) {
+            reporter.info(
+                new StringDiagnostic("Unexpected Kotlin-style lambda structure [" +
+                    lambda.clazz.type.toSourceString() + "]: " + error.getMessage())
+            );
+            invalidateLambda(lambda.clazz.type);
+          }
+        }));
+      }
+    }
+    ThreadUtils.awaitFutures(futures);
+  }
+
+  private synchronized Map<LambdaGroup, DexProgramClass> finalizeLambdaGroups() {
+    for (DexType lambda : invalidatedLambdas) {
+      LambdaGroup group = lambdas.get(lambda);
+      assert group != null;
+      lambdas.remove(lambda);
+      group.remove(lambda);
+    }
+    invalidatedLambdas.clear();
+
+    // Remove new trivial lambdas.
+    removeTrivialLambdaGroups();
+
+    // Compact lambda groups, synthesize lambda group classes.
+    Map<LambdaGroup, DexProgramClass> result = new LinkedHashMap<>();
+    for (LambdaGroup group : groups.values()) {
+      assert !group.isTrivial() : "No trivial group is expected here.";
+      group.compact();
+      result.put(group, group.synthesizeClass(factory));
+    }
+    return result;
+  }
+
+  private void removeTrivialLambdaGroups() {
+    Iterator<Entry<LambdaGroupId, LambdaGroup>> iterator = groups.entrySet().iterator();
+    while (iterator.hasNext()) {
+      Entry<LambdaGroupId, LambdaGroup> group = iterator.next();
+      if (group.getValue().isTrivial()) {
+        iterator.remove();
+        List<LambdaInfo> lambdas = group.getValue().lambdas();
+        assert lambdas.size() < 2;
+        for (LambdaInfo lambda : lambdas) {
+          this.lambdas.remove(lambda.clazz.type);
+        }
+      }
+    }
+  }
+
+  private void rewriteLambdaReferences(
+      IRConverter converter, OptimizationFeedback feedback) throws ApiLevelException {
+    List<DexEncodedMethod> methods =
+        methodsToReprocess
+            .stream()
+            .sorted(DexEncodedMethod::slowCompare)
+            .collect(Collectors.toList());
+    for (DexEncodedMethod method : methods) {
+      converter.processMethod(method, feedback,
+          x -> false, CallSiteInformation.empty(), Outliner::noProcessing);
+      assert method.isProcessed();
+    }
+  }
+
+  private void analyzeClass(DexProgramClass clazz) {
+    lambdaInvalidator.accept(clazz.superType);
+    lambdaInvalidator.accept(clazz.interfaces);
+    lambdaInvalidator.accept(clazz.annotations);
+
+    for (DexEncodedField field : clazz.staticFields()) {
+      lambdaInvalidator.accept(field.annotations);
+      if (field.field.type != clazz.type) {
+        // Ignore static fields of the same type.
+        lambdaInvalidator.accept(field.field, clazz.type);
+      }
+    }
+    for (DexEncodedField field : clazz.instanceFields()) {
+      lambdaInvalidator.accept(field.annotations);
+      lambdaInvalidator.accept(field.field, clazz.type);
+    }
+
+    for (DexEncodedMethod method : clazz.directMethods()) {
+      lambdaInvalidator.accept(method.annotations);
+      lambdaInvalidator.accept(method.parameterAnnotations);
+      lambdaInvalidator.accept(method.method, clazz.type);
+    }
+    for (DexEncodedMethod method : clazz.virtualMethods()) {
+      lambdaInvalidator.accept(method.annotations);
+      lambdaInvalidator.accept(method.parameterAnnotations);
+      lambdaInvalidator.accept(method.method, clazz.type);
+    }
+  }
+
+  private Strategy strategyProvider(DexType type) {
+    LambdaGroup group = this.getLambdaGroup(type);
+    return group != null ? group.getCodeStrategy() : CodeProcessor.NoOp;
+  }
+
+  private final class AnalysisStrategy extends CodeProcessor {
+    private AnalysisStrategy(DexEncodedMethod method, IRCode code) {
+      super(LambdaMerger.this.factory,
+          LambdaMerger.this::strategyProvider, lambdaInvalidator, method, code);
+    }
+
+    @Override
+    void process(Strategy strategy, InvokeMethod invokeMethod) {
+      queueForProcessing(method);
+    }
+
+    @Override
+    void process(Strategy strategy, NewInstance newInstance) {
+      queueForProcessing(method);
+    }
+
+    @Override
+    void process(Strategy strategy, InstancePut instancePut) {
+      queueForProcessing(method);
+    }
+
+    @Override
+    void process(Strategy strategy, InstanceGet instanceGet) {
+      queueForProcessing(method);
+    }
+
+    @Override
+    void process(Strategy strategy, StaticPut staticPut) {
+      queueForProcessing(method);
+    }
+
+    @Override
+    void process(Strategy strategy, StaticGet staticGet) {
+      queueForProcessing(method);
+    }
+  }
+
+  private final class ApplyStrategy extends CodeProcessor {
+    private ApplyStrategy(DexEncodedMethod method, IRCode code) {
+      super(LambdaMerger.this.factory,
+          LambdaMerger.this::strategyProvider, lambdaChecker, method, code);
+    }
+
+    @Override
+    void process(Strategy strategy, InvokeMethod invokeMethod) {
+      strategy.patch(this, invokeMethod);
+    }
+
+    @Override
+    void process(Strategy strategy, NewInstance newInstance) {
+      strategy.patch(this, newInstance);
+    }
+
+    @Override
+    void process(Strategy strategy, InstancePut instancePut) {
+      strategy.patch(this, instancePut);
+    }
+
+    @Override
+    void process(Strategy strategy, InstanceGet instanceGet) {
+      strategy.patch(this, instanceGet);
+    }
+
+    @Override
+    void process(Strategy strategy, StaticPut staticPut) {
+      strategy.patch(this, staticPut);
+    }
+
+    @Override
+    void process(Strategy strategy, StaticGet staticGet) {
+      strategy.patch(this, staticGet);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaTypeVisitor.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaTypeVisitor.java
new file mode 100644
index 0000000..4f56306
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaTypeVisitor.java
@@ -0,0 +1,155 @@
+// 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.lambda;
+
+import com.android.tools.r8.graph.DexAnnotation;
+import com.android.tools.r8.graph.DexAnnotationElement;
+import com.android.tools.r8.graph.DexAnnotationSet;
+import com.android.tools.r8.graph.DexAnnotationSetRefList;
+import com.android.tools.r8.graph.DexCallSite;
+import com.android.tools.r8.graph.DexEncodedAnnotation;
+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.DexMethodHandle;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
+import com.android.tools.r8.graph.DexValue;
+import com.android.tools.r8.graph.DexValue.DexValueArray;
+import com.android.tools.r8.graph.DexValue.DexValueField;
+import com.android.tools.r8.graph.DexValue.DexValueMethod;
+import com.android.tools.r8.graph.DexValue.DexValueMethodHandle;
+import com.android.tools.r8.graph.DexValue.DexValueMethodType;
+import com.android.tools.r8.graph.DexValue.DexValueType;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+// Encapsulates the logic of visiting all lambda classes.
+final class LambdaTypeVisitor {
+  private final DexItemFactory factory;
+  private final Predicate<DexType> isLambdaType;
+  private final Consumer<DexType> onLambdaType;
+
+  LambdaTypeVisitor(DexItemFactory factory,
+      Predicate<DexType> isLambdaType, Consumer<DexType> onLambdaType) {
+    this.factory = factory;
+    this.isLambdaType = isLambdaType;
+    this.onLambdaType = onLambdaType;
+  }
+
+  void accept(DexCallSite callSite) {
+    accept(callSite.methodProto);
+    accept(callSite.bootstrapMethod);
+    for (DexValue value : callSite.bootstrapArgs) {
+      accept(value);
+    }
+  }
+
+  private void accept(DexValue value) {
+    if (value instanceof DexValueType) {
+      accept(((DexValueType) value).value);
+      return;
+    }
+    if (value instanceof DexValueArray) {
+      for (DexValue subValue : ((DexValueArray) value).getValues()) {
+        accept(subValue);
+      }
+      return;
+    }
+    if (value instanceof DexValueMethod) {
+      accept(((DexValueMethod) value).value, null);
+      return;
+    }
+    if (value instanceof DexValueMethodHandle) {
+      accept(((DexValueMethodHandle) value).value);
+      return;
+    }
+    if (value instanceof DexValueMethodType) {
+      accept(((DexValueMethodType) value).value);
+      return;
+    }
+    if (value instanceof DexValueField) {
+      accept(((DexValueField) value).value, null);
+    }
+  }
+
+  void accept(DexMethodHandle handle) {
+    if (handle.isFieldHandle()) {
+      accept(handle.asField(), null);
+    } else {
+      assert handle.isMethodHandle();
+      accept(handle.asMethod(), null);
+    }
+  }
+
+  void accept(DexField field, DexType holderToIgnore) {
+    accept(field.type);
+    if (holderToIgnore != field.clazz) {
+      accept(field.clazz);
+    }
+  }
+
+  void accept(DexMethod method, DexType holderToIgnore) {
+    if (holderToIgnore != method.holder) {
+      accept(method.holder);
+    }
+    accept(method.proto);
+  }
+
+  void accept(DexProto proto) {
+    accept(proto.returnType);
+    accept(proto.parameters);
+  }
+
+  void accept(DexTypeList types) {
+    for (DexType type : types.values) {
+      accept(type);
+    }
+  }
+
+  void accept(DexAnnotationSet annotationSet) {
+    for (DexAnnotation annotation : annotationSet.annotations) {
+      accept(annotation);
+    }
+  }
+
+  void accept(DexAnnotationSetRefList annotationSetRefList) {
+    for (DexAnnotationSet annotationSet : annotationSetRefList.values) {
+      accept(annotationSet);
+    }
+  }
+
+  private void accept(DexAnnotation annotation) {
+    accept(annotation.annotation);
+  }
+
+  private void accept(DexEncodedAnnotation annotation) {
+    accept(annotation.type);
+    for (DexAnnotationElement element : annotation.elements) {
+      accept(element);
+    }
+  }
+
+  private void accept(DexAnnotationElement element) {
+    accept(element.value);
+  }
+
+  void accept(DexType type) {
+    if (type == null) {
+      return;
+    }
+    if (type.isPrimitiveType() || type.isVoidType() || type.isPrimitiveArrayType()) {
+      return;
+    }
+    if (type.isArrayType()) {
+      accept(type.toArrayElementType(factory));
+      return;
+    }
+    if (isLambdaType.test(type)) {
+      onLambdaType.accept(type);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/ClassInitializerSourceCode.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/ClassInitializerSourceCode.java
new file mode 100644
index 0000000..1cc0dcd
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/ClassInitializerSourceCode.java
@@ -0,0 +1,53 @@
+// 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.lambda.kstyle;
+
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.Invoke.Type;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.IRBuilder;
+import com.android.tools.r8.ir.synthetic.SyntheticSourceCode;
+import com.google.common.collect.Lists;
+import java.util.List;
+import java.util.function.IntFunction;
+
+final class ClassInitializerSourceCode extends SyntheticSourceCode {
+  private final DexType lambdaGroupType;
+  private final DexMethod lambdaConstructorMethod;
+  private final int count;
+  private final IntFunction<DexField> fieldGenerator;
+
+  ClassInitializerSourceCode(DexItemFactory factory,
+      DexType lambdaGroupType, int count, IntFunction<DexField> fieldGenerator) {
+    super(null, factory.createProto(factory.voidType));
+    this.lambdaGroupType = lambdaGroupType;
+    this.count = count;
+    this.fieldGenerator = fieldGenerator;
+    this.lambdaConstructorMethod = factory.createMethod(lambdaGroupType,
+        factory.createProto(factory.voidType, factory.intType), factory.constructorMethodName);
+  }
+
+  @Override
+  protected void prepareInstructions() {
+    int instance = nextRegister(ValueType.OBJECT);
+    int lambdaId = nextRegister(ValueType.INT);
+    List<ValueType> argValues = Lists.newArrayList(ValueType.OBJECT, ValueType.INT);
+    List<Integer> argRegisters = Lists.newArrayList(instance, lambdaId);
+
+    for (int id = 0; id < count; id++) {
+      int finalId = id;
+      add(builder -> builder.addNewInstance(instance, lambdaGroupType));
+      add(builder -> builder.addConst(ValueType.INT, lambdaId, finalId));
+      add(builder -> builder.addInvoke(Type.DIRECT,
+          lambdaConstructorMethod, lambdaConstructorMethod.proto, argValues, argRegisters));
+      add(builder -> builder.addStaticPut(instance, fieldGenerator.apply(finalId)));
+    }
+
+    add(IRBuilder::addReturn);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/InstanceInitializerSourceCode.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/InstanceInitializerSourceCode.java
new file mode 100644
index 0000000..00571be
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/InstanceInitializerSourceCode.java
@@ -0,0 +1,55 @@
+// 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.lambda.kstyle;
+
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.Invoke.Type;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.IRBuilder;
+import com.android.tools.r8.ir.synthetic.SyntheticSourceCode;
+import com.google.common.collect.Lists;
+import java.util.function.IntFunction;
+
+final class InstanceInitializerSourceCode extends SyntheticSourceCode {
+  private final DexField idField;
+  private final IntFunction<DexField> fieldGenerator;
+  private final int arity;
+  private final DexMethod lambdaInitializer;
+
+  InstanceInitializerSourceCode(DexItemFactory factory, DexType lambdaGroupType,
+      DexField idField, IntFunction<DexField> fieldGenerator, DexProto proto, int arity) {
+    super(lambdaGroupType, proto);
+    this.idField = idField;
+    this.fieldGenerator = fieldGenerator;
+    this.arity = arity;
+    this.lambdaInitializer = factory.createMethod(factory.kotlin.functional.lambdaType,
+        factory.createProto(factory.voidType, factory.intType), factory.constructorMethodName);
+  }
+
+  @Override
+  protected void prepareInstructions() {
+    int receiverRegister = getReceiverRegister();
+
+    add(builder -> builder.addInstancePut(getParamRegister(0), receiverRegister, idField));
+
+    DexType[] values = proto.parameters.values;
+    for (int i = 1; i < values.length; i++) {
+      int index = i;
+      add(builder -> builder.addInstancePut(
+          getParamRegister(index), receiverRegister, fieldGenerator.apply(index - 1)));
+    }
+
+    int arityRegister = nextRegister(ValueType.INT);
+    add(builder -> builder.addConst(ValueType.INT, arityRegister, arity));
+    add(builder -> builder.addInvoke(Type.DIRECT, lambdaInitializer, lambdaInitializer.proto,
+        Lists.newArrayList(ValueType.OBJECT, ValueType.INT),
+        Lists.newArrayList(receiverRegister, arityRegister)));
+    add(IRBuilder::addReturn);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleConstants.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleConstants.java
new file mode 100644
index 0000000..619fa67
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleConstants.java
@@ -0,0 +1,54 @@
+// 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.lambda.kstyle;
+
+import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.graph.ClassAccessFlags;
+import com.android.tools.r8.graph.FieldAccessFlags;
+import com.android.tools.r8.graph.MethodAccessFlags;
+
+interface KStyleConstants {
+  // Default lambda class flags.
+  ClassAccessFlags LAMBDA_CLASS_FLAGS =
+      ClassAccessFlags.fromDexAccessFlags(Constants.ACC_FINAL);
+  // Access-relaxed lambda class flags.
+  ClassAccessFlags PUBLIC_LAMBDA_CLASS_FLAGS =
+      ClassAccessFlags.fromDexAccessFlags(Constants.ACC_PUBLIC + Constants.ACC_FINAL);
+
+  // Default lambda class initializer flags.
+  MethodAccessFlags CLASS_INITIALIZER_FLAGS =
+      MethodAccessFlags.fromSharedAccessFlags(Constants.ACC_STATIC, true);
+  // Default lambda class constructor flags.
+  MethodAccessFlags CONSTRUCTOR_FLAGS =
+      MethodAccessFlags.fromSharedAccessFlags(0, true);
+  // Access-relaxed lambda class constructor flags.
+  MethodAccessFlags CONSTRUCTOR_FLAGS_RELAXED =
+      MethodAccessFlags.fromSharedAccessFlags(Constants.ACC_PUBLIC, true);
+
+  // Default main lambda method flags.
+  MethodAccessFlags MAIN_METHOD_FLAGS =
+      MethodAccessFlags.fromSharedAccessFlags(
+          Constants.ACC_PUBLIC + Constants.ACC_FINAL, false);
+  // Default bridge lambda method flags.
+  MethodAccessFlags BRIDGE_METHOD_FLAGS =
+      MethodAccessFlags.fromSharedAccessFlags(
+          Constants.ACC_PUBLIC + Constants.ACC_SYNTHETIC + Constants.ACC_BRIDGE, false);
+  // Bridge lambda method flags after inliner.
+  MethodAccessFlags BRIDGE_METHOD_FLAGS_FIXED =
+      MethodAccessFlags.fromSharedAccessFlags(Constants.ACC_PUBLIC, false);
+
+  // Default singleton instance folding field flags.
+  FieldAccessFlags SINGLETON_FIELD_FLAGS =
+      FieldAccessFlags.fromSharedAccessFlags(
+          Constants.ACC_PUBLIC + Constants.ACC_STATIC + Constants.ACC_FINAL);
+  // Default instance (lambda capture) field flags.
+  FieldAccessFlags CAPTURE_FIELD_FLAGS =
+      FieldAccessFlags.fromSharedAccessFlags(
+          Constants.ACC_FINAL + Constants.ACC_SYNTHETIC);
+  // access-relaxed instance (lambda capture) field flags.
+  FieldAccessFlags CAPTURE_FIELD_FLAGS_RELAXED =
+      FieldAccessFlags.fromSharedAccessFlags(
+          Constants.ACC_PUBLIC + Constants.ACC_FINAL + Constants.ACC_SYNTHETIC);
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaClassValidator.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaClassValidator.java
new file mode 100644
index 0000000..ffd0f7d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaClassValidator.java
@@ -0,0 +1,242 @@
+// 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.lambda.kstyle;
+
+import com.android.tools.r8.code.Const16;
+import com.android.tools.r8.code.Const4;
+import com.android.tools.r8.code.Format22c;
+import com.android.tools.r8.code.Iput;
+import com.android.tools.r8.code.IputBoolean;
+import com.android.tools.r8.code.IputByte;
+import com.android.tools.r8.code.IputChar;
+import com.android.tools.r8.code.IputObject;
+import com.android.tools.r8.code.IputShort;
+import com.android.tools.r8.code.IputWide;
+import com.android.tools.r8.code.ReturnVoid;
+import com.android.tools.r8.code.SputObject;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.Code;
+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.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.optimize.Inliner.Reason;
+import com.android.tools.r8.ir.optimize.lambda.CaptureSignature;
+import com.android.tools.r8.ir.optimize.lambda.LambdaGroup.LambdaStructureError;
+import com.android.tools.r8.kotlin.Kotlin;
+import com.android.tools.r8.utils.ThrowingConsumer;
+
+// Encapsulates the logic of deep-checking of the lambda class assumptions.
+//
+// For k-style lambdas we only check the code of class and instance
+// initializers to ensure that their code performs no unexpected actions:
+//
+//  (a) Class initializer is only present for stateless lambdas and does
+//      nothing expect instantiating the instance and storing it in
+//      static instance field.
+//
+//  (b) Instance initializers stores all captured values in proper capture
+//      fields and calls the super constructor passing arity to it.
+final class KStyleLambdaClassValidator implements ThrowingConsumer<DexClass, LambdaStructureError> {
+  private final Kotlin kotlin;
+  private final KStyleLambdaGroup group;
+  private final AppInfoWithSubtyping appInfo;
+
+  KStyleLambdaClassValidator(Kotlin kotlin, KStyleLambdaGroup group, AppInfoWithSubtyping appInfo) {
+    this.kotlin = kotlin;
+    this.group = group;
+    this.appInfo = appInfo;
+  }
+
+  @Override
+  public void accept(DexClass lambda) throws LambdaStructureError {
+    if (!CaptureSignature.getCaptureSignature(lambda.instanceFields()).equals(group.id().capture)) {
+      throw new LambdaStructureError("capture signature was modified");
+    }
+
+    DexEncodedMethod classInitializer = null;
+    DexEncodedMethod instanceInitializer = null;
+    for (DexEncodedMethod method : lambda.directMethods()) {
+      // We only check bodies of class and instance initializers since we don't expect to
+      // see any static or private methods and all virtual methods will be translated into
+      // same methods dispatching on lambda id to proper code.
+      if (method.isClassInitializer()) {
+        Code code = method.getCode();
+        if (!group.isStateless()) {
+          throw new LambdaStructureError("static initializer on stateful lambda");
+        }
+        if (classInitializer != null || code == null || !code.isDexCode() ||
+            !validateStatelessLambdaClassInitializer(lambda, code.asDexCode())) {
+          throw new LambdaStructureError("static initializer code verification failed");
+        }
+        classInitializer = method;
+
+      } else if (method.isInstanceInitializer()) {
+        Code code = method.getCode();
+        if (instanceInitializer != null || code == null || !code.isDexCode() ||
+            !validateInstanceInitializer(lambda, code.asDexCode())) {
+          throw new LambdaStructureError("instance initializer code verification failed");
+        }
+        instanceInitializer = method;
+      }
+    }
+
+    if (group.isStateless() && (classInitializer == null)) {
+      throw new LambdaStructureError("missing static initializer on stateless lambda");
+    }
+
+    // This check is actually not required for lambda class merging, we only have to do
+    // this because group class method composition piggybacks on inlining which has
+    // assertions checking when we can inline force-inlined methods. So we double-check
+    // these assertion never triggers.
+    //
+    // NOTE: the real type name for lambda group class is not generated yet, but we
+    //       can safely use a fake one here.
+    DexType fakeLambdaGroupType = kotlin.factory.createType(
+        "L" + group.getTypePackage() + "-$$LambdaGroup$XXXX;");
+    for (DexEncodedMethod method : lambda.virtualMethods()) {
+      if (!method.isInliningCandidate(fakeLambdaGroupType, Reason.SIMPLE, appInfo)) {
+        throw new LambdaStructureError("method " + method.method.toSourceString() +
+            " is not inline-able into lambda group class");
+      }
+    }
+  }
+
+  private boolean validateInstanceInitializer(DexClass lambda, Code code) {
+    DexEncodedField[] captures = lambda.instanceFields();
+    com.android.tools.r8.code.Instruction[] instructions = code.asDexCode().instructions;
+    int index = 0;
+
+    if (instructions.length != (captures.length + 3)) {
+      return false;
+    }
+
+    // Capture field assignments: go through captured fields in assumed order
+    // and ensure they are assigned values from appropriate parameters.
+    int wideFieldsSeen = 0;
+    for (DexEncodedField field : captures) {
+      switch (field.field.type.toShorty()) {
+        case 'Z':
+          if (!(instructions[index] instanceof IputBoolean) ||
+              (instructions[index].getField() != field.field) ||
+              (((Format22c) instructions[index]).A != (index + 1 + wideFieldsSeen))) {
+            return false;
+          }
+          break;
+
+        case 'B':
+          if (!(instructions[index] instanceof IputByte) ||
+              (instructions[index].getField() != field.field) ||
+              (((Format22c) instructions[index]).A != (index + 1 + wideFieldsSeen))) {
+            return false;
+          }
+          break;
+
+        case 'S':
+          if (!(instructions[index] instanceof IputShort) ||
+              (instructions[index].getField() != field.field) ||
+              (((Format22c) instructions[index]).A != (index + 1 + wideFieldsSeen))) {
+            return false;
+          }
+          break;
+
+        case 'C':
+          if (!(instructions[index] instanceof IputChar) ||
+              (instructions[index].getField() != field.field) ||
+              (((Format22c) instructions[index]).A != (index + 1 + wideFieldsSeen))) {
+            return false;
+          }
+          break;
+
+        case 'I':
+        case 'F':
+          if (!(instructions[index] instanceof Iput) ||
+              (instructions[index].getField() != field.field) ||
+              (((Format22c) instructions[index]).A != (index + 1 + wideFieldsSeen))) {
+            return false;
+          }
+          break;
+
+        case 'J':
+        case 'D':
+          if (!(instructions[index] instanceof IputWide) ||
+              (instructions[index].getField() != field.field) ||
+              (((Format22c) instructions[index]).A != (index + 1 + wideFieldsSeen))) {
+            return false;
+          }
+          wideFieldsSeen++;
+          break;
+
+        case 'L':
+          if (!(instructions[index] instanceof IputObject) ||
+              (instructions[index].getField() != field.field) ||
+              (((Format22c) instructions[index]).A != (index + 1 + wideFieldsSeen))) {
+            return false;
+          }
+          break;
+
+        default:
+          throw new Unreachable();
+      }
+      index++;
+    }
+
+    // Epilogue.
+    if (!(instructions[index] instanceof Const4) &&
+        !(instructions[index] instanceof Const16)) {
+      return false;
+    }
+    index++;
+    if (!(instructions[index] instanceof com.android.tools.r8.code.InvokeDirect) ||
+        instructions[index].getMethod() != kotlin.functional.lambdaInitializerMethod) {
+      return false;
+    }
+    index++;
+    if (!(instructions[index] instanceof ReturnVoid)) {
+      return false;
+    }
+    index++;
+
+    assert index == instructions.length;
+    return true;
+  }
+
+  private boolean validateStatelessLambdaClassInitializer(DexClass lambda, Code code) {
+    assert group.isStateless();
+    com.android.tools.r8.code.Instruction[] instructions = code.asDexCode().instructions;
+    if (instructions.length != 4) {
+      return false;
+    }
+    if (!(instructions[0] instanceof com.android.tools.r8.code.NewInstance) ||
+        ((com.android.tools.r8.code.NewInstance) instructions[0]).getType() != lambda.type) {
+      return false;
+    }
+    if (!(instructions[1] instanceof com.android.tools.r8.code.InvokeDirect) ||
+        !isLambdaInitializerMethod(lambda, instructions[1].getMethod())) {
+      return false;
+    }
+    if (!(instructions[2] instanceof SputObject) ||
+        !isLambdaSingletonField(lambda, instructions[2].getField())) {
+      return false;
+    }
+    if (!(instructions[3] instanceof ReturnVoid)) {
+      return false;
+    }
+    return true;
+  }
+
+  private boolean isLambdaSingletonField(DexClass lambda, DexField field) {
+    return field.type == lambda.type && field.clazz == lambda.type &&
+        field.name == kotlin.functional.kotlinStyleLambdaInstanceName;
+  }
+
+  private boolean isLambdaInitializerMethod(DexClass holder, DexMethod method) {
+    return method.holder == holder.type && method.name == kotlin.factory.constructorMethodName &&
+        method.proto.parameters.isEmpty() && method.proto.returnType == kotlin.factory.voidType;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaGroup.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaGroup.java
new file mode 100644
index 0000000..54f1d3a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaGroup.java
@@ -0,0 +1,163 @@
+// 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.lambda.kstyle;
+
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.optimize.lambda.CaptureSignature;
+import com.android.tools.r8.ir.optimize.lambda.CodeProcessor.Strategy;
+import com.android.tools.r8.ir.optimize.lambda.LambdaGroup;
+import com.android.tools.r8.ir.optimize.lambda.LambdaGroupClassBuilder;
+import com.android.tools.r8.kotlin.Kotlin;
+import com.android.tools.r8.utils.ThrowingConsumer;
+
+// Represents a k-style lambda group created to combine several lambda classes
+// generated by kotlin compiler for regular kotlin lambda expressions, like:
+//
+//      -----------------------------------------------------------------------
+//      fun foo(m: String, v: Int): () -> String {
+//        val lambda: (String, Int) -> String = { s, i -> s.substring(i) }
+//        return { "$m: $v" }
+//      }
+//      -----------------------------------------------------------------------
+//
+// Regular stateless k-style lambda class structure looks like below:
+//
+// -----------------------------------------------------------------------------------------------
+// // signature Lkotlin/jvm/internal/Lambda;Lkotlin/jvm/functions/Function2<
+//                          Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;>;
+// final class lambdas/LambdasKt$foo$lambda1$1
+//                  extends kotlin/jvm/internal/Lambda
+//                  implements kotlin/jvm/functions/Function2  {
+//
+//    public synthetic bridge invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+//
+//     public final invoke(Ljava/lang/String;I)Ljava/lang/String;
+//       @Lorg/jetbrains/annotations/NotNull;() // invisible
+//         @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
+//
+//     <init>()V
+//
+//     public final static Llambdas/LambdasKt$foo$lambda1$1; INSTANCE
+//
+//     static <clinit>()V
+//
+//     OUTERCLASS lambdas/LambdasKt foo (Ljava/lang/String;I)Lkotlin/jvm/functions/Function0;
+//     final static INNERCLASS lambdas/LambdasKt$foo$lambda1$1 null null
+// }
+// -----------------------------------------------------------------------------------------------
+//
+// Regular stateful k-style lambda class structure looks like below:
+//
+// -----------------------------------------------------------------------------------------------
+// // signature Lkotlin/jvm/internal/Lambda;Lkotlin/jvm/functions/Function0<Ljava/lang/String;>;
+// final class lambdas/LambdasKt$foo$1
+//                  extends kotlin/jvm/internal/Lambda
+//                  implements kotlin/jvm/functions/Function0  {
+//
+//     public synthetic bridge invoke()Ljava/lang/Object;
+//
+//     public final invoke()Ljava/lang/String;
+//       @Lorg/jetbrains/annotations/NotNull;() // invisible
+//
+//     <init>(Ljava/lang/String;I)V
+//
+//     final synthetic Ljava/lang/String; $m
+//     final synthetic I $v
+//
+//     OUTERCLASS lambdas/LambdasKt foo (Ljava/lang/String;I)Lkotlin/jvm/functions/Function0;
+//     final static INNERCLASS lambdas/LambdasKt$foo$1 null null
+// }
+// -----------------------------------------------------------------------------------------------
+//
+// Key k-style lambda class details:
+//   - extends kotlin.jvm.internal.Lambda
+//   - implements one of kotlin.jvm.functions.Function0..Function22, or FunctionN
+//     see: https://github.com/JetBrains/kotlin/blob/master/libraries/
+//                  stdlib/jvm/runtime/kotlin/jvm/functions/Functions.kt
+//     and: https://github.com/JetBrains/kotlin/blob/master/spec-docs/function-types.md
+//   - lambda class is created as an anonymous inner class
+//   - lambda class carries generic signature and kotlin metadata attribute
+//   - class instance fields represent captured values and have an instance constructor
+//     with matching parameters initializing them (see the second class above)
+//   - stateless lambda is implemented as a singleton with a static field storing the only
+//     instance and initialized in static class constructor (see the first class above)
+//   - main lambda method usually matches an exact lambda signature and may have
+//     generic signature attribute and nullability parameter annotations
+//   - optional bridge method created to satisfy interface implementation and
+//     forwarding call to lambda main method
+//
+final class KStyleLambdaGroup extends LambdaGroup {
+  private final Strategy strategy = new KStyleLambdaGroupCodeStrategy(this);
+
+  KStyleLambdaGroup(KStyleLambdaGroupId id) {
+    super(id);
+  }
+
+  final KStyleLambdaGroupId id() {
+    return (KStyleLambdaGroupId) id;
+  }
+
+  final boolean isStateless() {
+    return id().capture.isEmpty();
+  }
+
+  // Field referencing singleton instance for a lambda with specified id.
+  final DexField getSingletonInstanceField(DexItemFactory factory, int id) {
+    return factory.createField(this.getGroupClassType(),
+        this.getGroupClassType(), factory.createString("INSTANCE$" + id));
+  }
+
+  @Override
+  protected String getTypePackage() {
+    String pkg = id().pkg;
+    return pkg.isEmpty() ? "" : (pkg + "/");
+  }
+
+  final DexProto createConstructorProto(DexItemFactory factory) {
+    String capture = id().capture;
+    DexType[] newParameters = new DexType[capture.length() + 1];
+    newParameters[0] = factory.intType; // Lambda id.
+    for (int i = 0; i < capture.length(); i++) {
+      newParameters[i + 1] = CaptureSignature.fieldType(factory, capture, i);
+    }
+    return factory.createProto(factory.voidType, newParameters);
+  }
+
+  final DexField getLambdaIdField(DexItemFactory factory) {
+    return factory.createField(this.getGroupClassType(), factory.intType, "$id$");
+  }
+
+  final int mapFieldIntoCaptureIndex(DexType lambda, DexField field) {
+    return CaptureSignature.mapFieldIntoCaptureIndex(
+        id().capture, lambdaCaptureFields(lambda), field);
+  }
+
+  final DexField getCaptureField(DexItemFactory factory, int index) {
+    assert index >= 0 && index < id().capture.length();
+    return factory.createField(this.getGroupClassType(),
+        CaptureSignature.fieldType(factory, id().capture, index), "$capture$" + index);
+  }
+
+  @Override
+  protected LambdaGroupClassBuilder getBuilder(DexItemFactory factory) {
+    return new KStyleLambdaGroupClassBuilder(factory, this, "kotlin-style lambda group");
+  }
+
+  @Override
+  public Strategy getCodeStrategy() {
+    return strategy;
+  }
+
+  @Override
+  public ThrowingConsumer<DexClass, LambdaStructureError> lambdaClassValidator(
+      Kotlin kotlin, AppInfoWithSubtyping appInfo) {
+    return new KStyleLambdaClassValidator(kotlin, this, appInfo);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaGroupClassBuilder.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaGroupClassBuilder.java
new file mode 100644
index 0000000..59de393
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaGroupClassBuilder.java
@@ -0,0 +1,234 @@
+// 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.lambda.kstyle;
+
+import com.android.tools.r8.graph.ClassAccessFlags;
+import com.android.tools.r8.graph.DexAnnotation;
+import com.android.tools.r8.graph.DexAnnotationSet;
+import com.android.tools.r8.graph.DexAnnotationSetRefList;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
+import com.android.tools.r8.graph.DexValue.DexValueNull;
+import com.android.tools.r8.graph.EnclosingMethodAttribute;
+import com.android.tools.r8.graph.InnerClassAttribute;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.ir.optimize.lambda.LambdaGroup.LambdaInfo;
+import com.android.tools.r8.ir.optimize.lambda.LambdaGroupClassBuilder;
+import com.android.tools.r8.ir.synthetic.SynthesizedCode;
+import com.google.common.collect.Lists;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+// Builds components of k-style lambda group class.
+final class KStyleLambdaGroupClassBuilder
+    extends LambdaGroupClassBuilder<KStyleLambdaGroup> implements KStyleConstants {
+
+  private final KStyleLambdaGroupId id;
+
+  KStyleLambdaGroupClassBuilder(DexItemFactory factory, KStyleLambdaGroup group, String origin) {
+    super(group, factory, origin);
+    this.id = group.id();
+  }
+
+  @Override
+  protected DexType getSuperClassType() {
+    return factory.kotlin.functional.lambdaType;
+  }
+
+  // Always generate public final classes.
+  @Override
+  protected ClassAccessFlags buildAccessFlags() {
+    return PUBLIC_LAMBDA_CLASS_FLAGS;
+  }
+
+  // Take the attribute from the group, if exists.
+  @Override
+  protected EnclosingMethodAttribute buildEnclosingMethodAttribute() {
+    return id.enclosing;
+  }
+
+  // Take the attribute from the group, if exists.
+  @Override
+  protected List<InnerClassAttribute> buildInnerClasses() {
+    return id.innerClassAccess == KStyleLambdaGroupId.MISSING_INNER_CLASS_ATTRIBUTE
+        ? Collections.emptyList()
+        : Lists.newArrayList(new InnerClassAttribute(
+            id.innerClassAccess, group.getGroupClassType(), null, null));
+  }
+
+  @Override
+  protected DexAnnotationSet buildAnnotations() {
+    // Kotlin-style lambdas supported by the merged may only contain optional signature and
+    // kotlin metadata annotations. We remove the latter, but keep the signature if present.
+    String signature = id.signature;
+    return signature == null ? DexAnnotationSet.empty()
+        : new DexAnnotationSet(new DexAnnotation[]{
+            DexAnnotation.createSignatureAnnotation(signature, factory)});
+  }
+
+  @Override
+  protected DexEncodedMethod[] buildVirtualMethods() {
+    // All virtual method are dispatched on $id$ field.
+    //
+    // For each of the virtual method name/signatures seen in the group
+    // we generate a correspondent method in lambda group class with same
+    // name/signatures dispatching the call to appropriate code taken
+    // from the lambda class.
+
+    Map<DexString, Map<DexProto, List<DexEncodedMethod>>> methods = collectVirtualMethods();
+    List<DexEncodedMethod> result = new ArrayList<>();
+
+    for (Entry<DexString, Map<DexProto, List<DexEncodedMethod>>> upper : methods.entrySet()) {
+      DexString methodName = upper.getKey();
+      for (Entry<DexProto, List<DexEncodedMethod>> inner : upper.getValue().entrySet()) {
+        // Methods for unique name/signature pair.
+        DexProto methodProto = inner.getKey();
+        List<DexEncodedMethod> implMethods = inner.getValue();
+
+        boolean isMainMethod =
+            id.mainMethodName == methodName && id.mainMethodProto == methodProto;
+
+        // For bridge methods we still use same PUBLIC FINAL as for the main method,
+        // since inlining removes BRIDGE & SYNTHETIC attributes from the bridge methods
+        // anyways and our new method is a product of inlining.
+        MethodAccessFlags accessFlags = MAIN_METHOD_FLAGS.copy();
+
+        // Mark all the impl methods for force inlining
+        // LambdaGroupVirtualMethodSourceCode relies on.
+        for (DexEncodedMethod implMethod : implMethods) {
+          if (implMethod != null) {
+            implMethod.markForceInline();
+          }
+        }
+
+        result.add(new DexEncodedMethod(
+            factory.createMethod(group.getGroupClassType(), methodProto, methodName),
+            accessFlags,
+            isMainMethod ? id.mainMethodAnnotations : DexAnnotationSet.empty(),
+            isMainMethod ? id.mainMethodParamAnnotations : DexAnnotationSetRefList.empty(),
+            new SynthesizedCode(
+                new VirtualMethodSourceCode(factory, group.getGroupClassType(),
+                    methodProto, group.getLambdaIdField(factory), implMethods))));
+      }
+    }
+
+    return result.toArray(new DexEncodedMethod[result.size()]);
+  }
+
+  // Build a map of virtual methods with unique name/proto pointing to a list of methods
+  // from lambda classes implementing appropriate logic. The indices in the list correspond
+  // to lambda ids. Note that some of the slots in the lists may be empty, indicating the
+  // fact that corresponding lambda does not have a virtual method with this signature.
+  private Map<DexString, Map<DexProto, List<DexEncodedMethod>>> collectVirtualMethods() {
+    Map<DexString, Map<DexProto, List<DexEncodedMethod>>> methods = new LinkedHashMap<>();
+    assert lambdaIdsOrdered();
+    for (LambdaInfo lambda : lambdas) {
+      for (DexEncodedMethod method : lambda.clazz.virtualMethods()) {
+        List<DexEncodedMethod> list = methods
+            .computeIfAbsent(method.method.name,
+                k -> new LinkedHashMap<>())
+            .computeIfAbsent(method.method.proto,
+                k -> Lists.newArrayList(Collections.nCopies(lambdas.size(), null)));
+        assert list.get(lambda.id) == null;
+        list.set(lambda.id, method);
+      }
+    }
+    return methods;
+  }
+
+  private boolean lambdaIdsOrdered() {
+    for (int i = 0; i < lambdas.size(); i++) {
+      assert lambdas.get(i).id == i;
+    }
+    return true;
+  }
+
+  @Override
+  protected DexEncodedMethod[] buildDirectMethods() {
+    // We only build an instance initializer and optional class
+    // initializer for stateless lambdas.
+
+    boolean statelessLambda = group.isStateless();
+    DexType groupClassType = group.getGroupClassType();
+
+    DexEncodedMethod[] result = new DexEncodedMethod[statelessLambda ? 2 : 1];
+    // Instance initializer mapping parameters into capture fields.
+    DexProto initializerProto = group.createConstructorProto(factory);
+    result[0] = new DexEncodedMethod(
+        factory.createMethod(groupClassType, initializerProto, factory.constructorMethodName),
+        CONSTRUCTOR_FLAGS_RELAXED,  // always create access-relaxed constructor.
+        DexAnnotationSet.empty(),
+        DexAnnotationSetRefList.empty(),
+        new SynthesizedCode(
+            new InstanceInitializerSourceCode(factory, groupClassType,
+                group.getLambdaIdField(factory), id -> group.getCaptureField(factory, id),
+                initializerProto, id.mainMethodProto.parameters.size())));
+
+    // Static class initializer for stateless lambdas.
+    if (statelessLambda) {
+      result[1] = new DexEncodedMethod(
+          factory.createMethod(groupClassType,
+              factory.createProto(factory.voidType),
+              factory.classConstructorMethodName),
+          CLASS_INITIALIZER_FLAGS,
+          DexAnnotationSet.empty(),
+          DexAnnotationSetRefList.empty(),
+          new SynthesizedCode(
+              new ClassInitializerSourceCode(
+                  factory, groupClassType, lambdas.size(),
+                  id -> group.getSingletonInstanceField(factory, id))));
+    }
+
+    return result;
+  }
+
+  @Override
+  protected DexEncodedField[] buildInstanceFields() {
+    // Lambda id field plus other fields defined by the capture signature.
+    String capture = id.capture;
+    int size = capture.length();
+    DexEncodedField[] result = new DexEncodedField[1 + size];
+
+    result[0] = new DexEncodedField(group.getLambdaIdField(factory),
+        CAPTURE_FIELD_FLAGS_RELAXED, DexAnnotationSet.empty(), null);
+
+    for (int id = 0; id < size; id++) {
+      result[id + 1] = new DexEncodedField(group.getCaptureField(factory, id),
+          CAPTURE_FIELD_FLAGS_RELAXED, DexAnnotationSet.empty(), null);
+    }
+
+    return result;
+  }
+
+  @Override
+  protected DexEncodedField[] buildStaticFields() {
+    if (!group.isStateless()) {
+      return DexEncodedField.EMPTY_ARRAY;
+    }
+
+    // One field for each stateless lambda in the group.
+    int size = lambdas.size();
+    DexEncodedField[] result = new DexEncodedField[size];
+    for (int id = 0; id < size; id++) {
+      result[id] = new DexEncodedField(group.getSingletonInstanceField(factory, id),
+          SINGLETON_FIELD_FLAGS, DexAnnotationSet.empty(), DexValueNull.NULL);
+    }
+    return result;
+  }
+
+  @Override
+  protected DexTypeList buildInterfaces() {
+    return new DexTypeList(new DexType[]{id.iface});
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaGroupCodeStrategy.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaGroupCodeStrategy.java
new file mode 100644
index 0000000..dd066b6
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaGroupCodeStrategy.java
@@ -0,0 +1,256 @@
+// 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.lambda.kstyle;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.CheckCast;
+import com.android.tools.r8.ir.code.ConstNumber;
+import com.android.tools.r8.ir.code.InstanceGet;
+import com.android.tools.r8.ir.code.InstancePut;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.InvokeVirtual;
+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.code.ValueType;
+import com.android.tools.r8.ir.optimize.lambda.CaptureSignature;
+import com.android.tools.r8.ir.optimize.lambda.CodeProcessor;
+import com.android.tools.r8.ir.optimize.lambda.CodeProcessor.Strategy;
+import com.android.tools.r8.ir.optimize.lambda.LambdaGroup;
+import java.util.ArrayList;
+import java.util.List;
+
+// Defines the code processing strategy for k-style lambdas.
+final class KStyleLambdaGroupCodeStrategy implements Strategy {
+  private final KStyleLambdaGroup group;
+
+  KStyleLambdaGroupCodeStrategy(KStyleLambdaGroup group) {
+    this.group = group;
+  }
+
+  @Override
+  public LambdaGroup group() {
+    return group;
+  }
+
+  @Override
+  public boolean isValidStaticFieldWrite(CodeProcessor context, DexField field) {
+    DexType lambda = field.clazz;
+    assert group.containsLambda(lambda);
+    // Only support writes to singleton static field named 'INSTANCE' from lambda
+    // static class initializer.
+    return field.name == context.kotlin.functional.kotlinStyleLambdaInstanceName &&
+        lambda == field.type &&
+        context.factory.isClassConstructor(context.method.method) &&
+        context.method.method.holder == lambda;
+  }
+
+  @Override
+  public boolean isValidStaticFieldRead(CodeProcessor context, DexField field) {
+    DexType lambda = field.clazz;
+    assert group.containsLambda(lambda);
+    // Support all reads of singleton static field named 'INSTANCE'.
+    return field.name == context.kotlin.functional.kotlinStyleLambdaInstanceName &&
+        lambda == field.type;
+  }
+
+  @Override
+  public boolean isValidInstanceFieldWrite(CodeProcessor context, DexField field) {
+    DexType lambda = field.clazz;
+    DexMethod method = context.method.method;
+    assert group.containsLambda(lambda);
+    // Support writes to capture instance fields inside lambda constructor only.
+    return method.holder == lambda && context.factory.isConstructor(method);
+  }
+
+  @Override
+  public boolean isValidInstanceFieldRead(CodeProcessor context, DexField field) {
+    assert group.containsLambda(field.clazz);
+    // Support all reads from capture instance fields.
+    return true;
+  }
+
+  @Override
+  public boolean isValidNewInstance(CodeProcessor context, NewInstance invoke) {
+    // Only valid for stateful lambdas.
+    return !group.isStateless();
+  }
+
+  @Override
+  public boolean isValidInvoke(CodeProcessor context, InvokeMethod invoke) {
+    return isValidInitializerCall(context, invoke) || isValidVirtualCall(invoke);
+  }
+
+  private boolean isValidInitializerCall(CodeProcessor context, InvokeMethod invoke) {
+    DexMethod method = invoke.getInvokedMethod();
+    DexType lambda = method.holder;
+    assert group.containsLambda(lambda);
+    // Allow calls to a constructor from other classes if the lambda is stateful,
+    // otherwise allow such a call only from the same class static initializer.
+    return (group.isStateless() == (context.method.method.holder == lambda)) &&
+        invoke.isInvokeDirect() &&
+        context.factory.isConstructor(method) &&
+        CaptureSignature.getCaptureSignature(method.proto.parameters).equals(group.id().capture);
+  }
+
+  private boolean isValidVirtualCall(InvokeMethod invoke) {
+    assert group.containsLambda(invoke.getInvokedMethod().holder);
+    // Allow all virtual calls.
+    return invoke.isInvokeVirtual();
+  }
+
+  @Override
+  public void patch(CodeProcessor context, NewInstance newInstance) {
+    NewInstance patchedNewInstance = new NewInstance(
+        group.getGroupClassType(), context.code.createValue(ValueType.OBJECT));
+    context.instructions().replaceCurrentInstruction(patchedNewInstance);
+  }
+
+  @Override
+  public void patch(CodeProcessor context, InvokeMethod invoke) {
+    assert group.containsLambda(invoke.getInvokedMethod().holder);
+    if (isValidInitializerCall(context, invoke)) {
+      patchInitializer(context, invoke.asInvokeDirect());
+    } else {
+      // Regular calls to virtual methods only need target method be replaced.
+      assert isValidVirtualCall(invoke);
+      DexMethod method = invoke.getInvokedMethod();
+      context.instructions().replaceCurrentInstruction(
+          new InvokeVirtual(mapVirtualMethod(context.factory, method),
+              createValueForType(context, method.proto.returnType), invoke.arguments()));
+    }
+  }
+
+  @Override
+  public void patch(CodeProcessor context, InstancePut instancePut) {
+    // Instance put should only appear in lambda class instance constructor,
+    // we should never get here since we never rewrite them.
+    throw new Unreachable();
+  }
+
+  @Override
+  public void patch(CodeProcessor context, InstanceGet instanceGet) {
+    DexField field = instanceGet.getField();
+    DexType fieldType = field.type;
+
+    // We need to insert remapped values and in case the capture field
+    // of type Object optionally cast to expected field.
+    InstanceGet newInstanceGet = new InstanceGet(instanceGet.getType(),
+        createValueForType(context, fieldType), instanceGet.object(),
+        mapCaptureField(context.factory, field.clazz, field));
+    context.instructions().replaceCurrentInstruction(newInstanceGet);
+
+    if (fieldType.isPrimitiveType() || fieldType == context.factory.objectType) {
+      return;
+    }
+
+    // Since all captured values of non-primitive types are stored in fields of type
+    // java.lang.Object, we need to cast them to appropriate type to satisfy the verifier.
+    Value newValue = context.code.createValue(ValueType.OBJECT, newInstanceGet.getLocalInfo());
+    newInstanceGet.outValue().replaceUsers(newValue);
+    CheckCast cast = new CheckCast(newValue, newInstanceGet.outValue(), fieldType);
+    cast.setPosition(newInstanceGet.getPosition());
+    context.instructions().add(cast);
+    // If the current block has catch handlers split the check cast into its own block.
+    // Since new cast is never supposed to fail, we leave catch handlers empty.
+    if (cast.getBlock().hasCatchHandlers()) {
+      context.instructions().previous();
+      context.instructions().split(context.code, 1, context.blocks);
+    }
+  }
+
+  @Override
+  public void patch(CodeProcessor context, StaticPut staticPut) {
+    // Static put should only appear in lambda class static initializer,
+    // we should never get here since we never rewrite them.
+    throw new Unreachable();
+  }
+
+  @Override
+  public void patch(CodeProcessor context, StaticGet staticGet) {
+    context.instructions().replaceCurrentInstruction(
+        new StaticGet(staticGet.getType(), context.code.createValue(ValueType.OBJECT),
+            mapSingletonInstanceField(context.factory, staticGet.getField())));
+  }
+
+  private void patchInitializer(CodeProcessor context, InvokeDirect invoke) {
+    // Patching includes:
+    //  - change of methods
+    //  - adding lambda id as the first argument
+    //  - reshuffling other arguments (representing captured values)
+    //    according to capture signature of the group.
+
+    DexMethod method = invoke.getInvokedMethod();
+    DexType lambda = method.holder;
+
+    // Create constant with lambda id.
+    Value lambdaIdValue = context.code.createValue(ValueType.INT);
+    ConstNumber lambdaId = new ConstNumber(lambdaIdValue, group.lambdaId(lambda));
+    lambdaId.setPosition(invoke.getPosition());
+    context.instructions().previous();
+    context.instructions().add(lambdaId);
+
+    // Create a new InvokeDirect instruction.
+    Instruction next = context.instructions().next();
+    assert next == invoke;
+
+    DexMethod newTarget = mapInitializerMethod(context.factory, method);
+    List<Value> newArguments = mapInitializerArgs(lambdaIdValue, invoke.arguments(), method.proto);
+    context.instructions().replaceCurrentInstruction(
+        new InvokeDirect(newTarget, context.code.createValue(ValueType.OBJECT), newArguments)
+    );
+  }
+
+  private Value createValueForType(CodeProcessor context, DexType returnType) {
+    return returnType == context.factory.voidType ? null :
+        context.code.createValue(ValueType.fromDexType(returnType));
+  }
+
+  private List<Value> mapInitializerArgs(
+      Value lambdaIdValue, List<Value> oldArguments, DexProto proto) {
+    assert oldArguments.size() == proto.parameters.size() + 1;
+    List<Value> newArguments = new ArrayList<>();
+    newArguments.add(oldArguments.get(0)); // receiver
+    newArguments.add(lambdaIdValue); // lambda-id
+    List<Integer> reverseMapping =
+        CaptureSignature.getReverseCaptureMapping(proto.parameters.values);
+    for (int index : reverseMapping) {
+      // <original-capture-index> = mapping[<normalized-capture-index>]
+      newArguments.add(oldArguments.get(index + 1 /* after receiver */));
+    }
+    return newArguments;
+  }
+
+  // Map lambda class initializer into lambda group class initializer.
+  private DexMethod mapInitializerMethod(DexItemFactory factory, DexMethod method) {
+    assert factory.isConstructor(method);
+    assert CaptureSignature.getCaptureSignature(method.proto.parameters).equals(group.id().capture);
+    return factory.createMethod(group.getGroupClassType(),
+        group.createConstructorProto(factory), method.name);
+  }
+
+  // Map lambda class virtual method into lambda group class method.
+  private DexMethod mapVirtualMethod(DexItemFactory factory, DexMethod method) {
+    return factory.createMethod(group.getGroupClassType(), method.proto, method.name);
+  }
+
+  // Map lambda class capture field into lambda group class capture field.
+  private DexField mapCaptureField(DexItemFactory factory, DexType lambda, DexField field) {
+    return group.getCaptureField(factory, group.mapFieldIntoCaptureIndex(lambda, field));
+  }
+
+  // Map lambda class initializer into lambda group class initializer.
+  private DexField mapSingletonInstanceField(DexItemFactory factory, DexField field) {
+    return group.getSingletonInstanceField(factory, group.lambdaId(field.clazz));
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaGroupId.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaGroupId.java
new file mode 100644
index 0000000..6b6f73b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaGroupId.java
@@ -0,0 +1,138 @@
+// 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.lambda.kstyle;
+
+import com.android.tools.r8.graph.DexAnnotationSet;
+import com.android.tools.r8.graph.DexAnnotationSetRefList;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.EnclosingMethodAttribute;
+import com.android.tools.r8.graph.InnerClassAttribute;
+import com.android.tools.r8.ir.optimize.lambda.LambdaGroup;
+import com.android.tools.r8.ir.optimize.lambda.LambdaGroupId;
+
+final class KStyleLambdaGroupId implements LambdaGroupId {
+  public static final int MISSING_INNER_CLASS_ATTRIBUTE = -1;
+
+  private final int hash;
+
+  // Capture signature.
+  final String capture;
+  // Kotlin functional interface.
+  final DexType iface;
+  // Package (in case access relaxation is enabled root package is used).
+  final String pkg;
+  // Generic signature of the lambda class.
+  final String signature;
+
+  // Characteristics of the main lambda method. Kotlin generates one main method with
+  // the signature matching the lambda signature, and a bridge method (if needed)
+  // forwarding the call via interface to the main method. Main method may have
+  // generic signature and parameter annotations defining nullability.
+  //
+  // TODO: address cases when main method is removed after inlining.
+  // (the main method if created is supposed to be inlined, since it is always
+  //  only called from the bridge, and removed. In this case the method with
+  //  signature and annotations is removed allowing more lambda to be merged.)
+  final DexString mainMethodName;
+  final DexProto mainMethodProto;
+  final DexAnnotationSet mainMethodAnnotations;
+  final DexAnnotationSetRefList mainMethodParamAnnotations;
+
+  final EnclosingMethodAttribute enclosing;
+
+  // Note that lambda classes are always created as _anonymous_ inner classes. We only
+  // need to store the fact that the class had this attribute and if it had store the
+  // access from InnerClassAttribute.
+  final int innerClassAccess;
+
+  KStyleLambdaGroupId(String capture, DexType iface, String pkg, String signature,
+      DexEncodedMethod mainMethod, InnerClassAttribute inner, EnclosingMethodAttribute enclosing) {
+    assert capture != null && iface != null && pkg != null && mainMethod != null;
+    assert inner == null || (inner.isAnonymous() && inner.getOuter() == null);
+    this.capture = capture;
+    this.iface = iface;
+    this.pkg = pkg;
+    this.signature = signature;
+    this.mainMethodName = mainMethod.method.name;
+    this.mainMethodProto = mainMethod.method.proto;
+    this.mainMethodAnnotations = mainMethod.annotations;
+    this.mainMethodParamAnnotations = mainMethod.parameterAnnotations;
+    this.innerClassAccess = inner != null ? inner.getAccess() : MISSING_INNER_CLASS_ATTRIBUTE;
+    this.enclosing = enclosing;
+    this.hash = computeHashCode();
+  }
+
+  @Override
+  public int hashCode() {
+    return hash;
+  }
+
+  private int computeHashCode() {
+    int hash = capture.hashCode() * 7;
+    hash += iface.hashCode() * 17;
+    hash += pkg.hashCode() * 37;
+    hash += signature != null ? signature.hashCode() * 47 : 0;
+    hash += mainMethodName.hashCode() * 71;
+    hash += mainMethodProto.hashCode() * 89;
+    hash += mainMethodAnnotations != null ? mainMethodAnnotations.hashCode() * 101 : 0;
+    hash += mainMethodParamAnnotations != null ? mainMethodParamAnnotations.hashCode() * 113 : 0;
+    hash += innerClassAccess * 131;
+    hash += enclosing != null ? enclosing.hashCode() * 211 : 0;
+    return hash;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (!(obj instanceof KStyleLambdaGroupId)) {
+      return false;
+    }
+    KStyleLambdaGroupId other = (KStyleLambdaGroupId) obj;
+    return capture.equals(other.capture) &&
+        iface == other.iface &&
+        pkg.equals(other.pkg) &&
+        mainMethodName == other.mainMethodName &&
+        mainMethodProto == other.mainMethodProto &&
+        (mainMethodAnnotations == null ? other.mainMethodAnnotations == null
+            : mainMethodAnnotations.equals(other.mainMethodAnnotations)) &&
+        (mainMethodParamAnnotations == null ? other.mainMethodParamAnnotations == null
+            : mainMethodParamAnnotations.equals(other.mainMethodParamAnnotations)) &&
+        (signature == null ? other.signature == null : signature.equals(other.signature)) &&
+        innerClassAccess == other.innerClassAccess &&
+        (enclosing == null ? other.enclosing == null : enclosing.equals(other.enclosing));
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder builder = new StringBuilder("Kotlin-style lambda group")
+        .append("\n  capture: ").append(capture)
+        .append("\n  interface: ").append(iface.descriptor)
+        .append("\n  package: ").append(pkg)
+        .append("\n  signature: ").append(signature)
+        .append("\n  main method name: ").append(mainMethodName.toString())
+        .append("\n  main method: ").append(mainMethodProto.toSourceString())
+        .append("\n  main annotations: ").append(mainMethodAnnotations)
+        .append("\n  main param annotations: ").append(mainMethodParamAnnotations)
+        .append("\n  inner: ")
+        .append(innerClassAccess == MISSING_INNER_CLASS_ATTRIBUTE ? "none" : innerClassAccess);
+    if (enclosing != null) {
+      if (enclosing.getEnclosingClass() != null) {
+        builder.append("\n  enclosingClass: ")
+            .append(enclosing.getEnclosingClass().descriptor);
+      } else {
+        builder.append("\n  enclosingMethod: ")
+            .append(enclosing.getEnclosingMethod().toSourceString());
+      }
+    }
+    return builder.toString();
+  }
+
+  @Override
+  public LambdaGroup createGroup() {
+    return new KStyleLambdaGroup(this);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaGroupIdFactory.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaGroupIdFactory.java
new file mode 100644
index 0000000..7a516cd
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaGroupIdFactory.java
@@ -0,0 +1,240 @@
+// 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.lambda.kstyle;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AccessFlags;
+import com.android.tools.r8.graph.DexAnnotation;
+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.DexType;
+import com.android.tools.r8.graph.InnerClassAttribute;
+import com.android.tools.r8.ir.optimize.lambda.CaptureSignature;
+import com.android.tools.r8.ir.optimize.lambda.LambdaGroup.LambdaStructureError;
+import com.android.tools.r8.ir.optimize.lambda.LambdaGroupId;
+import com.android.tools.r8.kotlin.Kotlin;
+import com.android.tools.r8.utils.InternalOptions;
+import java.util.List;
+
+public final class KStyleLambdaGroupIdFactory implements KStyleConstants {
+  private KStyleLambdaGroupIdFactory() {
+  }
+
+  // Creates a lambda group id for kotlin style lambda. Should never return null, if the lambda
+  // does not pass pre-requirements (mostly by not meeting high-level structure expectations)
+  // should throw LambdaStructureError leaving the caller to decide if/how it needs to be reported.
+  //
+  // At this point we only perform high-level checks before qualifying the lambda as a candidate
+  // for merging and assigning lambda group id. We can NOT perform checks on method bodies since
+  // they may not be converted yet, we'll do that in KStyleLambdaClassValidator.
+  public static LambdaGroupId create(Kotlin kotlin, DexClass lambda, InternalOptions options)
+      throws LambdaStructureError {
+    boolean accessRelaxed = options.proguardConfiguration.isAccessModificationAllowed();
+
+    checkAccessFlags("class access flags", lambda.accessFlags,
+        PUBLIC_LAMBDA_CLASS_FLAGS, LAMBDA_CLASS_FLAGS);
+
+    validateStaticFields(kotlin, lambda);
+    String captureSignature = validateInstanceFields(lambda, accessRelaxed);
+    validateDirectMethods(lambda);
+    DexEncodedMethod mainMethod = validateVirtualMethods(lambda);
+    DexType iface = validateInterfaces(kotlin, lambda);
+    String genericSignature = validateAnnotations(kotlin, lambda);
+    InnerClassAttribute innerClass = validateInnerClasses(lambda);
+
+    return new KStyleLambdaGroupId(captureSignature, iface,
+        accessRelaxed ? "" : lambda.type.getPackageDescriptor(),
+        genericSignature, mainMethod, innerClass, lambda.getEnclosingMethod());
+  }
+
+  private static DexEncodedMethod validateVirtualMethods(DexClass lambda)
+      throws LambdaStructureError {
+    DexEncodedMethod mainMethod = null;
+
+    for (DexEncodedMethod method : lambda.virtualMethods()) {
+      if (method.accessFlags.equals(MAIN_METHOD_FLAGS)) {
+        if (mainMethod != null) {
+          throw new LambdaStructureError("more than one main method found");
+        }
+        mainMethod = method;
+      } else {
+        checkAccessFlags("unexpected virtual method access flags",
+            method.accessFlags, BRIDGE_METHOD_FLAGS, BRIDGE_METHOD_FLAGS_FIXED);
+        checkDirectMethodAnnotations(method);
+      }
+    }
+
+    if (mainMethod == null) {
+      throw new LambdaStructureError("no main method found");
+    }
+    return mainMethod;
+  }
+
+  private static InnerClassAttribute validateInnerClasses(DexClass lambda)
+      throws LambdaStructureError {
+    List<InnerClassAttribute> innerClasses = lambda.getInnerClasses();
+    InnerClassAttribute innerClass = null;
+    if (innerClasses != null) {
+      for (InnerClassAttribute inner : innerClasses) {
+        if (inner.getInner() == lambda.type) {
+          innerClass = inner;
+          if (!innerClass.isAnonymous()) {
+            throw new LambdaStructureError("is not anonymous");
+          }
+          return innerClass;
+        }
+      }
+    }
+    return null;
+  }
+
+  private static String validateAnnotations(Kotlin kotlin, DexClass lambda)
+      throws LambdaStructureError {
+    String signature = null;
+    if (!lambda.annotations.isEmpty()) {
+      for (DexAnnotation annotation : lambda.annotations.annotations) {
+        if (DexAnnotation.isSignatureAnnotation(annotation, kotlin.factory)) {
+          signature = DexAnnotation.getSignature(annotation);
+          continue;
+        }
+
+        if (annotation.annotation.type == kotlin.metadata.kotlinMetadataType) {
+          // Ignore kotlin metadata on lambda classes. Metadata on synthetic
+          // classes exists but is not used in the current Kotlin version (1.2.21)
+          // and newly generated lambda _group_ class is not exactly a kotlin class.
+          continue;
+        }
+
+        throw new LambdaStructureError(
+            "unexpected annotation: " + annotation.annotation.type.toSourceString());
+      }
+    }
+    return signature;
+  }
+
+  private static void validateStaticFields(Kotlin kotlin, DexClass lambda)
+      throws LambdaStructureError {
+    DexEncodedField[] staticFields = lambda.staticFields();
+    if (staticFields.length == 1) {
+      DexEncodedField field = staticFields[0];
+      if (field.field.name != kotlin.functional.kotlinStyleLambdaInstanceName ||
+          field.field.type != lambda.type || !field.accessFlags.isPublic() ||
+          !field.accessFlags.isFinal() || !field.accessFlags.isStatic()) {
+        throw new LambdaStructureError("unexpected static field " + field.toSourceString());
+      }
+      // No state if the lambda is a singleton.
+      if (lambda.instanceFields().length > 0) {
+        throw new LambdaStructureError("has instance fields along with INSTANCE");
+      }
+      checkAccessFlags("static field access flags", field.accessFlags, SINGLETON_FIELD_FLAGS);
+      checkFieldAnnotations(field);
+
+    } else if (staticFields.length > 1) {
+      throw new LambdaStructureError(
+          "only one static field max expected, found " + staticFields.length);
+
+    } else if (lambda.instanceFields().length == 0) {
+      throw new LambdaStructureError("stateless lambda without INSTANCE field");
+    }
+  }
+
+  private static DexType validateInterfaces(Kotlin kotlin, DexClass lambda)
+      throws LambdaStructureError {
+    if (lambda.interfaces.size() == 0) {
+      throw new LambdaStructureError("does not implement any interfaces");
+    }
+    if (lambda.interfaces.size() > 1) {
+      throw new LambdaStructureError(
+          "implements more than one interface: " + lambda.interfaces.size());
+    }
+    DexType iface = lambda.interfaces.values[0];
+    if (!kotlin.functional.isFunctionInterface(iface)) {
+      throw new LambdaStructureError("implements " + iface.toSourceString() +
+          " instead of kotlin functional interface.");
+    }
+    return iface;
+  }
+
+  private static String validateInstanceFields(DexClass lambda, boolean accessRelaxed)
+      throws LambdaStructureError {
+    DexEncodedField[] instanceFields = lambda.instanceFields();
+    for (DexEncodedField field : instanceFields) {
+      checkAccessFlags("capture field access flags", field.accessFlags,
+          accessRelaxed ? CAPTURE_FIELD_FLAGS_RELAXED : CAPTURE_FIELD_FLAGS);
+      checkFieldAnnotations(field);
+    }
+    return CaptureSignature.getCaptureSignature(instanceFields);
+  }
+
+  private static void validateDirectMethods(DexClass lambda) throws LambdaStructureError {
+    DexEncodedMethod[] directMethods = lambda.directMethods();
+    for (DexEncodedMethod method : directMethods) {
+      if (method.isClassInitializer()) {
+        // We expect to see class initializer only if there is a singleton field.
+        if (lambda.staticFields().length != 1) {
+          throw new LambdaStructureError("has static initializer, but no singleton field");
+        }
+        checkAccessFlags("unexpected static initializer access flags",
+            method.accessFlags, CLASS_INITIALIZER_FLAGS);
+        checkDirectMethodAnnotations(method);
+
+      } else if (method.isStaticMethod()) {
+        throw new LambdaStructureError(
+            "unexpected static method: " + method.method.toSourceString());
+
+      } else if (method.isInstanceInitializer()) {
+        // Lambda class is expected to have one constructor
+        // with parameters matching capture signature.
+        DexType[] parameters = method.method.proto.parameters.values;
+        DexEncodedField[] instanceFields = lambda.instanceFields();
+        if (parameters.length != instanceFields.length) {
+          throw new LambdaStructureError("constructor parameters don't match captured values.");
+        }
+        for (int i = 0; i < parameters.length; i++) {
+          if (parameters[i] != instanceFields[i].field.type) {
+            throw new LambdaStructureError("constructor parameters don't match captured values.");
+          }
+        }
+        checkAccessFlags("unexpected constructor access flags",
+            method.accessFlags, CONSTRUCTOR_FLAGS, CONSTRUCTOR_FLAGS_RELAXED);
+        checkDirectMethodAnnotations(method);
+
+      } else {
+        throw new Unreachable();
+      }
+    }
+  }
+
+  private static void checkDirectMethodAnnotations(DexEncodedMethod method)
+      throws LambdaStructureError {
+    if (!method.annotations.isEmpty()) {
+      throw new LambdaStructureError("unexpected method annotations [" +
+          method.annotations.toSmaliString() + "] on " + method.method.toSourceString());
+    }
+    if (!method.parameterAnnotations.isEmpty()) {
+      throw new LambdaStructureError("unexpected method parameters annotations [" +
+          method.annotations.toSmaliString() + "] on " + method.method.toSourceString());
+    }
+  }
+
+  private static void checkFieldAnnotations(DexEncodedField field) throws LambdaStructureError {
+    if (!field.annotations.isEmpty()) {
+      throw new LambdaStructureError("unexpected field annotations [" +
+          field.annotations.toSmaliString() + "] on " + field.field.toSourceString());
+    }
+  }
+
+  @SafeVarargs
+  private static <T extends AccessFlags> void checkAccessFlags(
+      String message, T actual, T... expected) throws LambdaStructureError {
+    for (T flag : expected) {
+      if (flag.equals(actual)) {
+        return;
+      }
+    }
+    throw new LambdaStructureError(message);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/VirtualMethodSourceCode.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/VirtualMethodSourceCode.java
new file mode 100644
index 0000000..8921299
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/VirtualMethodSourceCode.java
@@ -0,0 +1,102 @@
+// 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.lambda.kstyle;
+
+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.DexProto;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.Invoke.Type;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.IRBuilder;
+import com.android.tools.r8.ir.synthetic.SyntheticSourceCode;
+import java.util.ArrayList;
+import java.util.List;
+
+final class VirtualMethodSourceCode extends SyntheticSourceCode {
+  private final DexItemFactory factory;
+  private final DexField idField;
+  private final List<DexEncodedMethod> implMethods;
+
+  VirtualMethodSourceCode(DexItemFactory factory, DexType groupClass,
+      DexProto proto, DexField idField, List<DexEncodedMethod> implMethods) {
+    super(groupClass, proto);
+    this.factory = factory;
+    this.idField = idField;
+    this.implMethods = implMethods;
+  }
+
+  @Override
+  protected void prepareInstructions() {
+    int implMethodCount = implMethods.size();
+    int paramCount = getParamCount();
+    List<Value> arguments = new ArrayList<>(paramCount + 1);
+
+    // We generate a single switch on lambda $id value read from appropriate
+    // field, and for each lambda id generate a call to appropriate method of
+    // the lambda class. Since this methods are marked as 'force inline',
+    // they are inlined by the inliner.
+
+    // Return value register if needed.
+    DexType returnType = proto.returnType;
+    boolean returnsValue = returnType != factory.voidType;
+    ValueType retValueType = returnsValue ? ValueType.fromDexType(returnType) : null;
+    int retRegister = returnsValue ? nextRegister(retValueType) : -1;
+
+    // Lambda id register to switch on.
+    int idRegister = nextRegister(ValueType.INT);
+    add(builder -> builder.addInstanceGet(idRegister, getReceiverRegister(), idField));
+
+    // Switch on id.
+    // Note that 'keys' and 'offsets' are just captured here and filled
+    // in with values when appropriate basic blocks are created.
+    int[] keys = new int[implMethodCount];
+    int[] offsets = new int[implMethodCount];
+    int[] fallthrough = new int[1]; // Array as a container for late initialization.
+    int switchIndex = lastInstructionIndex();
+    add(builder -> builder.addSwitch(idRegister, keys, fallthrough[0], offsets),
+        builder -> endsSwitch(builder, switchIndex, fallthrough[0], offsets));
+
+    // Fallthrough treated as unreachable.
+    fallthrough[0] = nextInstructionIndex();
+    int nullRegister = nextRegister(ValueType.OBJECT);
+    add(builder -> builder.addNullConst(nullRegister));
+    add(builder -> builder.addThrow(nullRegister), endsBlock);
+
+    // Blocks for each lambda id.
+    for (int i = 0; i < implMethodCount; i++) {
+      keys[i] = i;
+      DexEncodedMethod impl = implMethods.get(i);
+      if (impl == null) {
+        // Virtual method is missing in lambda class.
+        offsets[i] = fallthrough[0];
+        continue;
+      }
+      offsets[i] = nextInstructionIndex();
+
+      // Emit fake call on `this` receiver.
+      add(builder -> {
+        if (arguments.isEmpty()) {
+          // Late initialization of argument list.
+          arguments.add(getReceiverValue());
+          for (int index = 0; index < paramCount; index++) {
+            arguments.add(getParamValue(index));
+          }
+        }
+        builder.addInvoke(Type.VIRTUAL, impl.method, impl.method.proto, arguments);
+      });
+
+      // Handle return value if needed.
+      if (returnsValue) {
+        add(builder -> builder.addMoveResult(retRegister));
+        add(builder -> builder.addReturn(retValueType, retRegister), endsBlock);
+      } else {
+        add(IRBuilder::addReturn, endsBlock);
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/ForwardMethodSourceCode.java b/src/main/java/com/android/tools/r8/ir/synthetic/ForwardMethodSourceCode.java
index 4c59686..9259485 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/ForwardMethodSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/ForwardMethodSourceCode.java
@@ -16,7 +16,7 @@
 import java.util.List;
 
 // Source code representing simple forwarding method.
-public final class ForwardMethodSourceCode extends SingleBlockSourceCode {
+public final class ForwardMethodSourceCode extends SyntheticSourceCode {
 
   private final DexType targetReceiver;
   private final DexMethod target;
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/SynthesizedCode.java b/src/main/java/com/android/tools/r8/ir/synthetic/SynthesizedCode.java
index 26ef333..62238c3 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/SynthesizedCode.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/SynthesizedCode.java
@@ -56,7 +56,7 @@
   public void registerCaughtTypes(Consumer<DexType> dexTypeConsumer) {
     // Support for synthesized code with catch handler is not implemented.
     // Let's check that we're in a well known where no catch handler is possible.
-    assert sourceCode.instructionCount() == 1 || sourceCode instanceof SingleBlockSourceCode;
+    assert sourceCode.instructionCount() == 1 || sourceCode instanceof SyntheticSourceCode;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/SingleBlockSourceCode.java b/src/main/java/com/android/tools/r8/ir/synthetic/SyntheticSourceCode.java
similarity index 82%
rename from src/main/java/com/android/tools/r8/ir/synthetic/SingleBlockSourceCode.java
rename to src/main/java/com/android/tools/r8/ir/synthetic/SyntheticSourceCode.java
index ed4a207..5d03dfd 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/SingleBlockSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/SyntheticSourceCode.java
@@ -21,8 +21,11 @@
 import com.android.tools.r8.utils.ThrowingConsumer;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Predicate;
 
-public abstract class SingleBlockSourceCode implements SourceCode {
+public abstract class SyntheticSourceCode implements SourceCode {
+  protected final static Predicate<IRBuilder> doesNotEndBlock = x -> false;
+  protected final static Predicate<IRBuilder> endsBlock = x -> true;
 
   protected final DexType receiver;
   protected final DexProto proto;
@@ -41,8 +44,9 @@
 
   // Instruction constructors
   private List<ThrowingConsumer<IRBuilder, ApiLevelException>> constructors = new ArrayList<>();
+  private List<Predicate<IRBuilder>> traceEvents = new ArrayList<>();
 
-  protected SingleBlockSourceCode(DexType receiver, DexProto proto) {
+  protected SyntheticSourceCode(DexType receiver, DexProto proto) {
     assert proto != null;
     this.receiver = receiver;
     this.proto = proto;
@@ -60,7 +64,13 @@
   }
 
   protected final void add(ThrowingConsumer<IRBuilder, ApiLevelException> constructor) {
+    add(constructor, doesNotEndBlock);
+  }
+
+  protected final void add(
+      ThrowingConsumer<IRBuilder, ApiLevelException> constructor, Predicate<IRBuilder> traceEvent) {
     constructors.add(constructor);
+    traceEvents.add(traceEvent);
   }
 
   protected final int nextRegister(ValueType type) {
@@ -104,6 +114,14 @@
     return constructors.size();
   }
 
+  protected final int lastInstructionIndex() {
+    return constructors.size() - 1;
+  }
+
+  protected final int nextInstructionIndex() {
+    return constructors.size();
+  }
+
   @Override
   public final int instructionIndex(int instructionOffset) {
     return instructionOffset;
@@ -121,7 +139,8 @@
 
   @Override
   public final int traceInstruction(int instructionIndex, IRBuilder builder) {
-    return (instructionIndex == constructors.size() - 1) ? instructionIndex : -1;
+    return (traceEvents.get(instructionIndex).test(builder) ||
+        (instructionIndex == constructors.size() - 1)) ? instructionIndex : -1;
   }
 
   @Override
@@ -139,6 +158,7 @@
   @Override
   public final void clear() {
     constructors = null;
+    traceEvents = null;
     paramRegisters = null;
     paramValues = null;
     receiverValue = null;
@@ -219,4 +239,15 @@
   public final boolean verifyRegister(int register) {
     return true;
   }
+
+  // To be used as a tracing event for switch instruction.,
+  protected boolean endsSwitch(
+      IRBuilder builder, int switchIndex, int fallthrough, int[] offsets) {
+    // ensure successors of switch instruction
+    for (int offset : offsets) {
+      builder.ensureNormalSuccessorBlock(switchIndex, offset);
+    }
+    builder.ensureNormalSuccessorBlock(switchIndex, fallthrough);
+    return true;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/kotlin/Kotlin.java b/src/main/java/com/android/tools/r8/kotlin/Kotlin.java
new file mode 100644
index 0000000..9b0415b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/kotlin/Kotlin.java
@@ -0,0 +1,156 @@
+// 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 com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.graph.DexAnnotation;
+import com.android.tools.r8.graph.DexAnnotationElement;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexValue;
+import com.android.tools.r8.graph.DexValue.DexValueInt;
+import com.android.tools.r8.kotlin.KotlinSyntheticClass.Flavour;
+import com.android.tools.r8.utils.StringDiagnostic;
+import com.google.common.collect.Sets;
+import java.util.Set;
+
+/** Class provides basic information about symbols related to Kotlin support. */
+public final class Kotlin {
+  public final DexItemFactory factory;
+
+  public final Functional functional;
+  public final Intrinsics intrinsics;
+  public final Metadata metadata;
+
+  public Kotlin(DexItemFactory factory) {
+    this.factory = factory;
+
+    this.functional = new Functional();
+    this.intrinsics = new Intrinsics();
+    this.metadata = new Metadata();
+  }
+
+  public final class Functional {
+    private final Set<DexType> functions = Sets.newIdentityHashSet();
+
+    private Functional() {
+      // NOTE: Kotlin stdlib defines interface Function0 till Function22 explicitly, see:
+      //    https://github.com/JetBrains/kotlin/blob/master/libraries/
+      //                    stdlib/jvm/runtime/kotlin/jvm/functions/Functions.kt
+      //
+      // For functions with arity bigger that 22 it is supposed to use FunctionN as described
+      // in https://github.com/JetBrains/kotlin/blob/master/spec-docs/function-types.md,
+      // but in current implementation (v1.2.21) defining such a lambda results in:
+      //   > "Error: A JNI error has occurred, please check your installation and try again"
+      //
+      // This implementation just ignores lambdas with arity > 22.
+      for (int i = 0; i <= 22; i++) {
+        functions.add(factory.createType("Lkotlin/jvm/functions/Function" + i + ";"));
+      }
+    }
+
+    public final DexString kotlinStyleLambdaInstanceName = factory.createString("INSTANCE");
+
+    public final DexType lambdaType = factory.createType("Lkotlin/jvm/internal/Lambda;");
+
+    public final DexMethod lambdaInitializerMethod = factory.createMethod(lambdaType,
+        factory.createProto(factory.voidType, factory.intType), factory.constructorMethodName);
+
+    public boolean isFunctionInterface(DexType type) {
+      return functions.contains(type);
+    }
+  }
+
+  public final class Metadata {
+    public final DexType kotlinMetadataType = factory.createType("Lkotlin/Metadata;");
+    public final DexString elementNameK = factory.createString("k");
+    public final DexString elementNameD1 = factory.createString("d1");
+    public final DexString elementNameD2 = factory.createString("d2");
+  }
+
+  // kotlin.jvm.internal.Intrinsics class
+  public final class Intrinsics {
+    public final DexType type = factory.createType("Lkotlin/jvm/internal/Intrinsics;");
+    public final DexMethod throwParameterIsNullException = factory.createMethod(type,
+        factory.createProto(factory.voidType, factory.stringType), "throwParameterIsNullException");
+    public final DexMethod throwNpe = factory.createMethod(
+        type, factory.createProto(factory.voidType), "throwNpe");
+  }
+
+  // Calculates kotlin info for a class.
+  public KotlinInfo getKotlinInfo(DexClass clazz, DiagnosticsHandler reporter) {
+    if (clazz.annotations.isEmpty()) {
+      return null;
+    }
+    DexAnnotation meta = clazz.annotations.getFirstMatching(metadata.kotlinMetadataType);
+    if (meta != null) {
+      try {
+        return createKotlinInfo(clazz, meta);
+      } catch (MetadataError e) {
+        reporter.warning(
+            new StringDiagnostic("Class " + clazz.type.toSourceString() +
+                " has malformed kotlin.Metadata: " + e.getMessage()));
+      }
+    }
+    return null;
+  }
+
+  private KotlinInfo createKotlinInfo(DexClass clazz, DexAnnotation meta) {
+    DexAnnotationElement kindElement = getAnnotationElement(meta, metadata.elementNameK);
+    if (kindElement == null) {
+      throw new MetadataError("element 'k' is missing");
+    }
+
+    DexValue value = kindElement.value;
+    if (!(value instanceof DexValueInt)) {
+      throw new MetadataError("invalid 'k' value: " + value.toSourceString());
+    }
+
+    DexValueInt intValue = (DexValueInt) value;
+    switch (intValue.value) {
+      case 1:
+        return new KotlinClass();
+      case 2:
+        return new KotlinFile();
+      case 3:
+        return createSyntheticClass(clazz);
+      case 4:
+        return new KotlinClassFacade();
+      case 5:
+        return new KotlinClassPart();
+      default:
+        throw new MetadataError("unsupported 'k' value: " + value.toSourceString());
+    }
+  }
+
+  private KotlinSyntheticClass createSyntheticClass(DexClass clazz) {
+    KotlinSyntheticClass.Flavour flavour =
+        isKotlinStyleLambda(clazz) ? Flavour.KotlinStyleLambda : Flavour.Unclassified;
+    return new KotlinSyntheticClass(flavour);
+  }
+
+  private boolean isKotlinStyleLambda(DexClass clazz) {
+    // TODO: replace with direct hints from kotlin metadata when available.
+    return clazz.superType == this.functional.lambdaType;
+  }
+
+  private DexAnnotationElement getAnnotationElement(DexAnnotation annotation, DexString name) {
+    for (DexAnnotationElement element : annotation.annotation.elements) {
+      if (element.name == name) {
+        return element;
+      }
+    }
+    return null;
+  }
+
+  private static class MetadataError extends RuntimeException {
+    MetadataError(String cause) {
+      super(cause);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinClass.java b/src/main/java/com/android/tools/r8/kotlin/KotlinClass.java
new file mode 100644
index 0000000..fc81994
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinClass.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.kotlin;
+
+public class KotlinClass extends KotlinInfo {
+  @Override
+  public Kind getKind() {
+    return Kind.Class;
+  }
+
+  @Override
+  public boolean isClass() {
+    return true;
+  }
+
+  @Override
+  public KotlinClass asClass() {
+    return this;
+  }
+
+  KotlinClass() {
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinClassFacade.java b/src/main/java/com/android/tools/r8/kotlin/KotlinClassFacade.java
new file mode 100644
index 0000000..e829a27
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinClassFacade.java
@@ -0,0 +1,26 @@
+// 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;
+
+public final class KotlinClassFacade extends KotlinInfo {
+  @Override
+  public Kind getKind() {
+    return Kind.Facade;
+  }
+
+  @Override
+  public boolean isClassFacade() {
+    return true;
+  }
+
+  @Override
+  public KotlinClassFacade asClassFacade() {
+    return this;
+  }
+
+  KotlinClassFacade() {
+    super();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinClassPart.java b/src/main/java/com/android/tools/r8/kotlin/KotlinClassPart.java
new file mode 100644
index 0000000..d6da817
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinClassPart.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.kotlin;
+
+public final class KotlinClassPart extends KotlinInfo {
+  @Override
+  public Kind getKind() {
+    return Kind.Part;
+  }
+
+  @Override
+  public boolean isClassPart() {
+    return true;
+  }
+
+  @Override
+  public KotlinClassPart asClassPart() {
+    return this;
+  }
+
+  KotlinClassPart() {
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinFile.java b/src/main/java/com/android/tools/r8/kotlin/KotlinFile.java
new file mode 100644
index 0000000..38a77ad
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinFile.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.kotlin;
+
+public final class KotlinFile extends KotlinInfo {
+  @Override
+  public Kind getKind() {
+    return Kind.File;
+  }
+
+  @Override
+  public boolean isFile() {
+    return true;
+  }
+
+  @Override
+  public KotlinFile asFile() {
+    return this;
+  }
+
+  KotlinFile() {
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinInfo.java b/src/main/java/com/android/tools/r8/kotlin/KotlinInfo.java
new file mode 100644
index 0000000..4043bc6
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinInfo.java
@@ -0,0 +1,57 @@
+// 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;
+
+// Provides access to kotlin information.
+public abstract class KotlinInfo {
+  KotlinInfo() {
+  }
+
+  public enum Kind {
+    Class, File, Synthetic, Part, Facade
+  }
+
+  public abstract Kind getKind();
+
+  public boolean isClass() {
+    return false;
+  }
+
+  public KotlinClass asClass() {
+    return null;
+  }
+
+  public boolean isFile() {
+    return false;
+  }
+
+  public KotlinFile asFile() {
+    return null;
+  }
+
+  public boolean isSyntheticClass() {
+    return false;
+  }
+
+  public KotlinSyntheticClass asSyntheticClass() {
+    return null;
+  }
+
+  public boolean isClassPart() {
+    return false;
+  }
+
+  public KotlinClassPart asClassPart() {
+    return null;
+  }
+
+  public boolean isClassFacade() {
+    return false;
+  }
+
+  public KotlinClassFacade asClassFacade() {
+    return null;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinSyntheticClass.java b/src/main/java/com/android/tools/r8/kotlin/KotlinSyntheticClass.java
new file mode 100644
index 0000000..b0e37b4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinSyntheticClass.java
@@ -0,0 +1,37 @@
+// 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;
+
+public final class KotlinSyntheticClass extends KotlinInfo {
+  public enum Flavour {
+    KotlinStyleLambda,
+    Unclassified
+  }
+
+  public final Flavour flavour;
+
+  KotlinSyntheticClass(Flavour flavour) {
+    this.flavour = flavour;
+  }
+
+  public boolean isKotlinStyleLambda() {
+    return flavour == Flavour.KotlinStyleLambda;
+  }
+
+  @Override
+  public final Kind getKind() {
+    return Kind.Synthetic;
+  }
+
+  @Override
+  public final boolean isSyntheticClass() {
+    return true;
+  }
+
+  @Override
+  public KotlinSyntheticClass asSyntheticClass() {
+    return this;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
index ff50c3e..dd00254 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
@@ -15,9 +15,6 @@
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.DexValue;
-import com.android.tools.r8.graph.DexValue.DexValueArray;
-import com.android.tools.r8.graph.DexValue.DexValueString;
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.naming.signature.GenericSignatureAction;
 import com.android.tools.r8.naming.signature.GenericSignatureParser;
@@ -162,7 +159,7 @@
     for (int i = 0; i < annotations.length; i++) {
       DexAnnotation annotation = annotations[i];
       if (DexAnnotation.isSignatureAnnotation(annotation, appInfo.dexItemFactory)) {
-        parser.accept(getSignatureFromAnnotation(annotation));
+        parser.accept(DexAnnotation.getSignature(annotation));
         annotations[i] = DexAnnotation.createSignatureAnnotation(
             genericSignatureRewriter.getRenamedSignature(),
             appInfo.dexItemFactory);
@@ -170,15 +167,6 @@
     }
   }
 
-  private static String getSignatureFromAnnotation(DexAnnotation signatureAnnotation) {
-    DexValueArray elements = (DexValueArray) signatureAnnotation.annotation.elements[0].value;
-    StringBuilder signature = new StringBuilder();
-    for (DexValue element : elements.getValues()) {
-      signature.append(((DexValueString) element).value.toString());
-    }
-    return signature.toString();
-  }
-
   /**
    * Registers the given type as used.
    * <p>
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 3831116..3414d43 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -160,6 +160,8 @@
   public boolean intermediate = false;
   public List<String> logArgumentsFilter = ImmutableList.of();
 
+  // Flag to turn on/off lambda class merging in R8.
+  public boolean enableLambdaMerging = false;
   // Flag to turn on/off desugaring in D8/R8.
   public boolean enableDesugaring = true;
   // Defines interface method rewriter behavior.
diff --git a/src/test/java/com/android/tools/r8/kotlin/KStyleLambdaMergingTest.java b/src/test/java/com/android/tools/r8/kotlin/KStyleLambdaMergingTest.java
new file mode 100644
index 0000000..48358f0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/KStyleLambdaMergingTest.java
@@ -0,0 +1,314 @@
+// 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 junit.framework.TestCase.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.optimize.lambda.CaptureSignature;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.google.common.collect.Lists;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import org.junit.Test;
+
+public class KStyleLambdaMergingTest extends AbstractR8KotlinTestBase {
+  private static final String KOTLIN_FUNCTION_IFACE = "Lkotlin/jvm/functions/Function";
+  private static final String KEEP_INNER_AND_ENCLOSING =
+      "-keepattributes InnerClasses,EnclosingMethod\n";
+  private static final String KEEP_SIGNATURE_INNER_ENCLOSING =
+      "-keepattributes Signature,InnerClasses,EnclosingMethod\n";
+
+  abstract static class LambdaOrGroup {
+    abstract boolean match(DexClass clazz);
+  }
+
+  static class Group extends LambdaOrGroup {
+    final String pkg;
+    final String capture;
+    final int arity;
+
+    private Group(String pkg, String capture, int arity) {
+      this.pkg = pkg;
+      this.capture = fixCapture(capture);
+      this.arity = arity;
+    }
+
+    private String fixCapture(String capture) {
+      capture += "I";
+      char[] chars = capture.toCharArray();
+      Arrays.sort(chars);
+      return new String(chars);
+    }
+
+    @Override
+    public String toString() {
+      return "group class " +
+          (pkg.length() == 0 ? "" : pkg + "/") +
+          "-$$LambdaGroup$XXXX (arity: " + arity + ", capture: " + capture + ")";
+    }
+
+    @Override
+    boolean match(DexClass clazz) {
+      return clazz.type.getPackageDescriptor().equals(pkg) &&
+          getLambdaGroupCapture(clazz).equals(capture) &&
+          getLambdaArity(clazz) == arity;
+    }
+  }
+
+  static class Lambda extends LambdaOrGroup {
+    final String pkg;
+    final String name;
+    final int arity;
+
+    private Lambda(String pkg, String name, int arity) {
+      this.pkg = pkg;
+      this.name = name;
+      this.arity = arity;
+    }
+
+    @Override
+    public String toString() {
+      return "lambda class " +
+          (pkg.length() == 0 ? "" : pkg + "/") +
+          name + " (arity: " + arity + ")";
+    }
+
+    @Override
+    boolean match(DexClass clazz) {
+      return clazz.type.getPackageDescriptor().equals(pkg) &&
+          clazz.type.getName().equals(name) &&
+          getLambdaArity(clazz) == arity;
+    }
+  }
+
+  static class Verifier {
+    final DexInspector dexInspector;
+    final List<DexClass> lambdas = new ArrayList<>();
+    final List<DexClass> groups = new ArrayList<>();
+
+    Verifier(AndroidApp app) throws IOException, ExecutionException {
+      this.dexInspector = new DexInspector(app);
+      dexInspector.forAllClasses(clazz -> {
+        DexClass dexClass = clazz.getDexClass();
+        if (extendsLambdaBase(dexClass)) {
+          if (isLambdaGroupClass(dexClass)) {
+            groups.add(dexClass);
+          } else {
+            lambdas.add(dexClass);
+          }
+        }
+      });
+    }
+
+    void assertLambdaGroups(Group... groups) {
+      assertLambdasOrGroups("Lambda group", this.groups, groups);
+    }
+
+    void assertLambdas(Lambda... lambdas) {
+      assertLambdasOrGroups("Lambda", this.lambdas, lambdas);
+    }
+
+    @SafeVarargs
+    private static <T extends LambdaOrGroup>
+    void assertLambdasOrGroups(String what, List<DexClass> objects, T... checks) {
+      ArrayList<DexClass> list = Lists.newArrayList(objects);
+      for (int i = 0; i < checks.length; i++) {
+        T check = checks[i];
+        for (DexClass clazz : list) {
+          if (check.match(clazz)) {
+            list.remove(clazz);
+            checks[i] = null;
+            break;
+          }
+        }
+      }
+
+      int notFound = 0;
+      for (T check : checks) {
+        if (check != null) {
+          System.err.println(what + " not found: " + check);
+          notFound++;
+        }
+      }
+
+      for (DexClass dexClass : list) {
+        System.err.println(what + " unexpected: " +
+            dexClass.type.descriptor.toString() +
+            ", arity: " + getLambdaArity(dexClass) +
+            ", capture: " + getLambdaGroupCapture(dexClass));
+        notFound++;
+      }
+
+      assertTrue(what + "s match failed", 0 == notFound && 0 == list.size());
+    }
+  }
+
+  private static int getLambdaArity(DexClass clazz) {
+    for (DexType iface : clazz.interfaces.values) {
+      String descr = iface.descriptor.toString();
+      if (descr.startsWith(KOTLIN_FUNCTION_IFACE)) {
+        return Integer.parseInt(
+            descr.substring(KOTLIN_FUNCTION_IFACE.length(), descr.length() - 1));
+      }
+    }
+    fail("Type " + clazz.type.descriptor.toString() +
+        " does not implement functional interface.");
+    throw new AssertionError();
+  }
+
+  private static boolean extendsLambdaBase(DexClass clazz) {
+    return clazz.superType.descriptor.toString().equals("Lkotlin/jvm/internal/Lambda;");
+  }
+
+  private static boolean isLambdaGroupClass(DexClass clazz) {
+    return clazz.type.getName().startsWith("-$$LambdaGroup$");
+  }
+
+  private static String getLambdaGroupCapture(DexClass clazz) {
+    return CaptureSignature.getCaptureSignature(clazz.instanceFields());
+  }
+
+  @Test
+  public void testTrivial() throws Exception {
+    final String mainClassName = "lambdas.kstyle.trivial.MainKt";
+    runTest("lambdas_kstyle_trivial", mainClassName, null, (app) -> {
+      Verifier verifier = new Verifier(app);
+      String pkg = "lambdas/kstyle/trivial";
+
+      verifier.assertLambdaGroups(
+          allowAccessModification ?
+              new Group[]{
+                  new Group("", "", 0),
+                  new Group("", "", 1),
+                  new Group("", "", 2), // -\
+                  new Group("", "", 2), // - 3 groups different by main method
+                  new Group("", "", 2), // -/
+                  new Group("", "", 3),
+                  new Group("", "", 22)} :
+              new Group[]{
+                  new Group(pkg, "", 0),
+                  new Group(pkg, "", 1),
+                  new Group(pkg, "", 2), // - 2 groups different by main method
+                  new Group(pkg, "", 2), // -/
+                  new Group(pkg, "", 3),
+                  new Group(pkg, "", 22),
+                  new Group(pkg + "/inner", "", 0),
+                  new Group(pkg + "/inner", "", 1)}
+      );
+
+      verifier.assertLambdas(
+          allowAccessModification ?
+              new Lambda[]{
+                  new Lambda(pkg, "MainKt$testStateless$6", 1) /* Banned for limited inlining */} :
+              new Lambda[]{
+                  new Lambda(pkg, "MainKt$testStateless$6", 1), /* Banned for limited inlining */
+                  new Lambda(pkg, "MainKt$testStateless$8", 2),
+                  new Lambda(pkg + "/inner", "InnerKt$testInnerStateless$7", 2)}
+
+      );
+    });
+  }
+
+  @Test
+  public void testCaptures() throws Exception {
+    final String mainClassName = "lambdas.kstyle.captures.MainKt";
+    runTest("lambdas_kstyle_captures", mainClassName, null, (app) -> {
+      Verifier verifier = new Verifier(app);
+      String pkg = "lambdas/kstyle/captures";
+      String grpPkg = allowAccessModification ? "" : pkg;
+
+      verifier.assertLambdaGroups(
+          new Group(grpPkg, "LLL", 0),
+          new Group(grpPkg, "ILL", 0),
+          new Group(grpPkg, "III", 0),
+          new Group(grpPkg, "BCDFIJLLLLSZ", 0),
+          new Group(grpPkg, "BCDFIJLLSZ", 0)
+      );
+
+      verifier.assertLambdas(
+          new Lambda(pkg, "MainKt$test1$15", 0),
+          new Lambda(pkg, "MainKt$test2$10", 0),
+          new Lambda(pkg, "MainKt$test2$11", 0),
+          new Lambda(pkg, "MainKt$test2$9", 0)
+      );
+    });
+  }
+
+  @Test
+  public void testGenericsNoSignature() throws Exception {
+    final String mainClassName = "lambdas.kstyle.generics.MainKt";
+    runTest("lambdas_kstyle_generics", mainClassName, null, (app) -> {
+      Verifier verifier = new Verifier(app);
+      String pkg = "lambdas/kstyle/generics";
+      String grpPkg = allowAccessModification ? "" : pkg;
+
+      verifier.assertLambdaGroups(
+          new Group(grpPkg, "", 1), // Group for Any
+          new Group(grpPkg, "L", 1), // Group for Beta
+          new Group(grpPkg, "LS", 1), // Group for Gamma
+          new Group(grpPkg, "", 1)  // Group for int
+      );
+
+      verifier.assertLambdas(
+          new Lambda(pkg, "MainKt$main$4", 1)
+      );
+    });
+  }
+
+  @Test
+  public void testInnerClassesAndEnclosingMethods() throws Exception {
+    final String mainClassName = "lambdas.kstyle.generics.MainKt";
+    runTest("lambdas_kstyle_generics", mainClassName, KEEP_INNER_AND_ENCLOSING, (app) -> {
+      Verifier verifier = new Verifier(app);
+      String pkg = "lambdas/kstyle/generics";
+      String grpPkg = allowAccessModification ? "" : pkg;
+
+      verifier.assertLambdaGroups(
+          new Group(grpPkg, "", 1), // Group for Any
+          new Group(grpPkg, "L", 1), // Group for Beta   // First
+          new Group(grpPkg, "L", 1), // Group for Beta   // Second
+          new Group(grpPkg, "LS", 1), // Group for Gamma // First
+          new Group(grpPkg, "LS", 1), // Group for Gamma // Second
+          new Group(grpPkg, "", 1)  // Group for int
+      );
+
+      verifier.assertLambdas(
+          new Lambda(pkg, "MainKt$main$4", 1)
+      );
+    });
+  }
+
+  @Test
+  public void testGenericsSignatureInnerEnclosing() throws Exception {
+    final String mainClassName = "lambdas.kstyle.generics.MainKt";
+    runTest("lambdas_kstyle_generics", mainClassName, KEEP_SIGNATURE_INNER_ENCLOSING, (app) -> {
+      Verifier verifier = new Verifier(app);
+      String pkg = "lambdas/kstyle/generics";
+      String grpPkg = allowAccessModification ? "" : pkg;
+
+      verifier.assertLambdaGroups(
+          new Group(grpPkg, "", 1), // Group for Any
+          new Group(grpPkg, "L", 1), // Group for Beta in First
+          new Group(grpPkg, "L", 1), // Group for Beta in Second
+          new Group(grpPkg, "LS", 1), // Group for Gamma<String> in First
+          new Group(grpPkg, "LS", 1), // Group for Gamma<Integer> in First
+          new Group(grpPkg, "LS", 1), // Group for Gamma<String> in Second
+          new Group(grpPkg, "LS", 1), // Group for Gamma<Integer> in Second
+          new Group(grpPkg, "", 1)  // Group for int
+      );
+
+      verifier.assertLambdas(
+          new Lambda(pkg, "MainKt$main$4", 1)
+      );
+    });
+  }
+}
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 82e6cd0..fbb14f1 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
@@ -10,7 +10,7 @@
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.jasmin.JasminBuilder;
 import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder;
-import com.android.tools.r8.kotlin.KotlinClass.Visibility;
+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.DexInspector;
@@ -25,14 +25,14 @@
 
   private static final String JAVA_LANG_STRING = "java.lang.String";
 
-  private static final KotlinCompanionClass ACCESSOR_COMPANION_PROPERTY_CLASS =
-      new KotlinCompanionClass("accessors.Accessor")
+  private static final TestKotlinCompanionClass ACCESSOR_COMPANION_PROPERTY_CLASS =
+      new TestKotlinCompanionClass("accessors.Accessor")
           .addProperty("property", JAVA_LANG_STRING, Visibility.PRIVATE);
 
   private static final String PROPERTIES_PACKAGE_NAME = "properties";
 
-  private static final KotlinCompanionClass COMPANION_PROPERTY_CLASS =
-      new KotlinCompanionClass("properties.CompanionProperties")
+  private static final TestKotlinCompanionClass COMPANION_PROPERTY_CLASS =
+      new TestKotlinCompanionClass("properties.CompanionProperties")
           .addProperty("privateProp", JAVA_LANG_STRING, Visibility.PRIVATE)
           .addProperty("protectedProp", JAVA_LANG_STRING, Visibility.PROTECTED)
           .addProperty("internalProp", JAVA_LANG_STRING, Visibility.INTERNAL)
@@ -170,7 +170,7 @@
         "accessor_accessCompanionPrivate");
     runTest("accessors", mainClass, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
-      KotlinCompanionClass testedClass = ACCESSOR_COMPANION_PROPERTY_CLASS;
+      TestKotlinCompanionClass testedClass = ACCESSOR_COMPANION_PROPERTY_CLASS;
       ClassSubject outerClass = checkClassExists(dexInspector,
           testedClass.getOuterClassName());
       ClassSubject companionClass = checkClassExists(dexInspector,
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
index daf05f5..29c5582 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
@@ -5,7 +5,7 @@
 package com.android.tools.r8.kotlin;
 
 import com.android.tools.r8.graph.DexCode;
-import com.android.tools.r8.kotlin.KotlinClass.Visibility;
+import com.android.tools.r8.kotlin.TestKotlinClass.Visibility;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.utils.DexInspector;
 import com.android.tools.r8.utils.DexInspector.ClassSubject;
@@ -15,7 +15,8 @@
 
 public class R8KotlinDataClassTest extends AbstractR8KotlinTestBase {
 
-  private static final KotlinDataClass TEST_DATA_CLASS = new KotlinDataClass("dataclass.Person")
+  private static final TestKotlinDataClass TEST_DATA_CLASS =
+      new TestKotlinDataClass("dataclass.Person")
       .addProperty("name", "java.lang.String", Visibility.PUBLIC)
       .addProperty("age", "int", Visibility.PUBLIC);
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinIntrinsicsTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinIntrinsicsTest.java
index b059382..8fef44c 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinIntrinsicsTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinIntrinsicsTest.java
@@ -17,8 +17,8 @@
 @RunWith(Parameterized.class)
 public class R8KotlinIntrinsicsTest extends AbstractR8KotlinTestBase {
 
-  private static final KotlinDataClass KOTLIN_INTRINSICS_CLASS =
-      new KotlinDataClass("kotlin.jvm.internal.Intrinsics");
+  private static final TestKotlinDataClass KOTLIN_INTRINSICS_CLASS =
+      new TestKotlinDataClass("kotlin.jvm.internal.Intrinsics");
 
   @Test
   public void testParameterNullCheckIsInlined() throws Exception {
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 499cc2f..cb3820e 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
@@ -6,7 +6,7 @@
 
 import static org.junit.Assert.assertTrue;
 
-import com.android.tools.r8.kotlin.KotlinClass.Visibility;
+import com.android.tools.r8.kotlin.TestKotlinClass.Visibility;
 import com.android.tools.r8.naming.MemberNaming;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.utils.DexInspector;
@@ -20,28 +20,28 @@
 
   private static final String JAVA_LANG_STRING = "java.lang.String";
 
-  private static final KotlinClass MUTABLE_PROPERTY_CLASS =
-      new KotlinClass("properties.MutableProperty")
+  private static final TestKotlinClass MUTABLE_PROPERTY_CLASS =
+      new TestKotlinClass("properties.MutableProperty")
           .addProperty("privateProp", JAVA_LANG_STRING, Visibility.PRIVATE)
           .addProperty("protectedProp", JAVA_LANG_STRING, Visibility.PROTECTED)
           .addProperty("internalProp", JAVA_LANG_STRING, Visibility.INTERNAL)
           .addProperty("publicProp", JAVA_LANG_STRING, Visibility.PUBLIC)
           .addProperty("primitiveProp", "int", Visibility.PUBLIC);
 
-  private static final KotlinClass USER_DEFINED_PROPERTY_CLASS =
-      new KotlinClass("properties.UserDefinedProperty")
+  private static final TestKotlinClass USER_DEFINED_PROPERTY_CLASS =
+      new TestKotlinClass("properties.UserDefinedProperty")
           .addProperty("durationInMilliSeconds", "int", Visibility.PUBLIC)
           .addProperty("durationInSeconds", "int", Visibility.PUBLIC);
 
-  private static final KotlinClass LATE_INIT_PROPERTY_CLASS =
-      new KotlinClass("properties.LateInitProperty")
+  private static final TestKotlinClass LATE_INIT_PROPERTY_CLASS =
+      new TestKotlinClass("properties.LateInitProperty")
           .addProperty("privateLateInitProp", JAVA_LANG_STRING, Visibility.PRIVATE)
           .addProperty("protectedLateInitProp", JAVA_LANG_STRING, Visibility.PROTECTED)
           .addProperty("internalLateInitProp", JAVA_LANG_STRING, Visibility.INTERNAL)
           .addProperty("publicLateInitProp", JAVA_LANG_STRING, Visibility.PUBLIC);
 
-  private static final KotlinCompanionClass COMPANION_PROPERTY_CLASS =
-      new KotlinCompanionClass("properties.CompanionProperties")
+  private static final TestKotlinCompanionClass COMPANION_PROPERTY_CLASS =
+      new TestKotlinCompanionClass("properties.CompanionProperties")
           .addProperty("privateProp", JAVA_LANG_STRING, Visibility.PRIVATE)
           .addProperty("protectedProp", JAVA_LANG_STRING, Visibility.PROTECTED)
           .addProperty("internalProp", JAVA_LANG_STRING, Visibility.INTERNAL)
@@ -51,8 +51,8 @@
           .addProperty("internalLateInitProp", JAVA_LANG_STRING, Visibility.INTERNAL)
           .addProperty("publicLateInitProp", JAVA_LANG_STRING, Visibility.PUBLIC);
 
-  private static final KotlinCompanionClass COMPANION_LATE_INIT_PROPERTY_CLASS =
-      new KotlinCompanionClass("properties.CompanionLateInitProperties")
+  private static final TestKotlinCompanionClass COMPANION_LATE_INIT_PROPERTY_CLASS =
+      new TestKotlinCompanionClass("properties.CompanionLateInitProperties")
           .addProperty("privateLateInitProp", JAVA_LANG_STRING, Visibility.PRIVATE)
           .addProperty("internalLateInitProp", JAVA_LANG_STRING, Visibility.INTERNAL)
           .addProperty("publicLateInitProp", JAVA_LANG_STRING, Visibility.PUBLIC);
@@ -461,7 +461,7 @@
 
   @Test
   public void testCompanionProperty_privateLateInitPropertyIsAlwaysInlined() throws Exception {
-    final KotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
+    final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_usePrivateLateInitProp");
     runTest(PACKAGE_NAME, mainClass, (app) -> {
@@ -492,7 +492,7 @@
 
   @Test
   public void testCompanionProperty_internalLateInitPropertyCannotBeInlined() throws Exception {
-    final KotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
+    final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_useInternalLateInitProp");
     runTest(PACKAGE_NAME, mainClass, (app) -> {
@@ -516,7 +516,7 @@
 
   @Test
   public void testCompanionProperty_publicLateInitPropertyCannotBeInlined() throws Exception {
-    final KotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
+    final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_usePublicLateInitProp");
     runTest(PACKAGE_NAME, mainClass, (app) -> {
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinClass.java b/src/test/java/com/android/tools/r8/kotlin/TestKotlinClass.java
similarity index 95%
rename from src/test/java/com/android/tools/r8/kotlin/KotlinClass.java
rename to src/test/java/com/android/tools/r8/kotlin/TestKotlinClass.java
index 56a727e..d364bb8 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinClass.java
+++ b/src/test/java/com/android/tools/r8/kotlin/TestKotlinClass.java
@@ -14,7 +14,7 @@
  *
  * <p>See https://kotlinlang.org/docs/reference/classes.html</p>
  */
-class KotlinClass {
+class TestKotlinClass {
 
   /**
    * This is the suffix appended by Kotlin compiler to getter and setter method names of
@@ -61,10 +61,11 @@
       return index;
     }
   }
+
   protected final String className;
   protected final Map<String, KotlinProperty> properties = Maps.newHashMap();
 
-  public KotlinClass(String className) {
+  public TestKotlinClass(String className) {
     this.className = className;
   }
 
@@ -72,7 +73,7 @@
     return className;
   }
 
-  public KotlinClass addProperty(String name, String type, Visibility visibility) {
+  public TestKotlinClass addProperty(String name, String type, Visibility visibility) {
     assert !properties.containsKey(name);
     properties.put(name, new KotlinProperty(name, type, visibility, properties.size()));
     return this;
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinCompanionClass.java b/src/test/java/com/android/tools/r8/kotlin/TestKotlinCompanionClass.java
similarity index 68%
rename from src/test/java/com/android/tools/r8/kotlin/KotlinCompanionClass.java
rename to src/test/java/com/android/tools/r8/kotlin/TestKotlinCompanionClass.java
index bf36dde..5c12baa 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinCompanionClass.java
+++ b/src/test/java/com/android/tools/r8/kotlin/TestKotlinCompanionClass.java
@@ -9,18 +9,18 @@
  *
  * <p>See https://kotlinlang.org/docs/reference/object-declarations.html#companion-objects</p>
  */
-public class KotlinCompanionClass extends KotlinClass {
+public class TestKotlinCompanionClass extends TestKotlinClass {
 
   private final String outerClassName;
 
-  public KotlinCompanionClass(String outerClassName) {
+  public TestKotlinCompanionClass(String outerClassName) {
     super(outerClassName + "$Companion");
     this.outerClassName = outerClassName;
   }
 
   @Override
-  public KotlinCompanionClass addProperty(String name, String type, Visibility visibility) {
-    return (KotlinCompanionClass) super.addProperty(name, type, visibility);
+  public TestKotlinCompanionClass addProperty(String name, String type, Visibility visibility) {
+    return (TestKotlinCompanionClass) super.addProperty(name, type, visibility);
   }
 
   public String getOuterClassName() {
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinDataClass.java b/src/test/java/com/android/tools/r8/kotlin/TestKotlinDataClass.java
similarity index 88%
rename from src/test/java/com/android/tools/r8/kotlin/KotlinDataClass.java
rename to src/test/java/com/android/tools/r8/kotlin/TestKotlinDataClass.java
index bb24adc..62cda72 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinDataClass.java
+++ b/src/test/java/com/android/tools/r8/kotlin/TestKotlinDataClass.java
@@ -16,15 +16,15 @@
  *
  * <p>See https://kotlinlang.org/docs/reference/data-classes.html</p>
  */
-class KotlinDataClass extends KotlinClass {
+class TestKotlinDataClass extends TestKotlinClass {
 
-  KotlinDataClass(String className) {
+  TestKotlinDataClass(String className) {
     super(className);
   }
 
   @Override
-  public KotlinDataClass addProperty(String name, String type, Visibility visibility) {
-    return (KotlinDataClass) super.addProperty(name, type, visibility);
+  public TestKotlinDataClass addProperty(String name, String type, Visibility visibility) {
+    return (TestKotlinDataClass) super.addProperty(name, type, visibility);
   }
 
   public MemberNaming.MethodSignature getComponentNFunctionForProperty(String name) {
diff --git a/src/test/kotlinR8TestResources/lambdas_kstyle_captures/main.kt b/src/test/kotlinR8TestResources/lambdas_kstyle_captures/main.kt
new file mode 100644
index 0000000..3ee0bf8
--- /dev/null
+++ b/src/test/kotlinR8TestResources/lambdas_kstyle_captures/main.kt
@@ -0,0 +1,65 @@
+// 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 lambdas.kstyle.captures
+
+fun consume(l: () -> String) = l()
+
+fun main(args: Array<String>) {
+    test()
+}
+
+private fun test() {
+    test1(1, 2, 3, "A", "B", "C", D("x"), D("y"), D("z"), 7, 8, 9)
+    test2(true, 10, '*', 20, 30, 40, 50.0f, 60.0, D("D"), "S", null, 70)
+}
+
+data class D(val d: String)
+
+private fun test1(
+        i1: Int, i2: Int, i3: Int,
+        s1: String, s2: String, s3: String,
+        d1: D, d2: D, d3: D,
+        o1: Int?, o2: Int?, o3: Int?
+) {
+    println(consume { "a: $i1 $i2 $i3" })
+    println(consume { "b: $i2 $i3 $i1" })
+    println(consume { "c: $i3 $i1 $i2" })
+
+    println(consume { "d: $i1 $s1 $d1" })
+    println(consume { "e: $i2 $d2 $s2" })
+    println(consume { "f: $i3 $d3 $d1" })
+    println(consume { "g: $o1 $d3 $i3" })
+    println(consume { "h: $o2 $o3 $i1" })
+
+    println(consume { "i: $s1 $s2 $s3" })
+    println(consume { "j: $d1 $d2 $d3" })
+    println(consume { "k: $o1 $o2 $o3" })
+    println(consume { "l: $s1 $d2 $o3" })
+    println(consume { "n: $o1 $s2 $d3" })
+    println(consume { "o: $d1 $o2 $s3" })
+
+    println(consume { "p: $i1 $i2 $s3" })
+}
+
+private fun test2(
+        z: Boolean, b: Byte, c: Char, s: Short,
+        i: Int, l: Long, f: Float, d: Double,
+        o1: D, o2: String, o3: Any?, o4: Byte?
+) {
+    println(consume { "a: $z $b $c $s $i $l $f $d $o1 $o2 $o3 $o4" })
+    println(consume { "a: $z $b $o1 $o2 $c $s $i $l $f $d $o3 $o4" })
+    println(consume { "a: $z $c $s $l $f $d $o2 $o3 $o4 $b $i $o1" })
+    println(consume { "a: $o1 $o2 $o3 $o4 $z $b $c $s $i $l $f $d" })
+
+    println(consume { "a: $z $b $c $s $i $l $f $d $o1 $o2 \$o3 \$o4" })
+    println(consume { "a: $z $b $c $s $i $l $f $d $o1 \$o2 \$o3 $o4" })
+    println(consume { "a: $z $b $c $s $i $l $f $d \$o1 \$o2 $o3 $o4" })
+    println(consume { "a: $z $b $c $s $i $l $f $d \$o1 $o2 $o3 \$o4" })
+
+    println(consume { "x: $z $b $c $s $i $l $f $d $o1 $o2 \$o3 $o4" })
+    println(consume { "y: $z $b $c $s $i $l \$f $d $o1 $o2 $o3 $o4" })
+    println(consume { "z: $z $b $c \$s $i $l $f $d $o1 $o2 $o3 $o4" })
+}
+
diff --git a/src/test/kotlinR8TestResources/lambdas_kstyle_generics/main.kt b/src/test/kotlinR8TestResources/lambdas_kstyle_generics/main.kt
new file mode 100644
index 0000000..7fb22eb
--- /dev/null
+++ b/src/test/kotlinR8TestResources/lambdas_kstyle_generics/main.kt
@@ -0,0 +1,60 @@
+// 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 lambdas.kstyle.generics
+
+private var COUNT = 11
+
+private fun next() = "${COUNT++}"
+
+data class Alpha(val id: String = next())
+
+data class Beta(val id: String = next())
+
+data class Gamma<out T>(val payload: T?, val id: String = next())
+
+fun <T> consume(t: T, l: (t: T) -> String) = l(t)
+
+fun main(args: Array<String>) {
+    println(consume(Any(), { "${Alpha()}" }))
+    println(consume(Any(), { "${Beta()}" }))
+    println(consume(Any(), { "${Gamma("{any}")}" }))
+    println(consume(Alpha(), { "$it" }))
+
+    testFirst(11)
+    testSecond(22)
+    testThird()
+}
+
+private fun testFirst(sh: Short) {
+    val prefix = "First"
+    println(consume(Beta(), { "$prefix-1-$it" }))
+    println(consume(Beta(), { "$prefix-2-$it" }))
+    println(consume(Beta(), { "$prefix-3-$it" }))
+    println(consume(Gamma(next()), { "$prefix-A-$it-$sh" }))
+    println(consume(Gamma(next()), { "$prefix-B-$it-$sh" }))
+    println(consume(Gamma(next()), { "$prefix-C-$it-$sh" }))
+    println(consume(Gamma(COUNT++), { "$prefix-D-$it-$sh" }))
+    println(consume(Gamma(COUNT++), { "$prefix-E-$it-$sh" }))
+    println(consume(Gamma(COUNT++), { "$prefix-F-$it-$sh" }))
+}
+
+private fun testSecond(sh: Short) {
+    val prefix = "Second"
+    println(consume(Beta(), { "$prefix-1-$it" }))
+    println(consume(Beta(), { "$prefix-2-$it" }))
+    println(consume(Beta(), { "$prefix-3-$it" }))
+    println(consume(Gamma(next()), { "$prefix-A-$it-$sh" }))
+    println(consume(Gamma(next()), { "$prefix-B-$it-$sh" }))
+    println(consume(Gamma(next()), { "$prefix-C-$it-$sh" }))
+    println(consume(Gamma(COUNT++), { "$prefix-D-$it-$sh" }))
+    println(consume(Gamma(COUNT++), { "$prefix-E-$it-$sh" }))
+    println(consume(Gamma(COUNT++), { "$prefix-F-$it-$sh" }))
+}
+
+private fun testThird() {
+    println(consume(4321, { "$it ${next()} ${next()} ${next()}" }))
+    println(consume(1234, { "$it ${Alpha()} ${Beta()} ${Gamma(next())}" }))
+}
+
diff --git a/src/test/kotlinR8TestResources/lambdas_kstyle_trivial/inner.kt b/src/test/kotlinR8TestResources/lambdas_kstyle_trivial/inner.kt
new file mode 100644
index 0000000..00587a9
--- /dev/null
+++ b/src/test/kotlinR8TestResources/lambdas_kstyle_trivial/inner.kt
@@ -0,0 +1,26 @@
+// 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 lambdas.kstyle.trivial.inner
+
+import lambdas.kstyle.trivial.consumeEmpty
+import lambdas.kstyle.trivial.consumeOne
+import lambdas.kstyle.trivial.consumeTwo
+
+fun testInner() {
+    testInnerStateless()
+}
+
+private fun testInnerStateless() {
+    println(consumeEmpty { "first empty" })
+    println(consumeEmpty { "second empty" })
+
+    println(consumeOne { _ -> "first single" })
+    println(consumeOne { _ -> "second single" })
+    println(consumeOne { _ -> "third single" })
+    println(consumeOne { x -> x })
+
+    println(consumeTwo { x, y -> x + "-" + y })
+}
+
diff --git a/src/test/kotlinR8TestResources/lambdas_kstyle_trivial/main.kt b/src/test/kotlinR8TestResources/lambdas_kstyle_trivial/main.kt
new file mode 100644
index 0000000..34ba194
--- /dev/null
+++ b/src/test/kotlinR8TestResources/lambdas_kstyle_trivial/main.kt
@@ -0,0 +1,89 @@
+// 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 lambdas.kstyle.trivial
+
+import lambdas.kstyle.trivial.inner.testInner
+
+private var COUNT = 11
+
+private fun next() = "${COUNT++}"
+
+fun consumeEmpty(l: () -> String) = l()
+fun consumeOne(l: (x: String) -> String): String = l(next())
+fun consumeTwo(l: (x: String, y: String) -> String): String = l(next(), next())
+fun consumeThree(l: (x: String, y: String, z: String) -> String) = l(next(), next(), next())
+fun consumeTwentyTwo(l: (v0: String, v1: String, v2: String, v3: String, v4: String,
+                         v5: String, v6: String, v7: String, v8: String, v9: String,
+                         v10: String, v11: String, v12: String, v13: String, v14: String,
+                         v15: String, v16: String, v17: String, v18: String, v19: String,
+                         v20: String, v21: String) -> String) = l(
+        next(), next(), next(), next(), next(), next(), next(), next(), next(), next(), next(),
+        next(), next(), next(), next(), next(), next(), next(), next(), next(), next(), next())
+
+fun main(args: Array<String>) {
+    test()
+    testInner()
+    testPrimitive()
+    testUnit()
+}
+
+private fun test() {
+    testStateless()
+}
+
+private fun testStateless() {
+    println(consumeEmpty { "first empty" })
+    println(consumeEmpty { "second empty" })
+
+    println(consumeOne { _ -> "first single" })
+    println(consumeOne { _ -> "second single" })
+    println(consumeOne { _ -> "third single" })
+    println(consumeOne { x ->
+        try {
+            throw RuntimeException("exception#$x")
+        } catch (e: RuntimeException) {
+            "caught: ${e.message}"
+        } catch (e: Exception) {
+            "NEVER"
+        }
+    })
+    println(consumeOne { x -> x })
+
+    println(consumeTwo { x, y -> x + "-" + y })
+
+    println(consumeThree { x, y, z -> x + y + z })
+    println(consumeThree { _, _, _ -> "one-two-three" })
+
+    println(consumeTwentyTwo { _, _, _, _, _, _, _, _, _, _, _,
+                               _, _, _, _, _, _, _, _, _, _, _ ->
+        "one-two-...-twentythree"
+    })
+    println(consumeTwentyTwo { v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11,
+                               v12, v13, v14, v15, v16, v17, v18, v19, v20, v21 ->
+        v0 + v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8 + v9 + v10 + v11 +
+                v12 + v13 + v14 + v15 + v16 + v17 + v18 + v19 + v20 + v21
+    })
+}
+
+private fun consumePrimitive(i: Int, l: (Int, Short) -> Int) = l(i, 5)
+
+private fun testPrimitive() {
+    println(consumePrimitive(1, { x, y -> x }))
+    println(consumePrimitive(2, { x, y -> y.toInt() }))
+    println(consumePrimitive(3, { x, y -> x + y }))
+    println(consumePrimitive(4, { x, y -> x * y }))
+    val l: (Int, Short) -> Int = { x, y -> x / y }
+    println(l(100, 20))
+}
+
+private fun consumeUnit(i: Int, l: (Int, Short) -> Unit) = l(i, 10)
+
+private fun testUnit() {
+    println(consumeUnit(11, { x, y -> println() }))
+    println(consumeUnit(12, { x, y -> println(y) }))
+    println(consumeUnit(13, { x, y -> println(x) }))
+    println(consumeUnit(14, { x, y -> println("$x -- $y") }))
+}
+