cljrs-value
Core runtime values and persistent collections for clojurust.
Phase: 3 (collections/Value) + 4 (CljxFn, Namespace) + 5 (LazySeq, CljxCons) + 6 (Protocol, ProtocolFn, MultiFn) + 7 (Volatile, Delay, CljxPromise, CljxFuture, Agent) + 6-ext (TypeInstance for defrecord/reify) + B2 (structured-clone boundary) + B3 (shared static arena: intern tables, SharedValue, SharedAtom, ByteBlob) — implemented.
Purpose
Defines Value, the single enum that represents every Clojure runtime value,
plus all persistent (immutable, structurally shared) collection types. The
cljrs-eval crate will operate on Values; cljrs-runtime will build the
standard library on top of them.
File layout
src/
lib.rs — module declarations and re-exports
clone.rs — SerializedValue (Send+Sync wire form), CloneError, serialize/deserialize for cross-isolate copy boundary (Phase B2); SerializedValue::byte_size for boundary metering; handles SharedAtom/ByteBlob/Var pass-through (B3 — Var shares its root cell, issue #171)
error.rs — ValueError enum, ValueResult<T> alias
hash.rs — ClojureHash trait, Murmur3 helpers, JVM-compatible hash_string
intern.rs — (Phase B3) global keyword/symbol intern tables backed by StaticGcPtr; intern_keyword, intern_symbol
jit_hooks.rs — (Phases 10.2/10.5) var-rebind hooks fired by Var::bind; set_var_rebind_hook registers multiple consumers (the JIT stales superseded native code; cljrs-eval invalidates cross-defn-specialized lowerings)
keyword.rs — Keyword { namespace, name }
publish.rs — (Phase 10.5, GC builds; identity stub under no-gc) heap-promotion publish barrier: publish_value(Value) -> Value scans for region-allocated boxes, deep-copies them to the GC heap (via clone.rs), or poisons the active regions when the value is opaque to the scan. Called by Var::bind, Atom::new/reset, Volatile::new/reset, CljxPromise::deliver, and cljrs-async channel puts
shared.rs — (Phase B3) SharedValue enum, SharedAtom (Arc<ArcSwap<SharedValue>>), promote/demote; PromoteError. Var roots reuse SharedValue via Var::shared_root (issue #171)
symbol.rs — Symbol { namespace, name }
type_hint.rs — TypeHint enum (^long/^double/^longs/… primitive type tags) + from_tag/is_array/element
native_object.rs — NativeObject trait, NativeObjectBox wrapper, gc_native_object helper (Phase 9 interop)
types.rs — Var, Atom, Namespace, NativeFn, CljxFn, Thunk, LazySeq, CljxCons, Protocol, ProtocolFn, ProtocolMethod, MultiFn, Volatile, Delay, CljxPromise, CljxFuture, Agent
value.rs — Value enum (incl. SharedAtom, ByteBlob variants), MapValue, SetValue, TypeInstance, pr_str, PartialEq, ClojureHash, std::hash::Hash
collections/
mod.rs — re-exports all collection types
array_map.rs — PersistentArrayMap (≤8 entries, linear scan)
hash_map.rs — PersistentHashMap (32-way HAMT)
hash_set.rs — PersistentHashSet (backed by PersistentHashMap)
list.rs — PersistentList (singly-linked cons list)
queue.rs — PersistentQueue (front-list + rear-vector)
vector.rs — PersistentVector (32-way trie + tail buffer)
hamt/
mod.rs — re-exports Node and bitmap helpers
bitmap.rs — BITS, WIDTH, fragment, sparse_index, bit_for
node.rs — Node<V> enum (Leaf, Branch, Collision); HAMT trie operations
Public API
Value
PartialEq implements cross-type numeric equality ((= 1 1N), (= 1 1.0))
and sequential collection equality between List and Vector.
Display / pr_str produce Clojure-readable output.
Symbol / Keyword
Both support simple(name), qualified(ns, name), parse(str), and
full_name() -> String. Symbol additionally carries an optional git-commit
version (the @<hash> suffix) with versioned_name() -> String. Free
helpers used by all execution tiers to detect versioned names:
symbol::is_commit_hash(s) -> bool and
symbol::split_version(name) -> (&str, Option<&str>).
Phase B3 — Shared static arena
Intern tables (intern module)
;
;
Global OnceLock<Mutex<HashMap<…>>> tables. First call allocates the
Keyword/Symbol into program-lifetime memory via static_alloc; subsequent
calls return a clone of the same StaticGcPtr (pointer-stable identity across
all isolates).
SharedValue and SharedAtom (shared module)
;
;
promote converts an isolate-local Value to SharedValue (fails for
closures, resources, atoms, …). demote converts back into a fresh
isolate-local Value. compare_and_set is the single lock-free CAS that
backs the Clojure-level compare-and-set! and the swap! retry loop (callers
that must run interpreter code between load and store use it instead of the
closure-based swap).
Var roots — two-tier, promote-on-def (issue #171)
A var's root binding uses the same cross-isolate mechanism as
shared-atom. Var carries two slots:
- Reads stay local. Every var deref, the IR tier, and the JIT/AOT
rt_*ABI readvalue— promotion never touches it, so inline caches and pointer-identity assumptions in compiled code remain valid (no JIT regression). bindpromotes-on-write.def/alter-var-root/set!all funnel throughVar::bind, which mirrors the new root intoshared_rootwhen it is promotable, and clears it toNoneotherwise.defis rare, so this write-path cost is acceptable.- Crossing isolates.
clone::serializepasses theshared_rootArcthrough (both isolates share the same cell); the receiver rebuilds the var withfrom_shared_root, seeding its local slot from the demoted snapshot. A var bound to a non-promotable root (closure / native resource) is explicitly isolate-local (ADR option (b)):serializereturnsCloneError::NotShareable { type_name: "var" }— a non-silent boundary error. Var-root watches stay isolate-local (the shared cell carries no watch callbacks), matchingshared-atom.
ClojureHash
Implemented for Value using Murmur3 + JVM String.hashCode semantics.
Whole-number doubles hash like their Long equivalent.
Collections
| Type | Description | Key operations |
|---|---|---|
PersistentList |
Singly-linked cons list | cons, first, rest, count (O(1)) |
PersistentVector |
32-way trie + tail buffer | conj, nth, assoc_nth, pop, iter |
PersistentArrayMap |
Flat key/value vec, ≤8 entries | assoc (returns AssocResult), get, dissoc, iter |
PersistentHashMap |
32-way HAMT | assoc, get, dissoc, merge, iter, keys, vals |
PersistentHashSet |
Backed by PersistentHashMap |
conj, disj, contains, iter |
PersistentQueue |
Front-list + rear-vector | enqueue, dequeue, peek |
PersistentArrayMap::assoc returns AssocResult::Array(Self) while under the
threshold, or AssocResult::Promote(Vec<(Value, Value)>) when the map is full.
MapValue::assoc handles the transparent promotion to PersistentHashMap.
All collections implement PartialEq, Debug, Clone, and cljrs_gc::Trace.
PersistentList, PersistentVector, and PersistentHashSet implement
std::iter::FromIterator<Value>.
All collection Trace impls also override gc_size_extra to report the heap
bytes owned by each collection beyond the GcBox struct. Approximations used:
| Type | Formula |
|---|---|
PersistentArrayMap |
16 + capacity × size_of::<Value>() |
PersistentHashMap |
n × (40 + 2×size_of::<Value>()) |
PersistentHashSet |
n × (40 + size_of::<Value>()) |
PersistentVector |
n × (24 + size_of::<Value>()) |
SortedMap |
n × (40 + 2×size_of::<Value>()) |
TransientMap/Set |
same as HashMap/Set (locked at alloc) |
TransientVector |
same as Vector (locked at alloc) |
ObjectArray |
capacity × size_of::<Value>() |
| Primitive arrays | capacity × size_of::<T>() |
BoundFn |
capacity × (1 + size_of::<usize>() + size_of::<Value>()) |
ExceptionInfo |
message.capacity() |
The 40-byte per-entry overhead for HAMT/RBTree is: 16 bytes Arc ref-counts +
16 bytes EntryWithHash/left-right pointers + 8 bytes tree-node sharing. The
24-byte overhead for trie vector elements is: 16 bytes Arc overhead + 8 bytes
thin pointer in the leaf-node Vec.
CljxFn / CljxFnArity (Phase 4)
// Requires cljrs-reader (for Vec<Form> body).
is_async is set by the interpreter when a fn/defn carries ^:async (or an
{:async true} attr-map). CljxFn::new defaults it to false; cljrs-env's
dispatch_if_async checks it at call time.
self_ptr is set immediately after GcPtr::new(cljrs_fn) in eval_fn (for
named anonymous functions) so that the self-reference returned from the function
body is the same GcPtr as the outer binding, preserving pointer-equality
semantics ((= f (f)) → true). CljxFn::new defaults it to None.
Var-rebind hooks (jit_hooks, Phases 10.2/10.5)
/// Register a rebind hook. Multiple hooks may be registered; each is called
/// with (old_value, new_value) in registration order.
;
Var::bind invokes every registered hook (via notify_var_rebind) whenever
it overwrites an existing binding. Two consumers exist: the JIT stales and
reclaims native code compiled for the superseded definition (10.2), and
cljrs-eval's defn registry invalidates lowerings of other functions that
specialized against it (10.5). When no hook is registered the cost is a
single atomic flag load.
Heap-promotion publish barrier (publish, Phase 10.5 — GC builds)
/// Prepare a value for publication into a program-lifetime cell (or another
/// thread): returns the value to store — the original when no region-
/// allocated box is reachable, or a heap deep-copy when one is. Values
/// opaque to the scan (closures, unrealized lazy seqs, native objects)
/// poison the thread's active regions instead
/// (cljrs_gc::region::poison_active_regions), retiring them at scope close.
/// One thread-local depth check when no region is open.
;
The runtime safety net for bump regions coexisting with the tracing GC:
correctness never depends on escape analysis being perfect. Invoked by
Var::bind, Atom::new/Atom::reset, Volatile::new/Volatile::reset,
CljxPromise::deliver, and cljrs-async's channel puts. Under no-gc the
module is an identity stub (that build keeps its StaticCtxGuard discipline).
Namespace (Phase 4)
Thunk / LazySeq / CljxCons (Phase 5)
Thunk implementations live in cljrs-eval (e.g. ClosureThunk) so that
cljrs-value stays free of evaluator dependencies while LazySeq can still
call back through the trait object.
TypeInstance (Phase 6-ext — defrecord/reify)
Used by defrecord (named type_tag, generates ->Name/map->Name constructors) and
reify (gensym'd type_tag, no constructors). Supports keyword field access (:field rec),
get, assoc (returns new TypeInstance), and count.
Volatile / Delay / CljxPromise / CljxFuture / Agent (Phase 7)
// Pending(Box<dyn Thunk>) | Forced(Value)
pub type AgentFn = ;
Protocol / ProtocolFn / MultiFn (Phase 6)
/// Phase 10.6 — protocol-dispatch inline-cache invalidation.
/// `bump_protocol_generation()` must follow every mutation of any
/// `Protocol::impls` map (extend-type / extend-protocol / inline impls);
/// `rt_call_ic` (cljrs-compiler) tags each cached dispatch with the
/// generation observed at fill time and re-resolves on mismatch.
;
;
When Protocol::extend_via_metadata is set, apply_value's ProtocolFn arm
(cljrs-env/src/apply.rs) checks the dispatch value's metadata for an entry
keyed by the exact ProtocolFn before falling back to the impls type-tag
lookup — see that crate's README for the dispatch order.
clone — isolate copy boundary (Phase B2)
/// A Send + Sync intermediate representation for cross-isolate transfer.
/// All heap data is owned (no GcPtr); safe to move across thread boundaries.
/// Reason a value cannot cross an isolate boundary.
/// Convert a Value to SerializedValue. Returns CloneError for mutable state,
/// closures, native resources, and other non-shareable types.
;
/// Allocate a fresh Value in the *current* GC heap from a SerializedValue.
/// Infallible — non-shareable types are rejected at serialize time.
;
Shareable types: all scalars, strings, BigInt/BigDecimal/Ratio, Symbol/Keyword, all persistent collections, TypeInstance records, Error chains, primitive and object arrays, lazy sequences (realized first), WithMeta/Reduced wrappers.
Cross-isolate shared references (Arc passed through, not deep-copied): SharedAtom,
ByteBlob, and Var — a var crosses by sharing its shared_root cell, so a value
def'd in one isolate is observable by value in another (issue #171). A var
whose current root is non-promotable (closure/resource) is the exception and
returns CloneError.
Non-shareable (returns CloneError): Atom, Volatile, Promise, Future, Agent
(mutable state); Fn, BoundFn, NativeFn, Macro, ProtocolFn, MultiFn (closures
with isolate-local captures); Namespace, Protocol (global singletons); Resource,
NativeObject (isolate-bound handles); TransientMap/Set/Vector; unforced Delay;
Matcher; Var whose root holds a non-promotable value.
Dependencies
cljrs-value depends on cljrs-reader so that CljxFnArity::body can store
Vec<Form> (unevaluated source bodies for interpreter evaluation and closure
capture).