from computed constants to stable values

John Rose john.r.rose at oracle.com
Fri Jan 24 00:41:16 UTC 2025



On 22 Jan 2025, at 9:22, Maurizio Cimadamore wrote:

> Hi all,
> Per and I have been busy polishing the Computed Constant API. So much so that it has now changed name to Stable Value API :-)
>
> https://openjdk.org/jeps/502
>
> The goal is still to provide a safe API around JVM's @Stable value annotation. But, as we experimented with the old API, we realized that its "lambda-oriented" design, while good, was too restrictive when clients wanted to "set" the computed constant in a more imperative fashion.
>
> In the new Stable Value API, stable values are created unset. They can be set imperatively, or more functionally using a method similar to Map::computeIfAbsent. If you squint, the old computed constants have not disappeared: a computed constant can be expressed as a supplier that wraps a stable value --
> we call such a supplier a "stable supplier".
>
> Per's talk at Devoxx 2024 is a good companion to the new JEP, with lots of interesting live examples:
>
> https://www.youtube.com/watch?v=3fXHrK3yV9w
>

I’m glad to see this vision move forward.  I have comments,
some about the JEP, some about the draft API, and some about
future connections with Leyden.

The JEP notes the existence of the internal @Stable annotation.
FTR, a forthcoming Leyden push to mainline will publish a fuller
documentation for it.

https://github.com/openjdk/leyden/blob/premain/src/java.base/share/classes/jdk/internal/vm/annotation/Stable.java

That’s not needed for the JEP, but there’s slightly misleading
language in the JEP about @Stable:

> This annotation, a common feature of low-level JDK code, guarantees that the field, even though non-final, can be trusted not to change after its initial and only update

That’s true as far as it goes, but it does not specify WHO is making
the guarantee, and the reader may wrongly surmise that it is the VM
which is somehow making the guarantee.  In fact it is the code author
(the JDK engineering team!) which makes this guarantee.  (That’s why
it is an internal annotation; we can only speak for ourselves.)

I suggest this amended text to avoid misleading the reader:

> This annotation, a common feature of low-level JDK code, -guarantees- +asserts+ that +the VM may trust+ the field, even though non-final, -can be trusted- not to change after -its initial and only update- +the VM considers its value for optimization+.

(There is more detail in the spec, of course.  The mention of the unique
update is an oversimplification.  The uniqueness of the stable value
is true for the uses of @Stable contemplated by the JEP, but not exactly
true for some other uses of @Stable!)

Minor grammar problem:

> Both FIB and Fibonacci::fib recurses into each other.

s/recurses/recurse/

IMO, the JEP and/or API docs could make it clearer that
composite stable values (what I would call composite lazy
values) can be expected to save on footprint, compared
to containers of multiple stable values.  A lazy list or
map probably has a compact backing store for its states,
as well as a shared lambda.

Maybe a good place to slip in a comment about the
scalability of stable (or lazy) composites is in
the JEP section where you talk about double-checked
locking on arrays.  What you are really saying there
is that existing workarounds for lazy values do not
scale well to composite bundles of lazy values.

Doing the tricky internal work, just once, to support
scalable groups of stable/lazy values, and then packaging
it into a usable API, is a deep service to the ecosystem.

(Perhaps you considered but dropped an API point to
expose List<StableValue<T>> for some IntFunction<T>.
We can add it later if it pays for itself.  It could
more directly expose the footprint saving effects of
composite stable values.  The point of such a list
is it would allow imperative setting of the list
elements.  The list would of course be a view on
the internal compact stable state that underlies
any stable composite.  I mean a private @Stable
array field somewhere, of course.  The list API
point you supply, which supports only non-imperative
lazy usage, is the correct one to lead with, since
lazy values are easier to use than full stable
values.)

By the way, your API rightly supports only finite
composites, by ensuring that the relevant domain
(int size for lists, set for functions and maps)
is specified.  This allows internal numbering for
the internal stable array that backs the composite.
Later on, we can lift the finiteness restriction
and support open-ended composites.  I wrote a POC
of that here:

https://cr.openjdk.org/~jrose/draft/StableList.java.txt

(It’s for later, not now.  But there’s the POC.  The
compiler can constant-fold through the stable links of
an unboundedly growable data structure.)

