Let me try and put some more color on the bike shed (but, again, let’s focus on
model, not syntax, for now.)
We have two axes of variation we want to express with non-identity classes:
atomicity constraints, and whether there is an additional zero-default
companion type. These can be mostly orthogonal; you can have either, neither,
or both. We've been previously assuming that "primitiveness" lumps this all
together; primitives get more flattening, primitives can be
non-nullable/zero-default, primitives means the good name goes to the "val"
type. Primitive-ness implicitly flips the "safety vs performance" priority,
which has been bothering us because primitives also code like a class. So we
were trying to claw back some atomicity for primitives.
But also, we're a little unhappy with B2 because B2 comes with _more_ atomicity
than is necessarily needed; a B2 with no invariants still gets less flattening
than a B3. That's a little sad. And also that it seems like a gratuitous
difference, which makes the user model more complicated. So we’re suggesting
restacking towards:
- Value classes are those without identity
- Value classes can be atomic or non-atomic, the default is atomic (safe by
default)
- Value classes can further opt into having a "val" projection (name TBD, val
is probably not it)
- Val projections are non-nullable, zero-default — this is the only difference
- Both the ref and val projections inherit the atomicity constraints of the
class, making atomicity mostly orthogonal to ref/val/zero/null
Example: classic B2
value class B2a { }
Because the default is atomic, we get the classic B2 semantics -- no identity,
but full final field safety guarantees. VM has several strategies for
flattening in the heap: single-field classes always flattened (“full flat”),
multi-field classes can be flattened with "fat load and store" heroics in the
future (“low flat”), otherwise, indirection (“no flat”)
Example: non-atomic B2
non-atomic value class B2n { }
Here, the user has said "I have no atomicity rquirements." A B2n is a loose
aggregation of fields that can be individually written and read (full B3-like
flattening), with maybe an extra boolean field to encode null (VM's choice how
to encode, could use slack pointer bits etc.)
Example: atomic B3
zero-capable value class B3a { }
This says I am declaring two types, B3a and B3a.zero. (The syntax in this
quadrant sucks; need to find better.) B3a is just like B2a above, because we
haven’t activated the zero capability at the use site.
B3a.zero/val/flat/whatever is non-nullable, zero-default, *but still has full
B2-classic atomicity*. With the same set of flattening choices on the part of
the VM.
Example: full primitive
non-atomic zero-capable value class B3n { }
Here, B3n is like B2n, and B3n.zero is a full classic-B3 Q primitive with full
flattening.
So:
- value-ness means "no identity, == means state equality"
- You can add non-atomic to value-ness, meaning you give up state integrity
- You can orthogonally add zero-capable to value-ness, meaning you get a
non-null, zero-happy companion, which inherits the atomic-ness
Some of the characteristics of this scheme:
- The default is atomicity / integrity FOR ALL BUCKETS (safe by default)
- The default is nullability FOR ALL BUCKETS
- All unadorned type names are reference types / nullable
- All Val-adorned type names (X.val) are non-nullable (or .zero, or .whatever)
- Atomicity is determined by declaration site, can’t be changed at use site
The main syntactic hole is finding the right spelling for "zeroable" / .val.
There is some chance we can get away with spelling it `T!`, though this has
risks.
Spelling zero-happy as any form of “flat” is probably a bad idea, because B2
can still be flat.
A possible spelling for “non-atomic” is “relaxed”:
relaxed value class B3n { }
Boilerplate-measurers would point out that to get full flattening, you have to
say three things at the declaration site and one extra thing at the use site:
relaxed zero-happy value class Complex { }
…
Complex! c;
If you forget relaxed, you might get atomicity (but might not cost anything, if
the value is small.) If you forget zero-happy, you can’t say `Complex!`, you
can only say Complex, and the compiler will remind you. If you forget the !,
you maybe get some extra footprint for the null bit. None of these are too
bad, but the verbosity police might want to issue a warning here.
It is possible we might want to flip the declaration of zero-capable, where
classes with no good default can opt OUT of the zero companion, rather than the
the other way around:
null-default value class LocalDate { }
which says that LocalDate must use the nullable (LocalDate) form, not the
non-nullable (LocalDate.val/zero/bang) form.