Merge "Take into account catch handlers when adding item-based string."
diff --git a/.gitignore b/.gitignore
index f743d94..86e3098 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,12 +28,16 @@
 third_party/android_jar/lib.tar.gz
 third_party/android_jar/lib-v[0-9][0-9]
 third_party/android_jar/lib-v[0-9][0-9].tar.gz
+third_party/benchmarks/santa-tracker
+third_party/benchmarks/santa-tracker.tar.gz
 third_party/gmail/*
 !third_party/gmail/*.sha1
 third_party/gmscore/*
 !third_party/gmscore/*.sha1
 third_party/gradle/gradle.tar.gz
 third_party/gradle/gradle
+third_party/gradle-plugin
+third_party/gradle-plugin.tar.gz
 third_party/jasmin.tar.gz
 third_party/jasmin
 third_party/jdwp-tests.tar.gz
diff --git a/build.gradle b/build.gradle
index 2cf3fe1..e0c50ee 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1190,12 +1190,30 @@
             def name = dir.getName()
             def taskName = "jar_kotlinR8TestResources_${name}_${kotlinTargetVersion}"
             def outputFile = "build/test/kotlinR8TestResources/${kotlinTargetVersion}/${name}.jar"
-            task "${taskName}"(type: kotlin.Kotlinc) {
-                source = fileTree(dir: file("${examplesDir}/${name}"), include: '**/*.kt')
+            def javaOutput = "build/test/kotlinR8TestResources/${kotlinTargetVersion}/${name}/java"
+            def javaOutputJarName = "${name}.java.jar"
+            def javaOutputJarDir = "build/test/kotlinR8TestResources/${kotlinTargetVersion}"
+            task "${taskName}Kotlin"(type: kotlin.Kotlinc) {
+                source = fileTree(dir: file("${examplesDir}/${name}"),
+                        include: ['**/*.kt', '**/*.java'])
                 destination = file(outputFile)
                 targetVersion = kotlinTargetVersion
             }
-            dependsOn taskName
+            task "${taskName}Java"(type: JavaCompile) {
+                source = fileTree(dir: file("${examplesDir}/${name}"), include: '**/*.java')
+                destinationDir = file(javaOutput)
+                classpath = sourceSets.main.compileClasspath
+                sourceCompatibility = JavaVersion.VERSION_1_6
+                targetCompatibility = JavaVersion.VERSION_1_6
+                options.compilerArgs += ["-g", "-Xlint:-options"]
+            }
+            task "${taskName}JavaJar"(type: Jar, dependsOn: "${taskName}Java") {
+                archiveName = javaOutputJarName
+                destinationDir = file(javaOutputJarDir)
+                from javaOutput
+                include "**/*.class"
+            }
+            dependsOn "${taskName}Kotlin", "${taskName}JavaJar"
         }
     }
 }
diff --git a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
index 42982d5..9a260ea 100644
--- a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
@@ -103,7 +103,7 @@
     private OutputMode outputMode = OutputMode.DexIndexed;
 
     private CompilationMode mode;
-    private int minApiLevel = AndroidApiLevel.getDefault().getLevel();
+    private int minApiLevel = 0;
     private boolean disableDesugaring = false;
 
     Builder() {}
@@ -228,13 +228,20 @@
 
     /** Get the minimum API level (aka SDK version). */
     public int getMinApiLevel() {
-      return minApiLevel;
+      return isMinApiLevelSet() ? minApiLevel : AndroidApiLevel.getDefault().getLevel();
+    }
+
+    boolean isMinApiLevelSet() {
+      return minApiLevel != 0;
     }
 
     /** Set the minimum required API level (aka SDK version). */
     public B setMinApiLevel(int minApiLevel) {
-      assert minApiLevel > 0;
-      this.minApiLevel = minApiLevel;
+      if (minApiLevel <= 0) {
+        getReporter().error("Invalid minApiLevel: " + minApiLevel);
+      } else {
+        this.minApiLevel = minApiLevel;
+      }
       return self();
     }
 
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 4615fa5..5f27356 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -256,6 +256,9 @@
               "R8 does not support compiling DEX inputs", new PathOrigin(file)));
         }
       }
