Skip to content
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
338f5d8
removing all checks from Resolve
vicente-romero-oracle Jul 22, 2025
68bd63e
additional experiments
vicente-romero-oracle Aug 14, 2025
32c3f4c
Merge branch 'lworld' into JDK-8359370-v2
vicente-romero-oracle Aug 14, 2025
808fbef
additional changes
vicente-romero-oracle Aug 15, 2025
993b657
additional changes
vicente-romero-oracle Aug 15, 2025
fd2cd23
restoring removed class files
vicente-romero-oracle Aug 15, 2025
3b4e77d
minor fixes
vicente-romero-oracle Aug 15, 2025
c175f76
test changes
vicente-romero-oracle Aug 16, 2025
6433fe2
refactoring
vicente-romero-oracle Aug 18, 2025
399bd51
more changes
vicente-romero-oracle Aug 18, 2025
841bfc0
renaming methods in LocalProxyVarGen
vicente-romero-oracle Aug 19, 2025
48f12d2
merge with lworld
vicente-romero-oracle Aug 19, 2025
bfd0cda
bug fix
vicente-romero-oracle Aug 20, 2025
af7939a
another bug fix
vicente-romero-oracle Aug 21, 2025
e55b897
refactorings
vicente-romero-oracle Aug 21, 2025
260ddf3
merge with lworld
vicente-romero-oracle Aug 21, 2025
0eb8168
fixing bugs: new test cases brought in with latest merge are not acce…
vicente-romero-oracle Aug 21, 2025
31c7b8c
minor refactorings and simplifications
vicente-romero-oracle Aug 22, 2025
a5f2947
more simplifications and bug fixes
vicente-romero-oracle Aug 22, 2025
5cb0f42
documentation
vicente-romero-oracle Aug 26, 2025
cdfdb76
minor bug fix
vicente-romero-oracle Aug 26, 2025
74945ee
some simplifications
vicente-romero-oracle Aug 26, 2025
b7eab98
adding documentation
vicente-romero-oracle Aug 27, 2025
3965918
code simplifications
vicente-romero-oracle Aug 27, 2025
ad1fb80
minor diff
vicente-romero-oracle Aug 28, 2025
29fc914
addressing review comments
vicente-romero-oracle Aug 28, 2025
48a4713
removing unnecessary imports
vicente-romero-oracle Aug 28, 2025
c987a08
addressing review comments
vicente-romero-oracle Aug 29, 2025
32ccabf
additional changes, more tests
vicente-romero-oracle Aug 29, 2025
b101d1e
moving isEarlyReference to Attr
vicente-romero-oracle Aug 29, 2025
e87108d
some documentation
vicente-romero-oracle Aug 29, 2025
9014afc
addressing review comments
vicente-romero-oracle Sep 1, 2025
792d98f
minor refactoring
vicente-romero-oracle Sep 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Attr.java
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ public class Attr extends JCTree.Visitor {
final ArgumentAttr argumentAttr;
final MatchBindingsComputer matchBindingsComputer;
final AttrRecover attrRecover;
final LocalProxyVarsGen localProxyVarsGen;

public static Attr instance(Context context) {
Attr instance = context.get(attrKey);
Expand Down Expand Up @@ -163,6 +164,7 @@ protected Attr(Context context) {
argumentAttr = ArgumentAttr.instance(context);
matchBindingsComputer = MatchBindingsComputer.instance(context);
attrRecover = AttrRecover.instance(context);
localProxyVarsGen = LocalProxyVarsGen.instance(context);

Options options = Options.instance(context);

Expand Down Expand Up @@ -1252,6 +1254,25 @@ public void visitMethodDef(JCMethodDecl tree) {

// Attribute method body.
attribStat(tree.body, localEnv);
if (isConstructor) {
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.

* including the constructor invocation, that way we don't need to worry in the visitor below if
* if we are dealing or not with prologue code
*/
if (stat instanceof JCExpressionStatement expStmt &&
expStmt.expr instanceof JCMethodInvocation mi &&
TreeInfo.isConstructorCall(mi)) {
break;
}
}
if (!prologueCode.isEmpty()) {
CtorPrologueVisitor ctorPrologueVisitor = new CtorPrologueVisitor(localEnv);
ctorPrologueVisitor.scan(prologueCode.toList());
}
}
}

localEnv.info.scope.leave();
Expand All @@ -1263,6 +1284,205 @@ public void visitMethodDef(JCMethodDecl tree) {
}
}

class CtorPrologueVisitor extends TreeScanner {
Env<AttrContext> localEnv;
CtorPrologueVisitor(Env<AttrContext> localEnv) {
this.localEnv = localEnv;
}

boolean insideLambdaOrClassDef = false;

@Override
public void visitLambda(JCLambda lambda) {
boolean previousInsideLambdaOrClassDef = insideLambdaOrClassDef;
try {
insideLambdaOrClassDef = true;
super.visitLambda(lambda);
} finally {
insideLambdaOrClassDef = previousInsideLambdaOrClassDef;
}
}

@Override
public void visitClassDef(JCClassDecl classDecl) {
boolean previousInsideLambdaOrClassDef = insideLambdaOrClassDef;
try {
insideLambdaOrClassDef = true;
super.visitClassDef(classDecl);
} finally {
insideLambdaOrClassDef = previousInsideLambdaOrClassDef;
}
}

private void reportPrologueError(JCTree tree, Symbol sym) {
preview.checkSourceLevel(tree, Feature.FLEXIBLE_CONSTRUCTORS);
log.error(tree, Errors.CantRefBeforeCtorCalled(sym));
}

@Override
public void visitApply(JCMethodInvocation tree) {
super.visitApply(tree);
Name name = TreeInfo.name(tree.meth);
boolean isConstructorCall = name == names._this || name == names._super;
Symbol msym = TreeInfo.symbolFor(tree.meth);
// is this an instance method call or an illegal constructor invocation like: `this.super()`?
if (msym != null && // for erroneous invocations msym can be null, ignore those
(!isConstructorCall ||
isConstructorCall && tree.meth.hasTag(SELECT))) {
if (rs.isEarlyReference(
true,
localEnv,
tree.meth instanceof JCFieldAccess fa ? fa.selected : null,
msym))
reportPrologueError(tree.meth, msym);
}
}

@Override
public void visitIdent(JCIdent tree) {
// skip if this identifier is part of a select, context is important here
if (!analyzingSelect) {
analyzeSymbol(tree);
}
}

boolean analyzingSelect = false;

@Override
public void visitSelect(JCFieldAccess tree) {
// skip if part of a larger select, context is important here
if (!analyzingSelect) {
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 :(

analyzeSymbol(tree);
} finally {
analyzingSelect = previousAnalyzingSelect;
}
}
}

@Override
public void visitNewClass(JCNewClass tree) {
super.visitNewClass(tree);
checkNewClassAndMethRefs(tree, tree.type);
}

@Override
public void visitReference(JCMemberReference tree) {
super.visitReference(tree);
if (tree.getMode() == JCMemberReference.ReferenceMode.NEW) {
checkNewClassAndMethRefs(tree, tree.expr.type);
}
}

void checkNewClassAndMethRefs(JCTree tree, Type t) {
if (t.tsym.isEnclosedBy(localEnv.enclClass.sym) &&
!t.tsym.isStatic() &&
!t.tsym.isDirectlyOrIndirectlyLocal()) {
reportPrologueError(tree, t.getEnclosingType().tsym);
}
}

/* if a symbol is in the LHS of an assignment expression we won't consider it as a candidate
* for a proxy local variable later on
*/
boolean isInLHS = false;

@Override
public void visitAssign(JCAssign tree) {
boolean previousIsInLHS = isInLHS;
try {
isInLHS = true;
scan(tree.lhs);
} finally {
isInLHS = previousIsInLHS;
}
scan(tree.rhs);
}

@Override
public void visitMethodDef(JCMethodDecl tree) {
// ignore any declarative part, mainly to avoid scanning receiver parameters
scan(tree.body);
}

void analyzeSymbol(JCTree tree) {
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.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

types,
(ClassType)localEnv.enclClass.sym.type,
tree)) {
reportPrologueError(tree, sym);
}
} else if (sym.kind == VAR && sym.owner.kind == TYP) { // now fields only
if (sym.owner != localEnv.enclClass.sym) {
if (localEnv.enclClass.sym.isSubClass(sym.owner, types) &&
sym.isInheritedIn(localEnv.enclClass.sym, types)) {
/* if we are dealing with a field that doesn't belong to the current class, but the
* field is inherited, this is an error. Unless, the super class is also an outer
* class and the field's qualifier refers to the outer class
*/
if (tree.hasTag(IDENT) ||
TreeInfo.isExplicitThisReference(
types,
(ClassType)localEnv.enclClass.sym.type,
((JCFieldAccess)tree).selected)) {
reportPrologueError(tree, sym);
}
}
} else if (rs.isEarlyReference( // now this is a `proper` instance field of the current class
true,
localEnv,
tree.hasTag(SELECT) ? ((JCFieldAccess)tree).selected : null,
sym
)) {
/* 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

(!localEnv.enclClass.sym.isValueClass() && (sym.flags_field & HASINIT) != 0))
reportPrologueError(tree, sym);
// we will need to generate a proxy for this field later on
if (!isInLHS)
localProxyVarsGen.addFieldReadInPrologue(localEnv.enclMethod, sym);
}
}
}
}
}

boolean isMethodArgument(JCTree tree) {
JCTree treeToCheck = null;
if (tree.hasTag(IDENT)) {
treeToCheck = tree;
} else if (tree instanceof JCFieldAccess) {
JCFieldAccess fa = (JCFieldAccess) tree;
while (fa.selected.hasTag(SELECT)) {
fa = (JCFieldAccess)fa.selected;
}
treeToCheck = fa;
}
if (treeToCheck != null) {
Symbol sym = TreeInfo.symbolFor(
treeToCheck instanceof JCFieldAccess fa ?
fa.selected :
treeToCheck
);
if (sym != null){
return sym.owner.kind == MTH;
}
}
return false;
}
}

public void visitVarDef(JCVariableDecl tree) {
// Local variables have not been entered yet, so we need to do it now:
if (env.info.scope.owner.kind == MTH || env.info.scope.owner.kind == VAR) {
Expand Down Expand Up @@ -1335,6 +1555,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

ctorPrologueVisitor.scan(tree.init);
}
} finally {
initEnv.info.ctorPrologue = previousCtorPrologue;
}
Expand Down
Loading