Forward references in initializers

John Rose john.r.rose at oracle.com
Sat Nov 9 22:59:23 UTC 2024


On 8 Nov 2024, at 13:49, Dan Smith wrote:

> On Nov 8, 2024, at 11:59 AM, Dan Smith <daniel.smith at oracle.com> wrote:
>
> Conclusion: I think I'm happy with a DA/DU analysis that treats initializers as if they run in left-to-right order, before the start of the constructor. It's not really true, but it detects the errors we need to detect with less complexity.
>
> Ugh, never mind, spoke too soon.
>
> We have rules that say:
> - All blank final instance fields must be DU before a 'this()' invocation occurs
> - All blank strict instance fields must be DA before a 'super()' invocation occurs
>
> These rules rely on a more nuanced analysis: the 'this()' rule assumes no initializers have run, while the 'super()' rule assumes early initializers have run. You only get those outcomes by modeling the assignments at the proper place where they will actually occur at run time, and excluding any initializers that won't run until after the invocation.
>
> Plus the rules about assignments in the initializers and the prologue need to somehow pass information between each other to prevent multiple assignments.
>
> So I think we'll have to stick with the early vs. late DA analysis, even though it works differently than the simpler left-to-right forward reference restriction. And this means DATest should be allowed.

Yes.  It’s ugly, though.

(CLARIFICATION: The fields in any value class can be redundantly
marked final, as you have here, BUT ALSO they should be viewed
as marked strict, even if we don’t have a syntax for that.
I’m going to pretend we have a __Strict keyword.)

Here’s what you wrote, with __Strict added:

value class DATest {
    __Strict final String s1;
    { System.out.println(s1); }
    __Strict final String s2 = (s1 = "abc");
}

It’s legal because the s2-init in pre-super, and so
precedes the instance-init, which is post-super.

Here is how that class SHOULD be written, to keep it
from being so puzzling to read:

value class DATest_b {
    __Strict final String s1;
    __Strict final String s2 = (s1 = "abc");
    { System.out.println(s1); }
}


Let’s just pretend for a second that we could annotate
the instance-init with __Strict as well, meaning the
code gets moved to pre-super.  Then we could try moving
the instance-init to pre-super:

value class DATest_c {
    __Strict final String s1;
    __Strict { System.out.println(s1); }
    __Strict final String s2 = (s1 = "abc");
}

Gross.  And also useless.  As I think you observed,
s1 cannot be read in a pre-super context.  I am happy
to acknowledge that there is no call for marking an
instance-init with __Strict or any other marker that
would change its ordering.

DATest_c is just wrong, and the original DATest is
a puzzler because it reads like it could be DATest_c.

OK, so given all that, I see that it would be nice
to employ the left-to-right rule, somehow, to encourage
(or even force) programmers to write DATest_b to avoid
appearances of disorder.

I have an idea here, which I think might pan out:
Amend the left-to-right order rule (for non-statics
only) to allow only the nicely readable DATest_b.
We keep unchanged the existing constraints from Java 1.1,
of DA/DU conditions, and left-to-right name def-to-use order,
among non-statics.  (Same story independently for statics,
of course.)  THEN, for non-statics only (because there is
a distinction between pre-super and post-super inits), we
require that all pre-super inits are to the left of all
post-super inits.  We have five cases of declarations
which interact with init-order (either as use or def of
fields).

A. a strict field with an initializer (pre-super, has-init)
B. a strict field with no initializer (pre-super, no-init)
C. an instance initialization block (post-super, has-init)
D. a non-strict field with an initializer (post-super, has-init)
E. a non-strict field with no initializer (post-super?, no-init)

The idea I’m trying out here is that any of the five cases
that executes init code (A, C, or D) must have an extra
left-to-right constraint that places it either with all
of the pre-super codes (all A cases together) or all of
the post-super codes (all C and D cases together).  It
boils down to a constraint that all strict initializers
(case A) must precede (in left-to-right order) all non-strict
initializers (cases C and D).  There would be no extra
constraint on the no-init cases (B, E), but the existing
rule (from Java 1) would keep them before any references.

Is case E uniquely strange?  Because, different constructors
might initialize E either before or after the super.

non-value class TwoCons {
  __NonStrict int caseE;
  TwoCons(int x) {
     caseE = x;
     super();
  }
  TwoCons() {
     super();
     caseE = 42;
  }
}

I don’t think any of the other cases can have this ambiguity.
I also think it would be harmless to let case E go anywhere in
the left-to-right order.  He doesn’t have an init, and he
can’t be read during the pre-super phase anyway.  The same
reasoning applies to B.  Both B and E fields still must be
declared in the left-to-right order before any init that uses
the B or E field, per the old (Java 1) rule.

Boiling it down:  Classify init actions (field initializers
or instance initializers) as pre-super or post-super; only
strict field initializers are pre-super.  Then, require that
a post-super init action must never precede (in left to right
order) a pre-super init action (i.e., a strict field init).

That would remove one kind of paradoxical appearance of
disorder.  Is it worth it?  Maybe.

— John


More information about the valhalla-spec-experts mailing list