Skip to content

Conversation

vicente-romero-oracle
Copy link
Contributor

@vicente-romero-oracle vicente-romero-oracle commented Aug 15, 2025

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

  • Change must not contain extraneous whitespace

Issue

  • JDK-8359370: [lworld] allow instance fields of identity classes to be readable in the prologue phase (Bug - P4)

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

@bridgekeeper
Copy link

bridgekeeper bot commented Aug 15, 2025

👋 Welcome back vromero! A progress list of the required criteria for merging this PR into lworld will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk
Copy link

openjdk bot commented Aug 15, 2025

@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:

8359370: [lworld] allow instance fields of identity classes to be readable in the prologue phase

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 lworld branch:

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 lworld branch, type /integrate in a new comment.

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
Copy link
Member

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
Copy link
Member

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
Copy link
Contributor Author

@vicente-romero-oracle vicente-romero-oracle Aug 18, 2025

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

@vicente-romero-oracle vicente-romero-oracle marked this pull request as ready for review August 18, 2025 18:13
@openjdk openjdk bot added ready Pull request is ready to be integrated rfr Pull request is ready for review labels Aug 18, 2025
@mlbridge
Copy link

mlbridge bot commented Aug 18, 2025

@openjdk
Copy link

openjdk bot commented Aug 19, 2025

@vicente-romero-oracle this pull request can not be integrated into lworld due to one or more merge conflicts. To resolve these merge conflicts and update this pull request you can run the following commands in the local repository for your personal fork:

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

@openjdk openjdk bot added merge-conflict Pull request has merge conflict with target branch and removed ready Pull request is ready to be integrated labels Aug 19, 2025
@openjdk openjdk bot added ready Pull request is ready to be integrated and removed merge-conflict Pull request has merge conflict with target branch labels Aug 19, 2025
@openjdk openjdk bot added merge-conflict Pull request has merge conflict with target branch and removed ready Pull request is ready to be integrated labels Aug 20, 2025
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,
Copy link
Collaborator

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.

Copy link
Contributor Author

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.

Copy link
Collaborator

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) {
Copy link
Collaborator

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?

Copy link
Contributor Author

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) {
Copy link
Collaborator

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));
Copy link
Collaborator

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?

Copy link
Collaborator

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 &&
Copy link
Collaborator

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)) {
Copy link
Collaborator

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 ?

Copy link
Contributor Author

@vicente-romero-oracle vicente-romero-oracle Aug 28, 2025

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();
    }
}

Copy link
Collaborator

@mcimadamore mcimadamore Aug 29, 2025

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")

Copy link
Collaborator

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(
Copy link
Collaborator

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 ?

Copy link
Contributor Author

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

Copy link
Collaborator

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();}
        }
}

Copy link
Contributor Author

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

Copy link
Collaborator

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?

Copy link
Contributor Author

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);
Copy link
Collaborator

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);
Copy link
Collaborator

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?

Copy link
Contributor Author

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

Copy link
Collaborator

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).

Copy link
Collaborator

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.

Copy link
Collaborator

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)

Copy link
Contributor Author

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

Copy link
Collaborator

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep :(

@vicente-romero-oracle
Copy link
Contributor Author

vicente-romero-oracle commented Aug 28, 2025

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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still needed?

Copy link
Contributor Author

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 ||
Copy link
Collaborator

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?

Copy link
Contributor Author

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)

Copy link
Collaborator

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?

Copy link
Collaborator

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?)

Copy link
Contributor Author

@vicente-romero-oracle vicente-romero-oracle Aug 29, 2025

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

Copy link
Contributor

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 capture this ?

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();
    }
}

Copy link
Collaborator

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).

Copy link
Collaborator

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)

Copy link
Contributor

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).

Copy link
Contributor Author

@vicente-romero-oracle vicente-romero-oracle Sep 1, 2025

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)) {
Copy link
Contributor Author

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) {
Copy link
Contributor Author

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) {
Copy link
Contributor Author

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

Copy link
Collaborator

@mcimadamore mcimadamore left a 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?

@mcimadamore
Copy link
Collaborator

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:

            if (isInLHS && !insideLambdaOrClassDef) {
                // Check instance field assignments that appear in constructor prologues
                if (isEarlyReference(localEnv, tree.hasTag(SELECT) ? ((JCFieldAccess)tree).selected : null, sym)) {
                    // Field may not be inherited from a superclass
                    if (sym.owner != localEnv.enclClass.sym) {
                        log.error(tree, Errors.CantRefBeforeCtorCalled(sym));
                        return;
                    }

                    // Field may not have an initializer
                    if ((sym.flags() & HASINIT) != 0) {
                        log.error(tree, Errors.CantAssignInitializedBeforeCtorCalled(sym));
                        return;
                    }
                }
                return;
            };

At the start of analyzeSymbol seems to work fine. Maybe we want to break analyzeSymbol in different parts, one for reads and one for writes.

But one nice consequence of this is that now isEarlyReference is only called inside this visitor -- this means we can probably avoid having to pass isPrologue = true and, we can also avoid this dance:

tree.hasTag(SELECT) ? ((JCFieldAccess)tree).selected : null

Which is required at every callsite.

@vicente-romero-oracle
Copy link
Contributor Author

vicente-romero-oracle commented Sep 1, 2025

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:

            if (isInLHS && !insideLambdaOrClassDef) {
                // Check instance field assignments that appear in constructor prologues
                if (isEarlyReference(localEnv, tree.hasTag(SELECT) ? ((JCFieldAccess)tree).selected : null, sym)) {
                    // Field may not be inherited from a superclass
                    if (sym.owner != localEnv.enclClass.sym) {
                        log.error(tree, Errors.CantRefBeforeCtorCalled(sym));
                        return;
                    }

                    // Field may not have an initializer
                    if ((sym.flags() & HASINIT) != 0) {
                        log.error(tree, Errors.CantAssignInitializedBeforeCtorCalled(sym));
                        return;
                    }
                }
                return;
            };

At the start of analyzeSymbol seems to work fine. Maybe we want to break analyzeSymbol in different parts, one for reads and one for writes.

But one nice consequence of this is that now isEarlyReference is only called inside this visitor -- this means we can probably avoid having to pass isPrologue = true and, we can also avoid this dance:

tree.hasTag(SELECT) ? ((JCFieldAccess)tree).selected : null

Which is required at every callsite.

addressed in last commit, 9014afc, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ready Pull request is ready to be integrated rfr Pull request is ready for review
Development

Successfully merging this pull request may close these issues.

4 participants