From 4339672d4668b7c4ba06b891606fc912189d115d Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Wed, 12 Aug 2026 10:39:29 -0700 Subject: [PATCH 01/13] Don't crash on re-entrant type argument inference for implicit lambda parameter. --- .../framework/type/TypeFromMemberVisitor.java | 25 ++++++++++++++ .../DefaultTypeArgumentInference.java | 11 +++++++ .../typeinference8/TypeArgumentInference.java | 21 ++++++++++++ framework/tests/all-systems/Issue7677.java | 33 +++++++++++++++++++ framework/tests/all-systems/Issue7678.java | 31 +++++++++++++++++ 5 files changed, 121 insertions(+) create mode 100644 framework/tests/all-systems/Issue7677.java create mode 100644 framework/tests/all-systems/Issue7678.java diff --git a/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java b/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java index af3ef2b29581..cd0126d7fbe4 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java +++ b/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java @@ -2,9 +2,12 @@ import com.sun.source.tree.AnnotationTree; import com.sun.source.tree.LambdaExpressionTree; +import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; +import com.sun.source.tree.NewClassTree; import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; +import com.sun.source.util.TreePath; import java.util.Collections; import java.util.List; import javax.lang.model.element.AnnotationMirror; @@ -167,6 +170,28 @@ public AnnotatedTypeMirror visitMethod(MethodTree tree, AnnotatedTypeFactory f) if (declaredInTree instanceof LambdaExpressionTree lambdaDecl && TreeUtils.isImplicitlyTypedLambda(declaredInTree)) { int index = lambdaDecl.getParameters().indexOf(f.declarationFromElement(paramElement)); + + TreePath pathToLambda = f.getPath(lambdaDecl); + Tree enclosingTree = + pathToLambda == null || pathToLambda.getParentPath() == null + ? null + : pathToLambda.getParentPath().getLeaf(); + if ((enclosingTree instanceof MethodInvocationTree || enclosingTree instanceof NewClassTree) + && f.getTypeArgumentInference().isCurrentlyInferring(enclosingTree)) { + // Type argument inference for the invocation that this lambda is passed to is + // already running further up the call stack: we were reached while that inference + // was computing the type of an expression in this lambda's body. Calling + // f.getFunctionTypeFromTree(lambdaDecl) below would re-derive the lambda's target + // type by re-invoking applicability/inference for the same invocation from an + // incomplete state, which can produce a different (and incorrect) answer than the + // one the outer, in-progress inference will eventually settle on -- see + // https://github.com/typetools/checker-framework/issues/7678. Fall back to the real, + // already fully-resolved Java type that javac assigned to this parameter instead. + AnnotatedTypeMirror result = f.toAnnotatedType(paramElement.asType(), false); + f.addDefaultAnnotations(result); + return result; + } + AnnotatedExecutableType functionType = f.getFunctionTypeFromTree(lambdaDecl); AnnotatedTypeMirror funcTypeParam = functionType.getParameterTypes().get(index); // During type argument inference, the type of the parameters is assumed to be the diff --git a/framework/src/main/java/org/checkerframework/framework/util/typeinference8/DefaultTypeArgumentInference.java b/framework/src/main/java/org/checkerframework/framework/util/typeinference8/DefaultTypeArgumentInference.java index 2f0a75148080..646a93f5dbab 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/typeinference8/DefaultTypeArgumentInference.java +++ b/framework/src/main/java/org/checkerframework/framework/util/typeinference8/DefaultTypeArgumentInference.java @@ -133,6 +133,17 @@ public InferenceResult inferTypeArgs( } } + @Override + @SuppressWarnings("interning:not.interned") // Checking for exact object. + public boolean isCurrentlyInferring(Tree invocation) { + for (InvocationTypeInference i : java8InferenceStack) { + if (i.getInferenceExpression() == invocation) { + return true; + } + } + return false; + } + /** * Returns the outermost tree required to find the type of {@code tree}. * diff --git a/framework/src/main/java/org/checkerframework/framework/util/typeinference8/TypeArgumentInference.java b/framework/src/main/java/org/checkerframework/framework/util/typeinference8/TypeArgumentInference.java index 410cf0317fbc..e18d76e80484 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/typeinference8/TypeArgumentInference.java +++ b/framework/src/main/java/org/checkerframework/framework/util/typeinference8/TypeArgumentInference.java @@ -1,6 +1,7 @@ package org.checkerframework.framework.util.typeinference8; import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.Tree; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; @@ -43,4 +44,24 @@ InferenceResult inferTypeArgs( AnnotatedTypeFactory typeFactory, ExpressionTree invocation, AnnotatedExecutableType executableType); + + /** + * Returns true if type argument inference for {@code invocation} is currently in progress, that + * is, if this method is called while inference is in the middle of computing the result of a + * previous, not-yet-returned call to {@link #inferTypeArgs} for the same {@code invocation}. + * + *

