User model stacking: current status [Observer discussion]
João Mendonça
jf.mend at gmail.com
Tue Jun 21 18:51:08 UTC 2022
Hello again,
Pondering a bit more on Tim Feuerbach's fine analysis of the current state
of the discussion concerning the illegal zeros leak problem, has made me
reconsider its effects on the user-model I had suggested in my "no-subject"
email:
*Inlining in shared-variables (§17.4.1.) makes it impossible to hide the
zero-value*
therefore:
*No constructor invariant of a value-class can exclude the zero-value*
This is actually bad. It seems to me that most Records are effectively
value-based classes, and I was thinking that it would be nice to make them
value-classes, not for the inlining, but to clearly express the intended
semantics and get help from the compiler/runtime to avoid bugs, such as
synchronizing on them or comparing their identity. This is a problem for
the user-model I had suggested, because all these Records are filled
with "requireNonNull"
in their constructors (using "!" is the same), and having to keep in mind
that none of this can really be trusted upon is just awful.
So, biting the bullet, how about this user-model:
For class-authors:
- *value-knob* to reject identity - Applicable on class declarations. Used
by a class-author to indicate that the class instances don't require
identity (a value-class).
- *zero-knob* to indicate that the value-class has a zero-value - if a
value-class does not have a zero-value, its instances won't be inlined in
any shared-variables since this is the only way for the language to ensure
the non-existence of the zero-value. If the value-class is declared with a
zero-value, then care must be taken when reading/writing constructors since
*no constructor invariant can exclude the zero-value*.
- *tearable-knob* to allow tearing - Applicable on zero value-class
declarations, may be used by the class-author to hand the class-user the
responsibility of how to avoid tearing, freeing the runtime to always
inline instances in shared-mutables (non-final shared-variables).
Conversely, if this knob is not used, instances will be kept atomic, which
allows the class-author to guarantee constructor invariants *provided
they're not broken by the zero-value*, which may be useful for the class
implementation and class-users to rely upon.
For class-users:
- *not-nullable-knob* to exclude null from a variable's value-set -
Applicable on any variable declaration. *All non-nullable shared-mutables
must be definitely-assigned*. For nullable variables, the default value is
null and, in either encoding-mode, the runtime is free to choose the
encoding for the extra bit of information required to represent whether or
not a variable is null.
- *atomic-knob* to avoid tearing - Applicable on shared-mutable
declarations, may be used by the class-user to reverse the effect of the
tearable-knob, thereby restoring atomicity.
Requiring definite-assignment to all non-nullable shared-mutables is useful
to get rid of missed-initialization-bugs. As Tim explained to me, to
initialize non-nullables with zero-values we can assign the constant
ValueClass.zero or the array constructor new ValueClass.zero[size] which
can be optimized away by the compiler.
In this model, all value-based classes are migrated to (non-tearable) zero
value-classes. So, even though LocalDate is a zero value-class, due to
definite-assignment, it will be very hard to get an accidental "Jan 1,
1970". Rational can also be a zero value-class but users will have to keep
in mind that it's possible to get a zero-denominator Rational, even if the
constructor throws when we try to build one.
Most Records, on the other hand, will be (no-zero) value-classes, making
all their constructor invariants always reliable. They won't be inlined in
shared-variables but, due to their big size, most wouldn't be anyway.
The runtime chooses the encoding-mode of a ClassType variable according to
this ternary expression:
var encodingMode =
: !valueClass(variable.type) ? REFERENCE
: tooBig(variable.type.bitSize) ? REFERENCE
// Illegal zeros:
: !shared(variable) ? INLINE
: !zeroValueClass(variable.type) ? REFERENCE
// Atomicity:
: final(variable) ? INLINE
: atomicWrite(variable.type.bitSize) ? INLINE
: atomic(variable) ? REFERENCE // atomic-knob
: tearableValueClass(variable.type) ? INLINE
: REFERENCE;
The variable.type.bitSize changes with nullability i.e. a nullable type
will have a higher bitSize due to the extra bits required to also represent
null.
The predicates tooBig and atomicWrite depend on the hardware specs. For
example, on my laptop they could be (written in the "Concise Method Bodies"
style):
boolean tooBig(int bitSize) -> bitSize>256;
boolean atomicWrite(int bitSize) -> bitSize<=64;
João Mendonça
PS: I realize that, probably, all of this has been analyzed and discussed
to death during the past 7 years in this mailing list, and I, for only
having learned a tiny bit of it, am just making a fool out of myself with
all these silly suggestions. If that is the case, please let me know and I
apologize in advance for wasting your time.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/valhalla-spec-observers/attachments/20220621/099899cd/attachment-0001.htm>
More information about the valhalla-spec-observers
mailing list