+      if (getProgramConsumer() instanceof ClassFileConsumer && isMinApiLevelSet()) {
+        reporter.error("R8 does not support --min-api when compiling to class files");
+      }
       super.validate();
     }
 
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 5afe490..ff3e30d 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -257,10 +257,13 @@
   public class ObjectMethods {
 
     public final DexMethod getClass;
+    public final DexMethod constructor;
 
     private ObjectMethods() {
       getClass = createMethod(objectDescriptor, getClassMethodName, classDescriptor,
           DexString.EMPTY_ARRAY);
+      constructor = createMethod(objectDescriptor,
+          constructorMethodName, voidType.descriptor, DexString.EMPTY_ARRAY);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexString.java b/src/main/java/com/android/tools/r8/graph/DexString.java
index ad187ec..9036fd7 100644
--- a/src/main/java/com/android/tools/r8/graph/DexString.java
+++ b/src/main/java/com/android/tools/r8/graph/DexString.java
@@ -25,7 +25,7 @@
 
   public DexString(String string) {
     this.size = string.length();
-    this.content = encode(string);
+    this.content = encodeToMutf8(string);
   }
 
   @Override
@@ -118,7 +118,7 @@
   }
 
   // Inspired from /dex/src/main/java/com/android/dex/Mutf8.java
-  private static byte[] encode(String string) {
+  public static byte[] encodeToMutf8(String string) {
     byte[] result = new byte[countBytes(string)];
     int offset = 0;
     for (int i = 0; i < string.length(); i++) {
@@ -207,15 +207,12 @@
     if (string.charAt(1) == '/' || string.charAt(string.length() - 2) == '/') {
       return false;
     }
-    for (int i = 1; i < string.length() - 1; i++) {
-      char ch = string.charAt(i);
-      if (ch == '/') {
-        continue;
+    int cp;
+    for (int i = 1; i < string.length() - 1; i += Character.charCount(cp)) {
+      cp = string.codePointAt(i);
+      if (cp != '/' && !IdentifierUtils.isDexIdentifierPart(cp)) {
+        return false;
       }
-      if (IdentifierUtils.isDexIdentifierPart(ch)) {
-        continue;
-      }
-      return false;
     }
     return true;
   }
@@ -232,12 +229,12 @@
             string.equals(Constants.CLASS_INITIALIZER_NAME))) {
       return true;
     }
-    for (int i = 0; i < string.length(); i++) {
-      char ch = string.charAt(i);
-      if (IdentifierUtils.isDexIdentifierPart(ch)) {
-        continue;
+    int cp;
+    for (int i = 0; i < string.length(); i += Character.charCount(cp)) {
+      cp = string.codePointAt(i);
+      if (!IdentifierUtils.isDexIdentifierPart(cp)) {
+        return false;
       }
-      return false;
     }
     return true;
   }
@@ -249,18 +246,19 @@
     int start = 0;
     int end = string.length();
     if (string.charAt(0) == '<') {
-      if (string.charAt(string.length() - 1) == '>') {
+      if (string.charAt(end - 1) == '>') {
         start = 1;
-        end = string.length() - 1;
+        --end;
       } else {
         return false;
       }
     }
-    for (int i = start; i < end; i++) {
-      if (IdentifierUtils.isDexIdentifierPart(string.charAt(i))) {
-        continue;
+    int cp;
+    for (int i = start; i < end; i += Character.charCount(cp)) {
+      cp = string.codePointAt(i);
+      if (!IdentifierUtils.isDexIdentifierPart(cp)) {
+        return false;
       }
-      return false;
     }
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java b/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
index 718d23c..01cd5f5 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
@@ -5,7 +5,6 @@
 
 import com.android.tools.r8.cf.code.CfInvoke;
 import com.android.tools.r8.code.InvokePolymorphicRange;
-import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -14,7 +13,6 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.conversion.JarSourceCode;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
 import com.android.tools.r8.ir.optimize.InliningOracle;
@@ -87,15 +85,12 @@
   public void buildCf(CfBuilder builder) {
     DexMethod dexMethod = getInvokedMethod();
     DexItemFactory factory = builder.getFactory();
-
-    if (dexMethod.holder.getInternalName().equals(JarSourceCode.INTERNAL_NAME_METHOD_HANDLE)) {
-      DexMethod method = factory.createMethod(dexMethod.holder, getProto(), dexMethod.name);
-      builder.add(new CfInvoke(Opcodes.INVOKEVIRTUAL, method));
-    } else {
-      assert dexMethod.holder.getInternalName().equals(JarSourceCode.INTERNAL_NAME_VAR_HANDLE);
-      // VarHandle is new in Java 9
-      throw new Unimplemented();
-    }
+    // When we translate InvokeVirtual on MethodHandle/VarHandle into InvokePolymorphic,
+    // we translate the invoked prototype into a generic prototype that simply accepts Object[].
+    // To translate InvokePolymorphic back into InvokeVirtual, use the original prototype
+    // that is stored in getProto().
+    DexMethod method = factory.createMethod(dexMethod.holder, getProto(), dexMethod.name);
+    builder.add(new CfInvoke(Opcodes.INVOKEVIRTUAL, method));
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
index 3ad0168..96b2122 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
@@ -984,7 +984,14 @@
       int source = builder.getInfo(jump).getOffset();
       Info targetInfo = builder.getTargetInfo(jump.getTarget());
       int relativeOffset = targetInfo.getOffset() - source;
-      if (size == relativeOffset) {
+      // Emit a return if the target is a return and the size of the return is the computed
+      // size of this instruction.
+      Return ret = targetInfo.getIR().asReturn();
+      if (ret != null && size == targetInfo.getSize() && ret.getPosition().isNone()) {
+        Instruction dex = ret.createDexInstruction(builder);
+        dex.setOffset(getOffset()); // for better printing of the dex code.
+        instructions.add(dex);
+      } else if (size == relativeOffset) {
         // We should never generate a goto targeting the next instruction. However, if we do
         // we replace it with nops. This works around a dalvik bug where the dalvik tracing
         // jit crashes on 'goto next instruction' on Android 4.1.1.
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
index 26a94d2..b584448 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
@@ -2658,8 +2658,8 @@
     Handle bsmHandle = insn.bsm;
     if (bsmHandle.getTag() != Opcodes.H_INVOKESTATIC &&
         bsmHandle.getTag() != Opcodes.H_NEWINVOKESPECIAL) {
-      throw new Unreachable(
-          "Bootstrap handle is not yet supported: tag == " + bsmHandle.getTag());
+      // JVM9 §4.7.23 note: Tag must be InvokeStatic or NewInvokeSpecial.
+      throw new Unreachable("Bootstrap handle invalid: tag == " + bsmHandle.getTag());
     }
     // Resolve the bootstrap method.
     DexMethodHandle bootstrapMethod = getMethodHandle(application, bsmHandle);
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
index daa7811..eb7826e 100644
--- 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
@@ -72,7 +72,7 @@
   }
 
   // No-op strategy.
-  public static final Strategy NoOp = new Strategy() {
+  static final Strategy NoOp = new Strategy() {
     @Override
     public LambdaGroup group() {
       return null;
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
index fea83eb..f3fecde 100644
--- 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
@@ -27,10 +27,14 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 // 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.
+//
+// NOTE: access to lambdas in lambda group is NOT thread-safe.
 public abstract class LambdaGroup {
   public final LambdaGroupId id;
 
@@ -43,9 +47,6 @@
 
   // 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 {
@@ -67,11 +68,28 @@
     return classType;
   }
 
-  public final List<LambdaInfo> lambdas() {
-    return Lists.newArrayList(lambdas.values());
+  public final int size() {
+    return lambdas.size();
   }
 
-  public final boolean shouldAddToMainDex(AppInfo appInfo) {
+  public final void forEachLambda(Consumer<LambdaInfo> action) {
+    assert verifyLambdaIds(false);
+    for (LambdaInfo info : lambdas.values()) {
+      action.accept(info);
+    }
+  }
+
+  public final boolean anyLambda(Predicate<LambdaInfo> predicate) {
+    assert verifyLambdaIds(false);
+    for (LambdaInfo info : lambdas.values()) {
+      if (predicate.test(info)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  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()) {
@@ -96,6 +114,13 @@
     return lambdas.get(lambda).clazz.instanceFields();
   }
 
+  protected final DexEncodedField lambdaSingletonField(DexType lambda) {
+    assert lambdas.containsKey(lambda);
+    DexEncodedField[] fields = lambdas.get(lambda).clazz.staticFields();
+    assert fields.length < 2;
+    return fields.length == 0 ? null : fields[0];
+  }
+
   // Contains less than 2 elements?
   final boolean isTrivial() {
     return lambdas.size() < 2;
@@ -112,6 +137,7 @@
   }
 
   final void compact() {
+    assert verifyLambdaIds(false);
     int lastUsed = -1;
     int lastSeen = -1;
     for (Entry<DexType, LambdaInfo> entry : lambdas.entrySet()) {
@@ -123,6 +149,7 @@
         entry.getValue().id = lastUsed;
       }
     }
+    assert verifyLambdaIds(true);
   }
 
   public abstract Strategy getCodeStrategy();
@@ -133,15 +160,14 @@
   // Package for a lambda group class to be created in.
   protected abstract String getTypePackage();
 
-  public final DexProgramClass synthesizeClass(DexItemFactory factory) {
+  protected abstract String getGroupSuffix();
+
+  final DexProgramClass synthesizeClass(DexItemFactory factory) {
     assert classType == null;
+    assert verifyLambdaIds(true);
     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);
+        "L" + getTypePackage() + "-$$LambdaGroup$" + getGroupSuffix() + createHash(lambdas) + ";");
     return getBuilder(factory).synthesizeClass();
   }
 
@@ -168,9 +194,25 @@
     }
   }
 
+  private boolean verifyLambdaIds(boolean strict) {
+    int previous = -1;
+    for (LambdaInfo info : lambdas.values()) {
+      assert strict ? (previous + 1) == info.id : previous < info.id;
+      previous = info.id;
+    }
+    return true;
+  }
+
   public static class LambdaStructureError extends Exception {
+    final boolean reportable;
+
     public LambdaStructureError(String cause) {
+      this(cause, true);
+    }
+
+    public LambdaStructureError(String cause, boolean reportable) {
       super(cause);
+      this.reportable = reportable;
     }
   }
 }
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
index 5c591ab..5127632 100644
--- 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
@@ -14,7 +14,6 @@
 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;
 
@@ -24,22 +23,23 @@
   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() {
+    DexType groupClassType = group.getGroupClassType();
+    DexType superClassType = getSuperClassType();
+
     return new DexProgramClass(
-        group.getGroupClassType(),
+        groupClassType,
         null,
         new SynthesizedOrigin(origin, getClass()),
         buildAccessFlags(),
-        getSuperClassType(),
+        superClassType,
         buildInterfaces(),
         factory.createString(origin),
         buildEnclosingMethodAttribute(),
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
index 11976b6..03daf44 100644
--- 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
@@ -28,9 +28,8 @@
 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.ir.optimize.lambda.kotlin.KotlinLambdaGroupIdFactory;
 import com.android.tools.r8.kotlin.Kotlin;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
@@ -59,7 +58,7 @@
 //   (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.
+// current implementation deals with both k- and j-style lambdas.
 //
 // In general we merge lambdas in 5 phases:
 //   1. collect all lambdas and compute group candidates. we do it synchronously
@@ -151,18 +150,20 @@
         .filter(cls -> !infoWithLiveness.isPinned(cls.type))
         .filter(cls -> cls.hasKotlinInfo() &&
             cls.getKotlinInfo().isSyntheticClass() &&
-            cls.getKotlinInfo().asSyntheticClass().isKotlinStyleLambda())
+            cls.getKotlinInfo().asSyntheticClass().isLambda())
         .sorted((a, b) -> a.type.slowCompareTo(b.type)) // Ensure stable ordering.
         .forEachOrdered(lambda -> {
           try {
-            LambdaGroupId id = KStyleLambdaGroupIdFactory.create(kotlin, lambda, options);
+            LambdaGroupId id = KotlinLambdaGroupIdFactory.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()));
+            if (error.reportable) {
+              reporter.warning(
+                  new StringDiagnostic("Unrecognized Kotlin lambda [" +
+                      lambda.type.toSourceString() + "]: " + error.getMessage()));
+            }
           }
         });
 
@@ -199,11 +200,12 @@
 
     // Analyse more complex aspects of lambda classes including method code.
     assert converter.appInfo.hasSubtyping();
-    analyzeLambdaClassesStructure(converter.appInfo.withSubtyping(), executorService);
+    AppInfoWithSubtyping appInfo = converter.appInfo.withSubtyping();
+    analyzeLambdaClassesStructure(appInfo, executorService);
 
     // Remove invalidated lambdas, compact groups to ensure
     // sequential lambda ids, create group lambda classes.
-    Map<LambdaGroup, DexProgramClass> lambdaGroupsClasses = finalizeLambdaGroups();
+    Map<LambdaGroup, DexProgramClass> lambdaGroupsClasses = finalizeLambdaGroups(appInfo);
 
     // Switch to APPLY strategy.
     this.strategyFactory = ApplyStrategy::new;
@@ -236,24 +238,25 @@
     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);
-          }
-        }));
-      }
+      group.forEachLambda(info ->
+          futures.add(service.submit(() -> {
+            try {
+              validator.accept(info.clazz);
+            } catch (LambdaStructureError error) {
+              if (error.reportable) {
+                reporter.info(
+                    new StringDiagnostic("Unexpected Kotlin lambda structure [" +
+                        info.clazz.type.toSourceString() + "]: " + error.getMessage())
+                );
+              }
+              invalidateLambda(info.clazz.type);
+            }
+          })));
     }
     ThreadUtils.awaitFutures(futures);
   }
 
-  private synchronized Map<LambdaGroup, DexProgramClass> finalizeLambdaGroups() {
+  private Map<LambdaGroup, DexProgramClass> finalizeLambdaGroups(AppInfoWithSubtyping appInfo) {
     for (DexType lambda : invalidatedLambdas) {
       LambdaGroup group = lambdas.get(lambda);
       assert group != null;
@@ -270,7 +273,11 @@
     for (LambdaGroup group : groups.values()) {
       assert !group.isTrivial() : "No trivial group is expected here.";
       group.compact();
-      result.put(group, group.synthesizeClass(factory));
+      DexProgramClass lambdaGroupClass = group.synthesizeClass(factory);
+      result.put(group, lambdaGroupClass);
+
+      // We have to register this new class as a subtype of object.
+      appInfo.registerNewType(lambdaGroupClass.type, lambdaGroupClass.superType);
     }
     return result;
   }
@@ -281,11 +288,8 @@
       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);
-        }
+        assert group.getValue().size() < 2;
+        group.getValue().forEachLambda(info -> this.lambdas.remove(info.clazz.type));
       }
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/ClassInitializerSourceCode.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/ClassInitializerSourceCode.java
new file mode 100644
index 0000000..a5b6793
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/ClassInitializerSourceCode.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.kotlin;
+
+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;
+
+final class ClassInitializerSourceCode extends SyntheticSourceCode {
+  private final DexItemFactory factory;
+  private final KotlinLambdaGroup group;
+
+  ClassInitializerSourceCode(DexItemFactory factory, KotlinLambdaGroup group) {
+    super(null, factory.createProto(factory.voidType));
+    this.factory = factory;
+    this.group = group;
+  }
+
+  @Override
+  protected void prepareInstructions() {
+    DexType groupClassType = group.getGroupClassType();
+    DexMethod lambdaConstructorMethod = factory.createMethod(groupClassType,
+        factory.createProto(factory.voidType, factory.intType), factory.constructorMethodName);
+
+    int instance = nextRegister(ValueType.OBJECT);
+    int lambdaId = nextRegister(ValueType.INT);
+    List<ValueType> argTypes = Lists.newArrayList(ValueType.OBJECT, ValueType.INT);
+    List<Integer> argRegisters = Lists.newArrayList(instance, lambdaId);
+
+    group.forEachLambda(info -> {
+      DexType lambda = info.clazz.type;
+      if (group.isSingletonLambda(lambda)) {
+        int id = group.lambdaId(lambda);
+        add(builder -> builder.addNewInstance(instance, groupClassType));
+        add(builder -> builder.addConst(ValueType.INT, lambdaId, id));
+        add(builder -> builder.addInvoke(Type.DIRECT,
+            lambdaConstructorMethod, lambdaConstructorMethod.proto, argTypes, argRegisters));
+        add(builder -> builder.addStaticPut(
+            instance, group.getSingletonInstanceField(factory, id)));
+      }
+    });
+
+    assert this.nextInstructionIndex() > 0 : "no single field initialized";
+    add(IRBuilder::addReturn);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/JStyleLambdaGroup.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/JStyleLambdaGroup.java
new file mode 100644
index 0000000..b4a1865
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/JStyleLambdaGroup.java
@@ -0,0 +1,212 @@
+// 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.kotlin;
+
+import com.android.tools.r8.code.ReturnVoid;
+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.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.EnclosingMethodAttribute;
+import com.android.tools.r8.graph.InnerClassAttribute;
+import com.android.tools.r8.ir.code.Invoke.Type;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.optimize.lambda.LambdaGroup;
+import com.android.tools.r8.ir.optimize.lambda.LambdaGroupClassBuilder;
+import com.android.tools.r8.ir.synthetic.SyntheticSourceCode;
+import com.android.tools.r8.kotlin.Kotlin;
+import com.android.tools.r8.utils.ThrowingConsumer;
+import com.google.common.collect.Lists;
+import java.util.function.IntFunction;
+
+// Represents a j-style lambda group created to combine several lambda classes
+// generated by kotlin compiler for kotlin lambda expressions passed to java receivers,
+// like:
+//
+//      --- Java --------------------------------------------------------------
+//      public static void acceptString(Supplier<String> s) {
+//        // ...
+//      }
+//      -----------------------------------------------------------------------
+//      acceptString({ "A" })
+//      -----------------------------------------------------------------------
+//
+// Regular stateless j-style lambda class structure looks like below:
+// NOTE: stateless j-style lambdas do not always have INSTANCE field.
+//
+// -----------------------------------------------------------------------------------------------
+// signature <T:Ljava/lang/Object;>Ljava/lang/Object;
+//                    Ljava/util/function/Supplier<Ljava/lang/String;>;
+// final class lambdas/LambdasKt$foo$4 implements java/util/function/Supplier {
+//
+//     public synthetic bridge get()Ljava/lang/Object;
+//
+//     public final get()Ljava/lang/String;
+//       @Lorg/jetbrains/annotations/NotNull;() // invisible
+//
+//     <init>()V
+//
+//     public final static Llambdas/LambdasKt$foo$4; INSTANCE
+//
+//     static <clinit>()V
+//
+//     OUTERCLASS lambdas/LambdasKt foo (Ljava/lang/String;I)Lkotlin/jvm/functions/Function0;
+//     final static INNERCLASS lambdas/LambdasKt$foo$4 null null
+// }
+// -----------------------------------------------------------------------------------------------
+//
+// Regular stateful j-style lambda class structure looks like below:
+//
+// -----------------------------------------------------------------------------------------------
+// signature <T:Ljava/lang/Object;>
+//                Ljava/lang/Object;Ljava/util/function/Supplier<Ljava/lang/String;>;
+// declaration: lambdas/LambdasKt$foo$5<T> implements java.util.function.Supplier<java.lang.String>
+// final class lambdas/LambdasKt$foo$5 implements java/util/function/Supplier  {
+//
+//     public synthetic bridge get()Ljava/lang/Object;
+//
+//     public final get()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$5 null null
+// }
+// -----------------------------------------------------------------------------------------------
+//
+// Key j-style lambda class details:
+//   - extends java.lang.Object
+//   - implements *any* functional interface (Kotlin does not seem to support scenarios when
+//     lambda can implement multiple interfaces).
+//   - 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 *may* be 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 JStyleLambdaGroup extends KotlinLambdaGroup {
+  private JStyleLambdaGroup(GroupId id) {
+    super(id);
+  }
+
+  @Override
+  protected LambdaGroupClassBuilder getBuilder(DexItemFactory factory) {
+    return new ClassBuilder(factory, "java-style lambda group");
+  }
+
+  @Override
+  public ThrowingConsumer<DexClass, LambdaStructureError> lambdaClassValidator(
+      Kotlin kotlin, AppInfoWithSubtyping appInfo) {
+    return new ClassValidator(kotlin, appInfo);
+  }
+
+  @Override
+  protected String getGroupSuffix() {
+    return "js$";
+  }
+
+  // Specialized group id.
+  final static class GroupId extends KotlinLambdaGroupId {
+    GroupId(String capture, DexType iface,
+        String pkg, String signature, DexEncodedMethod mainMethod,
+        InnerClassAttribute inner, EnclosingMethodAttribute enclosing) {
+      super(capture, iface, pkg, signature, mainMethod, inner, enclosing);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      return obj instanceof GroupId && computeEquals((KotlinLambdaGroupId) obj);
+    }
+
+    @Override
+    String getLambdaKindDescriptor() {
+      return "Kotlin j-style lambda group";
+    }
+
+    @Override
+    public LambdaGroup createGroup() {
+      return new JStyleLambdaGroup(this);
+    }
+  }
+
+  // Specialized class validator.
+  private class ClassValidator extends KotlinLambdaClassValidator {
+    ClassValidator(Kotlin kotlin, AppInfoWithSubtyping appInfo) {
+      super(kotlin, JStyleLambdaGroup.this, appInfo);
+    }
+
+    @Override
+    int getInstanceInitializerSize(DexEncodedField[] captures) {
+      return captures.length + 2;
+    }
+
+    @Override
+    int validateInstanceInitializerEpilogue(
+        com.android.tools.r8.code.Instruction[] instructions, int index)
+        throws LambdaStructureError {
+      if (!(instructions[index] instanceof com.android.tools.r8.code.InvokeDirect) ||
+          instructions[index].getMethod() != kotlin.factory.objectMethods.constructor) {
+        throw structureError(LAMBDA_INIT_CODE_VERIFICATION_FAILED);
+      }
+      index++;
+      if (!(instructions[index] instanceof ReturnVoid)) {
+        throw structureError(LAMBDA_INIT_CODE_VERIFICATION_FAILED);
+      }
+      return index + 1;
+    }
+  }
+
+  // Specialized class builder.
+  private final class ClassBuilder extends KotlinLambdaGroupClassBuilder<JStyleLambdaGroup> {
+    ClassBuilder(DexItemFactory factory, String origin) {
+      super(JStyleLambdaGroup.this, factory, origin);
+    }
+
+    @Override
+    protected DexType getSuperClassType() {
+      return factory.objectType;
+    }
+
+    @Override
+    SyntheticSourceCode createInstanceInitializerSourceCode(
+        DexType groupClassType, DexProto initializerProto) {
+      return new InstanceInitializerSourceCode(
+          factory, groupClassType, group.getLambdaIdField(factory),
+          id -> group.getCaptureField(factory, id), initializerProto);
+    }
+  }
+
+  // Specialized instance initializer code.
+  private static final class InstanceInitializerSourceCode
+      extends KotlinInstanceInitializerSourceCode {
+    private final DexMethod objectInitializer;
+
+    InstanceInitializerSourceCode(DexItemFactory factory, DexType lambdaGroupType,
+        DexField idField, IntFunction<DexField> fieldGenerator, DexProto proto) {
+      super(lambdaGroupType, idField, fieldGenerator, proto);
+      this.objectInitializer = factory.objectMethods.constructor;
+    }
+
+    @Override
+    void prepareSuperConstructorCall(int receiverRegister) {
+      add(builder -> builder.addInvoke(Type.DIRECT, objectInitializer, objectInitializer.proto,
+          Lists.newArrayList(ValueType.OBJECT), Lists.newArrayList(receiverRegister)));
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/JStyleLambdaGroupIdFactory.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/JStyleLambdaGroupIdFactory.java
new file mode 100644
index 0000000..f906137
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/JStyleLambdaGroupIdFactory.java
@@ -0,0 +1,69 @@
+// 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.kotlin;
+
+import com.android.tools.r8.graph.DexClass;
+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.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;
+
+final class JStyleLambdaGroupIdFactory extends KotlinLambdaGroupIdFactory {
+  static final JStyleLambdaGroupIdFactory INSTANCE = new JStyleLambdaGroupIdFactory();
+
+  @Override
+  LambdaGroupId validateAndCreate(Kotlin kotlin, DexClass lambda, InternalOptions options)
+      throws LambdaStructureError {
+    boolean accessRelaxed = options.proguardConfiguration.isAccessModificationAllowed();
+
+    assert lambda.hasKotlinInfo() && lambda.getKotlinInfo().isSyntheticClass();
+    assert lambda.getKotlinInfo().asSyntheticClass().isJavaStyleLambda();
+
+    checkAccessFlags("class access flags", lambda.accessFlags,
+        PUBLIC_LAMBDA_CLASS_FLAGS, LAMBDA_CLASS_FLAGS);
+
+    // Class and interface.
+    validateSuperclass(kotlin, lambda);
+    DexType iface = validateInterfaces(kotlin, lambda);
+
+    validateStaticFields(kotlin, lambda);
+    String captureSignature = validateInstanceFields(lambda, accessRelaxed);
+    validateDirectMethods(lambda);
+    DexEncodedMethod mainMethod = validateVirtualMethods(lambda);
+    String genericSignature = validateAnnotations(kotlin, lambda);
+    InnerClassAttribute innerClass = validateInnerClasses(lambda);
+
+    return new JStyleLambdaGroup.GroupId(captureSignature, iface,
+        accessRelaxed ? "" : lambda.type.getPackageDescriptor(),
+        genericSignature, mainMethod, innerClass, lambda.getEnclosingMethod());
+  }
+
+  @Override
+  void validateSuperclass(Kotlin kotlin, DexClass lambda) throws LambdaStructureError {
+    if (lambda.superType != kotlin.factory.objectType) {
+      throw new LambdaStructureError("implements " + lambda.superType.toSourceString() +
+          " instead of java.lang.Object");
+    }
+  }
+
+  @Override
+  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());
+    }
+
+    // We don't validate that the interface is actually a functional interface,
+    // since it may be desugared, or optimized in any other way which should not
+    // affect lambda class merging.
+    return lambda.interfaces.values[0];
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KStyleLambdaGroup.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KStyleLambdaGroup.java
new file mode 100644
index 0000000..46c14e9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KStyleLambdaGroup.java
@@ -0,0 +1,228 @@
+// 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.kotlin;
+
+import com.android.tools.r8.code.Const16;
+import com.android.tools.r8.code.Const4;
+import com.android.tools.r8.code.ReturnVoid;
+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.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.EnclosingMethodAttribute;
+import com.android.tools.r8.graph.InnerClassAttribute;
+import com.android.tools.r8.ir.code.Invoke.Type;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.optimize.lambda.LambdaGroup;
+import com.android.tools.r8.ir.optimize.lambda.LambdaGroupClassBuilder;
+import com.android.tools.r8.ir.synthetic.SyntheticSourceCode;
+import com.android.tools.r8.kotlin.Kotlin;
+import com.android.tools.r8.utils.ThrowingConsumer;
+import com.google.common.collect.Lists;
+import java.util.function.IntFunction;
+
+// 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:
+// NOTE: stateless j-style lambdas do not always have INSTANCE field.
+//
+// -----------------------------------------------------------------------------------------------
+// // 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 *may* be  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 KotlinLambdaGroup {
+  private KStyleLambdaGroup(GroupId id) {
+    super(id);
+  }
+
+  @Override
+  protected LambdaGroupClassBuilder getBuilder(DexItemFactory factory) {
+    return new ClassBuilder(factory, "kotlin-style lambda group");
+  }
+
+  @Override
+  public ThrowingConsumer<DexClass, LambdaStructureError> lambdaClassValidator(
+      Kotlin kotlin, AppInfoWithSubtyping appInfo) {
+    return new ClassValidator(kotlin, appInfo);
+  }
+
+  @Override
+  protected String getGroupSuffix() {
+    return "ks$";
+  }
+
+  // Specialized group id.
+  final static class GroupId extends KotlinLambdaGroupId {
+    GroupId(String capture, DexType iface,
+        String pkg, String signature, DexEncodedMethod mainMethod,
+        InnerClassAttribute inner, EnclosingMethodAttribute enclosing) {
+      super(capture, iface, pkg, signature, mainMethod, inner, enclosing);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      return obj instanceof GroupId && computeEquals((KotlinLambdaGroupId) obj);
+    }
+
+    @Override
+    String getLambdaKindDescriptor() {
+      return "Kotlin k-style lambda group";
+    }
+
+    @Override
+    public LambdaGroup createGroup() {
+      return new KStyleLambdaGroup(this);
+    }
+  }
+
+  // Specialized class validator.
+  private final class ClassValidator extends KotlinLambdaClassValidator {
+    ClassValidator(Kotlin kotlin, AppInfoWithSubtyping appInfo) {
+      super(kotlin, KStyleLambdaGroup.this, appInfo);
+    }
+
+    @Override
+    int getInstanceInitializerSize(DexEncodedField[] captures) {
+      return captures.length + 3;
+    }
+
+    @Override
+    int validateInstanceInitializerEpilogue(
+        com.android.tools.r8.code.Instruction[] instructions, int index)
+        throws LambdaStructureError {
+      if (!(instructions[index] instanceof Const4) &&
+          !(instructions[index] instanceof Const16)) {
+        throw structureError(LAMBDA_INIT_CODE_VERIFICATION_FAILED);
+      }
+      index++;
+      if (!(instructions[index] instanceof com.android.tools.r8.code.InvokeDirect) ||
+          instructions[index].getMethod() != kotlin.functional.lambdaInitializerMethod) {
+        throw structureError(LAMBDA_INIT_CODE_VERIFICATION_FAILED);
+      }
+      index++;
+      if (!(instructions[index] instanceof ReturnVoid)) {
+        throw structureError(LAMBDA_INIT_CODE_VERIFICATION_FAILED);
+      }
+      return index + 1;
+    }
+  }
+
+  // Specialized class builder.
+  private final class ClassBuilder extends KotlinLambdaGroupClassBuilder<KStyleLambdaGroup> {
+    ClassBuilder(DexItemFactory factory, String origin) {
+      super(KStyleLambdaGroup.this, factory, origin);
+    }
+
+    @Override
+    protected DexType getSuperClassType() {
+      return factory.kotlin.functional.lambdaType;
+    }
+
+    @Override
+    SyntheticSourceCode createInstanceInitializerSourceCode(
+        DexType groupClassType, DexProto initializerProto) {
+      return new InstanceInitializerSourceCode(factory, groupClassType,
+          group.getLambdaIdField(factory), id -> group.getCaptureField(factory, id),
+          initializerProto, id.mainMethodProto.parameters.size());
+    }
+  }
+
+  // Specialized instance initializer code.
+  private final static class InstanceInitializerSourceCode
+      extends KotlinInstanceInitializerSourceCode {
+    private final int arity;
+    private final DexMethod lambdaInitializer;
+
+    InstanceInitializerSourceCode(DexItemFactory factory, DexType lambdaGroupType,
+        DexField idField, IntFunction<DexField> fieldGenerator, DexProto proto, int arity) {
+      super(lambdaGroupType, idField, fieldGenerator, proto);
+      this.arity = arity;
+      this.lambdaInitializer = factory.createMethod(factory.kotlin.functional.lambdaType,
+          factory.createProto(factory.voidType, factory.intType), factory.constructorMethodName);
+    }
+
+    @Override
+    void prepareSuperConstructorCall(int receiverRegister) {
+      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)));
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KStyleLambdaGroupIdFactory.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KStyleLambdaGroupIdFactory.java
new file mode 100644
index 0000000..36f9ecd
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KStyleLambdaGroupIdFactory.java
@@ -0,0 +1,70 @@
+// 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.kotlin;
+
+import com.android.tools.r8.graph.DexClass;
+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.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;
+
+final class KStyleLambdaGroupIdFactory extends KotlinLambdaGroupIdFactory {
+  static final KotlinLambdaGroupIdFactory INSTANCE = new KStyleLambdaGroupIdFactory();
+
+  @Override
+  LambdaGroupId validateAndCreate(Kotlin kotlin, DexClass lambda, InternalOptions options)
+      throws LambdaStructureError {
+    boolean accessRelaxed = options.proguardConfiguration.isAccessModificationAllowed();
+
+    assert lambda.hasKotlinInfo() && lambda.getKotlinInfo().isSyntheticClass();
+    assert lambda.getKotlinInfo().asSyntheticClass().isKotlinStyleLambda();
+
+    checkAccessFlags("class access flags", lambda.accessFlags,
+        PUBLIC_LAMBDA_CLASS_FLAGS, LAMBDA_CLASS_FLAGS);
+
+    // Class and interface.
+    validateSuperclass(kotlin, lambda);
+    DexType iface = validateInterfaces(kotlin, lambda);
+
+    validateStaticFields(kotlin, lambda);
+    String captureSignature = validateInstanceFields(lambda, accessRelaxed);
+    validateDirectMethods(lambda);
+    DexEncodedMethod mainMethod = validateVirtualMethods(lambda);
+    String genericSignature = validateAnnotations(kotlin, lambda);
+    InnerClassAttribute innerClass = validateInnerClasses(lambda);
+
+    return new KStyleLambdaGroup.GroupId(captureSignature, iface,
+        accessRelaxed ? "" : lambda.type.getPackageDescriptor(),
+        genericSignature, mainMethod, innerClass, lambda.getEnclosingMethod());
+  }
+
+  @Override
+  void validateSuperclass(Kotlin kotlin, DexClass lambda) throws LambdaStructureError {
+    if (lambda.superType != kotlin.functional.lambdaType) {
+      throw new LambdaStructureError("implements " + lambda.superType.toSourceString() +
+          " instead of kotlin.jvm.internal.Lambda");
+    }
+  }
+
+  @Override
+  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;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinInstanceInitializerSourceCode.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinInstanceInitializerSourceCode.java
new file mode 100644
index 0000000..396cdca
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinInstanceInitializerSourceCode.java
@@ -0,0 +1,47 @@
+// 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.kotlin;
+
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.conversion.IRBuilder;
+import com.android.tools.r8.ir.synthetic.SyntheticSourceCode;
+import java.util.function.IntFunction;
+
+abstract class KotlinInstanceInitializerSourceCode extends SyntheticSourceCode {
+  private final DexField idField;
+  private final IntFunction<DexField> fieldGenerator;
+
+  KotlinInstanceInitializerSourceCode(DexType lambdaGroupType,
+      DexField idField, IntFunction<DexField> fieldGenerator, DexProto proto) {
+    super(lambdaGroupType, proto);
+    this.idField = idField;
+    this.fieldGenerator = fieldGenerator;
+  }
+
+  @Override
+  protected void prepareInstructions() {
+    int receiverRegister = getReceiverRegister();
+
+    // Initialize lambda id field.
+    add(builder -> builder.addInstancePut(getParamRegister(0), receiverRegister, idField));
+
+    // Initialize capture values.
+    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)));
+    }
+
+    // Call superclass constructor.
+    prepareSuperConstructorCall(receiverRegister);
+
+    add(IRBuilder::addReturn);
+  }
+
+  abstract void prepareSuperConstructorCall(int receiverRegister);
+}
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/kotlin/KotlinLambdaClassValidator.java
similarity index 67%
rename from src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaClassValidator.java
rename to src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaClassValidator.java
index ffd0f7d..3410703 100644
--- 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/kotlin/KotlinLambdaClassValidator.java
@@ -2,11 +2,10 @@
 // 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;
