cljrs-value 0.1.206

Runtime Value type and persistent collections for clojurust
Documentation

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

pub enum Value {
    // Scalars
    Nil,
    Bool(bool),
    Long(i64),
    Double(f64),
    BigInt(GcPtr<num_bigint::BigInt>),
    BigDecimal(GcPtr<bigdecimal::BigDecimal>),
    Ratio(GcPtr<num_rational::Ratio<num_bigint::BigInt>>),
    Char(char),
    Str(GcPtr<String>),
    // Identifiers
    Symbol(GcPtr<Symbol>),
    Keyword(GcPtr<Keyword>),
    // Collections
    List(GcPtr<PersistentList>),
    Vector(GcPtr<PersistentVector>),
    Map(MapValue),
    Set(GcPtr<PersistentHashSet>),
    Queue(GcPtr<PersistentQueue>),
    // Lazy sequences (Phase 5)
    LazySeq(GcPtr<LazySeq>),   // deferred sequence; forced at most once
    Cons(GcPtr<CljxCons>),     // cons cell with lazy-capable tail
    // Runtime objects
    Var(GcPtr<Var>),
    Atom(GcPtr<Atom>),
    SharedAtom(Arc<SharedAtom>),       // cross-isolate mutable ref (Phase B3)
    ByteBlob(Arc<[u8]>),               // refcounted immutable byte buffer (Phase B3)
    Namespace(GcPtr<Namespace>),
    NativeFn(GcPtr<NativeFn>),
    CljxFn(GcPtr<CljxFn>),
    // Protocols & Multimethods (Phase 6)
    Protocol(GcPtr<Protocol>),
    ProtocolFn(GcPtr<ProtocolFn>),
    MultiFn(GcPtr<MultiFn>),
    // Concurrency primitives (Phase 7)
    Volatile(GcPtr<Volatile>),
    Delay(GcPtr<Delay>),
    Promise(GcPtr<CljxPromise>),
    Future(GcPtr<CljxFuture>),
    Agent(GcPtr<Agent>),

    // Records / reify (Phase 6-ext)
    TypeInstance(GcPtr<TypeInstance>),
}

pub enum MapValue {
    Array(GcPtr<PersistentArrayMap>),
    Hash(GcPtr<PersistentHashMap>),
}

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

pub struct Symbol   { namespace: Option<Arc<str>>, name: Arc<str>, version: Option<Arc<str>> }
pub struct Keyword  { namespace: Option<Arc<str>>, name: Arc<str> }

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)

pub fn intern_keyword(namespace: Option<&str>, name: &str)
    -> StaticGcPtr<Keyword>;
pub fn intern_symbol(namespace: Option<&str>, name: &str, version: Option<&str>)
    -> StaticGcPtr<Symbol>;

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)

pub enum SharedValue {
    Nil, Bool(bool), Long(i64), Double(f64), Char(char), Uuid(u128),
    Str(Arc<str>),
    Keyword(StaticGcPtr<Keyword>),
    Symbol(StaticGcPtr<Symbol>),
    ByteBlob(Arc<[u8]>),              // BEAM off-heap-binary trick
}

pub struct SharedAtom {
    pub cell: Arc<ArcSwap<SharedValue>>,
    pub meta: Mutex<Option<SharedValue>>,
}

impl SharedAtom {
    pub fn new(val: SharedValue) -> Self
    pub fn deref_val(&self) -> Arc<SharedValue>     // atomic load
    pub fn reset(&self, val: SharedValue) -> Arc<SharedValue>
    pub fn swap<F>(&self, f: F) -> Arc<SharedValue> // CAS-retry (closure form)
    pub fn compare_and_set(&self, current: &Arc<SharedValue>, new: SharedValue) -> bool
}

pub fn promote(value: &Value)   -> Result<SharedValue, PromoteError>;
pub fn demote (sv:    &SharedValue) -> Value;

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:

