-
Notifications
You must be signed in to change notification settings - Fork 6.1k
8334037: Local class creation in lambda in pre-construction context crashes javac #19836
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
(as lambdas are translated last, XDprintSource shows them untouched)
Lower unnecessarily rewrites symbols for Foo.class where Foo is a reference class. We still have an issue with Lower generating "random" tmp local names.
Remove more dead code in LambdaToMethod
Simplify code not to stash method reference receiver in desugared lambda.
👋 Welcome back mcimadamore! A progress list of the required criteria for merging this PR into |
@mcimadamore This change now passes all automated pre-integration checks. ℹ️ This project also has non-automated pre-integration requirements. Please see the file CONTRIBUTING.md for details. After integration, the commit message for the final commit will be:
You can use pull request commands such as /summary, /contributor and /issue to adjust it as needed. At the time when this comment was updated there had been 83 new commits pushed to the
As there are no conflicts, your changes will automatically be rebased on top of these commits when integrating. If you prefer to avoid this automatic rebasing, please check the documentation for the /integrate command for further details. ➡️ To integrate this PR with the above commit message to the |
@mcimadamore The following label will be automatically applied to this pull request:
When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing list. If you would like to change these labels, use the /label pull request command. |
/solves JDK-8333313 |
@mcimadamore |
@mcimadamore |
@mcimadamore |
/contributor add acobbs |
@mcimadamore |
/** | ||
* Converts a method reference which cannot be used directly into a lambda | ||
*/ | ||
private class MemberReferenceToLambda { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code is now in Lower
} | ||
|
||
@Override | ||
public void visitNewClass(JCNewClass tree) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
most of the removed visitor methods had to do with the typesUnderConstruction
workaround, which has now been removed
translatedSymbols.put(LOCAL_VAR, new LinkedHashMap<Symbol, Symbol>()); | ||
translatedSymbols.put(CAPTURED_VAR, new LinkedHashMap<Symbol, Symbol>()); | ||
translatedSymbols.put(CAPTURED_THIS, new LinkedHashMap<Symbol, Symbol>()); | ||
translatedSymbols.put(CAPTURED_OUTER_THIS, new LinkedHashMap<Symbol, Symbol>()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note that CAPTURED_OUTER_THIS no longer exists
@@ -725,7 +657,7 @@ private JCMethodDecl makeDeserializeMethod(Symbol kSym) { | |||
deser.sym = kInfo.deserMethodSym; | |||
deser.type = kInfo.deserMethodSym.type; | |||
//System.err.printf("DESER: '%s'\n", deser); | |||
return deser; | |||
return lower.translateMethod(attrEnv, deser, make); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
explicit call to Lower is needed here
* This is required when a capturing local class is created from a lambda (in which | ||
* case the captured symbols should be replaced with the translated lambda symbols). | ||
*/ | ||
Map<Symbol, Symbol> lambdaTranslationMap = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is now no longer needed
@@ -344,11 +344,13 @@ public void visitExec(JCExpressionStatement stat) { | |||
@Override | |||
public void visitClassDef(JCClassDecl tree) { | |||
// don't descend any further | |||
result = tree; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was an issue in the current code: this is a tree translator (not just a tree scanner) and a tree translator has to side-effect the result
variable, otherwise clients can see null
popping up in strange places (which is what started happening when I moved LambdaToMethod
around).
Webrevs
|
@@ -22,7 +22,7 @@ | |||
*/ | |||
/* | |||
* @test | |||
* @bug 8194743 | |||
* @bug 8334252 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just a fix for the wrong bug ID
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In which case this also fixes JDK-8334679. If you want, add /solves 8334679
here and I'll close PR #19816. Thanks.
/solves 8334679 |
@mcimadamore |
/issue remove JDK-8334252 |
@mcimadamore |
result = tree; | ||
} | ||
|
||
@Override | ||
public void visitLambda(JCLambda tree) { | ||
Type expectedRet = types.findDescriptorType(tree.type).getReturnType(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't we erase the return type here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tree.type
should be erased (from TransTypes
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After following up offline, we realized that this does indeed require erasure of the getReturnType
, given findDescriptorType
calls findDescriptor
on the type's symbol (and symbols are not erased).
} | ||
|
||
boolean receiverAccessible(JCMemberReference tree) { | ||
//hack needed to workaround 292 bug (7087658) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
side: 7087658 has been fixed but if we comment the invocation to this method some tests fail, so I think this comment is stale, and we could probably remove it, and if we remove the comment then this predicate probably doesn't deserve to be a method anymore. Also not related to your patch so this could be tackle in another PR but is seems like the ownerAccessible field in method references reflects incomplete information. As we are adding here several predicates all related to accessibility to determine if we should generate a lambda for a particular method reference or not. See isProtectedInSuperClassOfEnclosingClassInOtherPackage
, isPrivateInOtherClass
, etc. So it seems like the degree of accessibility we want to determine for the expr
in a method reference is stricter than the one needed in general for other types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree there's a lot of cruft in here. We started adding predicates simply because the 292 machinery could not support all invocation modes. But then we also added other stuff: all the accessibility checks are there because if javac decides to generate an accessibility bridge for the method, then we can't use a MH directly (as the target method would be inaccessible - the bridge should be called instead). In these cases, it was preferred to just let Lower
do its job, and translate the method call correctly, using bridges etc.
But, with respect to this specific issue, I agree, ownerAccessible
is probably no longer needed, as the underlying MH deficiency was addressed long ago.
slam.target = tree.target; | ||
slam.type = tree.type; | ||
slam.pos = tree.pos; | ||
slam.wasMethodReference = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep I agree that we can remove this field, probably in another PR, if really needed we can add a separate DS to track those lambdas that were method references
if (receiverExpression != null) { | ||
// use a let expression so that the receiver expression is evaluated eagerly | ||
return make.at(tree.pos).LetExpr( | ||
make.VarDef(rcvr, translate(receiverExpression)), slam).setType(tree.type); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we erase this type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be erased already, since we're past TransTypes
and that visitor had a chance to translate the method reference expression?
@@ -4381,7 +4722,7 @@ public void visitSelect(JCFieldAccess tree) { | |||
TreeInfo.name(tree.selected) == names._super && | |||
!types.isDirectSuperInterface(((JCFieldAccess)tree.selected).selected.type.tsym, currentClass); | |||
tree.selected = translate(tree.selected); | |||
if (tree.name == names._class) { | |||
if (tree.name == names._class && tree.selected.type.isPrimitiveOrVoid()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure why this change was needed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is again lambda deduplication. The current code will take String.class
and rewrite it from scratch, with a new symbol in it (which is what makes deduplication fail).
translatedSymbols.put(PARAM, new LinkedHashMap<>()); | ||
translatedSymbols.put(LOCAL_VAR, new LinkedHashMap<>()); | ||
translatedSymbols.put(CAPTURED_VAR, new LinkedHashMap<>()); | ||
translatedSymbols.put(CAPTURED_THIS, new LinkedHashMap<>()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't seem to actually be doing anything fancy for captured_this, the symbol is self represented, I wonder if this category could be removed in a future PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CAPTURED_THIS is used to test whether a lambda needs to be instance or static (but it's true we don't really use any of the symbols in here, just using a boolean
would be ok)
awesome job, this change will make our lives a lot easier in the future |
Erase lambda functional return and simplifify lambda visitor in `Lower`
tree.body = translate((JCExpression) tree.body, expectedRet); | ||
} | ||
result = tree; | ||
currentRestype = types.findDescriptorType(tree.type).getReturnType(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We no longer need to mess with variableIndex
in the new code: if the lambda is a statement lambda, this will call visitBlock
which will update variableIndex
accordingly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I probably miss something, but where is the return type erased?
For the records, I've also put together a patch which moves the code to lower method references into I will pursue this as a separate PR. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, looks OK to me. Some comments inline. I also was thinking if the reference to lambda conversion should be a separate phase (in order to not put more work into Lower
), but that's probably overkill.
@@ -1647,6 +1639,16 @@ public void visitSwitchExpression(JCSwitchExpression tree) { | |||
if (shouldStop(CompileState.LOWER)) | |||
return; | |||
|
|||
if (scanner.hasLambdas) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC, this was added because the new phase slowed down performance running of very small, simple, testcase(s) without any lambda (https://bugs.openjdk.org/browse/JDK-8026936).
We also do the same for patterns/TransPatterns
.
To what degree this is needed currently is not obvious.
tree.body = translate((JCExpression) tree.body, expectedRet); | ||
} | ||
result = tree; | ||
currentRestype = types.findDescriptorType(tree.type).getReturnType(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I probably miss something, but where is the return type erased?
See my comment. I think it makes sense to move this code in a separate phase as this code really wants to be before |
Add missing erasure
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks great!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me.
/integrate |
Going to push as commit 4ce8822.
Your commit was automatically rebased without conflicts. |
@mcimadamore Pushed as commit 4ce8822. 💡 You may see a message that your pull request was closed with unmerged commits. This can be safely ignored. |
A long, long time ago, we used to have two competing translation strategies for lambda expressions. One strategy was to translate lambdas to anonymous inner classes. The other was to translate them as invokedynamic, pointing to a synthetic method containing the lambda body.
The simpler inner class traslation strategy predates the more complex indy-based one. Since that translation strategy was based on inner classes, it made a lot of sense to do that translation before
Lower
, so that any anon class generated by javac could then be translated correctly (and take into account issues such as local variable capture).When we added
LambdaToMethod
, it seemed natural to add it in the same step (e.g. beforeLower
) - after all, users could toggle between the two translation strategies with a switch, so it made sense to keep them close together.But, as we got rid of the inner class translation strategy, and doubled down on
LambdaToMethod
we have started to see some artifacts. For instance, if a lambda expression contains a local class declaration,LambdaToMethod
has to guess which captured variables might get captured later inLower
, and re-captured them in the lambda. Now, guessing the full set of local captures is not that easy, as the set depends on the state in whichLower
is in: more specifically, sometimesLower
might decide to also capture the enclosing this constructor parameter, when in early-construction context. Of courseLambdaToMethod
didn't know any of this, and over the years several workarounds have been introduced.Unfortunately, JEP 482 is in collision course with the current compiler pipeline:
Lower
is the only place in the compiler which knows how to correctly translate references to enclosing instances. Trying to mimic whatLower
is doing inLambdaToMethod
, doesn't scale, as the many bugs that were discovered in this area show.For this reason, instead of keep playing whack-a-mole, I think the time has come to finally move
LambdaToMethod
afterLower
. This move makesLambdaToMethod
the last compiler step (beforeGen
).The move is relatively straightforward, but there are some caveats:
LambdaToMethod
deals with separate classes, not a single toplevel one (as it now occurs after lowering);Lower
needs a to overridevisitLambda
, so that it can properly lower all the lambda return expressions;LambdaToMethod
to Lower (so that we can fixup references to inner classes etc.) - this means thatLambdaToMethod
now only has to worry about lambdas and simple method references;LambdaToMethod
contains string in switch, and implicitly requires boxing, so we need to explictly lower that (givenLower
has already ran, we need to trigger the lowering of the method manually)Lower
is not stable (this is a pre-existing issue), so lambda deduplication fails in new waysThis PR addresses all the issues above and removes all the workaround we have accumulated over the years to make up for the fact that
LambdaToMethod
runs at the wrong time. In doing so, we fix many issues related to JEP 482 (thanks to @archiecobbs for the tests which I've included here).Progress
Issues
Reviewers
Contributors
<acobbs@openjdk.org>
Reviewing
Using
git
Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/19836/head:pull/19836
$ git checkout pull/19836
Update a local copy of the PR:
$ git checkout pull/19836
$ git pull https://git.openjdk.org/jdk.git pull/19836/head
Using Skara CLI tools
Checkout this PR locally:
$ git pr checkout 19836
View PR using the GUI difftool:
$ git pr show -t 19836
Using diff file
Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/19836.diff
Webrev
Link to Webrev Comment