vj-sim 0.3.0

Netlist, scheduler and process model of the vj SystemVerilog simulator.
Documentation
//! Classes plan (`docs/superpowers/plans/2026-10-03-classes.md`) Task 14, LRM
//! 1800-2023 18.14: the per-process random-number-generator state and the
//! hierarchical seeding that makes it stable.
//!
//! Every process owns one `u64` generator state (the `splitmix64` state
//! `vj-runtime`'s `$urandom` family draws through). LRM 18.14.1/18.14.2:
//! a thread's generator is seeded with the next random value of its PARENT --
//! the process that executed the `fork` for a dynamic child, the
//! **initialization RNG** of the containing module/interface/program
//! instance for a static (`initial`/`always`/...) process. Seeding happens at
//! the moment the child is created (the spawner draws one value for each
//! child, in source order), never lazily when the child first draws, so what a
//! thread observes depends only on the program's own structure and never on the
//! order the scheduler happens to run threads in -- the property 18.14.2's
//! worked example is about.
//!
//! The store lives here, not in `vj-runtime`, because the spawn hooks
//! ([`crate::sched::Engine`]'s `register_process` and its fork path) are
//! scheduler code and `vj-runtime` depends on this crate. The runtime's
//! `random_sink` reads and advances the same cells through
//! [`with_process_state`]. The state is thread-local, like the sink it
//! replaces: a simulation run executes on one OS thread.
//!
//! An initialization RNG starts at [`DEFAULT_SEED`] for every instance (the LRM
//! only asks for "an implementation-dependent value"; a constant keeps runs
//! exactly reproducible, RT-082).

use std::cell::{Cell, RefCell};
use std::collections::HashMap;

use super::ProcessId;

/// A fixed, arbitrary, non-zero seed -- deterministic by construction (RT-082:
/// two runs of the same design produce byte-identical `$random`/`$urandom`
/// output, so no wall-clock or address-derived value may ever feed it).
pub const DEFAULT_SEED: u64 = 0x1234_5678_9ABC_DEF1;

/// One `splitmix64` step (Vigna/Steele's public-domain generator): advances
/// `state` in place and returns the mixed 64-bit output. The one copy of the
/// algorithm: `vj-runtime`'s `random` module and the spawn-time seeding
/// below both call it, never a second implementation.
pub fn splitmix64_next(state: &mut u64) -> u64 {
    *state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
    let mut z = *state;
    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
    z ^ (z >> 31)
}

thread_local! {
    /// One generator state per process; `None` is "no process context" (a
    /// direct call from a unit test, or compiled code running outside any
    /// process).
    static PROCESS_STATE: RefCell<HashMap<Option<ProcessId>, u64>> = RefCell::new(HashMap::new());
    /// One initialization RNG per instance scope (the instance id the static
    /// process is registered under).
    static INIT_STATE: RefCell<HashMap<u32, u64>> = RefCell::new(HashMap::new());
    /// Processes that run a static declaration initializer: LRM 18.14.1 gives
    /// such code no thread of its own, so what it draws comes straight from
    /// the containing instance's initialization RNG -- these processes have no
    /// generator cell of their own, only the scope whose cell they use.
    static INIT_ALIAS: RefCell<HashMap<ProcessId, u32>> = RefCell::new(HashMap::new());
    /// The state every generator starts from (`--seed`); [`DEFAULT_SEED`]
    /// unless [`set_base_seed`] ran after the last [`reset`].
    static BASE_SEED: Cell<u64> = const { Cell::new(DEFAULT_SEED) };
}

/// The state a generator that was never seeded starts at.
pub fn base_seed() -> u64 {
    BASE_SEED.with(Cell::get)
}

/// `--seed N`: every generator of this run starts from a state derived from
/// `n` instead of [`DEFAULT_SEED`] (so `--seed 1` and `--seed 2` differ, and
/// the same `n` always reproduces). Call after [`reset`], which restores the
/// default.
pub fn set_base_seed(n: u64) {
    let mut state = DEFAULT_SEED ^ n;
    BASE_SEED.with(|b| b.set(splitmix64_next(&mut state)));
}

/// Forgets every generator (every process and every initialization RNG goes
/// back to [`DEFAULT_SEED`] on first use). Called once at the start of each
/// run, so a second run on the same OS thread starts from the same state as
/// the first -- the same per-run reset discipline as the `$display` buffer.
pub fn reset() {
    PROCESS_STATE.with(|s| s.borrow_mut().clear());
    INIT_STATE.with(|s| s.borrow_mut().clear());
    INIT_ALIAS.with(|s| s.borrow_mut().clear());
    BASE_SEED.with(|b| b.set(DEFAULT_SEED));
}

/// Runs `f` on `key`'s generator state, creating it at [`DEFAULT_SEED`] if it
/// was never seeded.
pub fn with_process_state<R>(key: Option<ProcessId>, f: impl FnOnce(&mut u64) -> R) -> R {
    if let Some(scope) = key.and_then(|id| INIT_ALIAS.with(|a| a.borrow().get(&id).copied())) {
        return INIT_STATE.with(|s| f(s.borrow_mut().entry(scope).or_insert(base_seed())));
    }
    PROCESS_STATE.with(|s| f(s.borrow_mut().entry(key).or_insert(base_seed())))
}

/// Gives process `id` the generator state `seed`, replacing whatever an
/// earlier process that held the same id left behind.
pub fn preseed(id: ProcessId, seed: u64) {
    clear_alias(id);
    PROCESS_STATE.with(|s| {
        s.borrow_mut().insert(Some(id), seed);
    });
}

