-
Notifications
You must be signed in to change notification settings - Fork 120
8359370: [lworld] allow instance fields of identity classes to be readable in the prologue phase #1523
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
base: lworld
Are you sure you want to change the base?
8359370: [lworld] allow instance fields of identity classes to be readable in the prologue phase #1523
Conversation
👋 Welcome back vromero! A progress list of the required criteria for merging this PR into |
@vicente-romero-oracle 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 7 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 |
ListBuffer<JCTree> prologueCode = new ListBuffer<>(); | ||
for (JCTree stat : tree.body.stats) { | ||
prologueCode.add(stat); | ||
// gather all the stats in the body until a `super` or `this` invocation is found |
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 think this comment should explain that super/this invocation is included because its arguments are restricted too.
@@ -18,7 +18,7 @@ class InnerSuperclass { } | |||
static class InnerOuter extends EarlyIndirectOuterCapture { // accessible | |||
class InnerInnerOuter extends EarlyIndirectOuterCapture { // not accessible | |||
InnerInnerOuter() { | |||
super(/* which enclosing instance here ? */new InnerSuperclass() { }); | |||
super(new InnerSuperclass() { }); // should this be accepted?, InnerSuperclass is not an inner class of InnerInnerOuter |
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.
Probably yes, I think our agreement last time is that local/anonymous classes' captures, including the "outer this", are synthetic.
@@ -2,7 +2,7 @@ | |||
* @test /nodynamiccopyright/ | |||
* @bug 8334258 | |||
* @summary Disallow early assignment if FLEXIBLE_CONSTRUCTORS preview feature is not enabled | |||
* @compile/fail/ref=EarlyAssignmentNoPreview1.out -XDrawDiagnostics EarlyAssignmentNoPreview1.java | |||
* @compile/fail/ref=EarlyAssignmentNoPreview1.out --release 24 -XDrawDiagnostics EarlyAssignmentNoPreview1.java |
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 test along with EarlyAssigmentNoPreview 2 and 3 are failing. But the idea is to integrate this PR after the code for flexible constructor bodies have been merged into lworld, which should make these 3 tests pass
Webrevs
|
@vicente-romero-oracle this pull request can not be integrated into git checkout JDK-8359370-v2
git fetch https://git.openjdk.org/valhalla.git lworld
git merge FETCH_HEAD
# resolve conflicts and follow the instructions given by git merge
git commit -m "Merge lworld"
git push |
ListBuffer<JCTree> prologueCode = new ListBuffer<>(); | ||
for (JCTree stat : tree.body.stats) { | ||
prologueCode.add(stat); | ||
/* gather all the stats in the body until a `super` or `this` constructor invocation is found, |
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 understand that you wanted to simplify the visitor -- but doing a linear pass on the constructor and creating a new list of statements is also kind of expensive -- maybe when we're done with this change we can see if there's a way to set a flag on the visitor to shortcircuit the analysis after the super call is found.
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 think I like the trade-off, we could try to infer if a constructor invocation corresponds to the class we are interested in. Like for example analyzing the symbol associated to a super
or this
invocation. But for erroneous invocations the symbol could be null. So what to do when we find a null symbol? We would have no clues I think.
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.
let's wait until we address the other comments first. What I had in mind (but can be addressed in a separate PR) was maybe have a general visitor for prologue -- e.g. an helper visitor class that only visits things inside the prologue. Then you can extend that helper visitor here, to do what you need to do.
@@ -964,6 +966,41 @@ public static Name fullName(JCTree tree) { | |||
} | |||
} | |||
|
|||
public record SymAndTree(Symbol symbol, JCTree tree) {} | |||
|
|||
public static java.util.List<SymAndTree> symbolsFor(List<JCTree> nodes) { |
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.
Is the use of j.u.List
deliberate 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.
yes, I think the addAll
method is faster than our list
return result; | ||
} | ||
|
||
public static java.util.List<SymAndTree> symbolsFor(JCTree node) { |
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.
Can you provide some examples on what this method is supposed to do? E.g. example input and output?
@Override | ||
public void visitClassDef(JCClassDecl classDecl) { | ||
super.visitClassDef(classDecl); | ||
classDeclAndLambdaHelper(TreeInfo.symbolsFor(classDecl.defs)); |
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.
Does this look for early references in all the symbols referenced inside the class?
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.
Also, why do we use symbolsFor
instead of recursing this this visitor?
for (TreeInfo.SymAndTree symAndTree : symbols) { | ||
Symbol sym = symAndTree.symbol(); | ||
JCTree tree = TreeInfo.skipParens(symAndTree.tree()); | ||
if (sym.kind == VAR && |
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.
Why doesn't this call analyze symbol?
tree = TreeInfo.skipParens(tree); | ||
Symbol sym = TreeInfo.symbolFor(tree); | ||
if (sym != null) { | ||
if (!sym.isStatic() && !isMethodArgument(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.
if you have a sym
, in order to understand if something is a method parameter (not argument?) don't you need to check if sym.owner == MTH
?
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 for cases when we have an argument that is for example of the same type as the current class so like:
class Test {
String s;
Test(Test t) {
// the owner of s is Test not MTH so we need to check what is the qualifier for s which at the end is the argument
// `t` so we ignore it
String s1 = t.s;
super();
}
}
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.
yes, so isn't just checking owner.kind != TYP enough? (e.g. "not a field")
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 tried to replace this with:
if (!sym.isStatic() && sym.kind == VAR && sym.owner.kind == TYP) { ... }
And no tests seem to fail.
if (!sym.isStatic() && !isMethodArgument(tree)) { | ||
if (sym.name == names._this || sym.name == names._super) { | ||
// are we seeing something like `this` or `CurrentClass.this` or `SuperClass.super::foo`? | ||
if (TreeInfo.isExplicitThisReference( |
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.
Do we always report an error when seeing Foo.this
? What if we're not inside the prologue of Foo
?
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.
all the code we analyze in this visitor is in the prologue, this is why we pre-select what code we will see
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 understand -- but in the prologue of Foo we can have a Bar.this where Bar is some enclosing class?
class Foo {
class Bar {
Bar() { Object o = Foo.this; super();}
}
}
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 see, there seems to be a bug here, thanks
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.
What has fixed this exactly? It seems like this was already working as expected because TreeInfo.isExplicitThisReference
already checks for possible enclosing 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.
What has fixed this exactly? It seems like this was already working as expected because
TreeInfo.isExplicitThisReference
already checks for possible enclosing types?
yes that could be the case
@@ -1335,6 +1559,11 @@ public void visitVarDef(JCVariableDecl tree) { | |||
//fixup local variable type | |||
v.type = chk.checkLocalVarType(tree, tree.init.type, tree.name); | |||
} | |||
if (v.owner.kind == TYP && !v.isStatic() && v.isStrict()) { | |||
// strict field initializers are inlined in constructor's prologues | |||
CtorPrologueVisitor ctorPrologueVisitor = new CtorPrologueVisitor(initEnv); |
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.
Nice reuse
boolean previousAnalyzingSelect = analyzingSelect; | ||
try { | ||
analyzingSelect = true; | ||
super.visitSelect(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.
Can't we cut recursion here (instead of using analyzingSelect
? That's also what the new TreeInfo.symbolsFor
does. In general it seems like these two visitors are trying to do similar things but are not 100% aligned?
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.
if one has a complex select like for example: new SuperInitFails(){}.x
it is still necessary to look inside and see if there are some forbidden accesses
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.
Ok, the issue might then be that, while looking inside SuperInitFails
you find a plain early access to a field (e.g. an ident) and we end up skipping it because analyzingSelect
is set. At the very least we should unset inside classes (maybe lambdas too).
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.
Example:
class Test {
int x = 4;
static String m(Runnable r) { return null; }
Test() {
m(() -> System.out.println(x)).toString();
super();
}
public static void main(String[] args) {
new Test();
}
}
This seems to compile, but then fails with verifier error.
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.
(e.g. we really need to make sure that analyzeSelect
is not applied too broadly)
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.
true, now that we removed the visitor at TreeInfo that is a problem
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.
Actually -- this is enough:
class Test {
int x = 4;
static String m(Object r) { return null; }
Test() {
m(x).toString();
super();
}
public static void main(String[] args) {
new Test();
}
}
No lambda. So probably was an issue even before, with TreeInfo
.
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 :(
added a new iteration that removes the new visitor at TreeInfo, thanks for the comments so far! |
@@ -1007,6 +1007,8 @@ private static Symbol symbolForImpl(JCTree node) { | |||
if (node.type != null) | |||
return node.type.tsym; | |||
return null; | |||
case TYPECAST: |
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.
Is this still 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.
yes no for this patch, but I was wondering if it could be useful for future use. But I will remove it
/* references to fields of identity classes which happen to have initializers are | ||
* not allowed in the prologue | ||
*/ | ||
if (insideLambdaOrClassDef || |
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'm not entirely convinced about these checks. They seem to lead to very strange asymmetries:
import java.util.function.*;
class Test3 {
int x = 4;
int y;
Test3() {
System.out.println(x); //error
Supplier<Integer> s1 = () -> x; // error
y = 2;
System.out.println(y); // ok
Supplier<Integer> s2 = () -> y; // error
super();
}
}
I understand that references to x
are invalid here -- x
is not a strict field, so it will be initialized after the prologue. So the first couple of references are errors, fine.
But in the last couple, we have that print(x)
is good, but the reference from the lambda is flagged as an error. I'm not sure what's the rationale here? After all the lambda is defined after y
has been assigned, so what are we trying to protect against?
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 think that the idea of restricting the access from lambdas and local classes is that they will capture this
in order to access the field(s)
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.
Ah! Forgot about that one -- but... with proxy locals that's no longer the case, no?
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.
(or, do we only do local proxies for strict fields? If so, should we be more uniform 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.
we do local proxies for every field read in the prologue, a getField with a larval this
will be an error in the VM
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.
ok, so I guess I still don't get whether this must be an error. In principle
y
could have a local proxy, in which case the lambda could be thought of as accessing that proxy, so no need to capturethis
?
I wonder what is the mental model supposed to be here.
@mcimadamore what is your opinion on whether this should compile?
class A {
int y;
A() {
y = 1;
class B {
static void m() { // static context
System.out.println(y);
}
}
super();
}
}
If your answer is "No" then aren't you then implying that y
shouldn't be available whenever A.this
is not available? In which case doesn't that answer your question?
If your answer is "Yes", then doesn't that imply that this should also compile...
class A {
int y;
A() {
y = 1;
class B {
static void m() { // static context
System.out.println(A.this.y);
}
}
super();
}
}
even though this doesn't:
class A {
int y;
A() {
y = 1;
class B {
static void m() { // static context
System.out.println(A.this);
}
}
super();
}
}
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 suppose what I'm saying is: I understand why the code doesn't compile in today's world. But as we relax more restrictions and we resort to more complex translation strategies, I do wonder if some of these rules that prevent reads from lambdas will feel too tight. E.g. imagine the case of a final field -- that is written only once. If we already saw a write for that field, what stops us from being able to reference it from a lambda -- through a local proxy?
I don't buy the argument that A.this.y
working implies A.this
. This is already not the case in the code added by this PR, where reading an already written field in a prologue is fine, even through A.this.y
-- but accessing this
of a class from the prologue is never ok (if it was you could pass such a larval this
to another method).
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 your example with static context, my answer is that no, it should not compile. A static context can't access instance fields from an enclosing class)
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 your example with static context, my answer is that no, it should not compile. A static context can't access instance fields from an enclosing class)
Yep, sorry that was a dumb example - I should have replaced the static context with a lambda.
I'm all for increased flexibility, it's just that it would be nice if that came with a clean mental model.
For example, one possible mental model could be "works like effectively final" - which I think is what you're advocating - but for that we'd have to break the equivalence between y
and A.this.y
(probably worth it).
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 guess that we should discuss access from a lambda with Dan, and if we decide the rules should be relaxed then do it but I think as part of a future PR. I'm also all in for relaxing restrictions
@@ -318,7 +320,7 @@ void checkAssignable(DiagnosticPosition pos, VarSymbol v, JCTree base, Env<AttrC | |||
} | |||
|
|||
// Check instance field assignments that appear in constructor prologues | |||
if (rs.isEarlyReference(env, base, v)) { | |||
if (isEarlyReference(env, base, v)) { |
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 method, isEarlyReference, is only used in Attr, I moved it here
@@ -3942,101 +3921,6 @@ private List<Type> pruneInterfaces(Type t) { | |||
} | |||
return result.toList(); | |||
} | |||
private boolean isReceiverParameter(Env<AttrContext> env, JCFieldAccess 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 method, isReceiverParameter
, is not used any more, I removed it
* We also don't verify that the field has no initializer, which is required. | ||
* To catch those cases, we rely on similar logic in Attr.checkAssignable(). | ||
*/ | ||
private boolean isAllowedEarlyReference(DiagnosticPosition pos, Env<AttrContext> env, VarSymbol v) { |
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.
moved this one to Attr
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.
Do we still need the changes in Attr.checkAssignable
-- e.g. flexible constructor bodies used to check some extra properties in there. Now that we have an LHS variable in the prologue scanner, I wonder if we can unify the checks?
I did an experiment, and adding this:
At the start of But one nice consequence of this is that now
Which is required at every callsite. |
addressed in last commit, 9014afc, thanks! |
Before this fix only strict fields were readable in the prologue phase. The proposed fix should allow any instance fields of identity classes to be readable in the prologue phase. This implies changes in flow analysis as before we were only tracking final and strict fields. There is also some "cooperation" needed in the code to detect cases when reading a field is not allowed in the prologue phase. For example some methods in Resolve don't have all the needed information at the moment they are dealing with some ASTs and part of the processing needs to be done in Attr
TIA
This PR is a remake of #1490
Progress
Issue
Reviewing
Using
git
Checkout this PR locally:
$ git fetch https://git.openjdk.org/valhalla.git pull/1523/head:pull/1523
$ git checkout pull/1523
Update a local copy of the PR:
$ git checkout pull/1523
$ git pull https://git.openjdk.org/valhalla.git pull/1523/head
Using Skara CLI tools
Checkout this PR locally:
$ git pr checkout 1523
View PR using the GUI difftool:
$ git pr show -t 1523
Using diff file
Download this PR as a diff file:
https://git.openjdk.org/valhalla/pull/1523.diff
Using Webrev
Link to Webrev Comment