Code that needs the type of an expression that is only meaningful once {@code invocation}'s + * type arguments are known (for example, the type of an implicitly typed lambda parameter whose + * lambda is an argument to {@code invocation}) must check this method first. Re-deriving that + * type via the normal target-type machinery (which re-invokes method applicability/inference for + * {@code invocation}) while inference for {@code invocation} is already running can re-run parts + * of that inference from an incomplete state and produce a different, incorrect answer than the + * one the outer, in-progress inference will eventually settle on. + * + * @param invocation a method or constructor invocation tree + * @return true if type argument inference for {@code invocation} is currently in progress + */ + default boolean isCurrentlyInferring(Tree invocation) { + return false; + } } diff --git a/framework/tests/all-systems/Issue7677.java b/framework/tests/all-systems/Issue7677.java new file mode 100644 index 000000000000..431ddb9288b4 --- /dev/null +++ b/framework/tests/all-systems/Issue7677.java @@ -0,0 +1,33 @@ +package open.liam; + +// Bug in org.checkerframework.framework.type.TypeFromMemberVisitor.inferLambdaParamAnnotations. +@SuppressWarnings("all") // Checker for classes. +public class Issue7677 { + static class Box { + static Box of(T t) { + return new Box<>(); + } + + Box map(MyFunction f) { + return new Box<>(); + } + } + + interface MyConsumer { + void accept(V v); + } + + interface MyFunction { + B apply(A a); + } + + void use(V v, MyConsumer consumer) {} + + void test(Box box) { + use(box, m -> m.map(e -> Box.of(getBoolean() ? e : 0L))); + } + + Boolean getBoolean() { + return true; + } +} diff --git a/framework/tests/all-systems/Issue7678.java b/framework/tests/all-systems/Issue7678.java new file mode 100644 index 000000000000..6ff5647daaa1 --- /dev/null +++ b/framework/tests/all-systems/Issue7678.java @@ -0,0 +1,31 @@ +import java.util.function.Function; +import java.util.stream.Collector; +import java.util.stream.Stream; + +// Related to lambda parameters. +@SuppressWarnings("all") +public class Issue7678 { + static class MyMap { + static Collector> toMyMap( + Function keyFunction, + Function valueFunction) { + return null; + } + } + + static class BiStream { + static Collector> toBiStream( + Collector valueCollector) { + return null; + } + + BiStream mapValues(Function valueMapper) { + return null; + } + } + + private void test(Stream items) { + Function length = String::length; + items.collect(BiStream.toBiStream(MyMap.toMyMap(length, s -> s))).mapValues(byLen -> byLen); + } +} From d1862ebbee62642049db38715c2f5b4f26fd850c Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Wed, 12 Aug 2026 11:13:20 -0700 Subject: [PATCH 02/13] Broaden re-entrant type-argument-inference guard to the whole inference stack. The lambda-parameter fix only checked whether the lambda's immediate enclosing invocation was itself being re-inferred, which missed cases where the corruption comes from a different invocation already in-progress further up the stack (e.g. a receiver's receiver. Check whether any inference is running at all instead of one specific tree. Co-Authored-By: Claude Sonnet 5 EOF ) --- .../framework/type/TypeFromMemberVisitor.java | 20 +++++++------ .../DefaultTypeArgumentInference.java | 10 ++----- .../typeinference8/TypeArgumentInference.java | 25 ++++++++-------- framework/tests/all-systems/Issue7698.java | 30 +++++++++++++++++++ 4 files changed, 55 insertions(+), 30 deletions(-) create mode 100644 framework/tests/all-systems/Issue7698.java diff --git a/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java b/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java index cd0126d7fbe4..c0661d85ea3c 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java +++ b/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java @@ -177,15 +177,17 @@ public AnnotatedTypeMirror visitMethod(MethodTree tree, AnnotatedTypeFactory f) ? null : pathToLambda.getParentPath().getLeaf(); if ((enclosingTree instanceof MethodInvocationTree || enclosingTree instanceof NewClassTree) - && f.getTypeArgumentInference().isCurrentlyInferring(enclosingTree)) { - // Type argument inference for the invocation that this lambda is passed to is - // already running further up the call stack: we were reached while that inference - // was computing the type of an expression in this lambda's body. Calling - // f.getFunctionTypeFromTree(lambdaDecl) below would re-derive the lambda's target - // type by re-invoking applicability/inference for the same invocation from an - // incomplete state, which can produce a different (and incorrect) answer than the - // one the outer, in-progress inference will eventually settle on -- see - // https://github.com/typetools/checker-framework/issues/7678. Fall back to the real, + && f.getTypeArgumentInference().isAnyInferenceInProgress()) { + // Below, f.getFunctionTypeFromTree(lambdaDecl) re-derives this lambda's target type by + // re-invoking method applicability/inference for the enclosing invocation, which in turn + // needs the type of that invocation's receiver and arguments. If any type argument + // inference is already running further up the call stack -- not necessarily for this + // exact enclosing invocation, but for any invocation that the re-derivation ends up + // revisiting while resolving those receivers/arguments (e.g. a call nested a few levels + // out whose own inference has not returned yet) -- redoing that work here can compute a + // different, incomplete answer than the one the in-progress inference will eventually + // settle on. See https://github.com/typetools/checker-framework/issues/7678 and + // https://github.com/typetools/checker-framework/issues/7698. Fall back to the real, // already fully-resolved Java type that javac assigned to this parameter instead. AnnotatedTypeMirror result = f.toAnnotatedType(paramElement.asType(), false); f.addDefaultAnnotations(result); diff --git a/framework/src/main/java/org/checkerframework/framework/util/typeinference8/DefaultTypeArgumentInference.java b/framework/src/main/java/org/checkerframework/framework/util/typeinference8/DefaultTypeArgumentInference.java index 646a93f5dbab..b7f46e7fc531 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/typeinference8/DefaultTypeArgumentInference.java +++ b/framework/src/main/java/org/checkerframework/framework/util/typeinference8/DefaultTypeArgumentInference.java @@ -134,14 +134,8 @@ public InferenceResult inferTypeArgs( } @Override - @SuppressWarnings("interning:not.interned") // Checking for exact object. - public boolean isCurrentlyInferring(Tree invocation) { - for (InvocationTypeInference i : java8InferenceStack) { - if (i.getInferenceExpression() == invocation) { - return true; - } - } - return false; + public boolean isAnyInferenceInProgress() { + return !java8InferenceStack.isEmpty(); } /** diff --git a/framework/src/main/java/org/checkerframework/framework/util/typeinference8/TypeArgumentInference.java b/framework/src/main/java/org/checkerframework/framework/util/typeinference8/TypeArgumentInference.java index e18d76e80484..1c6c05105cd2 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/typeinference8/TypeArgumentInference.java +++ b/framework/src/main/java/org/checkerframework/framework/util/typeinference8/TypeArgumentInference.java @@ -1,7 +1,6 @@ package org.checkerframework.framework.util.typeinference8; import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.Tree; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; @@ -46,22 +45,22 @@ InferenceResult inferTypeArgs( AnnotatedExecutableType executableType); /** - * Returns true if type argument inference for {@code invocation} is currently in progress, that - * is, if this method is called while inference is in the middle of computing the result of a - * previous, not-yet-returned call to {@link #inferTypeArgs} for the same {@code invocation}. + * Returns true if type argument inference for some method or constructor invocation is currently + * in progress anywhere on the call stack, that is, if this method is called while a previous, + * not-yet-returned call to {@link #inferTypeArgs} is running. * - *

Code that needs the type of an expression that is only meaningful once {@code invocation}'s + *

Code that needs the type of an expression that is only meaningful once some invocation's * type arguments are known (for example, the type of an implicitly typed lambda parameter whose - * lambda is an argument to {@code invocation}) must check this method first. Re-deriving that - * type via the normal target-type machinery (which re-invokes method applicability/inference for - * {@code invocation}) while inference for {@code invocation} is already running can re-run parts - * of that inference from an incomplete state and produce a different, incorrect answer than the - * one the outer, in-progress inference will eventually settle on. + * lambda is an argument to a still-uninferred invocation) must check this method first. + * Re-deriving that type via the normal target-type machinery -- which re-invokes method + * applicability/inference for the invocation, and in turn needs the types of that invocation's + * receiver and arguments -- can revisit an invocation whose own inference is mid-flight further + * up the stack (not necessarily the immediately enclosing one) and compute a different, + * incomplete answer than the one the in-progress inference will eventually settle on. * - * @param invocation a method or constructor invocation tree - * @return true if type argument inference for {@code invocation} is currently in progress + * @return true if type argument inference for some invocation is currently in progress */ - default boolean isCurrentlyInferring(Tree invocation) { + default boolean isAnyInferenceInProgress() { return false; } } diff --git a/framework/tests/all-systems/Issue7698.java b/framework/tests/all-systems/Issue7698.java new file mode 100644 index 000000000000..0063edf092f8 --- /dev/null +++ b/framework/tests/all-systems/Issue7698.java @@ -0,0 +1,30 @@ +// Hard crash +public abstract class Issue7698 { + interface AsyncWork { + Promise apply(Promise>> promise) throws Exception; + } + + interface Box { + T value(); + } + + interface Promise { + Promise then(Function function); + } + + interface MyStream { + MyStream map(Function mapper); + } + + interface Function { + T apply(F f); + } + + abstract Promise run(AsyncWork work); + + abstract MyStream myStream(Iterable iterable); + + Promise> main() { + return run(promise -> promise.then(results -> myStream(results).map(result -> result.value()))); + } +} From bf6572695c146ebd40d15ea5cfc9536f627e8eba Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Wed, 12 Aug 2026 11:43:11 -0700 Subject: [PATCH 03/13] Remove now-dead erased-subtype/asSuper workaround in inferLambdaParamAnnotations. This code compensated for getFunctionTypeFromTree producing a funcTypeParam whose underlying type did not match the lambda parameter's real type while type argument inference was still in progress. The re-entrancy guard added for #7678/#7698 already diverts that in-progress case to a different fallback before this code runs, so by the time it is reached, inference is never running and the underlying types always agree. Verified via the full nullness test suite, all-systems, tainting, and the 2020 regression test (Issue3217.java) that this workaround was originally added for. Co-Authored-By: Claude Sonnet 5 --- .../framework/type/TypeFromMemberVisitor.java | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java b/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java index c0661d85ea3c..636bb76e3f39 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java +++ b/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java @@ -22,7 +22,6 @@ import org.checkerframework.framework.util.AnnotatedTypes; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.TreeUtils; -import org.checkerframework.javacutil.TypesUtils; /** * Converts a field or method tree into an AnnotatedTypeMirror. @@ -119,7 +118,7 @@ public AnnotatedTypeMirror visitVariable(VariableTree variableTree, AnnotatedTyp } } - AnnotatedTypeMirror lambdaParamType = inferLambdaParamAnnotations(f, result, elt); + AnnotatedTypeMirror lambdaParamType = inferLambdaParamAnnotations(f, elt); if (lambdaParamType != null) { return lambdaParamType; } @@ -157,7 +156,7 @@ public AnnotatedTypeMirror visitMethod(MethodTree tree, AnnotatedTypeFactory f) * @return the type of the lambda parameter, or null if paramElement is not a lambda parameter */ private static @Nullable AnnotatedTypeMirror inferLambdaParamAnnotations( - AnnotatedTypeFactory f, AnnotatedTypeMirror lambdaParam, Element paramElement) { + AnnotatedTypeFactory f, Element paramElement) { if (paramElement.getKind() != ElementKind.PARAMETER || f.declarationFromElement(paramElement) == null || f.getPath(f.declarationFromElement(paramElement)) == null @@ -195,18 +194,7 @@ public AnnotatedTypeMirror visitMethod(MethodTree tree, AnnotatedTypeFactory f) } AnnotatedExecutableType functionType = f.getFunctionTypeFromTree(lambdaDecl); - AnnotatedTypeMirror funcTypeParam = functionType.getParameterTypes().get(index); - // During type argument inference, the type of the parameters is assumed to be the - // same as the function parameter. - // (https://docs.oracle.com/javase/specs/jls/se25/html/jls-18.html#jls-18.2.1). So if - // the underlying types are not the same type, then assume the lambda parameter is the - // same as the function type. (Use the erased types because the type arguments are not - // substituted when the annotated type arguments are.) - if (TypesUtils.isErasedSubtype( - funcTypeParam.underlyingType, lambdaParam.underlyingType, f.types)) { - return AnnotatedTypes.asSuper(f, funcTypeParam, lambdaParam); - } - return funcTypeParam; + return functionType.getParameterTypes().get(index); } return null; } From 934192b8b3abbfe5f5b1dc5e162956fe1e3464b1 Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Wed, 12 Aug 2026 12:31:52 -0700 Subject: [PATCH 04/13] Look through parenthesized/conditional/switch wrappers for the re-entrancy guard. inferLambdaParamAnnotations used the lambda's immediate parent tree to decide whether the enclosing invocation might already be mid type-argument inference. Wrapping the lambda in parens, using it as a conditional-expression branch, or as a switch expression's result made the immediate parent PARENTHESIZED/CONDITIONAL_EXPRESSION/CASE instead of the actual MethodInvocationTree or NewClassTree, silently disabling the guard and letting the #7678 crash resurface. Add effectiveEnclosingTree, mirroring the wrapper-skipping done by DefaultTypeArgumentInference#outerInference, and use it in place of the direct parent lookup. Add regression tests for all three wrapper kinds to framework/tests/all-systems/Issue7678.java. Co-Authored-By: Claude Sonnet 5 --- .../framework/type/TypeFromMemberVisitor.java | 44 +++++++++++++++++-- framework/tests/all-systems/Issue7678.java | 33 ++++++++++++++ 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java b/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java index 636bb76e3f39..d8b6f913f764 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java +++ b/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java @@ -21,6 +21,7 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.framework.util.AnnotatedTypes; import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; /** @@ -171,10 +172,7 @@ public AnnotatedTypeMirror visitMethod(MethodTree tree, AnnotatedTypeFactory f) int index = lambdaDecl.getParameters().indexOf(f.declarationFromElement(paramElement)); TreePath pathToLambda = f.getPath(lambdaDecl); - Tree enclosingTree = - pathToLambda == null || pathToLambda.getParentPath() == null - ? null - : pathToLambda.getParentPath().getLeaf(); + Tree enclosingTree = pathToLambda == null ? null : effectiveEnclosingTree(pathToLambda); if ((enclosingTree instanceof MethodInvocationTree || enclosingTree instanceof NewClassTree) && f.getTypeArgumentInference().isAnyInferenceInProgress()) { // Below, f.getFunctionTypeFromTree(lambdaDecl) re-derives this lambda's target type by @@ -198,4 +196,42 @@ public AnnotatedTypeMirror visitMethod(MethodTree tree, AnnotatedTypeFactory f) } return null; } + + /** + * Returns the nearest enclosing tree of the tree at {@code path}, skipping over parenthesized, + * conditional-expression, and switch-expression wrappers -- the same wrapper kinds that {@link + * org.checkerframework.framework.util.typeinference8.DefaultTypeArgumentInference#outerInference} + * sees through when locating the invocation whose inference determines a nested poly expression's + * target type. Without this, a lambda written as {@code foo((x -> x))}, as a branch of a + * conditional argument, or as a switch expression's result would report its immediate wrapper as + * its enclosing tree instead of the enclosing invocation, so the re-entrancy check in the caller + * would never trigger for it. + * + * @param path the path to a tree whose effective enclosing tree is needed + * @return the nearest enclosing tree that is not itself one of the wrapper kinds above, or {@code + * null} if there is none + */ + private static @Nullable Tree effectiveEnclosingTree(TreePath path) { + TreePath parentPath = path.getParentPath(); + while (parentPath != null) { + Tree.Kind kind = parentPath.getLeaf().getKind(); + if (kind == Tree.Kind.PARENTHESIZED || kind == Tree.Kind.CONDITIONAL_EXPRESSION) { + parentPath = parentPath.getParentPath(); + } else if (kind == Tree.Kind.CASE || kind == Tree.Kind.YIELD) { + TreePath switchExpressionPath = + TreePathUtil.pathTillOfKind(parentPath, Tree.Kind.SWITCH_EXPRESSION); + if (switchExpressionPath == null) { + // The case or yield is in a switch statement, not a switch expression, so there is + // no enclosing invocation to find through it. + return null; + } + // Treat the switch expression itself as transparent too, and keep walking outward + // from its parent, mirroring outerInference's fall-through to SWITCH_EXPRESSION. + parentPath = switchExpressionPath.getParentPath(); + } else { + return parentPath.getLeaf(); + } + } + return null; + } } diff --git a/framework/tests/all-systems/Issue7678.java b/framework/tests/all-systems/Issue7678.java index 6ff5647daaa1..4df5d3686c82 100644 --- a/framework/tests/all-systems/Issue7678.java +++ b/framework/tests/all-systems/Issue7678.java @@ -28,4 +28,37 @@ private void test(Stream items) { Function length = String::length; items.collect(BiStream.toBiStream(MyMap.toMyMap(length, s -> s))).mapValues(byLen -> byLen); } + + // Same as test(), but with the lambda parenthesized when passed as an argument. The + // enclosing-tree lookup used to see the immediate PARENTHESIZED wrapper instead of the + // enclosing invocation, so the re-entrancy guard never triggered for this case. + private void testParenthesized(Stream items) { + Function length = String::length; + items.collect(BiStream.toBiStream(MyMap.toMyMap(length, (s -> s)))).mapValues((byLen -> byLen)); + } + + // Same as test(), but with the lambda as a branch of a conditional expression passed as an + // argument. The enclosing-tree lookup used to see the immediate CONDITIONAL_EXPRESSION + // wrapper instead of the enclosing invocation, so the re-entrancy guard never triggered for + // this case. + private void testConditional(Stream items, boolean cond) { + Function length = String::length; + items + .collect(BiStream.toBiStream(MyMap.toMyMap(length, s -> s))) + .mapValues(cond ? byLen -> byLen : byLen2 -> byLen2); + } + + // Same as test(), but with the lambda as the result of a switch expression passed as an + // argument. The enclosing-tree lookup used to see the immediate CASE wrapper instead of the + // enclosing invocation, so the re-entrancy guard never triggered for this case. + private void testSwitch(Stream items, int cond) { + Function length = String::length; + items + .collect(BiStream.toBiStream(MyMap.toMyMap(length, s -> s))) + .mapValues( + switch (cond) { + case 1 -> byLen -> byLen; + default -> byLen2 -> byLen2; + }); + } } From 14fc31677f0ace17fbf9931d29d9f543a52778ff Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Wed, 12 Aug 2026 12:34:36 -0700 Subject: [PATCH 05/13] Fix javadoc. --- .../checkerframework/framework/type/TypeFromMemberVisitor.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java b/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java index d8b6f913f764..360f843dc97b 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java +++ b/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java @@ -154,6 +154,8 @@ public AnnotatedTypeMirror visitMethod(MethodTree tree, AnnotatedTypeFactory f) /** * Returns the type of the lambda parameter, or null if paramElement is not a lambda parameter. * + * @param f the annotated type factory + * @param paramElement that might be a lambda parameter * @return the type of the lambda parameter, or null if paramElement is not a lambda parameter */ private static @Nullable AnnotatedTypeMirror inferLambdaParamAnnotations( From d8266c2774edd994b3acf10c5ca3457581c5ec6a Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Fri, 14 Aug 2026 08:39:32 -0700 Subject: [PATCH 06/13] Resolve a non-capture variable together with the capture variable it equals, not before it. Resolution.resolveSmallestSet handled a minimal resolvable set containing a capture variable by resolving every non-capture variable in the set first (resolveWithoutCapture), then resolving the leftover captures (resolveWithCapture). That splits apart pairs that need to be resolved together: findProperLowerBounds/findProperUpperBounds never look at EQUAL bounds, so a non-capture variable whose only bound is EQUAL to a not-yet-resolved capture variable has no proper bound of its own and gets resolved from its trivial declared bound (e.g. Object) instead of from what it actually equals. This caused framework/tests/all-systems/Issue7678(Smaller).java to crash UnitsChecker: BiStream.toBiStream's captured return-type argument (from MyMap.toMyMap's wildcard-containing Collector> return type resolved to plain Object well before the capture variable it should have come from, silently dropping the MyMap<...> structure. mapValues's parameter type was then Object while the lambda parameter's javac-attributed type was correctly MyMap<...>, and BaseTypeVisitor's lambda-parameter check crashed comparing the two. Only defer a non-capture variable when it is EQUAL to a capture variable specifically. Deferring on EQUAL to any other non-capture variable too (the more obvious-looking fix) breaks framework/tests/all-systems/DiamondMethodRef.java: that file's mutually-EQUAL non-capture variables (from Collectors.toMap and LinkedHashMap::new) are already resolved correctly by resolveWithoutCapture's own iterate-to-a-fixed-point loop, and forcing them through a second, separate resolveWithCapture call synthesizes inconsistent fresh type variables for them. Verified: framework/tests/all-systems (280 files), checker/tests/ tainting (71 files), and checker/tests/nullness (447 files) are byte-identical to baseline except for Issue7678.java and Issue7678Smaller.java, which now pass instead of crashing. Co-Authored-By: Claude Sonnet 5 EOF ) --- .../util/typeinference8/util/Resolution.java | 54 +++++++++++++++++-- .../tests/all-systems/Issue7678Smaller.java | 29 ++++++++++ 2 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 framework/tests/all-systems/Issue7678Smaller.java diff --git a/framework/src/main/java/org/checkerframework/framework/util/typeinference8/util/Resolution.java b/framework/src/main/java/org/checkerframework/framework/util/typeinference8/util/Resolution.java index 9bce7bf408d4..d0dff4a950da 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/typeinference8/util/Resolution.java +++ b/framework/src/main/java/org/checkerframework/framework/util/typeinference8/util/Resolution.java @@ -204,10 +204,34 @@ private BoundSet resolveSmallestSet(Set as, BoundSet boundSet) { checkNoFalse(boundSet, "on entry to resolveSmallestSet for", as); if (boundSet.containsCapture(as)) { - BoundSet resolvedBounds = resolveWithoutCapture(as, boundSet); - boundSet.getInstantiatedVariables().forEach(as::remove); - // Then resolve the capture variables - return resolveWithCapture(as, resolvedBounds, context); + // A non-capture variable in `as` whose only useful bound is an EQUAL bound directly to a + // capture variable that is also in `as` (e.g. H2 = capture#1, where capture#1 is on the + // left-hand side of a G<...>=capture(G<...>) bound) must not be resolved below, in + // resolveWithoutCapture: findProperLowerBounds/findProperUpperBounds never look at EQUAL + // bounds, so it would otherwise be resolved from its trivial declared bound (e.g. Object) + // before the capture variable it actually equals has a value. Defer such variables until + // after the captures in `as` are resolved, then let them re-check their bounds. + // + // Variables whose EQUAL bound mentions another *non-capture* variable in `as` are not + // deferred: resolveWithoutCapture's own lower/upper-bound loop already resolves chains of + // those correctly by iterating to a fixed point, and folding them into the capture-only + // deferral below would split a group that needs to be resolved together (by + // resolveWithCapture, in one call, so its simultaneous substitution of fresh type variables + // sees all of them) into two separate resolveWithCapture calls instead. + Set deferred = new LinkedHashSet<>(); + for (Variable v : as) { + if (!v.isCaptureVariable() && hasUnresolvedEqualBoundToCaptureWithin(v, as)) { + deferred.add(v); + } + } + Set toResolveNow = new LinkedHashSet<>(as); + toResolveNow.removeAll(deferred); + + BoundSet resolvedBounds = resolveWithoutCapture(toResolveNow, boundSet); + toResolveNow.removeAll(boundSet.getInstantiatedVariables()); + // Then resolve the capture variables (and any non-captures that depend on them directly). + deferred.addAll(toResolveNow); + return resolveWithCapture(deferred, resolvedBounds, context); } else { BoundSet copy = new BoundSet(boundSet); // Save the current bounds in case the first attempt at resolution fails. @@ -227,6 +251,28 @@ private BoundSet resolveSmallestSet(Set as, BoundSet boundSet) { } } + /** + * Returns true if {@code v} has an {@code EQUAL} bound that mentions a capture variable in {@code + * as} that does not yet have an instantiation. + * + * @param v a variable + * @param as a set of variables being resolved together + * @return true if {@code v} has an unresolved {@code EQUAL} bound to a capture variable in {@code + * as} + */ + private static boolean hasUnresolvedEqualBoundToCaptureWithin(Variable v, Set as) { + for (AbstractType t : v.getBounds().bounds.get(VariableBounds.BoundKind.EQUAL)) { + for (Variable mentioned : t.getInferenceVariables()) { + if (mentioned.isCaptureVariable() + && as.contains(mentioned) + && !mentioned.getBounds().hasInstantiation()) { + return true; + } + } + } + return false; + } + /** * Apply the instantiated variables to the bounds of the variables in {@code variables}. This may * result in an instantiation being found of a variable in {@code variables}. All instantiated diff --git a/framework/tests/all-systems/Issue7678Smaller.java b/framework/tests/all-systems/Issue7678Smaller.java new file mode 100644 index 000000000000..bcf56e22a532 --- /dev/null +++ b/framework/tests/all-systems/Issue7678Smaller.java @@ -0,0 +1,29 @@ +import java.util.function.Function; +import java.util.stream.Collector; +import java.util.stream.Stream; + +// A smaller version of Issue7678.java. +@SuppressWarnings("all") +public class Issue7678Smaller { + static class MyMap { + static Collector> toMyMap(Function f) { + return null; + } + } + + static class BiStream { + static Collector> toBiStream(Collector valueCollector) { + return null; + } + + V2 mapValues(Function f) { + return null; + } + } + + void test(Stream items, Function stringStringFunction) { + items + .collect(BiStream.toBiStream(MyMap.toMyMap(stringStringFunction))) + .mapValues(byLen -> byLen); + } +} From 9c37d2aaf061ae05d28f20100d118420bf329c8f Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Fri, 14 Aug 2026 08:45:57 -0700 Subject: [PATCH 07/13] Add test cases. --- framework/tests/all-systems/Issue7677.java | 33 +++++++++++ framework/tests/all-systems/Issue7678.java | 64 ++++++++++++++++++++++ framework/tests/all-systems/Issue7698.java | 30 ++++++++++ 3 files changed, 127 insertions(+) create mode 100644 framework/tests/all-systems/Issue7677.java create mode 100644 framework/tests/all-systems/Issue7678.java create mode 100644 framework/tests/all-systems/Issue7698.java diff --git a/framework/tests/all-systems/Issue7677.java b/framework/tests/all-systems/Issue7677.java new file mode 100644 index 000000000000..431ddb9288b4 --- /dev/null +++ b/framework/tests/all-systems/Issue7677.java @@ -0,0 +1,33 @@ +package open.liam; + +// Bug in org.checkerframework.framework.type.TypeFromMemberVisitor.inferLambdaParamAnnotations. +@SuppressWarnings("all") // Checker for classes. +public class Issue7677 { + static class Box { + static Box of(T t) { + return new Box<>(); + } + + Box map(MyFunction f) { + return new Box<>(); + } + } + + interface MyConsumer { + void accept(V v); + } + + interface MyFunction { + B apply(A a); + } + + void use(V v, MyConsumer consumer) {} + + void test(Box box) { + use(box, m -> m.map(e -> Box.of(getBoolean() ? e : 0L))); + } + + Boolean getBoolean() { + return true; + } +} diff --git a/framework/tests/all-systems/Issue7678.java b/framework/tests/all-systems/Issue7678.java new file mode 100644 index 000000000000..4df5d3686c82 --- /dev/null +++ b/framework/tests/all-systems/Issue7678.java @@ -0,0 +1,64 @@ +import java.util.function.Function; +import java.util.stream.Collector; +import java.util.stream.Stream; + +// Related to lambda parameters. +@SuppressWarnings("all") +public class Issue7678 { + static class MyMap { + static Collector> toMyMap( + Function keyFunction, + Function valueFunction) { + return null; + } + } + + static class BiStream { + static Collector> toBiStream( + Collector valueCollector) { + return null; + } + + BiStream mapValues(Function valueMapper) { + return null; + } + } + + private void test(Stream items) { + Function length = String::length; + items.collect(BiStream.toBiStream(MyMap.toMyMap(length, s -> s))).mapValues(byLen -> byLen); + } + + // Same as test(), but with the lambda parenthesized when passed as an argument. The + // enclosing-tree lookup used to see the immediate PARENTHESIZED wrapper instead of the + // enclosing invocation, so the re-entrancy guard never triggered for this case. + private void testParenthesized(Stream items) { + Function length = String::length; + items.collect(BiStream.toBiStream(MyMap.toMyMap(length, (s -> s)))).mapValues((byLen -> byLen)); + } + + // Same as test(), but with the lambda as a branch of a conditional expression passed as an + // argument. The enclosing-tree lookup used to see the immediate CONDITIONAL_EXPRESSION + // wrapper instead of the enclosing invocation, so the re-entrancy guard never triggered for + // this case. + private void testConditional(Stream items, boolean cond) { + Function length = String::length; + items + .collect(BiStream.toBiStream(MyMap.toMyMap(length, s -> s))) + .mapValues(cond ? byLen -> byLen : byLen2 -> byLen2); + } + + // Same as test(), but with the lambda as the result of a switch expression passed as an + // argument. The enclosing-tree lookup used to see the immediate CASE wrapper instead of the + // enclosing invocation, so the re-entrancy guard never triggered for this case. + private void testSwitch(Stream items, int cond) { + Function length = String::length; + items + .collect(BiStream.toBiStream(MyMap.toMyMap(length, s -> s))) + .mapValues( + switch (cond) { + case 1 -> byLen -> byLen; + default -> byLen2 -> byLen2; + }); + } +} diff --git a/framework/tests/all-systems/Issue7698.java b/framework/tests/all-systems/Issue7698.java new file mode 100644 index 000000000000..0063edf092f8 --- /dev/null +++ b/framework/tests/all-systems/Issue7698.java @@ -0,0 +1,30 @@ +// Hard crash +public abstract class Issue7698 { + interface AsyncWork { + Promise apply(Promise>> promise) throws Exception; + } + + interface Box { + T value(); + } + + interface Promise { + Promise then(Function function); + } + + interface MyStream { + MyStream map(Function mapper); + } + + interface Function { + T apply(F f); + } + + abstract Promise run(AsyncWork work); + + abstract MyStream myStream(Iterable iterable); + + Promise> main() { + return run(promise -> promise.then(results -> myStream(results).map(result -> result.value()))); + } +} From ff2c417e3cf861093e610c658f6b815718acc57f Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Fri, 14 Aug 2026 09:01:48 -0700 Subject: [PATCH 08/13] remove test. --- framework/tests/all-systems/Issue7698.java | 30 ---------------------- 1 file changed, 30 deletions(-) delete mode 100644 framework/tests/all-systems/Issue7698.java diff --git a/framework/tests/all-systems/Issue7698.java b/framework/tests/all-systems/Issue7698.java deleted file mode 100644 index 0063edf092f8..000000000000 --- a/framework/tests/all-systems/Issue7698.java +++ /dev/null @@ -1,30 +0,0 @@ -// Hard crash -public abstract class Issue7698 { - interface AsyncWork { - Promise apply(Promise>> promise) throws Exception; - } - - interface Box { - T value(); - } - - interface Promise { - Promise then(Function function); - } - - interface MyStream { - MyStream map(Function mapper); - } - - interface Function { - T apply(F f); - } - - abstract Promise run(AsyncWork work); - - abstract MyStream myStream(Iterable iterable); - - Promise> main() { - return run(promise -> promise.then(results -> myStream(results).map(result -> result.value()))); - } -} From 1a0beef5a9f96ec8cd0782dd06d099da68fe2da8 Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Fri, 14 Aug 2026 09:17:00 -0700 Subject: [PATCH 09/13] Remove failing test case. --- framework/tests/all-systems/Issue7678.java | 64 ---------------------- 1 file changed, 64 deletions(-) delete mode 100644 framework/tests/all-systems/Issue7678.java diff --git a/framework/tests/all-systems/Issue7678.java b/framework/tests/all-systems/Issue7678.java deleted file mode 100644 index 4df5d3686c82..000000000000 --- a/framework/tests/all-systems/Issue7678.java +++ /dev/null @@ -1,64 +0,0 @@ -import java.util.function.Function; -import java.util.stream.Collector; -import java.util.stream.Stream; - -// Related to lambda parameters. -@SuppressWarnings("all") -public class Issue7678 { - static class MyMap { - static Collector> toMyMap( - Function keyFunction, - Function valueFunction) { - return null; - } - } - - static class BiStream { - static Collector> toBiStream( - Collector valueCollector) { - return null; - } - - BiStream mapValues(Function valueMapper) { - return null; - } - } - - private void test(Stream items) { - Function length = String::length; - items.collect(BiStream.toBiStream(MyMap.toMyMap(length, s -> s))).mapValues(byLen -> byLen); - } - - // Same as test(), but with the lambda parenthesized when passed as an argument. The - // enclosing-tree lookup used to see the immediate PARENTHESIZED wrapper instead of the - // enclosing invocation, so the re-entrancy guard never triggered for this case. - private void testParenthesized(Stream items) { - Function length = String::length; - items.collect(BiStream.toBiStream(MyMap.toMyMap(length, (s -> s)))).mapValues((byLen -> byLen)); - } - - // Same as test(), but with the lambda as a branch of a conditional expression passed as an - // argument. The enclosing-tree lookup used to see the immediate CONDITIONAL_EXPRESSION - // wrapper instead of the enclosing invocation, so the re-entrancy guard never triggered for - // this case. - private void testConditional(Stream items, boolean cond) { - Function length = String::length; - items - .collect(BiStream.toBiStream(MyMap.toMyMap(length, s -> s))) - .mapValues(cond ? byLen -> byLen : byLen2 -> byLen2); - } - - // Same as test(), but with the lambda as the result of a switch expression passed as an - // argument. The enclosing-tree lookup used to see the immediate CASE wrapper instead of the - // enclosing invocation, so the re-entrancy guard never triggered for this case. - private void testSwitch(Stream items, int cond) { - Function length = String::length; - items - .collect(BiStream.toBiStream(MyMap.toMyMap(length, s -> s))) - .mapValues( - switch (cond) { - case 1 -> byLen -> byLen; - default -> byLen2 -> byLen2; - }); - } -} From 9d4a71aace7cddeb14628841226e30dfdf730f24 Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Fri, 14 Aug 2026 09:41:14 -0700 Subject: [PATCH 10/13] Unify effectiveEnclosingTree with TreePathUtil.getContextForPolyExpression. The wrapper-skipping logic in effectiveEnclosingTree duplicated TreePathUtil.getContextForPolyExpression, which already skips the same parenthesized/conditional/switch-expression wrappers to find a poly expression's target-type context. Reuse it instead of maintaining a second copy. Co-Authored-By: Claude Sonnet 5 --- .../framework/type/TypeFromMemberVisitor.java | 41 +------------------ 1 file changed, 2 insertions(+), 39 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java b/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java index 360f843dc97b..854863260011 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java +++ b/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java @@ -174,7 +174,8 @@ public AnnotatedTypeMirror visitMethod(MethodTree tree, AnnotatedTypeFactory f) int index = lambdaDecl.getParameters().indexOf(f.declarationFromElement(paramElement)); TreePath pathToLambda = f.getPath(lambdaDecl); - Tree enclosingTree = pathToLambda == null ? null : effectiveEnclosingTree(pathToLambda); + Tree enclosingTree = + pathToLambda == null ? null : TreePathUtil.getContextForPolyExpression(pathToLambda); if ((enclosingTree instanceof MethodInvocationTree || enclosingTree instanceof NewClassTree) && f.getTypeArgumentInference().isAnyInferenceInProgress()) { // Below, f.getFunctionTypeFromTree(lambdaDecl) re-derives this lambda's target type by @@ -198,42 +199,4 @@ public AnnotatedTypeMirror visitMethod(MethodTree tree, AnnotatedTypeFactory f) } return null; } - - /** - * Returns the nearest enclosing tree of the tree at {@code path}, skipping over parenthesized, - * conditional-expression, and switch-expression wrappers -- the same wrapper kinds that {@link - * org.checkerframework.framework.util.typeinference8.DefaultTypeArgumentInference#outerInference} - * sees through when locating the invocation whose inference determines a nested poly expression's - * target type. Without this, a lambda written as {@code foo((x -> x))}, as a branch of a - * conditional argument, or as a switch expression's result would report its immediate wrapper as - * its enclosing tree instead of the enclosing invocation, so the re-entrancy check in the caller - * would never trigger for it. - * - * @param path the path to a tree whose effective enclosing tree is needed - * @return the nearest enclosing tree that is not itself one of the wrapper kinds above, or {@code - * null} if there is none - */ - private static @Nullable Tree effectiveEnclosingTree(TreePath path) { - TreePath parentPath = path.getParentPath(); - while (parentPath != null) { - Tree.Kind kind = parentPath.getLeaf().getKind(); - if (kind == Tree.Kind.PARENTHESIZED || kind == Tree.Kind.CONDITIONAL_EXPRESSION) { - parentPath = parentPath.getParentPath(); - } else if (kind == Tree.Kind.CASE || kind == Tree.Kind.YIELD) { - TreePath switchExpressionPath = - TreePathUtil.pathTillOfKind(parentPath, Tree.Kind.SWITCH_EXPRESSION); - if (switchExpressionPath == null) { - // The case or yield is in a switch statement, not a switch expression, so there is - // no enclosing invocation to find through it. - return null; - } - // Treat the switch expression itself as transparent too, and keep walking outward - // from its parent, mirroring outerInference's fall-through to SWITCH_EXPRESSION. - parentPath = switchExpressionPath.getParentPath(); - } else { - return parentPath.getLeaf(); - } - } - return null; - } } From 9396b1fd1ec876dcd4559bbd4f49321941935a45 Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Fri, 14 Aug 2026 11:48:36 -0700 Subject: [PATCH 11/13] Put test back. --- framework/tests/all-systems/Issue7678.java | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 framework/tests/all-systems/Issue7678.java diff --git a/framework/tests/all-systems/Issue7678.java b/framework/tests/all-systems/Issue7678.java new file mode 100644 index 000000000000..0063edf092f8 --- /dev/null +++ b/framework/tests/all-systems/Issue7678.java @@ -0,0 +1,30 @@ +// Hard crash +public abstract class Issue7698 { + interface AsyncWork { + Promise apply(Promise>> promise) throws Exception; + } + + interface Box { + T value(); + } + + interface Promise { + Promise then(Function function); + } + + interface MyStream { + MyStream map(Function mapper); + } + + interface Function { + T apply(F f); + } + + abstract Promise run(AsyncWork work); + + abstract MyStream myStream(Iterable iterable); + + Promise> main() { + return run(promise -> promise.then(results -> myStream(results).map(result -> result.value()))); + } +} From 126bf2750870538927babb97484e669cfde834d4 Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Fri, 14 Aug 2026 12:08:48 -0700 Subject: [PATCH 12/13] Fix test. --- framework/tests/all-systems/Issue7678.java | 45 +++++++++++----------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/framework/tests/all-systems/Issue7678.java b/framework/tests/all-systems/Issue7678.java index 0063edf092f8..6ff5647daaa1 100644 --- a/framework/tests/all-systems/Issue7678.java +++ b/framework/tests/all-systems/Issue7678.java @@ -1,30 +1,31 @@ -// Hard crash -public abstract class Issue7698 { - interface AsyncWork { - Promise apply(Promise>> promise) throws Exception; - } - - interface Box { - T value(); - } +import java.util.function.Function; +import java.util.stream.Collector; +import java.util.stream.Stream; - interface Promise { - Promise then(Function function); +// Related to lambda parameters. +@SuppressWarnings("all") +public class Issue7678 { + static class MyMap { + static Collector> toMyMap( + Function keyFunction, + Function valueFunction) { + return null; + } } - interface MyStream { - MyStream map(Function mapper); - } + static class BiStream { + static Collector> toBiStream( + Collector valueCollector) { + return null; + } - interface Function { - T apply(F f); + BiStream mapValues(Function valueMapper) { + return null; + } } - abstract Promise run(AsyncWork work); - - abstract MyStream myStream(Iterable iterable); - - Promise> main() { - return run(promise -> promise.then(results -> myStream(results).map(result -> result.value()))); + private void test(Stream items) { + Function length = String::length; + items.collect(BiStream.toBiStream(MyMap.toMyMap(length, s -> s))).mapValues(byLen -> byLen); } } From b63d5e1a52b39bf3a4422dcd40f9f3955ad8a944 Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Mon, 17 Aug 2026 10:51:54 -0700 Subject: [PATCH 13/13] Simplfy comment. --- .../util/typeinference8/util/Resolution.java | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/util/typeinference8/util/Resolution.java b/framework/src/main/java/org/checkerframework/framework/util/typeinference8/util/Resolution.java index d0dff4a950da..bc73faecd034 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/typeinference8/util/Resolution.java +++ b/framework/src/main/java/org/checkerframework/framework/util/typeinference8/util/Resolution.java @@ -204,20 +204,8 @@ private BoundSet resolveSmallestSet(Set as, BoundSet boundSet) { checkNoFalse(boundSet, "on entry to resolveSmallestSet for", as); if (boundSet.containsCapture(as)) { - // A non-capture variable in `as` whose only useful bound is an EQUAL bound directly to a - // capture variable that is also in `as` (e.g. H2 = capture#1, where capture#1 is on the - // left-hand side of a G<...>=capture(G<...>) bound) must not be resolved below, in - // resolveWithoutCapture: findProperLowerBounds/findProperUpperBounds never look at EQUAL - // bounds, so it would otherwise be resolved from its trivial declared bound (e.g. Object) - // before the capture variable it actually equals has a value. Defer such variables until - // after the captures in `as` are resolved, then let them re-check their bounds. - // - // Variables whose EQUAL bound mentions another *non-capture* variable in `as` are not - // deferred: resolveWithoutCapture's own lower/upper-bound loop already resolves chains of - // those correctly by iterating to a fixed point, and folding them into the capture-only - // deferral below would split a group that needs to be resolved together (by - // resolveWithCapture, in one call, so its simultaneous substitution of fresh type variables - // sees all of them) into two separate resolveWithCapture calls instead. + // Wait to resolve variables that have an equal bound to a capture variable that has not been + // resloved. Set deferred = new LinkedHashSet<>(); for (Variable v : as) { if (!v.isCaptureVariable() && hasUnresolvedEqualBoundToCaptureWithin(v, as)) {