/// Makes process `id` run as a static declaration initializer of instance scope
/// `scope` (LRM 18.14.1): from now on its generator IS the scope's
/// initialization RNG, so `$urandom` and `new` in the initializer draw from it
/// directly and the process consumes no seed of its own. Replaces whatever an
/// earlier process that held the same id left behind (until [`preseed`] runs).
pub fn alias_init(id: ProcessId, scope: u32) {
    INIT_ALIAS.with(|a| {
        a.borrow_mut().insert(id, scope);
    });
}

/// Detaches process `id` from any scope [`alias_init`] bound it to.
pub fn clear_alias(id: ProcessId) {
    INIT_ALIAS.with(|a| {
        a.borrow_mut().remove(&id);
    });
}

/// The next random value of `parent`'s generator -- the seed of a thread
/// forked by `parent` (LRM 18.14.2).
pub fn draw_from_process(parent: ProcessId) -> u64 {
    with_process_state(Some(parent), splitmix64_next)
}

/// The next random value of instance scope `scope`'s initialization RNG -- the
/// seed of a static process of that instance (LRM 18.14.1).
pub fn draw_from_init(scope: u32) -> u64 {
    INIT_STATE.with(|s| splitmix64_next(s.borrow_mut().entry(scope).or_insert(base_seed())))
}

/// `srandom(seed)`'s `int` seed as a generator state: zero-extended, the same
/// convention `$urandom(seed)` uses.
pub fn state_from_srandom(seed: i32) -> u64 {
    u64::from(seed as u32)
}

/// A generator state as `get_randstate()`'s string. The LRM calls the format
/// "implementation dependent"; the decimal digits of the state round-trip
/// exactly through [`parse_state`].
pub fn format_state(state: u64) -> String {
    state.to_string()
}

/// `set_randstate`'s string back to a state. A string this implementation did
/// not produce is undefined by the LRM (18.13.5); it resets to
/// [`DEFAULT_SEED`] rather than failing.
pub fn parse_state(text: &[u8]) -> u64 {
    std::str::from_utf8(text)
        .ok()
        .and_then(|t| t.parse::<u64>().ok())
        .unwrap_or(DEFAULT_SEED)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn an_unseeded_process_starts_at_the_default_seed() {
        reset();
        assert_eq!(with_process_state(Some(3), |s| *s), DEFAULT_SEED);
    }

    #[test]
    fn a_base_seed_moves_every_unseeded_start_and_reset_restores_the_default() {
        reset();
        set_base_seed(1);
        let one = with_process_state(Some(3), |s| *s);
        assert_ne!(one, DEFAULT_SEED);
        reset();
        set_base_seed(2);
        let two = with_process_state(Some(3), |s| *s);
        assert_ne!(two, one, "different --seed, different start");
        reset();
        set_base_seed(1);
        assert_eq!(
            with_process_state(Some(3), |s| *s),
            one,
            "same --seed, same start"
        );
        assert_eq!(draw_from_init(9), {
            let mut s = one;
            splitmix64_next(&mut s)
        });
        reset();
        assert_eq!(base_seed(), DEFAULT_SEED);
    }

    #[test]
    fn preseed_replaces_a_stale_state_left_by_a_reused_process_id() {
        reset();
        with_process_state(Some(1), |s| *s = 5);
        preseed(1, 99);
        assert_eq!(with_process_state(Some(1), |s| *s), 99);
    }

    /// The parent's own stream advances by one draw per child; a child's
    /// seed is that draw, not the parent's state.
    /// A static-initializer process shares its scope's generator: what it draws
    /// advances that RNG (so a later static process of the scope is seeded from
    /// the continuation), and `preseed` detaches a reused id again.
    #[test]
    fn an_aliased_process_draws_from_the_init_rng_until_preseeded() {
        reset();
        alias_init(5, 2);
        let mut shadow = DEFAULT_SEED;
        let first = splitmix64_next(&mut shadow);
        assert_eq!(with_process_state(Some(5), splitmix64_next), first);
        assert_eq!(
            draw_from_init(2),
            splitmix64_next(&mut shadow),
            "the aliased draw advanced the scope's own RNG"
        );
        preseed(5, 77);
        assert_eq!(with_process_state(Some(5), |s| *s), 77);
        assert_eq!(draw_from_init(2), splitmix64_next(&mut shadow));
    }

    #[test]
    fn a_fork_draw_advances_the_parent_by_exactly_one_step() {
        reset();
        let mut shadow = DEFAULT_SEED;
        let first = splitmix64_next(&mut shadow);
        let second = splitmix64_next(&mut shadow);
        assert_eq!(draw_from_process(7), first);
        assert_eq!(draw_from_process(7), second);
        assert_eq!(with_process_state(Some(7), |s| *s), shadow);
    }

    #[test]
    fn instance_scopes_have_independent_init_rngs() {
        reset();
        let a1 = draw_from_init(0);
        let b1 = draw_from_init(1);
        assert_eq!(a1, b1, "every init RNG starts at the same constant");
        let a2 = draw_from_init(0);
        assert_ne!(a1, a2, "drawing advances the scope's own RNG");
        assert_eq!(draw_from_init(1), a2, "the other scope is untouched");
    }

    #[test]
    fn randstate_strings_round_trip_and_garbage_resets_to_the_default() {
        for state in [0, 1, DEFAULT_SEED, u64::MAX] {
            assert_eq!(parse_state(format_state(state).as_bytes()), state);
        }
        assert_eq!(parse_state(b"not a state"), DEFAULT_SEED);
        assert_eq!(parse_state(b""), DEFAULT_SEED);
        assert_eq!(parse_state(&[0xff, 0xfe]), DEFAULT_SEED);
    }
}