pub struct Var {
    pub value: Mutex<Option<Value>>,                          // isolate-local fast path
    pub shared_root: Arc<ArcSwap<Option<SharedValue>>>,       // cross-isolate mirror (B3)
    // …namespace, name, is_macro, meta, watches
}

impl Var {
    pub fn deref(&self) -> Option<Value>          // reads the local fast path
    pub fn deref_shared(&self) -> Option<Value>   // demotes the shared cell
    pub fn bind(&self, v: Value)                  // promote-on-def: updates both slots
    pub fn from_shared_root(ns, name, is_macro, shared_root) -> Self  // receiver side
}
  • Reads stay local. Every var deref, the IR tier, and the JIT/AOT rt_* ABI read value — promotion never touches it, so inline caches and pointer-identity assumptions in compiled code remain valid (no JIT regression).
  • bind promotes-on-write. def / alter-var-root / set! all funnel through Var::bind, which mirrors the new root into shared_root when it is promotable, and clears it to None otherwise. def is rare, so this write-path cost is acceptable.
  • Crossing isolates. clone::serialize passes the shared_root Arc through (both isolates share the same cell); the receiver rebuilds the var with from_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)): serialize returns CloneError::NotShareable { type_name: "var" } — a non-silent boundary error. Var-root watches stay isolate-local (the shared cell carries no watch callbacks), matching shared-atom.

ClojureHash

pub trait ClojureHash { fn clojure_hash(&self) -> u32; }

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, map_entry, is_map_entry
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>.

A PersistentVector may be tagged as a map entry — the [key val] pairs produced by seq'ing a map, find, or the map-entry builtin. PersistentVector::map_entry(key, val) builds one; is_map_entry() reads the tag (there is also a Value::map_entry(k, v) / Value::is_map_entry() convenience pair). The tag is invisible to equality, hashing, and printing — (= (first {:a 1}) [:a 1]) still holds — and it exists only so map-entry? can distinguish real entries from plain 2-element vectors. As in Clojure, any derived vector (conj, assoc_nth, pop, from_iter, ...) is a plain vector again.

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).
pub struct CljxFnArity {
    pub params: Vec<Arc<str>>,        // positional param names
    pub rest_param: Option<Arc<str>>, // name after & (if any)
    pub body: Vec<Form>,              // forms in this arity's body
    pub param_hints: Vec<Option<TypeHint>>, // primitive type hint per param (^long, …)
    pub rest_hint: Option<TypeHint>,         // hint on the rest param (rarely used)
    // (also: destructure_params, destructure_rest, ir_arity_id)
}

pub struct CljxFn {
    pub name: Option<Arc<str>>,
    pub arities: Vec<CljxFnArity>,
    pub closed_over_names: Vec<Arc<str>>,
    pub closed_over_vals: Vec<Value>,
    pub is_macro: bool,
    pub is_async: bool,       // ^:async — dispatched via the async runtime when one is registered
    pub defining_ns: Arc<str>,
    pub self_ptr: Option<GcPtr<CljxFn>>, // back-pointer for named-fn pointer identity (issue #194)
}

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.
pub fn set_var_rebind_hook(f: impl Fn(&Value, &Value) + Send + Sync + 'static);

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.
pub fn publish_value(v: Value) -> Value;

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)

pub struct Namespace {
    pub name: Arc<str>,
    pub interns: Mutex<HashMap<Arc<str>, GcPtr<Var>>>,        // own vars
    pub refers: Mutex<HashMap<Arc<str>, GcPtr<Var>>>,         // imported names
    pub aliases: Mutex<HashMap<Arc<str>, Arc<str>>>,          // ns alias → ns name
    pub source_file: Mutex<Option<Arc<str>>>,                 // path the ns was loaded from
    pub git_repo_root: Mutex<Option<Arc<str>>>,               // repo root of source_file, if any
    pub is_versioned: bool,                                   // true for `name@commit` namespaces
    pub meta: Mutex<Option<Value>>,                           // from `(ns ^{...} name ...)` / attr-map
}

