Bug 452788 - [1.8][compiler] Type not correctly inferred in lambda expression
Summary: [1.8][compiler] Type not correctly inferred in lambda expression
Status: VERIFIED FIXED
Alias: None
Product: JDT
Classification: Eclipse Project
Component: Core (show other bugs)
Version: 4.4   Edit
Hardware: PC Mac OS X
: P3 normal (vote)
Target Milestone: 4.5 M4   Edit
Assignee: Stephan Herrmann CLA
QA Contact:
URL:
Whiteboard:
Keywords:
: 453422 (view as bug list)
Depends on:
Blocks:
 
Reported: 2014-11-22 10:59 EST by Daniel Dietrich CLA
Modified: 2014-12-09 04:24 EST (History)
5 users (show)

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Daniel Dietrich CLA 2014-11-22 10:59:21 EST
Hi,

using Eclipse Mars M3 and jdk1.8.0_25, the following does not compile in Eclipse (but with jdk).

This may be the same reason as in Bug 451677.

// - - - - - - - - - - - - - - - - - - - - - - - - - - - 
import java.util.function.Function;

interface Test<A> {

	<B> Test<B> create(B b);

	<B> Test<B> transform(Function<? extends A, Test<B>> f);

	default <B> Test<B> wrap(Function<? super A, ? extends B> f) {
		// THIS WORKS WITH ECJ: return transform(a -> create((B) f.apply(a)));
		return transform(a -> create(f.apply(a)));
	}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - 

Thanks,

Daniel
Comment 1 Stephan Herrmann CLA 2014-11-22 11:08:03 EST
Thanks for the report, I can reproduce in HEAD.


(In reply to Daniel Dietrich from comment #0)
> This may be the same reason as in Bug 451677.

I don't thinks so. In bug 451677 I think the problem relates to auto-boxing whereas the current issue has nothing of that in the picture.

Interestingly, at M2 we did accept the program, only since M3 we report:

        return transform(a -> create(f.apply(a)));
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Type mismatch: cannot convert from Test<Object> to Test<B>
Comment 2 Stephan Herrmann CLA 2014-11-22 12:24:04 EST
For clarity let's rename the two type parameters B to
 B1 for wrap's type parameter
 B2 for transform's type parameter

We have two conflicting type bounds for B2#0 (inference variable representing B2):

From the target type Test<B1>:
  B2#0 = B1

From the inner create call:
  B2#0 = capture#2-of ? extends B1

This lets invocation type inference fail.

We fall back to the result of applicability inference, where
  B2#0 = j.o.Object
which makes transform return Test<Object>

Hence the error:
  Type mismatch: cannot convert from Test<Object> to Test<B1>


From all I've seen so far, I could not see any wrong in our inference.

OTOH, the following version compiles fine:

//----
import java.util.function.Function;

interface Test<A> {

    <B> Test<B> create(B b);

    <B> Test<B> transform(Function<? extends A, Test<B>> f);

    default <B> Test<? extends B> wrap(Function<? super A, ? extends B> f) {
        return transform(a -> create(f.apply(a)));
    }
}
//----

So does this:

//----
import java.util.function.Function;

interface Test<A> {

    <B> Test<B> create(B b);

    <B> Test<B> transform(Function<? extends A, Test<B>> f);

    default <B> Test<B> wrap(Function<? super A, B> f) {
        return transform(a -> create(f.apply(a)));
    }
}
//----

Read this as: it is illegal to ignore an incoming wildcard.

To guess a reason, why ecj behavior has changed from M2 to M3:
Previously, we probably failed to see the constraint from the inner call.
The corresponding spec change would then be
  https://bugs.openjdk.java.net/browse/JDK-8038747
We should check with the spec author, if our new behavior in this case is indeed the intended behavior, despite javac still answering differently.

I'll let it sink in a little more, but I don't see a JDT bug, currently.
Comment 3 Stephan Herrmann CLA 2014-11-22 13:36:11 EST
Questioning: is it correct to resolve create to
   Test<capture#2-of ? extends B1> create(capture#2-of ? extends B1 b)
?

Answering
   Test<B1> create(B1 b)
should be legal, too, and probably produce the desired final outcome.

This resolving happens when reducing the following contraint:
  (<no type> a) -> create(f.apply(a)) → java.util.function.Function<? extends A,Test<B2#0>>

The lambda is resolved with target type 
  Function<A,Test<B2#0>>

The inner create-call is resolved with target type
  Test<B2#0>

The argument f.apply(a) produces this type bound:
  B3#0 :> capture#2-of ? extends B1

Matching target type against return type of f yields:
  ⟨Test<B3#0> → Test<B2#0>⟩
  B3#0 = B2#0
which together with the above produces:
  B2#0 :> capture#2-of ? extends B1
resolved to:
  B2#0 = capture#2-of ? extends B1

Given that all this happens in the context of still inferring B2#0 this last resolve step seems premature.

Experiment: avoid full resolution of create() and answer its type as
  Test<B2#0> create(B2#0)

Then the lambda would resolve to
  Function<A,Test<B2#0>>

This would completely avoid the use of the capture and allow outer inference to equate B2#0 with B1, thus accepting the program.


To be investigated: does JLS provide the necessary device for binding an inference variable to an unresolved inference variable from outer inference?
Comment 4 Stephan Herrmann CLA 2014-11-22 14:31:57 EST
Disregarding the spec for a second, I checked technical feasibility of the idea in comment 3.

Two challenges:
(1) can we somehow connect inner and outer InferenceContext18?

(2) can we faithfully recognize which inference variables can/should be answered from IC18.getSolutions()?


Ad 1: Interestingly, at the point when we need it, there is no connection whatsoever from what we have at hand when resolving create(..), back to the inference for the enclosing lambda.
This information would have to be actively pushed down. I've drafted a solution by
- storing an 'InferenceContext18 inferenceContext' in LambdaExpression:
  - assign it during IC18.addConstraintsToC_OneExpr()
  - propagate it in LE.copy()
- storing an 'InferenceContext18 outerInferenceContext' in MessageSend
  (would have to be duplicated in other Invocations)
  - assign during MS.findMethodBinding()
    - lookup the LE via the methodScope() and fetch its inferenceContext
  - attach it as outerContext in MS.freshInferenceContext()


Ad 2: given IC18.getSolutions() has access to an outerContext, we can
- fetch the inference variables of the outer context
- ask the BoundSet if any of the outer variables are equivalent to a given inner inference variable
  - do this by searching a sameBound mentioning inner & outer variable
- if an equivalent outer variable is found prefer this over any exact solution

Ergo: Feasibility is established.

Also: RunAllJava8Tests has nothing to complain.

Remains to be seen: is this solution correct / within the bounds of JLS?
Comment 5 Daniel Dietrich CLA 2014-11-22 14:36:07 EST
Wow, that was fast! :-)
Comment 6 Stephan Herrmann CLA 2014-11-22 15:12:02 EST
(In reply to Daniel Dietrich from comment #5)
> Wow, that was fast! :-)

We're not done yet :) the legalese part is still to come - hold on ...
Comment 7 Stephan Herrmann CLA 2014-11-22 15:21:32 EST
Let's try to match the implementation change against JLS plus pending amendment from https://bugs.openjdk.java.net/browse/JDK-8038747

Pivot is this new spec sentence:
 "For a lambda expression, the search is applied recursively to the lambda body"

This is implemented by IC18.addConstraintsToC_OneExpr inside "if (expri instanceof LambdaExpression)"


Current candidate is this lambda:
  a -> create(f.apply(a))

Goal is to add more constraints wrt
  create(f.apply(a))
against an appropriate target type, where we take the return type of the lambdas function type, OK.


The recursion seems to do the right thing in the block between enterPolyInvocation() and resumeSuspendedInference().
Problem is, that all this recursive work needs a bunch of resolved types, like the lambda's function type, the applicable binding of the inner invocation etc.

Now, the goal of recursion is in feeding more constraints into the IC18, but in preparation we need to call
  lambda = lambda.resolveExpressionExpecting(t, this.scope)
which is what triggers the unwanted final resolve of the inner inference!

OTOH, the recursion doesn't seem well defined in the JLS amendment:
- we are supposed to loop over ei & Fi, where 
  - ei are the arguments of the current invocation
  - Fi are the formal parameters of the most specific applicable method
While recurring, only ei is strictly defined, we aren't told what types to use for Fi, this requires a fair bit of reading between the lines.

Given that lambda.resolveExpressionExpecting() is invoked basically with the purpose to fill this spec gap, it seems fair to be careful not to let this have the effect of over-eager resolution, finalizing any bit of the inference. IOW: we're left to our interpretation and judgment anyway, so we shouldn't be shy in this judgment.


Keeping a few unresolved inference variables in this context certainly isn't a crime, the context being outlined by this call stack:

BoundSet.getEquivalentOuterVariable(InferenceVariable, InferenceVariable[]) line: 1100	
InferenceContext18.getSolutions(TypeVariableBinding[], InvocationSite, BoundSet) line: 862	
ParameterizedGenericMethodBinding.computeCompatibleMethod18(MethodBinding, TypeBinding[], Scope, InvocationSite) line: 229	
ParameterizedGenericMethodBinding.computeCompatibleMethod(MethodBinding, TypeBinding[], Scope, InvocationSite) line: 80	
ClassScope(Scope).computeCompatibleMethod(MethodBinding, TypeBinding[], InvocationSite, boolean) line: 731	
ClassScope(Scope).computeCompatibleMethod(MethodBinding, TypeBinding[], InvocationSite) line: 688	
ClassScope(Scope).findMethod0(ReferenceBinding, char[], TypeBinding[], InvocationSite, boolean) line: 1620	
ClassScope(Scope).findMethod(ReferenceBinding, char[], TypeBinding[], InvocationSite, boolean) line: 1521	
MethodScope(Scope).getImplicitMethod(char[], TypeBinding[], InvocationSite) line: 2481	
MessageSend.findMethodBinding(BlockScope) line: 883	
MessageSend.resolveType(BlockScope) line: 702	
ReturnStatement.resolve(BlockScope) line: 335	
LambdaExpression.resolveType(BlockScope) line: 424	
LambdaExpression.cachedResolvedCopy(TypeBinding, boolean, boolean) line: 853	
LambdaExpression.resolveExpressionExpecting(TypeBinding, Scope) line: 883	
InferenceContext18.addConstraintsToC_OneExpr(Expression, Set<ConstraintFormula>, TypeBinding, TypeBinding, MethodBinding, boolean) line: 461	

For this particular call chain I'm comfortable with the change.

Next chapter: can the tweak leak outside this situation, spilling into situations that *are* well defined and shouldn't be "cheated"?
Comment 8 Stephan Herrmann CLA 2014-11-22 15:43:30 EST
For posterity: within GRT_1_8, one more test triggers the new code path, which is the test for bug 442916, which is an indirect duplicate of bug 444891, leading us back to https://bugs.openjdk.java.net/browse/JDK-8038747 :)

That test, however, passes with or without the pending change alike.
Comment 9 Stephan Herrmann CLA 2014-11-22 19:51:57 EST
Hudson gave +1 to what I have so far, see https://git.eclipse.org/r/36882

Remaining tasks:

Answer this question:

(In reply to Stephan Herrmann from comment #7)
> Next chapter: can the tweak leak outside this situation, spilling into
> situations that *are* well defined and shouldn't be "cheated"?


And add more tests, at least covering the addition on AllocationExpression.
Comment 10 Stephan Herrmann CLA 2014-11-23 06:53:41 EST
When challenging AllocationExpression, this test already works in M3:

//----
import java.util.function.Function;

public interface Test2<A> {
	<B> Test2<B> transform(Function<? extends A, Test2<B>> f);

	default <B> Test2<B> wrap(Function<? super A, ? extends B> f) {
		return transform(a -> new TestImpl<>(f.apply(a)));
	}
}

class TestImpl<A> implements Test2<A> {

	public TestImpl(A a) { }

	@Override
	public <B> Test2<B> transform(Function<? extends A, Test2<B>> f) {
		return null;
	}	
}
//----

All I changed was replace the generic method create() with a constructor, invoked as diamond.

Two things to take home:

- a hint, that the wildcard should not prevent us from finding a solution

- follow-up task in bug 452806
Comment 11 Stephan Herrmann CLA 2014-11-23 08:17:40 EST
The patch increases the risk of leaking inference variables into the wild.

In fact it is *intended* that a lambda's result expression (synthetically wrapped in a ReturnExpression) may have an improper type.

To prevent this from failing resolve on the ReturnExpression I had to make sure that isCompatibleWith() can understand inference variables.
OTOH, we *need* other places like IC18.getReturnProblemMethodIfNeeded() to answer false on inference variables.
To make the necessary change context sensitive, I inserted it into isCompatibleWith0(TypeBinding,Scope) and search the scope to see if we are inside lambda resolving. Callers not passing a scope will never get the new behavior. This is more a heuristic than a provably correct solution.

Other means for delimiting the scope affected by the change:

The main change is given by BoundSet.getEquivalentOuterVariable()
- This method is only called if the current IC18 has an outerContext
- outerContext is set in 
  - IC18.addConstraintsToC_OneExpr (old behavior)
  - when starting inner inference in CExprF.reduce() (old behavior)
  - when creating an IC18 for an invocation that has an outerInferenceContext
    - outerInferenceContext is set when findMethodBinding() is called
      - inside the MethodScope of a lambda, and
      - if the lambda has an inference context set
        - which only occurs when working on a copy()

For the last branch we seem to be safe, since everything only happens in the universe of a lambda copy, which is only used for creating new constraints.

Are the two old assignments of outerContext OK? 
In both cases the immediate situation is to create constraints from inner and lift them into outer, but the IC18 of course lives longer and later it might be inappropriate to answer solutions with improper types.
For that reason I strengthened the checks in IC18.getSolution to respect an outerContext only if it is not yet completed (TYPE_INFERRED).


Some of the scenarios I could never trigger, neither in any of RunAllJava8Tests nor by crafting new tests. I hold the above precautions are good enough. 
Awaiting just one more test run.
Comment 13 Jay Arthanareeswaran CLA 2014-11-28 00:59:40 EST
*** Bug 453422 has been marked as a duplicate of this bug. ***
Comment 14 shankha banerjee CLA 2014-12-09 04:24:40 EST
Verified for 4.5M4 using I20141208-2000 build.