+package com.android.tools.r8.ir.optimize.lambda.kotlin;
 
-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.Instruction;
 import com.android.tools.r8.code.Iput;
 import com.android.tools.r8.code.IputBoolean;
 import com.android.tools.r8.code.IputByte;
@@ -33,7 +32,7 @@
 
 // Encapsulates the logic of deep-checking of the lambda class assumptions.
 //
-// For k-style lambdas we only check the code of class and instance
+// For k- and j-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
@@ -42,21 +41,33 @@
 //
 //  (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;
+abstract class KotlinLambdaClassValidator
+    implements ThrowingConsumer<DexClass, LambdaStructureError> {
+
+  static final String LAMBDA_INIT_CODE_VERIFICATION_FAILED =
+      "instance initializer code verification failed";
+  private static final String LAMBDA_CLINIT_CODE_VERIFICATION_FAILED =
+      "static initializer code verification failed";
+
+  final Kotlin kotlin;
+  private final KotlinLambdaGroup group;
   private final AppInfoWithSubtyping appInfo;
 
-  KStyleLambdaClassValidator(Kotlin kotlin, KStyleLambdaGroup group, AppInfoWithSubtyping appInfo) {
+  KotlinLambdaClassValidator(Kotlin kotlin, KotlinLambdaGroup group, AppInfoWithSubtyping appInfo) {
     this.kotlin = kotlin;
     this.group = group;
     this.appInfo = appInfo;
   }
 
+  // Report a structure error.
+  final LambdaStructureError structureError(String s) {
+    return new LambdaStructureError(s, false);
+  }
+
   @Override
   public void accept(DexClass lambda) throws LambdaStructureError {
     if (!CaptureSignature.getCaptureSignature(lambda.instanceFields()).equals(group.id().capture)) {
-      throw new LambdaStructureError("capture signature was modified");
+      throw structureError("capture signature was modified");
     }
 
     DexEncodedMethod classInitializer = null;
@@ -67,27 +78,27 @@
       // 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 (!(group.isStateless() && group.isSingletonLambda(lambda.type))) {
+          throw structureError("static initializer on non-singleton lambda");
         }
-        if (classInitializer != null || code == null || !code.isDexCode() ||
-            !validateStatelessLambdaClassInitializer(lambda, code.asDexCode())) {
-          throw new LambdaStructureError("static initializer code verification failed");
+        if (classInitializer != null || code == null || !code.isDexCode()) {
+          throw structureError(LAMBDA_CLINIT_CODE_VERIFICATION_FAILED);
         }
+        validateStatelessLambdaClassInitializer(lambda, code.asDexCode());
         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");
+        if (instanceInitializer != null || code == null || !code.isDexCode()) {
+          throw structureError(LAMBDA_INIT_CODE_VERIFICATION_FAILED);
         }
+        validateInstanceInitializer(lambda, code.asDexCode());
         instanceInitializer = method;
       }
     }
 
-    if (group.isStateless() && (classInitializer == null)) {
-      throw new LambdaStructureError("missing static initializer on stateless lambda");
+    if (group.isStateless() && group.isSingletonLambda(lambda.type) && (classInitializer == null)) {
+      throw structureError("missing static initializer on singleton lambda");
     }
 
     // This check is actually not required for lambda class merging, we only have to do
@@ -101,23 +112,38 @@
         "L" + group.getTypePackage() + "-$$LambdaGroup$XXXX;");
     for (DexEncodedMethod method : lambda.virtualMethods()) {
       if (!method.isInliningCandidate(fakeLambdaGroupType, Reason.SIMPLE, appInfo)) {
-        throw new LambdaStructureError("method " + method.method.toSourceString() +
+        throw structureError("method " + method.method.toSourceString() +
             " is not inline-able into lambda group class");
       }
     }
   }
 
-  private boolean validateInstanceInitializer(DexClass lambda, Code code) {
+  abstract int getInstanceInitializerSize(DexEncodedField[] captures);
+
+  abstract int validateInstanceInitializerEpilogue(
+      com.android.tools.r8.code.Instruction[] instructions, int index) throws LambdaStructureError;
+
+  private void validateInstanceInitializer(DexClass lambda, Code code)
+      throws LambdaStructureError {
     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;
+    if (instructions.length != getInstanceInitializerSize(captures)) {
+      throw structureError(LAMBDA_INIT_CODE_VERIFICATION_FAILED);
     }
 
     // Capture field assignments: go through captured fields in assumed order
     // and ensure they are assigned values from appropriate parameters.
+    index = validateInstanceInitializerParameterMapping(captures, instructions, index);
+
+    // Check the constructor epilogue: a call to superclass constructor.
+    index = validateInstanceInitializerEpilogue(instructions, index);
+    assert index == instructions.length;
+  }
+
+  private int validateInstanceInitializerParameterMapping(DexEncodedField[] captures,
+      Instruction[] instructions, int index) throws LambdaStructureError {
     int wideFieldsSeen = 0;
     for (DexEncodedField field : captures) {
       switch (field.field.type.toShorty()) {
@@ -125,7 +151,7 @@
           if (!(instructions[index] instanceof IputBoolean) ||
               (instructions[index].getField() != field.field) ||
               (((Format22c) instructions[index]).A != (index + 1 + wideFieldsSeen))) {
-            return false;
+            throw structureError(LAMBDA_INIT_CODE_VERIFICATION_FAILED);
           }
           break;
 
@@ -133,7 +159,7 @@
           if (!(instructions[index] instanceof IputByte) ||
               (instructions[index].getField() != field.field) ||
               (((Format22c) instructions[index]).A != (index + 1 + wideFieldsSeen))) {
-            return false;
+            throw structureError(LAMBDA_INIT_CODE_VERIFICATION_FAILED);
           }
           break;
 
@@ -141,7 +167,7 @@
           if (!(instructions[index] instanceof IputShort) ||
               (instructions[index].getField() != field.field) ||
               (((Format22c) instructions[index]).A != (index + 1 + wideFieldsSeen))) {
-            return false;
+            throw structureError(LAMBDA_INIT_CODE_VERIFICATION_FAILED);
           }
           break;
 
@@ -149,7 +175,7 @@
           if (!(instructions[index] instanceof IputChar) ||
               (instructions[index].getField() != field.field) ||
               (((Format22c) instructions[index]).A != (index + 1 + wideFieldsSeen))) {
-            return false;
+            throw structureError(LAMBDA_INIT_CODE_VERIFICATION_FAILED);
           }
           break;
 
@@ -158,7 +184,7 @@
           if (!(instructions[index] instanceof Iput) ||
               (instructions[index].getField() != field.field) ||
               (((Format22c) instructions[index]).A != (index + 1 + wideFieldsSeen))) {
-            return false;
+            throw structureError(LAMBDA_INIT_CODE_VERIFICATION_FAILED);
           }
           break;
 
@@ -167,7 +193,7 @@
           if (!(instructions[index] instanceof IputWide) ||
               (instructions[index].getField() != field.field) ||
               (((Format22c) instructions[index]).A != (index + 1 + wideFieldsSeen))) {
-            return false;
+            throw structureError(LAMBDA_INIT_CODE_VERIFICATION_FAILED);
           }
           wideFieldsSeen++;
           break;
@@ -176,7 +202,7 @@
           if (!(instructions[index] instanceof IputObject) ||
               (instructions[index].getField() != field.field) ||
               (((Format22c) instructions[index]).A != (index + 1 + wideFieldsSeen))) {
-            return false;
+            throw structureError(LAMBDA_INIT_CODE_VERIFICATION_FAILED);
           }
           break;
 
@@ -185,49 +211,31 @@
       }
       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;
+    return index;
   }
 
-  private boolean validateStatelessLambdaClassInitializer(DexClass lambda, Code code) {
-    assert group.isStateless();
+  private void validateStatelessLambdaClassInitializer(DexClass lambda, Code code)
+      throws LambdaStructureError {
+    assert group.isStateless() && group.isSingletonLambda(lambda.type);
     com.android.tools.r8.code.Instruction[] instructions = code.asDexCode().instructions;
     if (instructions.length != 4) {
-      return false;
+      throw structureError(LAMBDA_CLINIT_CODE_VERIFICATION_FAILED);
     }
     if (!(instructions[0] instanceof com.android.tools.r8.code.NewInstance) ||
         ((com.android.tools.r8.code.NewInstance) instructions[0]).getType() != lambda.type) {
-      return false;
+      throw structureError(LAMBDA_CLINIT_CODE_VERIFICATION_FAILED);
     }
     if (!(instructions[1] instanceof com.android.tools.r8.code.InvokeDirect) ||
         !isLambdaInitializerMethod(lambda, instructions[1].getMethod())) {
-      return false;
+      throw structureError(LAMBDA_CLINIT_CODE_VERIFICATION_FAILED);
     }
     if (!(instructions[2] instanceof SputObject) ||
         !isLambdaSingletonField(lambda, instructions[2].getField())) {
-      return false;
+      throw structureError(LAMBDA_CLINIT_CODE_VERIFICATION_FAILED);
     }
     if (!(instructions[3] instanceof ReturnVoid)) {
-      return false;
+      throw structureError(LAMBDA_CLINIT_CODE_VERIFICATION_FAILED);
     }
-    return true;
   }
 
   private boolean isLambdaSingletonField(DexClass lambda, DexField field) {
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/kotlin/KotlinLambdaConstants.java
similarity index 95%
rename from src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleConstants.java
rename to src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaConstants.java
index 619fa67..9ac5ca7 100644
--- 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/kotlin/KotlinLambdaConstants.java
@@ -2,14 +2,14 @@
 // 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;
+package com.android.tools.r8.ir.optimize.lambda.kotlin;
 
 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 {
+interface KotlinLambdaConstants {
   // Default lambda class flags.
   ClassAccessFlags LAMBDA_CLASS_FLAGS =
       ClassAccessFlags.fromDexAccessFlags(Constants.ACC_FINAL);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroup.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroup.java
new file mode 100644
index 0000000..fc3f2bd
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroup.java
@@ -0,0 +1,85 @@
+// 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.kotlin;
+
+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.LambdaGroupId;
+
+// Represents a lambda group created to combine several lambda classes generated
+// by kotlin compiler for either regular kotlin lambda expressions (k-style lambdas)
+// or lambda expressions created to implement java SAM interface.
+abstract class KotlinLambdaGroup extends LambdaGroup {
+  private final Strategy strategy = new KotlinLambdaGroupCodeStrategy(this);
+
+  KotlinLambdaGroup(LambdaGroupId id) {
+    super(id);
+  }
+
+  final KotlinLambdaGroupId id() {
+    return (KotlinLambdaGroupId) id;
+  }
+
+  final boolean isStateless() {
+    return id().capture.isEmpty();
+  }
+
+  final boolean hasAnySingletons() {
+    assert isStateless();
+    return anyLambda(info -> this.isSingletonLambda(info.clazz.type));
+  }
+
+  final boolean isSingletonLambda(DexType lambda) {
+    assert isStateless();
+    return lambdaSingletonField(lambda) != null;
+  }
+
+  // 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
+  public Strategy getCodeStrategy() {
+    return strategy;
+  }
+}
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/kotlin/KotlinLambdaGroupClassBuilder.java
similarity index 76%
rename from src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaGroupClassBuilder.java
rename to src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupClassBuilder.java
index 59de393..d441a00 100644
--- 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/kotlin/KotlinLambdaGroupClassBuilder.java
@@ -2,7 +2,7 @@
 // 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;
+package com.android.tools.r8.ir.optimize.lambda.kotlin;
 
 import com.android.tools.r8.graph.ClassAccessFlags;
 import com.android.tools.r8.graph.DexAnnotation;
@@ -19,9 +19,9 @@
 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.android.tools.r8.ir.synthetic.SyntheticSourceCode;
 import com.google.common.collect.Lists;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -30,21 +30,19 @@
 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 {
+// Builds components of kotlin lambda group class.
+abstract class KotlinLambdaGroupClassBuilder<T extends KotlinLambdaGroup>
+    extends LambdaGroupClassBuilder<T> implements KotlinLambdaConstants {
 
-  private final KStyleLambdaGroupId id;
+  final KotlinLambdaGroupId id;
 
-  KStyleLambdaGroupClassBuilder(DexItemFactory factory, KStyleLambdaGroup group, String origin) {
+  KotlinLambdaGroupClassBuilder(T group, DexItemFactory factory, String origin) {
     super(group, factory, origin);
     this.id = group.id();
   }
 
-  @Override
-  protected DexType getSuperClassType() {
-    return factory.kotlin.functional.lambdaType;
-  }
+  abstract SyntheticSourceCode createInstanceInitializerSourceCode(
+      DexType groupClassType, DexProto initializerProto);
 
   // Always generate public final classes.
   @Override
@@ -61,8 +59,7 @@
   // Take the attribute from the group, if exists.
   @Override
   protected List<InnerClassAttribute> buildInnerClasses() {
-    return id.innerClassAccess == KStyleLambdaGroupId.MISSING_INNER_CLASS_ATTRIBUTE
-        ? Collections.emptyList()
+    return !id.hasInnerClassAttribute() ? Collections.emptyList()
         : Lists.newArrayList(new InnerClassAttribute(
             id.innerClassAccess, group.getGroupClassType(), null, null));
   }
@@ -118,7 +115,7 @@
             isMainMethod ? id.mainMethodAnnotations : DexAnnotationSet.empty(),
             isMainMethod ? id.mainMethodParamAnnotations : DexAnnotationSetRefList.empty(),
             new SynthesizedCode(
-                new VirtualMethodSourceCode(factory, group.getGroupClassType(),
+                new KotlinLambdaVirtualMethodSourceCode(factory, group.getGroupClassType(),
                     methodProto, group.getLambdaIdField(factory), implMethods))));
       }
     }
@@ -132,37 +129,30 @@
   // 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()) {
+    int size = group.size();
+    group.forEachLambda(info -> {
+      for (DexEncodedMethod method : info.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);
+                k -> Lists.newArrayList(Collections.nCopies(size, null)));
+        assert list.get(info.id) == null;
+        list.set(info.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();
+    boolean needsSingletonInstances = group.isStateless() && group.hasAnySingletons();
     DexType groupClassType = group.getGroupClassType();
 
-    DexEncodedMethod[] result = new DexEncodedMethod[statelessLambda ? 2 : 1];
+    DexEncodedMethod[] result = new DexEncodedMethod[needsSingletonInstances ? 2 : 1];
     // Instance initializer mapping parameters into capture fields.
     DexProto initializerProto = group.createConstructorProto(factory);
     result[0] = new DexEncodedMethod(
@@ -170,13 +160,10 @@
         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())));
+        new SynthesizedCode(createInstanceInitializerSourceCode(groupClassType, initializerProto)));
 
     // Static class initializer for stateless lambdas.
-    if (statelessLambda) {
+    if (needsSingletonInstances) {
       result[1] = new DexEncodedMethod(
           factory.createMethod(groupClassType,
               factory.createProto(factory.voidType),
@@ -184,10 +171,7 @@
           CLASS_INITIALIZER_FLAGS,
           DexAnnotationSet.empty(),
           DexAnnotationSetRefList.empty(),
-          new SynthesizedCode(
-              new ClassInitializerSourceCode(
-                  factory, groupClassType, lambdas.size(),
-                  id -> group.getSingletonInstanceField(factory, id))));
+          new SynthesizedCode(new ClassInitializerSourceCode(factory, group)));
     }
 
     return result;
@@ -216,15 +200,16 @@
     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;
+    // One field for each singleton lambda in the group.
+    List<DexEncodedField> result = new ArrayList<>(group.size());
+    group.forEachLambda(info -> {
+      if (group.isSingletonLambda(info.clazz.type)) {
+        result.add(new DexEncodedField(group.getSingletonInstanceField(factory, info.id),
+            SINGLETON_FIELD_FLAGS, DexAnnotationSet.empty(), DexValueNull.NULL));
+      }
+    });
+    assert result.isEmpty() == !group.hasAnySingletons();
+    return result.toArray(new DexEncodedField[result.size()]);
   }
 
   @Override
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/kotlin/KotlinLambdaGroupCodeStrategy.java
similarity index 94%
rename from src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaGroupCodeStrategy.java
rename to src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupCodeStrategy.java
index dd066b6..d42ecf6 100644
--- 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/kotlin/KotlinLambdaGroupCodeStrategy.java
@@ -2,7 +2,7 @@
 // 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;
+package com.android.tools.r8.ir.optimize.lambda.kotlin;
 
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexField;
@@ -30,11 +30,11 @@
 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;
+// Defines the code processing strategy for kotlin lambdas.
+final class KotlinLambdaGroupCodeStrategy implements Strategy {
+  private final KotlinLambdaGroup group;
 
-  KStyleLambdaGroupCodeStrategy(KStyleLambdaGroup group) {
+  KotlinLambdaGroupCodeStrategy(KotlinLambdaGroup group) {
     this.group = group;
   }
 
@@ -83,7 +83,7 @@
   @Override
   public boolean isValidNewInstance(CodeProcessor context, NewInstance invoke) {
     // Only valid for stateful lambdas.
-    return !group.isStateless();
+    return !(group.isStateless() && group.isSingletonLambda(invoke.clazz));
   }
 
   @Override
@@ -95,9 +95,10 @@
     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,
+    // Allow calls to a constructor from other classes if the lambda is singleton,
     // otherwise allow such a call only from the same class static initializer.
-    return (group.isStateless() == (context.method.method.holder == lambda)) &&
+    boolean isSingletonLambda = group.isStateless() && group.isSingletonLambda(lambda);
+    return (isSingletonLambda == (context.method.method.holder == lambda)) &&
         invoke.isInvokeDirect() &&
         context.factory.isConstructor(method) &&
         CaptureSignature.getCaptureSignature(method.proto.parameters).equals(group.id().capture);
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/kotlin/KotlinLambdaGroupId.java
similarity index 89%
rename from src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaGroupId.java
rename to src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupId.java
index 6b6f73b..0540bef 100644
--- 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/kotlin/KotlinLambdaGroupId.java
@@ -2,7 +2,7 @@
 // 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;
+package com.android.tools.r8.ir.optimize.lambda.kotlin;
 
 import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexAnnotationSetRefList;
@@ -15,8 +15,8 @@
 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;
+abstract class KotlinLambdaGroupId implements LambdaGroupId {
+  private static final int MISSING_INNER_CLASS_ATTRIBUTE = -1;
 
   private final int hash;
 
@@ -50,7 +50,7 @@
   // access from InnerClassAttribute.
   final int innerClassAccess;
 
-  KStyleLambdaGroupId(String capture, DexType iface, String pkg, String signature,
+  KotlinLambdaGroupId(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);
@@ -67,8 +67,12 @@
     this.hash = computeHashCode();
   }
 
+  final boolean hasInnerClassAttribute() {
+    return innerClassAccess != MISSING_INNER_CLASS_ATTRIBUTE;
+  }
+
   @Override
-  public int hashCode() {
+  public final int hashCode() {
     return hash;
   }
 
@@ -87,11 +91,9 @@
   }
 
   @Override
-  public boolean equals(Object obj) {
-    if (!(obj instanceof KStyleLambdaGroupId)) {
-      return false;
-    }
-    KStyleLambdaGroupId other = (KStyleLambdaGroupId) obj;
+  public abstract boolean equals(Object obj);
+
+  boolean computeEquals(KotlinLambdaGroupId other) {
     return capture.equals(other.capture) &&
         iface == other.iface &&
         pkg.equals(other.pkg) &&
@@ -108,7 +110,7 @@
 
   @Override
   public String toString() {
-    StringBuilder builder = new StringBuilder("Kotlin-style lambda group")
+    StringBuilder builder = new StringBuilder(getLambdaKindDescriptor())
         .append("\n  capture: ").append(capture)
         .append("\n  interface: ").append(iface.descriptor)
         .append("\n  package: ").append(pkg)
@@ -131,8 +133,8 @@
     return builder.toString();
   }
 
+  abstract String getLambdaKindDescriptor();
+
   @Override
-  public LambdaGroup createGroup() {
-    return new KStyleLambdaGroup(this);
-  }
+  public abstract LambdaGroup createGroup();
 }
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/kotlin/KotlinLambdaGroupIdFactory.java
similarity index 71%
rename from src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaGroupIdFactory.java
rename to src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupIdFactory.java
index 7a516cd..f5698aa 100644
--- 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/kotlin/KotlinLambdaGroupIdFactory.java
@@ -2,7 +2,7 @@
 // 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;
+package com.android.tools.r8.ir.optimize.lambda.kotlin;
 
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AccessFlags;
@@ -19,8 +19,8 @@
 import com.android.tools.r8.utils.InternalOptions;
 import java.util.List;
 
-public final class KStyleLambdaGroupIdFactory implements KStyleConstants {
-  private KStyleLambdaGroupIdFactory() {
+public abstract class KotlinLambdaGroupIdFactory implements KotlinLambdaConstants {
+  KotlinLambdaGroupIdFactory() {
   }
 
   // Creates a lambda group id for kotlin style lambda. Should never return null, if the lambda
@@ -32,26 +32,24 @@
   // 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);
+    assert lambda.hasKotlinInfo() && lambda.getKotlinInfo().isSyntheticClass();
+    if (lambda.getKotlinInfo().asSyntheticClass().isKotlinStyleLambda()) {
+      return KStyleLambdaGroupIdFactory.INSTANCE.validateAndCreate(kotlin, lambda, options);
+    }
 
-    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());
+    assert lambda.getKotlinInfo().asSyntheticClass().isJavaStyleLambda();
+    return JStyleLambdaGroupIdFactory.INSTANCE.validateAndCreate(kotlin, lambda, options);
   }
 
-  private static DexEncodedMethod validateVirtualMethods(DexClass lambda)
-      throws LambdaStructureError {
+  abstract LambdaGroupId validateAndCreate(Kotlin kotlin, DexClass lambda, InternalOptions options)
+      throws LambdaStructureError;
+
+  abstract void validateSuperclass(Kotlin kotlin, DexClass lambda) throws LambdaStructureError;
+
+  abstract DexType validateInterfaces(Kotlin kotlin, DexClass lambda) throws LambdaStructureError;
+
+  DexEncodedMethod validateVirtualMethods(DexClass lambda) throws LambdaStructureError {
     DexEncodedMethod mainMethod = null;
 
     for (DexEncodedMethod method : lambda.virtualMethods()) {
@@ -68,31 +66,28 @@
     }
 
     if (mainMethod == null) {
-      throw new LambdaStructureError("no main method found");
+      // Missing main method may be a result of tree shaking.
+      throw new LambdaStructureError("no main method found", false);
     }
     return mainMethod;
   }
 
-  private static InnerClassAttribute validateInnerClasses(DexClass lambda)
-      throws LambdaStructureError {
+  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()) {
+          if (!inner.isAnonymous()) {
             throw new LambdaStructureError("is not anonymous");
           }
-          return innerClass;
+          return inner;
         }
       }
     }
     return null;
   }
 
-  private static String validateAnnotations(Kotlin kotlin, DexClass lambda)
-      throws LambdaStructureError {
+  String validateAnnotations(Kotlin kotlin, DexClass lambda) throws LambdaStructureError {
     String signature = null;
     if (!lambda.annotations.isEmpty()) {
       for (DexAnnotation annotation : lambda.annotations.annotations) {
@@ -115,8 +110,7 @@
     return signature;
   }
 
-  private static void validateStaticFields(Kotlin kotlin, DexClass lambda)
-      throws LambdaStructureError {
+  void validateStaticFields(Kotlin kotlin, DexClass lambda) throws LambdaStructureError {
     DexEncodedField[] staticFields = lambda.staticFields();
     if (staticFields.length == 1) {
       DexEncodedField field = staticFields[0];
@@ -135,30 +129,10 @@
     } 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)
+  String validateInstanceFields(DexClass lambda, boolean accessRelaxed)
       throws LambdaStructureError {
     DexEncodedField[] instanceFields = lambda.instanceFields();
     for (DexEncodedField field : instanceFields) {
@@ -169,7 +143,7 @@
     return CaptureSignature.getCaptureSignature(instanceFields);
   }
 
-  private static void validateDirectMethods(DexClass lambda) throws LambdaStructureError {
+  void validateDirectMethods(DexClass lambda) throws LambdaStructureError {
     DexEncodedMethod[] directMethods = lambda.directMethods();
     for (DexEncodedMethod method : directMethods) {
       if (method.isClassInitializer()) {
@@ -194,8 +168,12 @@
           throw new LambdaStructureError("constructor parameters don't match captured values.");
         }
         for (int i = 0; i < parameters.length; i++) {
+          // Kotlin compiler sometimes reshuffles the parameters so that their order
+          // in the constructor don't match order of capture fields. We could add
+          // support for it, but it happens quite rarely so don't bother for now.
           if (parameters[i] != instanceFields[i].field.type) {
-            throw new LambdaStructureError("constructor parameters don't match captured values.");
+            throw new LambdaStructureError(
+                "constructor parameters don't match captured values.", false);
           }
         }
         checkAccessFlags("unexpected constructor access flags",
@@ -208,8 +186,7 @@
     }
   }
 
-  private static void checkDirectMethodAnnotations(DexEncodedMethod method)
-      throws LambdaStructureError {
+  void checkDirectMethodAnnotations(DexEncodedMethod method) throws LambdaStructureError {
     if (!method.annotations.isEmpty()) {
       throw new LambdaStructureError("unexpected method annotations [" +
           method.annotations.toSmaliString() + "] on " + method.method.toSourceString());
@@ -228,7 +205,7 @@
   }
 
   @SafeVarargs
-  private static <T extends AccessFlags> void checkAccessFlags(
+  static <T extends AccessFlags> void checkAccessFlags(
       String message, T actual, T... expected) throws LambdaStructureError {
     for (T flag : expected) {
       if (flag.equals(actual)) {
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/kotlin/KotlinLambdaVirtualMethodSourceCode.java
similarity index 94%
rename from src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/VirtualMethodSourceCode.java
rename to src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaVirtualMethodSourceCode.java
index 8921299..1bcc2c7 100644
--- 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/kotlin/KotlinLambdaVirtualMethodSourceCode.java
@@ -2,7 +2,7 @@
 // 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;
+package com.android.tools.r8.ir.optimize.lambda.kotlin;
 
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
@@ -17,12 +17,12 @@
 import java.util.ArrayList;
 import java.util.List;
 
-final class VirtualMethodSourceCode extends SyntheticSourceCode {
+final class KotlinLambdaVirtualMethodSourceCode extends SyntheticSourceCode {
   private final DexItemFactory factory;
   private final DexField idField;
   private final List<DexEncodedMethod> implMethods;
 
-  VirtualMethodSourceCode(DexItemFactory factory, DexType groupClass,
+  KotlinLambdaVirtualMethodSourceCode(DexItemFactory factory, DexType groupClass,
       DexProto proto, DexField idField, List<DexEncodedMethod> implMethods) {
     super(groupClass, proto);
     this.factory = factory;
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
deleted file mode 100644
index 1cc0dcd..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/ClassInitializerSourceCode.java
+++ /dev/null
@@ -1,53 +0,0 @@
-// 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
deleted file mode 100644
index 00571be..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/InstanceInitializerSourceCode.java
+++ /dev/null
@@ -1,55 +0,0 @@
-// 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/KStyleLambdaGroup.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaGroup.java
deleted file mode 100644
index 54f1d3a..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kstyle/KStyleLambdaGroup.java
+++ /dev/null
@@ -1,163 +0,0 @@
-// 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/kotlin/Kotlin.java b/src/main/java/com/android/tools/r8/kotlin/Kotlin.java
index 9b0415b..cf2b2bc 100644
--- a/src/main/java/com/android/tools/r8/kotlin/Kotlin.java
+++ b/src/main/java/com/android/tools/r8/kotlin/Kotlin.java
@@ -13,7 +13,9 @@
 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.DexValueInt;
+import com.android.tools.r8.graph.DexValue.DexValueString;
 import com.android.tools.r8.kotlin.KotlinSyntheticClass.Flavour;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.google.common.collect.Sets;
@@ -118,7 +120,7 @@
       case 2:
         return new KotlinFile();
       case 3:
-        return createSyntheticClass(clazz);
+        return createSyntheticClass(clazz, meta);
       case 4:
         return new KotlinClassFacade();
       case 5:
@@ -128,10 +130,14 @@
     }
   }
 
-  private KotlinSyntheticClass createSyntheticClass(DexClass clazz) {
-    KotlinSyntheticClass.Flavour flavour =
-        isKotlinStyleLambda(clazz) ? Flavour.KotlinStyleLambda : Flavour.Unclassified;
-    return new KotlinSyntheticClass(flavour);
+  private KotlinSyntheticClass createSyntheticClass(DexClass clazz, DexAnnotation meta) {
+    if (isKotlinStyleLambda(clazz)) {
+      return new KotlinSyntheticClass(Flavour.KotlinStyleLambda);
+    }
+    if (isJavaStyleLambda(clazz, meta)) {
+      return new KotlinSyntheticClass(Flavour.JavaStyleLambda);
+    }
+    return new KotlinSyntheticClass(Flavour.Unclassified);
   }
 
   private boolean isKotlinStyleLambda(DexClass clazz) {
@@ -139,6 +145,13 @@
     return clazz.superType == this.functional.lambdaType;
   }
 
+  private boolean isJavaStyleLambda(DexClass clazz, DexAnnotation meta) {
+    assert !isKotlinStyleLambda(clazz);
+    return clazz.superType == this.factory.objectType &&
+        clazz.interfaces.size() == 1 &&
+        isAnnotationElementNotEmpty(meta, metadata.elementNameD1);
+  }
+
   private DexAnnotationElement getAnnotationElement(DexAnnotation annotation, DexString name) {
     for (DexAnnotationElement element : annotation.annotation.elements) {
       if (element.name == name) {
@@ -148,6 +161,20 @@
     return null;
   }
 
+  private boolean isAnnotationElementNotEmpty(DexAnnotation annotation, DexString name) {
+    for (DexAnnotationElement element : annotation.annotation.elements) {
+      if (element.name == name && element.value instanceof DexValueArray) {
+        DexValue[] values = ((DexValueArray) element.value).getValues();
+        if (values.length == 1 && values[0] instanceof DexValueString) {
+          return true;
+        }
+        // Must be broken metadata.
+        assert false;
+      }
+    }
+    return false;
+  }
+
   private static class MetadataError extends RuntimeException {
     MetadataError(String cause) {
       super(cause);
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinSyntheticClass.java b/src/main/java/com/android/tools/r8/kotlin/KotlinSyntheticClass.java
index b0e37b4..68721bb 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinSyntheticClass.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinSyntheticClass.java
@@ -7,19 +7,28 @@
 public final class KotlinSyntheticClass extends KotlinInfo {
   public enum Flavour {
     KotlinStyleLambda,
+    JavaStyleLambda,
     Unclassified
   }
 
-  public final Flavour flavour;
+  private final Flavour flavour;
 
   KotlinSyntheticClass(Flavour flavour) {
     this.flavour = flavour;
   }
 
+  public boolean isLambda() {
+    return flavour == Flavour.KotlinStyleLambda || flavour == Flavour.JavaStyleLambda;
+  }
+
   public boolean isKotlinStyleLambda() {
     return flavour == Flavour.KotlinStyleLambda;
   }
 
+  public boolean isJavaStyleLambda() {
+    return flavour == Flavour.JavaStyleLambda;
+  }
+
   @Override
   public final Kind getKind() {
     return Kind.Synthetic;
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
index 23a92a0..deab69f 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
@@ -69,11 +69,11 @@
   private int lineOffset = 0;
   private String line;
 
-  private char peek() {
-    return peek(0);
+  private int peekCodePoint() {
+    return lineOffset < line.length() ? line.codePointAt(lineOffset) : '\n';
   }
 
-  private char peek(int distance) {
+  private char peekChar(int distance) {
     return lineOffset + distance < line.length()
         ? line.charAt(lineOffset + distance)
         : '\n';
@@ -83,7 +83,17 @@
     return lineOffset < line.length();
   }
 
-  private char next() {
+  private int nextCodePoint() {
+    try {
+      int cp = line.codePointAt(lineOffset);
+      lineOffset += Character.charCount(cp);
+      return cp;
+    } catch (ArrayIndexOutOfBoundsException e) {
+      throw new ParseException("Unexpected end of line");
+    }
+  }
+
+  private char nextChar() {
     try {
       return line.charAt(lineOffset++);
     } catch (ArrayIndexOutOfBoundsException e) {
@@ -128,8 +138,8 @@
 
   // Helpers for common pattern
   private void skipWhitespace() {
-    while (Character.isWhitespace(peek())) {
-      next();
+    while (Character.isWhitespace(peekCodePoint())) {
+      nextCodePoint();
     }
   }
 
@@ -137,7 +147,7 @@
     if (!hasNext()) {
       throw new ParseException("Expected '" + c + "'", true);
     }
-    if (next() != c) {
+    if (nextChar() != c) {
       throw new ParseException("Expected '" + c + "'");
     }
     return c;
@@ -197,7 +207,7 @@
       // In the last round we're only here to flush the last line read (which may trigger adding a
       // new MemberNaming) and flush activeMemberNaming, so skip parsing.
       if (!lastRound) {
-        if (!Character.isWhitespace(peek())) {
+        if (!Character.isWhitespace(peekCodePoint())) {
           lastRound = true;
           continue;
         }
@@ -212,9 +222,9 @@
           expect(':');
         }
         signature = parseSignature();
-        if (peek() == ':') {
+        if (peekChar(0) == ':') {
           // This is a mapping or inlining definition
-          next();
+          nextChar();
           originalRange = maybeParseRangeOrInt();
           if (originalRange == null) {
             throw new ParseException("No number follows the colon after the method signature.");
@@ -294,22 +304,22 @@
 
   private void skipIdentifier(boolean allowInit) {
     boolean isInit = false;
-    if (allowInit && peek() == '<') {
+    if (allowInit && peekChar(0) == '<') {
       // swallow the leading < character
-      next();
+      nextChar();
       isInit = true;
     }
-    if (!IdentifierUtils.isDexIdentifierStart(peek())) {
+    if (!IdentifierUtils.isDexIdentifierStart(peekCodePoint())) {
       throw new ParseException("Identifier expected");
     }
-    next();
-    while (IdentifierUtils.isDexIdentifierPart(peek())) {
-      next();
+    nextCodePoint();
+    while (IdentifierUtils.isDexIdentifierPart(peekCodePoint())) {
+      nextCodePoint();
     }
     if (isInit) {
       expect('>');
     }
-    if (IdentifierUtils.isDexIdentifierPart(peek())) {
+    if (IdentifierUtils.isDexIdentifierPart(peekCodePoint())) {
       throw new ParseException("End of identifier expected");
     }
   }
@@ -330,8 +340,8 @@
   private String parseMethodName() {
     int startPosition = lineOffset;
     skipIdentifier(true);
-    while (peek() == '.') {
-      next();
+    while (peekChar(0) == '.') {
+      nextChar();
       skipIdentifier(true);
     }
     return substring(startPosition);
@@ -340,13 +350,13 @@
   private String parseType(boolean allowArray) {
     int startPosition = lineOffset;
     skipIdentifier(false);
-    while (peek() == '.') {
-      next();
+    while (peekChar(0) == '.') {
+      nextChar();
       skipIdentifier(false);
     }
     if (allowArray) {
-      while (peek() == '[') {
-        next();
+      while (peekChar(0) == '[') {
+        nextChar();
         expect(']');
       }
     }
@@ -358,15 +368,15 @@
     expect(' ');
     String name = parseMethodName();
     Signature signature;
-    if (peek() == '(') {
-      next();
+    if (peekChar(0) == '(') {
+      nextChar();
       String[] arguments;
-      if (peek() == ')') {
+      if (peekChar(0) == ')') {
         arguments = new String[0];
       } else {
         List<String> items = new LinkedList<>();
         items.add(parseType(true));
-        while (peek() != ')') {
+        while (peekChar(0) != ')') {
           expect(',');
           items.add(parseType(true));
         }
@@ -386,9 +396,9 @@
   }
 
   private boolean acceptArrow() {
-    if (peek() == '-' && peek(1) == '>') {
-      next();
-      next();
+    if (peekChar(0) == '-' && peekChar(1) == '>') {
+      nextChar();
+      nextChar();
       return true;
     }
     return false;
@@ -396,22 +406,26 @@
 
   private boolean acceptString(String s) {
     for (int i = 0; i < s.length(); i++) {
-      if (peek(i) != s.charAt(i)) {
+      if (peekChar(i) != s.charAt(i)) {
         return false;
       }
     }
     for (int i = 0; i < s.length(); i++) {
-      next();
+      nextChar();
     }
     return true;
   }
 
+  private boolean isSimpleDigit(char c) {
+    return '0' <= c && c <= '9';
+  }
+
   private Object maybeParseRangeOrInt() {
-    if (!Character.isDigit(peek())) {
+    if (!isSimpleDigit(peekChar(0))) {
       return null;
     }
     int from = parseNumber();
-    if (peek() != ':') {
+    if (peekChar(0) != ':') {
       return from;
     }
     expect(':');
@@ -421,13 +435,13 @@
 
   private int parseNumber() {
     int result = 0;
-    if (!Character.isDigit(peek())) {
+    if (!isSimpleDigit(peekChar(0))) {
       throw new ParseException("Number expected");
     }
     do {
       result *= 10;
-      result += Character.getNumericValue(next());
-    } while (Character.isDigit(peek()));
+      result += Character.getNumericValue(nextChar());
+    } while (isSimpleDigit(peekChar(0)));
     return result;
   }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index 5cb4ce8..35c8e47 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -1104,14 +1104,15 @@
       return Integer.parseInt(s);
     }
 
-    private final Predicate<Character> CLASS_NAME_PREDICATE =
-        character -> IdentifierUtils.isDexIdentifierPart(character)
-            || character == '.'
-            || character == '*'
-            || character == '?'
-            || character == '%'
-            || character == '['
-            || character == ']';
+    private final Predicate<Integer> CLASS_NAME_PREDICATE =
+        codePoint ->
+            IdentifierUtils.isDexIdentifierPart(codePoint)
+                || codePoint == '.'
+                || codePoint == '*'
+                || codePoint == '?'
+                || codePoint == '%'
+                || codePoint == '['
+                || codePoint == ']';
 
     private String acceptClassName() {
       return acceptString(CLASS_NAME_PREDICATE);
@@ -1123,7 +1124,7 @@
       int start = position;
       int end = position;
       while (!eof(end)) {
-        char current = contents.charAt(end);
+        int current = contents.codePointAt(end);
         if (currentBackreference != null) {
           if (current == '>') {
             try {
@@ -1138,10 +1139,10 @@
                   origin, getPosition()));
             }
             currentBackreference = null;
-          } else if (Character.isDigit(current)
+          } else if (('0' <= current && current <= '9')
               // Only collect integer literal for the backreference.
               || (current == '-' && currentBackreference.length() == 0)) {
-            currentBackreference.append(current);
+            currentBackreference.append((char) current);
           } else if (kind == IdentifierType.CLASS_NAME) {
             throw reporter.fatalError(new StringDiagnostic(
                 "Use of generics not allowed for java type.", origin, getPosition()));
@@ -1150,12 +1151,12 @@
             // collection of the backreference.
             currentBackreference = null;
           }
-          end++;
+          end += Character.charCount(current);
         } else if (CLASS_NAME_PREDICATE.test(current) || current == '>') {
-          end++;
+          end += Character.charCount(current);
         } else if (current == '<') {
           currentBackreference = new StringBuilder();
-          end++;
+          ++end;
         } else {
           break;
         }
@@ -1177,14 +1178,15 @@
       int start = position;
       int end = position;
       while (!eof(end)) {
-        char current = contents.charAt(end);
+        int current = contents.codePointAt(end);
         if (current == '.' && !eof(end + 1) && peekCharAt(end + 1) == '.') {
           // The grammar is ambiguous. End accepting before .. token used in return ranges.
           break;
         }
-        if ((start == end && IdentifierUtils.isDexIdentifierStart(current)) ||
-            ((start < end) && (IdentifierUtils.isDexIdentifierPart(current) || current == '.'))) {
-          end++;
+        if ((start == end && IdentifierUtils.isDexIdentifierStart(current))
+            || ((start < end)
+                && (IdentifierUtils.isDexIdentifierPart(current) || current == '.'))) {
+          end += Character.charCount(current);
         } else {
           break;
         }
@@ -1216,18 +1218,21 @@
     }
 
     private String acceptPattern() {
-      return acceptString(character ->
-          IdentifierUtils.isDexIdentifierPart(character) || character == '!' || character == '*');
+      return acceptString(
+          codePoint ->
+              IdentifierUtils.isDexIdentifierPart(codePoint)
+                  || codePoint == '!'
+                  || codePoint == '*');
     }
 
-    private String acceptString(Predicate<Character> characterAcceptor) {
+    private String acceptString(Predicate<Integer> codepointAcceptor) {
       skipWhitespace();
       int start = position;
       int end = position;
       while (!eof(end)) {
-        char current = contents.charAt(end);
-        if (characterAcceptor.test(current)) {
-          end++;
+        int current = contents.codePointAt(end);
+        if (codepointAcceptor.test(current)) {
+          end += Character.charCount(current);
         } else {
           break;
         }
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceStrings.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceStrings.java
index 019e18c..cf966ee 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceStrings.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceStrings.java
@@ -9,7 +9,6 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.List;
-import joptsimple.internal.Strings;
 
 public class ProguardConfigurationSourceStrings implements ProguardConfigurationSource {
 
@@ -40,7 +39,7 @@
 
   @Override
   public String get() {
-    return Strings.join(config, System.lineSeparator());
+    return String.join(System.lineSeparator(), config);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/utils/IdentifierUtils.java b/src/main/java/com/android/tools/r8/utils/IdentifierUtils.java
index 3097a07..5fb617b 100644
--- a/src/main/java/com/android/tools/r8/utils/IdentifierUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/IdentifierUtils.java
@@ -5,43 +5,27 @@
 package com.android.tools.r8.utils;
 
 public class IdentifierUtils {
-  public static boolean isDexIdentifierStart(char ch) {
+  public static boolean isDexIdentifierStart(int cp) {
     // Dex does not have special restrictions on the first char of an identifier.
-    return isDexIdentifierPart(ch);
+    return isDexIdentifierPart(cp);
   }
 
-  public static boolean isDexIdentifierPart(char ch) {
-    return isSimpleNameChar(ch);
+  public static boolean isDexIdentifierPart(int cp) {
+    return isSimpleNameChar(cp);
   }
 
-  private static boolean isSimpleNameChar(char ch) {
-    if (ch >= 'A' && ch <= 'Z') {
-      return true;
-    }
-    if (ch >= 'a' && ch <= 'z') {
-      return true;
-    }
-    if (ch >= '0' && ch <= '9') {
-      return true;
-    }
-    if (ch == '$' || ch == '-' || ch == '_') {
-      return true;
-    }
-    if (ch >= 0x00a1 && ch <= 0x1fff) {
-      return true;
-    }
-    if (ch >= 0x2010 && ch <= 0x2027) {
-      return true;
-    }
-    if (ch >= 0x2030 && ch <= 0xd7ff) {
-      return true;
-    }
-    if (ch >= 0xe000 && ch <= 0xffef) {
-      return true;
-    }
-    if (ch >= 0x10000 && ch <= 0x10ffff) {
-      return true;
-    }
-    return false;
+  private static boolean isSimpleNameChar(int cp) {
+    // See https://source.android.com/devices/tech/dalvik/dex-format#string-syntax.
+    return ('A' <= cp && cp <= 'Z')
+        || ('a' <= cp && cp <= 'z')
+        || ('0' <= cp && cp <= '9')
+        || cp == '$'
+        || cp == '-'
+        || cp == '_'
+        || (0x00a1 <= cp && cp <= 0x1fff)
+        || (0x2010 <= cp && cp <= 0x2027)
+        || (0x2030 <= cp && cp <= 0xd7ff)
+        || (0xe000 <= cp && cp <= 0xffef)
+        || (0x10000 <= cp && cp <= 0x10ffff);
   }
 }
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 1073cf9..cc8c9ce 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -401,50 +401,54 @@
   }
 
   public boolean canUseInvokePolymorphicOnVarHandle() {
-    return minApiLevel >= AndroidApiLevel.P.getLevel();
+    return hasMinApi(AndroidApiLevel.P);
   }
 
   public boolean canUseInvokePolymorphic() {
-    return minApiLevel >= AndroidApiLevel.O.getLevel();
+    return hasMinApi(AndroidApiLevel.O);
   }
 
   public boolean canUseConstantMethodHandle() {
-    return minApiLevel >= AndroidApiLevel.P.getLevel();
+    return hasMinApi(AndroidApiLevel.P);
+  }
+
+  private boolean hasMinApi(AndroidApiLevel level) {
+    return isGeneratingClassFiles() || minApiLevel >= level.getLevel();
   }
 
   public boolean canUseConstantMethodType() {
-    return minApiLevel >= AndroidApiLevel.P.getLevel();
+    return hasMinApi(AndroidApiLevel.P);
   }
 
   public boolean canUseInvokeCustom() {
-    return minApiLevel >= AndroidApiLevel.O.getLevel();
+    return hasMinApi(AndroidApiLevel.O);
   }
 
   public boolean canUseDefaultAndStaticInterfaceMethods() {
-    return minApiLevel >= AndroidApiLevel.N.getLevel();
+    return hasMinApi(AndroidApiLevel.N);
   }
 
   public boolean canUsePrivateInterfaceMethods() {
-    return minApiLevel >= AndroidApiLevel.N.getLevel();
+    return hasMinApi(AndroidApiLevel.N);
   }
 
   public boolean canUseMultidex() {
-    return intermediate || minApiLevel >= AndroidApiLevel.L.getLevel();
+    return intermediate || hasMinApi(AndroidApiLevel.L);
   }
 
   public boolean canUseLongCompareAndObjectsNonNull() {
-    return minApiLevel >= AndroidApiLevel.K.getLevel();
+    return hasMinApi(AndroidApiLevel.K);
   }
 
   public boolean canUseSuppressedExceptions() {
-    return minApiLevel >= AndroidApiLevel.K.getLevel();
+    return hasMinApi(AndroidApiLevel.K);
   }
 
   // APIs for accessing parameter names annotations are not available before Android O, thus does
   // not emit them to avoid wasting space in Dex files because runtimes before Android O will ignore
   // them.
   public boolean canUseParameterNameAnnotations() {
-    return minApiLevel >= AndroidApiLevel.O.getLevel();
+    return hasMinApi(AndroidApiLevel.O);
   }
 
   // Dalvik x86-atom backend had a bug that made it crash on filled-new-array instructions for
@@ -456,7 +460,7 @@
   //
   // https://android.googlesource.com/platform/dalvik/+/ics-mr0/vm/mterp/out/InterpAsm-x86-atom.S#25106
   public boolean canUseFilledNewArrayOfObjects() {
-    return minApiLevel >= AndroidApiLevel.K.getLevel();
+    return hasMinApi(AndroidApiLevel.K);
   }
 
   // Art had a bug (b/68761724) for Android N and O in the arm32 interpreter
@@ -492,7 +496,7 @@
   // we can only use not instructions if we are targeting Art-based
   // phones.
   public boolean canUseNotInstruction() {
-    return minApiLevel >= AndroidApiLevel.L.getLevel();
+    return hasMinApi(AndroidApiLevel.L);
   }
 
   // Art before M has a verifier bug where the type of the contents of the receiver register is
diff --git a/src/test/java/com/android/tools/r8/R8CFRunExamplesJava9Test.java b/src/test/java/com/android/tools/r8/R8CFRunExamplesJava9Test.java
new file mode 100644
index 0000000..f4d3da5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/R8CFRunExamplesJava9Test.java
@@ -0,0 +1,127 @@
+// 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;
+
+import static com.android.tools.r8.utils.FileUtils.ZIP_EXTENSION;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.DexInspector;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.UnaryOperator;
+
+public class R8CFRunExamplesJava9Test extends RunExamplesJava9Test<R8Command.Builder> {
+
+  class R8CFTestRunner extends TestRunner<R8CFTestRunner> {
+
+    R8CFTestRunner(String testName, String packageName, String mainClass) {
+      super(testName, packageName, mainClass);
+    }
+
+    @Override
+    R8CFTestRunner withMinApiLevel(int minApiLevel) {
+      return self();
+    }
+
+    @Override
+    void build(Path inputFile, Path out) throws Throwable {
+      R8Command.Builder builder = R8Command.builder();
+      for (UnaryOperator<R8Command.Builder> transformation : builderTransformations) {
+        builder = transformation.apply(builder);
+      }
+      builder.addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P));
+      R8Command command =
+          builder.addProgramFiles(inputFile).setOutput(out, OutputMode.ClassFile).build();
+      ToolHelper.runR8(command, this::combinedOptionConsumer);
+    }
+
+    @Override
+    void run() throws Throwable {
+      boolean expectedToThrow = minSdkErrorExpectedCf(testName);
+      if (expectedToThrow) {
+        thrown.expect(ApiLevelException.class);
+      }
+
+      String qualifiedMainClass = packageName + "." + mainClass;
+      Path inputFile = getInputJar();
+      Path out = temp.getRoot().toPath().resolve(testName + ZIP_EXTENSION);
+
+      build(inputFile, out);
+
+      if (!ToolHelper.isJava9Runtime()) {
+        System.out.println("No Java 9 support; skip execution tests");
+        return;
+      }
+
+      if (!dexInspectorChecks.isEmpty()) {
+        DexInspector inspector = new DexInspector(out);
+        for (Consumer<DexInspector> check : dexInspectorChecks) {
+          check.accept(inspector);
+        }
+      }
+
+      execute(testName, qualifiedMainClass, new Path[] {inputFile}, new Path[] {out});
+
+      if (expectedToThrow) {
+        System.out.println("Did not throw ApiLevelException as expected");
+      }
+    }
+
+    @Override
+    R8CFTestRunner self() {
+      return this;
+    }
+  }
+
+  @Override
+  R8CFTestRunner test(String testName, String packageName, String mainClass) {
+    return new R8CFTestRunner(testName, packageName, mainClass);
+  }
+
+  void execute(String testName, String qualifiedMainClass, Path[] inputJars, Path[] outputJars)
+      throws IOException {
+    boolean expectedToFail = expectedToFailCf(testName);
+    if (expectedToFail) {
+      thrown.expect(Throwable.class);
+    }
+    ProcessResult outputResult = ToolHelper.runJava(Arrays.asList(outputJars), qualifiedMainClass);
+    ToolHelper.ProcessResult inputResult =
+        ToolHelper.runJava(ImmutableList.copyOf(inputJars), qualifiedMainClass);
+    assertEquals(inputResult.toString(), outputResult.toString());
+    if (inputResult.exitCode != 0) {
+      System.out.println(inputResult);
+    }
+    assertEquals(0, inputResult.exitCode);
+    if (expectedToFail) {
+      System.out.println("Did not fail as expected");
+    }
+  }
+
+  private static List<String> expectedFailures =
+      ImmutableList.of(
+          "native-private-interface-methods",
+          "desugared-private-interface-methods"
+      );
+
+  private boolean expectedToFailCf(String testName) {
+    System.out.println(testName + " " + expectedFailures.contains(testName));
+    return expectedFailures.contains(testName);
+  }
+
+  private static List<String> minSdkErrorExpected =
+      ImmutableList.of(
+      );
+
+  private boolean minSdkErrorExpectedCf(String testName) {
+    System.out.println(testName);
+    return minSdkErrorExpected.contains(testName);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
index ebb8100..4d7240e 100644
--- a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
@@ -1488,7 +1488,6 @@
           AndroidApiLevel minSdkVersion = needMinSdkVersion.get(name);
           if (minSdkVersion != null) {
             builder.setMinApiLevel(minSdkVersion.getLevel());
-            r8builder.setMinApiLevel(minSdkVersion.getLevel());
             builder.addLibraryFiles(ToolHelper.getAndroidJar(minSdkVersion));
             r8builder.addLibraryFiles(ToolHelper.getAndroidJar(minSdkVersion));
           } else {
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index d30b2e2..024be59 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -28,6 +28,7 @@
 import com.android.tools.r8.utils.PreloadedClassFileProvider;
 import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.io.ByteStreams;
 import java.io.File;
@@ -47,7 +48,6 @@
 import java.util.stream.Stream;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
-import joptsimple.internal.Strings;
 import org.junit.Rule;
 import org.junit.rules.TemporaryFolder;
 
@@ -364,12 +364,13 @@
    */
   public static String keepMainProguardConfiguration(Class clazz, List<String> additionalLines) {
     String modifier = (clazz.getModifiers() & Modifier.PUBLIC) == Modifier.PUBLIC ? "public " : "";
-    return Strings.join(ImmutableList.of(
-        "-keep " + modifier + "class " + getJavacGeneratedClassName(clazz) + " {",
-        "  public static void main(java.lang.String[]);",
-        "}",
-        "-printmapping"
-    ), "\n") + (additionalLines.size() > 0 ? ("\n" + Strings.join(additionalLines, "\n")) : "");
+    return String.join(System.lineSeparator(),
+        Iterables.concat(ImmutableList.of(
+            "-keep " + modifier + "class " + getJavacGeneratedClassName(clazz) + " {",
+            "  public static void main(java.lang.String[]);",
+            "}",
+            "-printmapping"),
+            additionalLines));
   }
 
   /**
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index f63f4f2..6ffe54d 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -60,7 +60,6 @@
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.stream.Collectors;
-import joptsimple.internal.Strings;
 import org.junit.Assume;
 import org.junit.rules.TemporaryFolder;
 
@@ -90,8 +89,8 @@
   private static final String ANDROID_JAR_PATTERN = "third_party/android_jar/lib-v%d/android.jar";
   private static final AndroidApiLevel DEFAULT_MIN_SDK = AndroidApiLevel.I;
 
-  private static final String PROGUARD5_2_1 = "third_party/proguard/proguard5.2.1/bin/proguard.sh";
-  private static final String PROGUARD6_0_1 = "third_party/proguard/proguard6.0.1/bin/proguard.sh";
+  private static final String PROGUARD5_2_1 = "third_party/proguard/proguard5.2.1/bin/proguard";
+  private static final String PROGUARD6_0_1 = "third_party/proguard/proguard6.0.1/bin/proguard";
   private static final String PROGUARD = PROGUARD5_2_1;
 
   public enum DexVm {
@@ -267,7 +266,7 @@
       }
       if (!classpaths.isEmpty()) {
         result.add("-cp");
-        result.add(Strings.join(classpaths, ":"));
+        result.add(String.join(":", classpaths));
       }
       if (!bootClassPaths.isEmpty()) {
         result.add("-Xbootclasspath:" + String.join(":", bootClassPaths));
@@ -475,6 +474,20 @@
     }
   }
 
+  private static String getProguardScript() {
+    if (isWindows()) {
+      return PROGUARD + ".bat";
+    }
+    return PROGUARD + ".sh";
+  }
+
+  private static String getProguard6Script() {
+    if (isWindows()) {
+      return PROGUARD6_0_1 + ".bat";
+    }
+    return PROGUARD6_0_1 + ".sh";
+  }
+
   private static Path getDxExecutablePath() {
     String toolsDir = toolsDir();
     String executableName = toolsDir.equals("windows") ? "dx.bat" : "dx";
@@ -1164,7 +1177,7 @@
 
   public static ProcessResult runProguardRaw(Path inJar, Path outJar, Path config, Path map)
       throws IOException {
-    return runProguardRaw(PROGUARD, inJar, outJar, ImmutableList.of(config), map);
+    return runProguardRaw(getProguardScript(), inJar, outJar, ImmutableList.of(config), map);
   }
 
   public static String runProguard(Path inJar, Path outJar, Path config, Path map)
@@ -1174,12 +1187,12 @@
 
   public static String runProguard(Path inJar, Path outJar, List<Path> config, Path map)
       throws IOException {
-    return runProguard(PROGUARD, inJar, outJar, config, map);
+    return runProguard(getProguardScript(), inJar, outJar, config, map);
   }
 
   public static ProcessResult runProguard6Raw(Path inJar, Path outJar, Path config, Path map)
       throws IOException {
-    return runProguardRaw(PROGUARD6_0_1, inJar, outJar, ImmutableList.of(config), map);
+    return runProguardRaw(getProguard6Script(), inJar, outJar, ImmutableList.of(config), map);
   }
 
   public static String runProguard6(Path inJar, Path outJar, Path config, Path map)
@@ -1189,7 +1202,7 @@
 
   public static String runProguard6(Path inJar, Path outJar, List<Path> configs, Path map)
       throws IOException {
-    return runProguard(PROGUARD6_0_1, inJar, outJar, configs, map);
+    return runProguard(getProguard6Script(), inJar, outJar, configs, map);
   }
 
   public static class ProcessResult {
diff --git a/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java b/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
index ba25396..d92ff90 100644
--- a/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
@@ -85,10 +85,12 @@
     AndroidApiLevel apiLevel = AndroidApiLevel.P;
     Builder cfBuilder =
         R8Command.builder()
-            .setMinApiLevel(apiLevel.getLevel())
             .setMode(CompilationMode.DEBUG)
             .addLibraryFiles(ToolHelper.getAndroidJar(apiLevel))
             .setProgramConsumer(programConsumer);
+    if (!(programConsumer instanceof ClassFileConsumer)) {
+      cfBuilder.setMinApiLevel(apiLevel.getLevel());
+    }
     for (Class<?> c : inputClasses) {
       byte[] classAsBytes = getClassAsBytes(c);
       cfBuilder.addClassProgramData(classAsBytes, Origin.unknown());
diff --git a/src/test/java/com/android/tools/r8/debug/R8CfDebugTestResourcesConfig.java b/src/test/java/com/android/tools/r8/debug/R8CfDebugTestResourcesConfig.java
index 4916e1d..e4a88f0 100644
--- a/src/test/java/com/android/tools/r8/debug/R8CfDebugTestResourcesConfig.java
+++ b/src/test/java/com/android/tools/r8/debug/R8CfDebugTestResourcesConfig.java
@@ -25,7 +25,6 @@
       AndroidAppConsumers sink = new AndroidAppConsumers();
       R8.run(
           R8Command.builder()
-              .setMinApiLevel(minApi.getLevel())
               .setMode(CompilationMode.DEBUG)
               .addProgramFiles(DebugTestBase.DEBUGGEE_JAR)
               .setProgramConsumer(sink.wrapClassFileConsumer(null))
diff --git a/src/test/java/com/android/tools/r8/jasmin/InvalidFieldNames.java b/src/test/java/com/android/tools/r8/jasmin/InvalidFieldNames.java
index 7caac8e..bad64ac 100644
--- a/src/test/java/com/android/tools/r8/jasmin/InvalidFieldNames.java
+++ b/src/test/java/com/android/tools/r8/jasmin/InvalidFieldNames.java
@@ -4,14 +4,25 @@
 package com.android.tools.r8.jasmin;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.smali.SmaliBuilder;
+import com.android.tools.r8.smali.SmaliBuilder.MethodSignature;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.base.Strings;
+import com.google.common.primitives.Bytes;
+import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.zip.Adler32;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -20,64 +31,183 @@
 @RunWith(Parameterized.class)
 public class InvalidFieldNames extends JasminTestBase {
 
-  public boolean runsOnJVM;
-  public String name;
+  private static final String CLASS_NAME = "Test";
+  private static String FIELD_VALUE = "42";
 
-  public InvalidFieldNames(String name, boolean runsOnJVM) {
-    this.name = name;
-    this.runsOnJVM = runsOnJVM;
-  }
-
-  private void runTest(JasminBuilder builder, String main, String expected) throws Exception {
-    if (runsOnJVM) {
-      String javaResult = runOnJava(builder, main);
-      assertEquals(expected, javaResult);
-    }
-    String artResult = null;
-    try {
-      artResult = runOnArtD8(builder, main);
-      fail();
-    } catch (CompilationError t) {
-      assertTrue(t.getMessage().contains(name));
-    }
-    assertNull("Invalid dex field names should be rejected.", artResult);
-  }
-
-  @Parameters
+  @Parameters(name = "\"{0}\", jvm: {1}, art: {2}")
   public static Collection<Object[]> data() {
-    return Arrays.asList(new Object[][]{
-        {"\u00a0", !ToolHelper.isJava9Runtime()},
-        {"\u2000", !ToolHelper.isJava9Runtime()},
-        {"\u200f", !ToolHelper.isJava9Runtime()},
-        {"\u2028", !ToolHelper.isJava9Runtime()},
-        {"\u202f", !ToolHelper.isJava9Runtime()},
-        {"\ud800", !ToolHelper.isJava9Runtime()},
-        {"\udfff", !ToolHelper.isJava9Runtime()},
-        {"\ufff0", !ToolHelper.isJava9Runtime()},
-        {"\uffff", !ToolHelper.isJava9Runtime()},
-        {"a/b", false},
-        {"<a", false},
-        {"a>", !ToolHelper.isJava9Runtime()},
-        {"a<b>", !ToolHelper.isJava9Runtime()},
-        {"<a>b", !ToolHelper.isJava9Runtime()}
-    });
+    return Arrays.asList(
+        new Object[][] {
+          {new TestStringParameter("azAZ09$_"), true, true},
+          {new TestStringParameter("_"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("a-b"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("\u00a0"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("\u00a1"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("\u1fff"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("\u2000"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("\u200f"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("\u2010"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("\u2027"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("\u2028"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("\u202f"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("\u2030"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("\ud7ff"), !ToolHelper.isJava9Runtime(), true},
+
+          // Standalone high and low surrogates.
+          {new TestStringParameter("\ud800"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("\udbff"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("\udc00"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("\udfff"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("\ue000"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("\uffef"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("\ufff0"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("\uffff"), !ToolHelper.isJava9Runtime(), false},
+
+          // Single and double code points above 0x10000.
+          {new TestStringParameter("\ud800\udc00"), true, true},
+          {new TestStringParameter("\ud800\udcfa"), true, true},
+          {new TestStringParameter("\ud800\udcfb"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("\udbff\udfff"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("\ud800\udc00\ud800\udcfa"), true, true},
+          {new TestStringParameter("\ud800\udc00\udbff\udfff"), !ToolHelper.isJava9Runtime(), true},
+          {new TestStringParameter("a/b"), false, false},
+          {new TestStringParameter("<a"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("a>"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("a<b>"), !ToolHelper.isJava9Runtime(), false},
+          {new TestStringParameter("<a>b"), !ToolHelper.isJava9Runtime(), false}
+        });
   }
 
-  @Test
-  public void invalidFieldNames() throws Exception {
-    JasminBuilder builder = new JasminBuilder();
-    JasminBuilder.ClassBuilder clazz = builder.addClass("Test");
+  // TestStringParameter is a String with modified toString() which prints \\uXXXX for
+  // characters outside 0x20..0x7e.
+  static class TestStringParameter {
+    private final String value;
 
-    clazz.addStaticField(name, "I", "42");
+    TestStringParameter(String value) {
+      this.value = value;
+    }
+
+    String getValue() {
+      return value;
+    }
+
+    @Override
+    public String toString() {
+      return StringUtils.toASCIIString(value);
+    }
+  }
+
+  private String name;
+  private boolean validForJVM;
+  private boolean validForArt;
+
+  public InvalidFieldNames(TestStringParameter name, boolean validForJVM, boolean validForArt) {
+    this.name = name.getValue();
+    this.validForJVM = validForJVM;
+    this.validForArt = validForArt;
+  }
+
+  private byte[] trimLastZeroByte(byte[] bytes) {
+    assert bytes.length > 0 && bytes[bytes.length - 1] == 0;
+    byte[] result = new byte[bytes.length - 1];
+    System.arraycopy(bytes, 0, result, 0, result.length);
+    return result;
+  }
+
+  private JasminBuilder createJasminBuilder() {
+    JasminBuilder builder = new JasminBuilder();
+    JasminBuilder.ClassBuilder clazz = builder.addClass(CLASS_NAME);
+
+    clazz.addStaticField(name, "I", FIELD_VALUE);
 
     clazz.addMainMethod(
         ".limit stack 2",
         ".limit locals 1",
         "  getstatic java/lang/System/out Ljava/io/PrintStream;",
-        "  getstatic Test/" + name + " I",
+        "  getstatic " + CLASS_NAME + "/" + name + " I",
         "  invokevirtual java/io/PrintStream.print(I)V",
         "  return");
+    return builder;
+  }
 
-    runTest(builder, clazz.name, "42");
+  private AndroidApp createAppWithSmali() throws Exception {
+
+    SmaliBuilder smaliBuilder = new SmaliBuilder(CLASS_NAME);
+    String originalSourceFile = CLASS_NAME + FileUtils.JAVA_EXTENSION;
+    smaliBuilder.setSourceFile(originalSourceFile);
+
+    // We're using a valid placeholder string which will be replaced by the actual name.
+    byte[] nameMutf8 = trimLastZeroByte(DexString.encodeToMutf8(name));
+
+    String placeholderString = Strings.repeat("A", nameMutf8.length);
+
+    smaliBuilder.addStaticField(placeholderString, "I", FIELD_VALUE);
+    MethodSignature mainSignature =
+        smaliBuilder.addMainMethod(
+            1,
+            "sget-object p0, Ljava/lang/System;->out:Ljava/io/PrintStream;",
+            "sget v0, LTest;->" + placeholderString + ":I",
+            "invoke-virtual {p0, v0}, Ljava/io/PrintStream;->print(I)V",
+            "return-void");
+    byte[] dexCode = smaliBuilder.compile();
+
+    // Replace placeholder by mutf8-encoded name
+    byte[] placeholderBytes = trimLastZeroByte(DexString.encodeToMutf8(placeholderString));
+    assert placeholderBytes.length == nameMutf8.length;
+    int index = Bytes.indexOf(dexCode, placeholderBytes);
+    if (index >= 0) {
+      System.arraycopy(nameMutf8, 0, dexCode, index, nameMutf8.length);
+    }
+    assert Bytes.indexOf(dexCode, placeholderBytes) < 0;
+
+    // Update checksum
+    Adler32 adler = new Adler32();
+    adler.update(dexCode, Constants.SIGNATURE_OFFSET, dexCode.length - Constants.SIGNATURE_OFFSET);
+    int checksum = (int) adler.getValue();
+    for (int i = 0; i < 4; ++i) {
+      dexCode[Constants.CHECKSUM_OFFSET + i] = (byte) (checksum >> (8 * i) & 0xff);
+    }
+
+    return AndroidApp.builder()
+        .addDexProgramData(dexCode, new PathOrigin(Paths.get(originalSourceFile)))
+        .build();
+  }
+
+  @Test
+  public void invalidFieldNames() throws Exception {
+    JasminBuilder jasminBuilder = createJasminBuilder();
+
+    if (validForJVM) {
+      String javaResult = runOnJava(jasminBuilder, CLASS_NAME);
+      assertEquals(FIELD_VALUE, javaResult);
+    } else {
+      try {
+        runOnJava(jasminBuilder, CLASS_NAME);
+        fail("Should have failed on JVM.");
+      } catch (AssertionError e) {
+        // Silent on expected failure.
+      }
+    }
+
+    if (validForArt) {
+      String artResult = runOnArtD8(jasminBuilder, CLASS_NAME);
+      assertEquals(FIELD_VALUE, artResult);
+    } else {
+      // Make sure the compiler fails.
+      try {
+        runOnArtD8(jasminBuilder, CLASS_NAME);
+        fail("D8 should have rejected this case.");
+      } catch (CompilationError t) {
+        assertTrue(t.getMessage().contains(name));
+      }
+
+      // Make sure ART also fail, if D8 rejects it.
+      try {
+        runOnArt(createAppWithSmali(), CLASS_NAME);
+        fail("Art should have failed.");
+      } catch (AssertionError e) {
+        // Silent on expected failure.
+      }
+    }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java b/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
index 1419b95..3e46ff8 100644
--- a/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
+++ b/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
@@ -185,8 +185,6 @@
       AndroidAppInspector inspector) throws Exception {
     Assume.assumeTrue(ToolHelper.artSupported());
 
-    Path jarFile = getJarFile(folder);
-
     String proguardRules = buildProguardRules(mainClass);
     if (extraProguardRules != null) {
       proguardRules += extraProguardRules;
@@ -194,7 +192,8 @@
 
     // Build classpath for compilation (and java execution)
     List<Path> classpath = new ArrayList<>(extraClasspath.size() + 1);
-    classpath.add(jarFile);
+    classpath.add(getKotlinJarFile(folder));
+    classpath.add(getJavaJarFile(folder));
     classpath.addAll(extraClasspath);
 
     // Build with R8
@@ -222,11 +221,16 @@
     inspector.inspectApp(app);
   }
 
-  private Path getJarFile(String folder) {
+  private Path getKotlinJarFile(String folder) {
     return Paths.get(ToolHelper.TESTS_BUILD_DIR, "kotlinR8TestResources",
         targetVersion.getFolderName(), folder + FileUtils.JAR_EXTENSION);
   }
 
+  private Path getJavaJarFile(String folder) {
+    return Paths.get(ToolHelper.TESTS_BUILD_DIR, "kotlinR8TestResources",
+        targetVersion.getFolderName(), folder + ".java" + FileUtils.JAR_EXTENSION);
+  }
+
   @FunctionalInterface
   interface AndroidAppInspector {
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/KStyleLambdaMergingTest.java b/src/test/java/com/android/tools/r8/kotlin/KStyleLambdaMergingTest.java
deleted file mode 100644
index 48358f0..0000000
--- a/src/test/java/com/android/tools/r8/kotlin/KStyleLambdaMergingTest.java
+++ /dev/null
@@ -1,314 +0,0 @@
-// 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/KotlinLambdaMergingTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinLambdaMergingTest.java
new file mode 100644
index 0000000..65dc98f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinLambdaMergingTest.java
@@ -0,0 +1,451 @@
+// 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.assertEquals;
+import static junit.framework.TestCase.assertTrue;
+import static org.junit.Assert.assertFalse;
+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 KotlinLambdaMergingTest extends AbstractR8KotlinTestBase {
+  private static final String KOTLIN_FUNCTION_IFACE = "Lkotlin/jvm/functions/Function";
+  private static final String KOTLIN_FUNCTION_IFACE_STR = "kotlin.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);
+  }
+
+  private static class Group extends LambdaOrGroup {
+    final String pkg;
+    final String capture;
+    final int arity;
+    final String sam;
+    final int singletons;
+
+    private Group(String pkg, String capture, int arity, String sam, int singletons) {
+      this.pkg = pkg;
+      this.capture = fixCapture(capture);
+      this.arity = arity;
+      this.sam = sam;
+      this.singletons = singletons;
+    }
+
+    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 + ", iface: " + sam + ", sing: " + singletons + ")";
+    }
+
+    @Override
+    boolean match(DexClass clazz) {
+      return clazz.type.getPackageDescriptor().equals(pkg) &&
+          getLambdaOrGroupCapture(clazz).equals(capture) &&
+          getLambdaSam(clazz).equals(sam) &&
+          getLambdaSingletons(clazz) == singletons &&
+          getLambdaOrGroupArity(clazz) == arity;
+    }
+  }
+
+  private Group kstyleImpl(String pkg, String capture, int arity, int singletons) {
+    assertEquals(capture.isEmpty(), singletons != 0);
+    return new Group(pkg, capture, arity, KOTLIN_FUNCTION_IFACE_STR + arity, singletons);
+  }
+
+  private Group kstyle(String pkg, int arity, int singletons) {
+    assertTrue(singletons != 0);
+    return kstyleImpl(pkg, "", arity, singletons);
+  }
+
+  private Group kstyle(String pkg, String capture, int arity) {
+    assertFalse(capture.isEmpty());
+    return kstyleImpl(pkg, capture, arity, 0);
+  }
+
+  private Group jstyleImpl(String pkg, String capture, int arity, String sam, int singletons) {
+    assertTrue(capture.isEmpty() || singletons == 0);
+    return new Group(pkg, capture, arity, sam, singletons);
+  }
+
+  private Group jstyle(String pkg, String capture, int arity, String sam) {
+    return jstyleImpl(pkg, capture, arity, sam, 0);
+  }
+
+  private Group jstyle(String pkg, int arity, String sam, int singletons) {
+    return jstyleImpl(pkg, "", arity, sam, singletons);
+  }
+
+  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) &&
+          getLambdaOrGroupArity(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 (isLambdaOrGroup(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)) {
+            // Validate static initializer.
+            if (check instanceof Group) {
+              assertEquals(clazz.directMethods().length, ((Group) check).singletons == 0 ? 1 : 2);
+            }
+
+            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: " + getLambdaOrGroupArity(dexClass) +
+            ", capture: " + getLambdaOrGroupCapture(dexClass) +
+            ", sam: " + getLambdaSam(dexClass) +
+            ", sing: " + getLambdaSingletons(dexClass));
+        notFound++;
+      }
+
+      assertTrue(what + "s match failed", 0 == notFound && 0 == list.size());
+    }
+  }
+
+  private static int getLambdaOrGroupArity(DexClass clazz) {
+    if (isKStyleLambdaOrGroup(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));
+        }
+      }
+
+    } else {
+      assertTrue(isJStyleLambdaOrGroup(clazz));
+      // Taking the number of any virtual method parameters seems to be good enough.
+      assertTrue(clazz.virtualMethods().length > 0);
+      return clazz.virtualMethods()[0].method.proto.parameters.size();
+    }
+    fail("Failed to get arity for " + clazz.type.descriptor.toString());
+    throw new AssertionError();
+  }
+
+  private static String getLambdaSam(DexClass clazz) {
+    assertEquals(1, clazz.interfaces.size());
+    return clazz.interfaces.values[0].toSourceString();
+  }
+
+  private static int getLambdaSingletons(DexClass clazz) {
+    assertEquals(1, clazz.interfaces.size());
+    return clazz.staticFields().length;
+  }
+
+  private static boolean isLambdaOrGroup(DexClass clazz) {
+    return !clazz.type.getPackageDescriptor().startsWith("kotlin") &&
+        (isKStyleLambdaOrGroup(clazz) || isJStyleLambdaOrGroup(clazz));
+  }
+
+  private static boolean isKStyleLambdaOrGroup(DexClass clazz) {
+    return clazz.superType.descriptor.toString().equals("Lkotlin/jvm/internal/Lambda;");
+  }
+
+  private static boolean isJStyleLambdaOrGroup(DexClass clazz) {
+    return clazz.superType.descriptor.toString().equals("Ljava/lang/Object;") &&
+        clazz.interfaces.size() == 1;
+  }
+
+  private static boolean isLambdaGroupClass(DexClass clazz) {
+    return clazz.type.getName().startsWith("-$$LambdaGroup$");
+  }
+
+  private static String getLambdaOrGroupCapture(DexClass clazz) {
+    return CaptureSignature.getCaptureSignature(clazz.instanceFields());
+  }
+
+  @Test
+  public void testTrivialKs() 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[]{
+                  kstyle("", 0, 4),
+                  kstyle("", 1, 8),
+                  kstyle("", 2, 2), // -\
+                  kstyle("", 2, 5), // - 3 groups different by main method
+                  kstyle("", 2, 4), // -/
+                  kstyle("", 3, 2),
+                  kstyle("", 22, 2)} :
+              new Group[]{
+                  kstyle(pkg, 0, 2),
+                  kstyle(pkg, 1, 4),
+                  kstyle(pkg, 2, 5), // - 2 groups different by main method
+                  kstyle(pkg, 2, 4), // -/
+                  kstyle(pkg, 3, 2),
+                  kstyle(pkg, 22, 2),
+                  kstyle(pkg + "/inner", 0, 2),
+                  kstyle(pkg + "/inner", 1, 4)}
+      );
+
+      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 testCapturesKs() 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(
+          kstyle(grpPkg, "LLL", 0),
+          kstyle(grpPkg, "ILL", 0),
+          kstyle(grpPkg, "III", 0),
+          kstyle(grpPkg, "BCDFIJLLLLSZ", 0),
+          kstyle(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 testGenericsNoSignatureKs() 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(
+          kstyle(grpPkg, 1, 3), // Group for Any
+          kstyle(grpPkg, "L", 1), // Group for Beta
+          kstyle(grpPkg, "LS", 1), // Group for Gamma
+          kstyle(grpPkg, 1, 2)  // Group for int
+      );
+
+      verifier.assertLambdas(
+          new Lambda(pkg, "MainKt$main$4", 1)
+      );
+    });
+  }
+
+  @Test
+  public void testInnerClassesAndEnclosingMethodsKs() 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(
+          kstyle(grpPkg, 1, 3), // Group for Any
+          kstyle(grpPkg, "L", 1), // Group for Beta   // First
+          kstyle(grpPkg, "L", 1), // Group for Beta   // Second
+          kstyle(grpPkg, "LS", 1), // Group for Gamma // First
+          kstyle(grpPkg, "LS", 1), // Group for Gamma // Second
+          kstyle(grpPkg, 1, 2)  // Group for int
+      );
+
+      verifier.assertLambdas(
+          new Lambda(pkg, "MainKt$main$4", 1)
+      );
+    });
+  }
+
+  @Test
+  public void testGenericsSignatureInnerEnclosingKs() 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(
+          kstyle(grpPkg, 1, 3), // Group for Any
+          kstyle(grpPkg, "L", 1), // Group for Beta in First
+          kstyle(grpPkg, "L", 1), // Group for Beta in Second
+          kstyle(grpPkg, "LS", 1), // Group for Gamma<String> in First
+          kstyle(grpPkg, "LS", 1), // Group for Gamma<Integer> in First
+          kstyle(grpPkg, "LS", 1), // Group for Gamma<String> in Second
+          kstyle(grpPkg, "LS", 1), // Group for Gamma<Integer> in Second
+          kstyle(grpPkg, 1, 2)  // Group for int
+      );
+
+      verifier.assertLambdas(
+          new Lambda(pkg, "MainKt$main$4", 1)
+      );
+    });
+  }
+
+  @Test
+  public void testTrivialJs() throws Exception {
+    final String mainClassName = "lambdas.jstyle.trivial.MainKt";
+    runTest("lambdas_jstyle_trivial", mainClassName, null, (app) -> {
+      Verifier verifier = new Verifier(app);
+      String pkg = "lambdas/jstyle/trivial";
+      String grp = allowAccessModification ? "" : pkg;
+
+      String supplier = "Lambdas$Supplier";
+      String intSupplier = "Lambdas$IntSupplier";
+      String consumer = "Lambdas$Consumer";
+      String intConsumer = "Lambdas$IntConsumer";
+      String multiFunction = "Lambdas$MultiFunction";
+
+      verifier.assertLambdaGroups(
+          jstyle(grp, 0, intSupplier, 2),
+          jstyle(grp, "L", 0, supplier),
+          jstyle(grp, "LL", 0, supplier),
+          jstyle(grp, "LLL", 0, supplier),
+          jstyle(grp, 1, intConsumer, allowAccessModification ? 3 : 2),
+          jstyle(grp, "I", 1, consumer),
+          jstyle(grp, "II", 1, consumer),
+          jstyle(grp, "III", 1, consumer),
+          jstyle(grp, "IIII", 1, consumer),
+          jstyle(grp, 3, multiFunction, 2),
+          jstyle(grp, 3, multiFunction, 2),
+          jstyle(grp, 3, multiFunction, 4),
+          jstyle(grp, 3, multiFunction, 6)
+      );
+
+      verifier.assertLambdas(
+          allowAccessModification ?
+              new Lambda[]{
+                  new Lambda(pkg + "/inner", "InnerKt$testInner1$4", 1),
+                  new Lambda(pkg + "/inner", "InnerKt$testInner1$5", 1)
+              } :
+              new Lambda[]{
+                  new Lambda(pkg + "/inner", "InnerKt$testInner1$1", 1),
+                  new Lambda(pkg + "/inner", "InnerKt$testInner1$2", 1),
+                  new Lambda(pkg + "/inner", "InnerKt$testInner1$3", 1),
+                  new Lambda(pkg + "/inner", "InnerKt$testInner1$4", 1),
+                  new Lambda(pkg + "/inner", "InnerKt$testInner1$5", 1)
+              }
+
+      );
+    });
+  }
+
+  @Test
+  public void testSingleton() throws Exception {
+    final String mainClassName = "lambdas.singleton.MainKt";
+    runTest("lambdas_singleton", mainClassName, null, (app) -> {
+      Verifier verifier = new Verifier(app);
+      String pkg = "lambdas/singleton";
+      String grp = allowAccessModification ? "" : pkg;
+
+      verifier.assertLambdaGroups(
+          kstyle(grp, 1, 1 /* 1 out of 5 lambdas in the group */),
+          jstyle(grp, 2, "java.util.Comparator", 0 /* 0 out of 2 lambdas in the group */)
+      );
+
+      verifier.assertLambdas(/* None */);
+    });
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/b72391662/B72391662.java b/src/test/java/com/android/tools/r8/naming/b72391662/B72391662.java
index b96c17a..c82bbc9 100644
--- a/src/test/java/com/android/tools/r8/naming/b72391662/B72391662.java
+++ b/src/test/java/com/android/tools/r8/naming/b72391662/B72391662.java
@@ -15,7 +15,6 @@
 import com.android.tools.r8.utils.AndroidApp;
 import com.google.common.collect.ImmutableList;
 import java.util.List;
-import joptsimple.internal.Strings;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -41,7 +40,7 @@
     AndroidApp app = readClassesAndAndriodJar(ImmutableList.of(
         mainClass, Interface.class, Super.class, TestClass.class,
         OtherPackageSuper.class, OtherPackageTestClass.class));
-    app = compileWithR8(app, Strings.join(config, System.lineSeparator()));
+    app = compileWithR8(app, String.join(System.lineSeparator(), config));
     assertEquals("123451234567\nABC\n", runOnArt(app, mainClass.getCanonicalName()));
   }
 
diff --git a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ProguardCompatabilityTestBase.java b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ProguardCompatabilityTestBase.java
index 9a31c0d..004c31f 100644
--- a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ProguardCompatabilityTestBase.java
+++ b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ProguardCompatabilityTestBase.java
@@ -17,7 +17,6 @@
 import java.io.File;
 import java.nio.file.Path;
 import java.util.List;
-import joptsimple.internal.Strings;
 
 public class ProguardCompatabilityTestBase extends TestBase {
 
@@ -31,7 +30,7 @@
 
   protected DexInspector runShrinker(
       Shrinker mode, List<Class> programClasses, List<String> proguadConfigs) throws Exception {
-    return runShrinker(mode, programClasses, Strings.join(proguadConfigs, System.lineSeparator()));
+    return runShrinker(mode, programClasses, String.join(System.lineSeparator(), proguadConfigs));
   }
 
   protected DexInspector runShrinker(
diff --git a/src/test/kotlinR8TestResources/lambdas_jstyle_trivial/Lambdas.java b/src/test/kotlinR8TestResources/lambdas_jstyle_trivial/Lambdas.java
new file mode 100644
index 0000000..69e9a78
--- /dev/null
+++ b/src/test/kotlinR8TestResources/lambdas_jstyle_trivial/Lambdas.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.
+
+public class Lambdas {
+  public interface IntConsumer {
+    void put(int value);
+  }
+
+  public interface Consumer<T> {
+    void put(T t);
+  }
+
+  public interface Supplier<T> {
+    T get();
+  }
+
+  public interface IntSupplier {
+    int get();
+  }
+
+  public interface MultiFunction<R, P1, P2, P3> {
+    R get(P1 a, P2 b, P3 c);
+  }
+
+  public synchronized static void acceptStringSupplier(Supplier<String> s) {
+    System.out.println(s.get());
+  }
+
+  public synchronized static <T> void acceptGenericSupplier(Supplier<T> s) {
+    System.out.println(s.get());
+  }
+
+  public synchronized static void acceptIntSupplier(IntSupplier s) {
+    System.out.println(s.get());
+  }
+
+  public synchronized static void acceptStringConsumer(Consumer<String> s, String arg) {
+    s.put(arg);
+  }
+
+  public synchronized static <T> void acceptGenericConsumer(Consumer<T> s, T arg) {
+    s.put(arg);
+  }
+
+  public synchronized static void acceptIntConsumer(IntConsumer s, int arg) {
+    s.put(arg);
+  }
+
+  public synchronized static <R, P1, P2, P3>
+  void acceptMultiFunction(MultiFunction<R, P1, P2, P3> s, P1 a, P2 b, P3 c) {
+    System.out.println(s.get(a, b, c));
+  }
+}
diff --git a/src/test/kotlinR8TestResources/lambdas_jstyle_trivial/inner.kt b/src/test/kotlinR8TestResources/lambdas_jstyle_trivial/inner.kt
new file mode 100644
index 0000000..5da8884
--- /dev/null
+++ b/src/test/kotlinR8TestResources/lambdas_jstyle_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.jstyle.trivial.inner
+
+import Lambdas
+import lambdas.jstyle.trivial.next
+import lambdas.jstyle.trivial.nextInt
+
+fun testInner() {
+    testInner1(nextInt(), nextInt(), nextInt(), nextInt())
+}
+
+private data class InnerLocal<out T>(val id: T)
+
+private fun testInner1(c0: Int, c1: Int, c2: Int, c3: Int) {
+    Lambdas.acceptIntConsumer({ println("{${next()}:$it}") }, 100)
+    Lambdas.acceptStringConsumer({ println("${next()}:{$it}:{$c0}") }, next())
+    Lambdas.acceptGenericConsumer({ println("${next()}:{$it}:{$c0}:{$c1}") }, next())
+    Lambdas.acceptGenericConsumer(
+            { println("${next()}:{$it}:{$c0}:{$c1}:{$c2}") }, InnerLocal(next()))
+    Lambdas.acceptGenericConsumer(
+            { println("${next()}:{$it}:{$c0}:{$c1}:{$c2}:{$c3") }, InnerLocal(InnerLocal(next())))
+}
+
diff --git a/src/test/kotlinR8TestResources/lambdas_jstyle_trivial/main.kt b/src/test/kotlinR8TestResources/lambdas_jstyle_trivial/main.kt
new file mode 100644
index 0000000..39673af
--- /dev/null
+++ b/src/test/kotlinR8TestResources/lambdas_jstyle_trivial/main.kt
@@ -0,0 +1,88 @@
+// 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.jstyle.trivial
+
+import Lambdas
+import lambdas.jstyle.trivial.inner.testInner
+
+private var COUNT = 0
+
+fun nextInt() = COUNT++
+fun next() = "${nextInt()}".padStart(3, '0')
+
+fun main(args: Array<String>) {
+    test()
+    testInner()
+}
+
+private fun test() {
+    test1(nextInt(), nextInt(), nextInt(), nextInt())
+    test2(next(), next(), next())
+    test3a(next(), next(), next())
+    test3a(next(), Local(next()), Local(Local(next())))
+    test3b(next(), next(), next())
+    test3b(next(), Local(next()), Local(next()))
+}
+
+private data class Local<out T>(val id: T)
+
+private fun test1(c0: Int, c1: Int, c2: Int, c3: Int) {
+    Lambdas.acceptIntConsumer({ println("{${next()}:$it}") }, nextInt())
+    Lambdas.acceptIntConsumer({ println("{${next()}:$it}") }, nextInt())
+
+    Lambdas.acceptStringConsumer({ println("${next()}:{$it}:{$c0}") }, next())
+    Lambdas.acceptStringConsumer({ println("${next()}:{$it}:{$c0}") }, next())
+
+    Lambdas.acceptGenericConsumer({ println("${next()}:{$it}:{$c0}:{$c1}") }, next())
+    Lambdas.acceptGenericConsumer({ println("${next()}:{$it}:{$c0}:{$c1}") }, next())
+
+    Lambdas.acceptGenericConsumer({ println("${next()}:{$it}:{$c0}:{$c1}:{$c2}") }, Local(next()))
+    Lambdas.acceptGenericConsumer({ println("${next()}:{$it}:{$c0}:{$c1}:{$c2}") }, Local(next()))
+
+    Lambdas.acceptGenericConsumer(
+            { println("${next()}:{$it}:{$c0}:{$c1}:{$c2}:{$c3}") }, Local(Local(next())))
+    Lambdas.acceptGenericConsumer(
+            { println("${next()}:{$it}:{$c0}:{$c1}:{$c2}:{$c3}") }, Local(Local(next())))
+}
+
+private fun test2(c0: String, c1: String, c2: String) {
+    println(Lambdas.acceptIntSupplier { nextInt() })
+    println(Lambdas.acceptIntSupplier { nextInt() })
+
+    println(Lambdas.acceptStringSupplier { "${next()}:$c0" })
+    println(Lambdas.acceptStringSupplier { "${next()}:$c0" })
+
+    println(Lambdas.acceptGenericSupplier { "${next()}:$c0" })
+    println(Lambdas.acceptGenericSupplier { "${next()}:$c0" })
+
+    println(Lambdas.acceptGenericSupplier { "${Local(next())}:$c0:$c1" })
+    println(Lambdas.acceptGenericSupplier { "${Local(next())}:$c0:$c1" })
+
+    println(Lambdas.acceptGenericSupplier { "${Local(Local(next()))}:$c0:$c1:$c2" })
+    println(Lambdas.acceptGenericSupplier { "${Local(Local(next()))}:$c0:$c1:$c2" })
+}
+
+private fun <P1, P2, P3> test3a(a: P1, b: P2, c: P3) {
+    Lambdas.acceptMultiFunction({ x, y, z -> "$x:$y:$z" }, a, b, c)
+    Lambdas.acceptMultiFunction({ x, y, z -> "$x:$y:$z" }, c, a, b)
+    Lambdas.acceptMultiFunction({ x, y, z -> "$x:$y:$z" }, b, c, a)
+    Lambdas.acceptMultiFunction({ x, y, z -> "$x:$y:$z" }, Local(a), b, c)
+    Lambdas.acceptMultiFunction({ x, y, z -> "$x:$y:$z" }, Local(b), a, c)
+    Lambdas.acceptMultiFunction({ x, y, z -> "$x:$y:$z" }, a, Local(b), c)
+    Lambdas.acceptMultiFunction({ x, y, z -> "$x:$y:$z" }, a, Local(c), b)
+    Lambdas.acceptMultiFunction(
+            { x, y, z -> "$x:$y:$z" }, Local(Local(a)), Local(Local(b)), Local(Local(c)))
+    Lambdas.acceptMultiFunction(
+            { x, y, z -> "$x:$y:$z" }, Local(Local(c)), Local(Local(a)), Local(Local(b)))
+}
+
+private fun <P> test3b(a: P, b: P, c: P) {
+    Lambdas.acceptMultiFunction({ x, y, z -> "$x:$y:$z" }, a, b, c)
+    Lambdas.acceptMultiFunction({ x, y, z -> "$x:$y:$z" }, c, a, b)
+    Lambdas.acceptMultiFunction({ x, y, z -> "$x:$y:$z" }, b, c, a)
+    Lambdas.acceptMultiFunction({ x, y, z -> "$x:$y:$z" }, Local(a), b, c)
+    Lambdas.acceptMultiFunction({ x, y, z -> "$x:$y:$z" }, Local(b), a, c)
+}
+
diff --git a/src/test/kotlinR8TestResources/lambdas_singleton/Lambdas.java b/src/test/kotlinR8TestResources/lambdas_singleton/Lambdas.java
new file mode 100644
index 0000000..4140293
--- /dev/null
+++ b/src/test/kotlinR8TestResources/lambdas_singleton/Lambdas.java
@@ -0,0 +1,13 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+public class Lambdas {
+  public interface Function<R, P1, P2> {
+    R get(P1 a, P2 b);
+  }
+
+  public synchronized static <R, P1, P2> void accept(Function<R, P1, P2> s, P1 a, P2 b) {
+    System.out.println(s.get(a, b));
+  }
+}
diff --git a/src/test/kotlinR8TestResources/lambdas_singleton/main.kt b/src/test/kotlinR8TestResources/lambdas_singleton/main.kt
new file mode 100644
index 0000000..8e34a0c
--- /dev/null
+++ b/src/test/kotlinR8TestResources/lambdas_singleton/main.kt
@@ -0,0 +1,46 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package lambdas.singleton
+
+private var COUNT = 0
+
+fun nextInt() = COUNT++
+fun next() = "${nextInt()}".padStart(3, '0')
+
+fun main(args: Array<String>) {
+    test()
+}
+
+private fun test() {
+    test2(listOf(next(), next(), next(), next(), next(), next(), next(), next(), next(), next()))
+}
+
+private fun Collection<String>.flatten() =
+        this.joinToString(prefix = "(*", postfix = "*)", separator = "*")
+
+private fun Array<String>.flatten() =
+        this.joinToString(prefix = "(*", postfix = "*)", separator = "*")
+
+private fun test2(args: Collection<String>) {
+    println(args.sortedByDescending { it.length }.flatten())
+    println(args.sortedByDescending { -it.length }.flatten())
+    process(::println)
+    process(::println)
+    val lambda: (Array<String>) -> Unit = {}
+}
+
+private inline fun process(crossinline f: (String) -> Unit) {
+    feed2 { f(it.flatten()) }
+    feed3 { f(it.flatten()) }
+}
+
+private fun feed3(f: (Array<String>) -> Unit) {
+    f(arrayOf(next(), next(), next()))
+}
+
+private fun feed2(f: (Array<String>) -> Unit) {
+    f(arrayOf(next(), next()))
+}
+