The JEP claims you can pass OrderController::new to the
list combinator.  It makes a nice demo, but there is a
problem:  Either there is an OrderController constructor
that accepts an int argument (to be used as a list index)
or else StableValue::list is fatally flawed, since it
cannot accept a lambda that takes a argument to specify
the index of the lazy value within the list. I see the
draft javadoc has an IntFunction argument to list, so
all is well there; the JEP sample code is oversimplified.
You could add overloads to the API to save the JEP sample
code. (That is, Supplier as well as IntFunction inputs.)

Personally, I find it odd that the JEP uses the
word “eagerly” several times, but “lazy” (“laziness”)
only once in the context of the class-holder idiom.
I see the list, supplier, function, and map API points
in StableValue as (finite) lazy value combinators.
(Or factories.  They are combinators because they
combine with lambdas, although they do not all
produce further functions.)

I also feel a slight dissonance between the
“of” and ofUnset API points and the combinators.
The first pair are used to create stable values
per se, and the second are used to create lazy
values.  I don’t have a proposal to improve that.
I don’t think you need to split out LazyList,
LazyMap, and other types for the lazy combinators.

Relatedly, I feel the “of” method is misnamed.
Normally, a method named “of” is the first one
you should reach for when using an API.  But in
this case it is the last, the least useful API
point, a stable value with no future changes.
But stable values are all about leaving the
future flexible.  Suggest s/of/ofConstant/.

(And personally, I think “ofFoo” methods which
take no arguments are ugly.  But that’s just
me, for sure.)

To summarize:  There are a number of directions
for future growth of lazy data structures, and
even explicitly imperative ones (using Stable<T>
views of internal states).  The existing API
and JEP provide a foundation for this.

There are three possible connections here to
Leyden proper (especially Premain work).

First and most simply, if you have an app that
contains stable values (either scalars or composites),
and the training run binds them, then there is a
reasonable hope that we can include such bindings
in a future version of Leyden.  The access paths
which reach the stable values must be made shiftable
first, which means it’s not a slam dunk, but we are
getting there by steps.

(What do I mean by access paths?  I mean API points
like static finals and access methods which reach
stable values.  A Leyden Premain assembly phase needs
to acquire permission to execute such API points.
This means shifting of class initializers, which
presupposes class initializers that are simple
enough to shift, and transparent enough to be
proved simple enough to shift.  Working on it…)

Second, and presupposing that we have found a way
to make access paths shiftable to a premain assembly
phase, we may wish to provide a way to mark stable
values as desirable to execute in an assembly phase,
even if the training run has not provided evidence
that they should be executed.  This is more speculative,
because the best evidence that something should be
executed is that a training run has executed it.
But there might be something here.  There are
proposals for marking the stable data structures
themselves by calling special factories, and other
proposals for annotations, and yet others for
command sets, and still others which perform
global analysis that selects the stable values
without the need for special markings.  Clearly
this is an area for further research.  The common
foundation of all such research is the existence of
a stable value for which early execution is an
option to consider.   This JEP provides that
foundation!

A third connection for Leyden is even more researchy,
and that is the question of language features to support
refactoring of Java APIs to be more Leyden friendly.
Since Leyden is (in part) about shifting computations,
and stable values are units of shiftable computation,
it seems likely that stable values will help us manage
the shifting of computations to their correct time
(assembly phase, production run, etc.).  If we figure
out a few idioms with stable values that really pay
off, we might want to enshrine them in the language,
as an extra encouragement to the rest of the world,
to refactor their code to be more Leyden-friendly.
Did I mention this is very very researchy?  I think
there could be a payoff if we figure out, not only
some sort of sugar for “just the right kind” of
stable values for Leyden, but also the same sugar
for making class initializer code more declarative
(analyzable, provably shiftable).

For example, lambdas are already provably shiftable.
This is true as of JDK 24 in JEP 483.  If we could
encourage programmers to use only lambdas and well
behaved data structures like those of StableValue,
and have nothing else in class initializers, then
said initializers could be shifted to premain
(to an assembly phase that runs long before main).

This can be done, with discipline, in today’s language.
But I have to wonder if, eventually, we will want to
ask the language to help.  Full disclosure:  That is
why I think we will eventually want lazy statics, which
have those provable properties.  But it’s still just
a guess, at this point.  If we do add lazy statics,
they will be built on top of something like stable
value composites, I think.  Again, that is just a
speculation about the future, but I think the present
work is foundational for many interesting things.

Cheers!
— John


More information about the leyden-dev mailing list