Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@

import static org.eclipse.jdt.internal.compiler.ast.ExpressionContext.INVOCATION_CONTEXT;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
Expand Down Expand Up @@ -985,9 +986,7 @@ private LambdaExpression cachedResolvedCopy(TypeBinding targetType, boolean anyT
return null;

targetType = copy.expectedType; // possibly updated local types
if (this.copiesPerTargetType == null)
this.copiesPerTargetType = new HashMap<>();
this.copiesPerTargetType.put(targetType, copy);
this.copiesPerTargetType.put(targetType, copy); // copy() has linked this lambda to the original's cache
}
if (!requireExceptionAnalysis)
return copy;
Expand Down Expand Up @@ -1116,14 +1115,52 @@ LambdaExpression copy() {
if (copy != null) { // ==> syntax errors == null
if (copy.sourceStart != this.sourceStart || copy.sourceEnd != this.sourceEnd)
return null; // something wrong
copy.original = this;
shareInferenceCaches(copy);
copy.assistNode = this.assistNode;
copy.enclosingScope = this.enclosingScope;
copy.text = this.text; // discard redundant textual copy
}
return copy;
}

private void shareInferenceCaches(LambdaExpression copy) {
// A speculative copy can contain nested lambdas. Link each collected lambda to its
// original lambda and share the per-target inference cache.
// A cache entry is a resolved lambda copy and owns its parameter bindings. Nested
// lambdas may use parameters from an enclosing lambda, so collectLambdas() stops
// below a lambda with parameters. Caches below that point stay local to the
// enclosing copy.
// Both traversals start at the root and visit nested lambdas in source order.
List<LambdaExpression> sourceLambdas = collectLambdas(this);
List<LambdaExpression> copiedLambdas = collectLambdas(copy);
if (sourceLambdas.size() != copiedLambdas.size())
throw new CopyFailureException();
for (int i = 0; i < sourceLambdas.size(); i++) {
LambdaExpression sourceLambda = sourceLambdas.get(i);
LambdaExpression copiedLambda = copiedLambdas.get(i);
if (sourceLambda.sourceStart != copiedLambda.sourceStart || sourceLambda.sourceEnd != copiedLambda.sourceEnd)
throw new CopyFailureException();
LambdaExpression originalLambda = sourceLambda.original;
copiedLambda.original = originalLambda;
if (originalLambda.copiesPerTargetType == null)
originalLambda.copiesPerTargetType = new HashMap<>();
Comment thread
stephan-herrmann marked this conversation as resolved.
sourceLambda.copiesPerTargetType = originalLambda.copiesPerTargetType;
copiedLambda.copiesPerTargetType = originalLambda.copiesPerTargetType;
}
}

private static List<LambdaExpression> collectLambdas(LambdaExpression root) {
List<LambdaExpression> lambdas = new ArrayList<>();
root.traverse(new ASTVisitor() {
@Override
public boolean visit(LambdaExpression lambda, BlockScope skope) {
lambdas.add(lambda);
return lambda.arguments.length == 0; // nested lambdas may use these parameters
}
}, root.enclosingScope);
return lambdas;
}

public void returnsExpression(Expression expression, TypeBinding resultType) {
if (this.original == this) // Not in overload resolution context. result expressions not relevant.
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*******************************************************************************
* Copyright (c) 2026 François Martin and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* François Martin - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.core.tests.compiler.regression;

import junit.framework.Test;

public class NestedLambdaInferenceTest extends AbstractRegressionTest {
private static final int NESTING_DEPTH = 24;

public NestedLambdaInferenceTest(String name) {
super(name);
}

public static Test suite() {
return buildMinimalComplianceTestSuite(NestedLambdaInferenceTest.class, F_1_8);
}

// https://github.com/eclipse-jdt/eclipse.jdt.core/issues/5206
public void testIssue5206GenericRouteChain() {
runNestedLambdaTest("GenericRouteChain", """
<V extends Red> V route(Red marker, Work<V> work) { return null; }
<V extends Blue> V route(Blue marker, Work<V> work) { return null; }
<V extends Green> V route(Green marker, Work<V> work) { return null; }
<V extends Gold> V route(Gold marker, Work<V> work) { return null; }
<V> V route(Object marker, Work<V> work) { return null; }

<V extends Red> V select(Red marker, Work<V> work) { return null; }
<V extends Blue> V select(Blue marker, Work<V> work) { return null; }
<V extends Green> V select(Green marker, Work<V> work) { return null; }
<V extends Gold> V select(Gold marker, Work<V> work) { return null; }
<V> V select(Object marker, Work<V> work) { return null; }
""");
}

// https://github.com/eclipse-jdt/eclipse.jdt.core/issues/5206
public void testIssue5206ConcreteRouteChain() {
runNestedLambdaTest("ConcreteRouteChain", """
String route(Red marker, Work<Red> work) { return ""; }
String route(Blue marker, Work<Blue> work) { return ""; }
String route(Green marker, Work<Green> work) { return ""; }
String route(Gold marker, Work<Gold> work) { return ""; }
String route(Object marker, Work<String> work) { return ""; }

String select(Red marker, Work<Red> work) { return ""; }
String select(Blue marker, Work<Blue> work) { return ""; }
String select(Green marker, Work<Green> work) { return ""; }
String select(Gold marker, Work<Gold> work) { return ""; }
String select(Object marker, Work<String> work) { return ""; }
""");
}

// https://github.com/eclipse-jdt/eclipse.jdt.core/issues/5206
public void testIssue5206InterleavedParameterizedLambdas() {
this.runConformTest(new String[] {
"InterleavedLambdas.java",
"""
public class InterleavedLambdas {
interface Producer<T> {
T produce();
}
interface Mapper<T, R> {
R map(T value);
}

static <T> T produce(Producer<T> producer) {
return producer.produce();
}
static <T, R> R map(T value, Mapper<T, R> mapper) {
return mapper.map(value);
}

public static void main(String[] args) {
String result = produce(() ->
map("left", left ->
produce(() ->
map(7, number -> left + number)))
+ produce(() -> "!"));
System.out.print(result);
}
}
"""
},
"left7!");
}

private void runNestedLambdaTest(String className, String overloads) {
this.runConformTest(new String[] {
className + ".java",
"""
class %s {
interface Work<V> {
V perform();
}
static final Work<String> TERMINAL = null;

void test() {
%s;
}
static class Red { }
static class Blue { }
static class Green { }
static class Gold { }
%s
}
""".formatted(className, createNestedInvocation(), overloads.indent(4).stripTrailing())
});
}

private static String createNestedInvocation() {
String invocation = "route(null, TERMINAL)";
for (int level = 1; level < NESTING_DEPTH; level++) {
String selector = level % 2 == 0 ? "route" : "select";
invocation = selector + "(null, () -> " + invocation + ")";
}
return invocation;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ public static Test suite() {
standardTests.add(NullTypeAnnotationTest.class);
standardTests.add(NegativeLambdaExpressionsTest.class);
standardTests.add(LambdaExpressionsTest.class);
standardTests.add(NestedLambdaInferenceTest.class);
standardTests.add(LambdaRegressionTest.class);
standardTests.add(SerializableLambdaTest.class);
standardTests.add(OverloadResolutionTest8.class);
Expand Down