Fix scoping issue with lambda placed in local anonymous class.

GwtAstBuilder.replaceLambdaWithInnerClassAllocation now tries to
resolve labmda's synthetic arguments from enclosing anonymous class if
one exists and argument was not previously resolved from local scope
of the enclosing method.

E.g.:
{
  int x = 1;
  new Runnable() {
    public void run(){
      Lambda l = () -> x + 1;
    }
  }
}

Change-Id: I28d74e65a0d82b545778fa522b2fc06c004a52a1
Bug-Link: https://github.com/gwtproject/gwt/issues/8991
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java b/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java
index b6ad090..baaed7e 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java
@@ -231,6 +231,7 @@
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 
 /**
@@ -1362,7 +1363,7 @@
       return lambdaMethod;
     }
 
-    private void replaceLambdaWithInnerClassAllocation(LambdaExpression x, SourceInfo info,
+    private void replaceLambdaWithInnerClassAllocation(LambdaExpression x, final SourceInfo info,
         JClassType innerLambdaClass, JConstructor ctor, SyntheticArgumentBinding[] synthArgs) {
       // Finally, we replace the LambdaExpression with
       // new InnerLambdaClass(this, local1, local2, ...);
@@ -1373,23 +1374,47 @@
         allocLambda.addArg(new JThisRef(info, innerLambdaClass.getEnclosingType()));
       }
       for (final SyntheticArgumentBinding sa : synthArgs) {
-        MethodInfo method = methodStack.peek();
+        final MethodInfo method = methodStack.peek();
+        JExpression capturedLocalReference = null;
         // Find the local variable in the current method context that is referred by the inner
         // lambda.
-        LocalVariableBinding argument = FluentIterable.from(method.locals.keySet()).firstMatch(
+        LocalVariableBinding localVariable = FluentIterable.from(method.locals.keySet()).firstMatch(
             new Predicate<LocalVariableBinding>() {
               @Override
               public boolean apply(LocalVariableBinding enclosingLocal) {
-                // Either the inner lambda refers direcly to the enclosing scope variable, or
-                // it is a capture from an enclosing scope, in which case both synthetic arguments
-                // point to the same outer local variable.
-                return enclosingLocal == sa.actualOuterLocalVariable ||
-                    (enclosingLocal instanceof SyntheticArgumentBinding) &&
-                        ((SyntheticArgumentBinding) enclosingLocal)
-                            .actualOuterLocalVariable == sa.actualOuterLocalVariable;
+                // Either the inner lambda refers directly to the enclosing scope variable, or
+                // it is a capture from an enclosing scope, in which case both synthetic
+                // arguments point to the same outer local variable.
+                return enclosingLocal == sa.actualOuterLocalVariable
+                    || (enclosingLocal instanceof SyntheticArgumentBinding)
+                        && ((SyntheticArgumentBinding) enclosingLocal).actualOuterLocalVariable == 
+                          sa.actualOuterLocalVariable;
               }
-            }).get();
-        allocLambda.addArg(makeLocalRef(info, argument, method));
+            }).orNull();
+        if (localVariable != null) {
+          // lambda is capturing a local from the immediate context
+          capturedLocalReference = makeLocalRef(info, localVariable, method);
+        } else {
+          // Local variable not found in current method context. Trying to find corresponding
+          // synthetic field in case if lambda is placed in anonymous/local class
+          // e.g. { int x = 1; new Outer(){ void m (){ Lambda l = () -> x+1;} }; }
+          Entry<SyntheticArgumentBinding, JField> capturedLocalInOuterClass = FluentIterable.from(
+              curClass.syntheticFields.entrySet()).firstMatch(
+                  new Predicate<Entry<SyntheticArgumentBinding, JField>>() {
+                    @Override
+                    public boolean apply(Entry<SyntheticArgumentBinding, JField> entry) {
+                      return entry.getKey().actualOuterLocalVariable == sa.actualOuterLocalVariable;
+                    }
+                  }).orNull();
+          if (capturedLocalInOuterClass != null) {
+            // local from outer scope has already been captured by enclosing class.
+            capturedLocalReference = makeInstanceFieldRef(info, capturedLocalInOuterClass
+                .getValue());
+          }
+        }
+
+        assert capturedLocalReference != null;
+        allocLambda.addArg(capturedLocalReference);
       }
       // put the result on the stack, and pop out synthetic method from the scope
       push(allocLambda);
diff --git a/dev/core/test/com/google/gwt/dev/jjs/impl/Java8AstTest.java b/dev/core/test/com/google/gwt/dev/jjs/impl/Java8AstTest.java
index 0a2cfde..584fab5 100644
--- a/dev/core/test/com/google/gwt/dev/jjs/impl/Java8AstTest.java
+++ b/dev/core/test/com/google/gwt/dev/jjs/impl/Java8AstTest.java
@@ -423,6 +423,122 @@
         formatSource(lambdaMethod2.toSource()));
   }
 
+  public void testLambdaNestingInAnonymousCaptureLocal() throws Exception {
+    String lambda =
+        "int x = 42;\n" +
+        "new Runnable() { public void run() { Lambda<Integer> l = (a, b) -> x + a + b; l.run(1, 2); } }.run();";
+    assertEqualBlock("int x=42;(new EntryPoint$1(this,x)).run();", lambda);
+    JProgram program = compileSnippet("void", lambda, false);
+    JClassType outerClass = (JClassType) getType(program, "test.EntryPoint$1");
+
+    // check that anonymous class implementation uses synthetic field val$x2 to initialize lambda
+    // synthetic class
+    assertEquals(
+        "public void run(){Lambda l=new EntryPoint$1$lambda$0$Type(this.val$x2);l.run(1,2);}",
+        formatSource(findMethod(outerClass, "run").toSource()));
+  }
+
+  public void testLambdaNestingInMultipleAnonymousCaptureLocal() throws Exception {
+    addSnippetClassDecl("interface I { int foo(Integer i); }");
+    // checks that lambda has access to local variable and arguments when placed in local anonymous
+    // class with multiple nesting
+    String snippet =
+        "int[] x = new int[] {42};\n" +
+        "int result = new I(){\n" +
+        "  public int foo(Integer i1){\n" +
+        "    return new I(){\n" +
+        "      public int foo(Integer i2){\n" +
+        "        return new I(){\n" +
+        "          public int foo(Integer i3){\n" +
+        "            Lambda<Integer> l = (a, b) -> x[0] = x[0] + a + b + i1 + i2 + i3;\n" +
+        "            return l.run(1, 2);\n" +
+        "          }\n" +
+        "        }.foo(3);\n" +
+        "      }\n" +
+        "    }.foo(2);\n" +
+        "  }\n" +
+        "}.foo(1);\n";
+
+    JProgram program = compileSnippet("void", snippet, false);
+    JClassType outer1 = (JClassType) getType(program, "test.EntryPoint$1");
+    JClassType outer2 = (JClassType) getType(program, "test.EntryPoint$1$1");
+    JClassType outer3 = (JClassType) getType(program, "test.EntryPoint$1$1$1");
+
+    assertNotNull(outer1);
+    assertNotNull(outer2);
+    assertNotNull(outer3);
+
+    JMethod outer1Method = findMethod(outer1, "foo");
+    JMethod outer2Method = findMethod(outer2, "foo");
+    JMethod outer3Method = findMethod(outer3, "foo");
+    assertNotNull(outer3Method);
+    assertEquals(
+        "public int foo(Integer i1){"
+        + "return(new EntryPoint$1$1(this,this.val$x2,i1)).foo(Integer.valueOf(2));"
+        + "}",
+        formatSource(outer1Method.toSource()));
+    assertEquals(
+        "public int foo(Integer i2){"
+        + "return(new EntryPoint$1$1$1(this,this.val$x2,this.val$i13,i2)).foo(Integer.valueOf(3));"
+        + "}",
+        formatSource(outer2Method.toSource()));
+    // checks that lambda scope initialized similar to anonymous class
+    assertEquals(
+        "public int foo(Integer i3){"
+        + "Lambda l=new EntryPoint$1$1$1$lambda$0$Type(this.val$x2,this.val$i13,this.val$i24,i3);"
+        + "return((Integer)l.run(1,2)).intValue();"
+        + "}",
+        formatSource(outer3Method.toSource()));
+  }
+
+  public void testLambdaNestingInMultipleMixedAnonymousCaptureLocal() throws Exception {
+    // checks that lambda has access to local variable and arguments when placed in mixed scopes
+    // Local Class -> Local Class -> Local Anonymous -> lambda -> Local Anonymous
+    addSnippetClassDecl("interface I { int foo(Integer i); }");
+    addSnippetClassDecl("class A {\n" +
+      "int a() {\n" +
+      "  int[] x = new int[] {42};\n" +
+      "  class B {\n" +
+      "    void b() {\n" +
+      "      I i = new I(){\n" +
+      "        public int foo(Integer arg){\n" +
+      "          Runnable r = () ->{\n" +
+      "            new Runnable() {\n" +
+      "              public void run() {\n" +
+      "                Lambda<Integer> l = (a, b) -> x[0] = x[0] + a + b + arg;\n" +
+      "                x[0] = l.run(1, 2);\n" +
+      "              }\n" +
+      "            }.run();\n" +
+      "          };\n" +
+      "          r.run();\n" +
+      "          return x[0];\n" +
+      "        }\n" +
+      "      };\n" +
+      "      i.foo(1);\n" +
+      "    }\n" +
+      "  }\n" +
+      "  B b = new B();\n" +
+      "  b.b();\n" +
+      "  return x[0];\n" +
+      "}\n" +
+    "}\n");
+
+    String snippet = "A a = new A();";
+    JProgram program = compileSnippet("void", snippet, false);
+    JClassType lambdaOuter = (JClassType) getType(program, "test.EntryPoint$A$1B$1$1");
+    assertNotNull(lambdaOuter);
+
+    JMethod lambdaOuterMethod = findMethod(lambdaOuter, "run");
+    assertNotNull(lambdaOuterMethod);
+    // checks that lambda initialization properly uses synthetic fields from outer class
+    assertEquals(
+        "public void run(){"
+        + "Lambda l=new EntryPoint$A$1B$1$1$lambda$0$Type(this.val$x2,this.val$arg3);"
+        + "this.val$x2[0]=((Integer)l.run(1,2)).intValue();"
+        + "}",
+        formatSource(lambdaOuterMethod.toSource()));
+  }
+
   public void testLambdaNestingCaptureField() throws Exception {
     addSnippetClassDecl("interface Inner {\n" +
         "    void f();\n" +
diff --git a/user/test-super/com/google/gwt/dev/jjs/super/com/google/gwt/dev/jjs/test/Java8Test.java b/user/test-super/com/google/gwt/dev/jjs/super/com/google/gwt/dev/jjs/test/Java8Test.java
index e9774f8..5f51cea 100644
--- a/user/test-super/com/google/gwt/dev/jjs/super/com/google/gwt/dev/jjs/test/Java8Test.java
+++ b/user/test-super/com/google/gwt/dev/jjs/super/com/google/gwt/dev/jjs/test/Java8Test.java
@@ -804,6 +804,190 @@
     assertEquals(10, success[0]);
   }
 
+  public void testLambdaNestingInAnonymousCaptureLocal() {
+    int[] x = new int[] {42};
+    new Runnable() {
+      public void run() {
+        Lambda<Integer> l = (a, b) -> x[0] = x[0] + a + b;
+        l.run(1, 2);
+      }
+    }.run();
+    assertEquals(45, x[0]);
+  }
+
+  public void testLambdaNestingInMultipleMixedAnonymousCaptureLocal() {
+    // checks that lambda has access to local variable and arguments when placed in mixed scopes
+    // Local Class -> Local Class -> Local Anonymous -> lambda -> Local Anonymous
+    class A {
+      int a() {
+        int[] x = new int[] {42};
+        class B {
+          void b() {
+            I i = new I() {
+              public int foo(Integer arg) {
+                Runnable r = () -> {
+                  new Runnable() {
+                    public void run() {
+                      Lambda<Integer> l = (a, b) -> x[0] = x[0] + a + b + arg;
+                      l.run(1, 2);
+                    }
+                  }.run();
+                };
+                r.run();
+                return x[0];
+              }
+            };
+            i.foo(1);
+          }
+        }
+        B b = new B();
+        b.b();
+        return x[0];
+      }
+    }
+    A a = new A();
+    assertEquals(46, a.a());
+  }
+
+  public void testLambdaNestingInMultipleMixedAnonymousCaptureLocal_withInterference() {
+    // checks that lambda has access to NEAREST local variable and arguments when placed in mixed
+    // scopes Local Class -> Local Class -> Local Anonymous -> lambda -> Local Anonymous
+    class A {
+      int a() {
+        int[] x = new int[] {42};
+        class B {
+          int b() {
+            int[] x = new int[] {22};
+            I i = new I() {
+              public int foo(Integer arg) {
+                Runnable r = () -> {
+                  new Runnable() {
+                    public void run() {
+                      Lambda<Integer> l = (a, b) -> x[0] = x[0] + a + b + arg;
+                      l.run(1, 2);
+                    }
+                  }.run();
+                };
+                r.run();
+                return x[0];
+              }
+            };
+            return i.foo(1);
+          }
+        }
+        B b = new B();
+        return b.b();
+      }
+    }
+    A a = new A();
+    assertEquals(26, a.a());
+  }
+
+  public void testLambdaNestingInMultipleMixedAnonymousCaptureLocalAndField() {
+    // checks that lambda has access to local variable, field and arguments when placed in mixed
+    // scopes - Local Class -> Local Class -> Local Anonymous -> lambda -> Local Anonymous
+    class A {
+      int fA = 1;
+
+      int a() {
+        int[] x = new int[] {42};
+        class B {
+          int fB = 2;
+
+          int b() {
+            I i = new I() {
+              int fI = 3;
+
+              public int foo(Integer arg) {
+                Runnable r = () -> {
+                  new Runnable() {
+                    public void run() {
+                      Lambda<Integer> l = (a, b) -> x[0] = x[0] + a + b + arg + fA + fB + fI;
+                      l.run(1, 2);
+                    }
+                  }.run();
+                };
+                r.run();
+                return x[0];
+              }
+            };
+            return i.foo(1);
+          }
+        }
+        B b = new B();
+        return b.b();
+      }
+    }
+    A a = new A();
+    assertEquals(52, a.a());
+  }
+
+  public void testLambdaNestingInMultipleAnonymousCaptureLocal() {
+    // checks that lambda has access to local variable and arguments when placed in local anonymous
+    // class with multile nesting
+    int[] x = new int[] {42};
+    int result = new I() {
+      public int foo(Integer i1) {
+        return new I() {
+          public int foo(Integer i2) {
+            return new I() {
+              public int foo(Integer i3) {
+                Lambda<Integer> l = (a, b) -> x[0] = x[0] + a + b + i1 + i2 + i3;
+                return l.run(1, 2);
+              }
+            }.foo(3);
+          }
+        }.foo(2);
+      }
+    }.foo(1);
+    assertEquals(51, x[0]);
+  }
+
+  static class TestLambda_ClassA {
+    int[] f = new int[] {42};
+
+    class B {
+      void m() {
+        Runnable r = () -> f[0] = f[0] + 1;
+        r.run();
+      }
+    }
+
+    int a() {
+      B b = new B();
+      b.m();
+      return f[0];
+    }
+  }
+
+  public void testLambdaNestingCaptureField_InnerClassCapturingOuterClassVariable() {
+    TestLambda_ClassA a = new TestLambda_ClassA();
+    assertEquals(43, a.a());
+  }
+
+  public void testInnerClassCaptureLocalFromOuterLambda() {
+    int[] x = new int[] {42};
+    Lambda<Integer> l = (a, b) -> {
+      int[] x1 = new int[] {32};
+      Lambda<Integer> r = (rA, rB) -> {
+        int[] x2 = new int[] {22};
+        I i = new I() {
+          public int foo(Integer arg) {
+            x1[0] = x1[0] + 1;
+            x[0] = x[0] + 1;
+            return x2[0] = x2[0] + rA + rB + a + b;
+          }
+        };
+        return i.foo(1);
+      };
+      return r.run(3, 4) + x1[0];
+    };
+
+    // x1[0](32) + 1 + x2[0](22) + rA(3) + rB(4) + a(1) + b(2)
+    assertEquals(65, l.run(1, 2).intValue());
+    assertEquals(43, x[0]);
+  }
+
   static class TestLambda_Class {
     public int[] s = new int[] {0};
     public void call(TestLambda_Outer a) {
diff --git a/user/test/com/google/gwt/dev/jjs/test/Java8Test.java b/user/test/com/google/gwt/dev/jjs/test/Java8Test.java
index 3731e42..e2c8084 100644
--- a/user/test/com/google/gwt/dev/jjs/test/Java8Test.java
+++ b/user/test/com/google/gwt/dev/jjs/test/Java8Test.java
@@ -158,6 +158,27 @@
   public void testLambdaNestingCaptureLocal() {
   }
 
+  public void testLambdaNestingInAnonymousCaptureLocal() {
+  }
+
+  public void testLambdaNestingInMultipleMixedAnonymousCaptureLocal() {
+  }
+
+  public void testLambdaNestingInMultipleMixedAnonymousCaptureLocal_withInterference() {
+  }
+
+  public void testLambdaNestingInMultipleMixedAnonymousCaptureLocalAndField() {
+  }
+
+  public void testLambdaNestingInMultipleAnonymousCaptureLocal() {
+  }
+
+  public void testLambdaNestingCaptureField_InnerClassCapturingOuterClassVariable() {
+  }
+
+  public void testInnerClassCaptureLocalFromOuterLambda() {
+  }
+
   public void testLambdaNestingCaptureField() {
   }