impl Namespace {
    pub fn new(name: impl Into<Arc<str>>) -> Self;
    pub fn new_versioned(name: impl Into<Arc<str>>) -> Self;
    pub fn set_source_location(&self, file: &str, repo_root: Option<&str>);
    pub fn get_meta(&self) -> Option<Value>;
    pub fn set_meta(&self, m: Value);
}

Thunk / LazySeq / CljxCons (Phase 5)

pub trait Thunk: Send + Sync + std::fmt::Debug {
    fn force(&self) -> Value;
}

pub struct LazySeq {
    pub state: Mutex<LazySeqState>,  // Pending(Box<dyn Thunk>) | Forced(Value)
}
impl LazySeq {
    pub fn new(thunk: Box<dyn Thunk>) -> Self
    pub fn realize(&self) -> Value   // forces once, caches result
}

pub struct CljxCons {
    pub head: Value,
    pub tail: Value,   // may be LazySeq, Cons, List, or Nil
}

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)

pub struct TypeInstance {
    pub type_tag: Arc<str>,  // record name (defrecord) or gensym (reify)
    pub fields: MapValue,    // keyword → value
}

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)

pub struct Volatile { pub value: Mutex<Value> }

pub struct Delay { pub state: Mutex<DelayState> }  // Pending(Box<dyn Thunk>) | Forced(Value)

pub struct CljxPromise {
    pub value: Mutex<Option<Value>>,
    pub cond: Condvar,
}

pub struct CljxFuture {
    pub state: Mutex<FutureState>,  // Running | Done(Value) | Failed(String) | Cancelled
    pub cond: Condvar,
}

pub struct Agent {
    pub state: Arc<Mutex<Value>>,
    pub error: Arc<Mutex<Option<String>>>,
    pub sender: Mutex<SyncSender<AgentMsg>>,
}
pub type AgentFn = Box<dyn FnOnce(Value) -> Result<Value, String> + Send>;

Protocol / ProtocolFn / MultiFn (Phase 6)

pub struct Protocol {
    pub name: Arc<str>,
    pub ns: Arc<str>,
    pub methods: Vec<ProtocolMethod>,
    /// type_tag → { method_name → impl fn }
    pub impls: Mutex<HashMap<Arc<str>, MethodMap>>,
    /// Set by `(defprotocol Name :extend-via-metadata true ...)`; see dispatch
    /// note below.
    pub extend_via_metadata: bool,
}

pub struct ProtocolMethod {
    pub name: Arc<str>,
    pub min_arity: usize,
    pub variadic: bool,
}

pub struct ProtocolFn {
    pub protocol: GcPtr<Protocol>,
    pub method_name: Arc<str>,
    pub min_arity: usize,
    pub variadic: bool,
}

pub struct MultiFn {
    pub name: Arc<str>,
    pub dispatch_fn: Value,
    pub methods: Mutex<HashMap<String, Value>>,
    pub prefers: Mutex<HashMap<String, Vec<String>>>,
    pub default_dispatch: String,  // normally ":default"
}

/// 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.
pub fn protocol_generation() -> u64;
pub fn bump_protocol_generation();

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.
pub enum SerializedValue { Nil, Bool(bool), Long(i64), /**/ }

impl SerializedValue {
    /// Estimated heap bytes a deep copy of this value materializes on the
    /// receiver. Telemetry approximation for the metered boundary seam
    /// (`docs/isolate-boundary-plan.md`); Arc-shared payloads count as zero.
    pub fn byte_size(&self) -> usize;
}

/// Reason a value cannot cross an isolate boundary.
pub enum CloneError {
    NotShareable { type_name: &'static str },
    Disconnected,
}

/// Convert a Value to SerializedValue.  Returns CloneError for mutable state,
/// closures, native resources, and other non-shareable types.
pub fn serialize(v: &Value) -> Result<SerializedValue, CloneError>;

/// Allocate a fresh Value in the *current* GC heap from a SerializedValue.
/// Infallible — non-shareable types are rejected at serialize time.
pub fn deserialize(sv: SerializedValue) -> Value;

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).