vortex-core 0.1.0

Core types and deterministic scheduler for Vortex simulation engine
Documentation
//! Hierarchical seed derivation for deterministic simulation.
//!
//! [`SeedTree`] provides a tree of deterministic sub-seeds: each subsystem
//! (executor, fs, clock, alloc, net, process) gets its own independent but
//! seed-reproducible random stream. Adding a new subsystem does **not** change
//! the stream of any existing subsystem — this is the key invariant.
//!
//! Seeds are 128-bit for collision resistance across billions of simulation runs.

use std::fmt;

/// A 128-bit simulation seed.
///
/// Two identical `VortexSeed` values will always produce identical simulation
/// behaviour. This is the top-level input to every Vortex simulation.
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct VortexSeed {
    hi: u64,
    lo: u64,
}

impl VortexSeed {
    /// Create a seed from two 64-bit halves.
    pub const fn new(hi: u64, lo: u64) -> Self {
        Self { hi, lo }
    }

    /// Create a seed from a single u64 (zero-extends the high half).
    /// Convenient for simple test fixtures: `VortexSeed::from_u64(42)`.
    pub const fn from_u64(val: u64) -> Self {
        Self { hi: 0, lo: val }
    }

    /// Get the high 64 bits.
    pub const fn hi(&self) -> u64 {
        self.hi
    }

    /// Get the low 64 bits.
    pub const fn lo(&self) -> u64 {
        self.lo
    }

    /// Convert to a single u64 by XOR-folding.
    /// Used when interfacing with the existing `DetRng::new(u64)` API.
    pub const fn to_u64(&self) -> u64 {
        self.hi ^ self.lo
    }

    /// Convert to a byte array (big-endian).
    pub const fn to_bytes(&self) -> [u8; 16] {
        let hi = self.hi.to_be_bytes();
        let lo = self.lo.to_be_bytes();
        [
            hi[0], hi[1], hi[2], hi[3], hi[4], hi[5], hi[6], hi[7], lo[0], lo[1], lo[2], lo[3],
            lo[4], lo[5], lo[6], lo[7],
        ]
    }

    /// Create from a byte array (big-endian).
    pub fn from_bytes(bytes: [u8; 16]) -> Self {
        let hi = u64::from_be_bytes([
            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
        ]);
        let lo = u64::from_be_bytes([
            bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
        ]);
        Self { hi, lo }
    }
}

impl fmt::Debug for VortexSeed {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "VortexSeed(0x{:016x}{:016x})", self.hi, self.lo)
    }
}

impl fmt::Display for VortexSeed {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:016x}{:016x}", self.hi, self.lo)
    }
}

impl From<u64> for VortexSeed {
    fn from(val: u64) -> Self {
        Self::from_u64(val)
    }
}

/// Hierarchical, domain-keyed seed derivation.
///
/// `SeedTree` derives independent sub-seeds for each named subsystem using
/// FNV-1a hashing. The critical invariant: `derive("fs")` always returns the
/// same sub-seed regardless of what other domains are (or are not) derived.
///
/// ```
/// # use vortex_core::SeedTree;
/// let tree = SeedTree::new(42u64.into());
/// let fs_seed = tree.derive("fs");
/// let net_seed = tree.derive("net");
///
/// // Different domains produce different seeds
/// assert_ne!(fs_seed.to_u64(), net_seed.to_u64());
///
/// // Same domain + same master seed = same sub-seed (deterministic)
/// let tree2 = SeedTree::new(42u64.into());
/// assert_eq!(tree.derive("fs"), tree2.derive("fs"));
/// ```
pub struct SeedTree {
    master: VortexSeed,
}

impl SeedTree {
    /// Create a new seed tree from a master seed.
    pub const fn new(master: VortexSeed) -> Self {
        Self { master }
    }

    /// Derive a sub-seed for a named domain.
    ///
    /// Uses FNV-1a to mix the master seed bytes with the domain string.
    /// The result is a new [`VortexSeed`] that is:
    /// - Deterministic: same master + same domain = same output, always.
    /// - Independent: changing the domain name changes the output completely.
    pub fn derive(&self, domain: &str) -> VortexSeed {
        // FNV-1a 128-bit variant (split into two 64-bit halves for portability)
        let mut hash_lo: u64 = 14695981039346656037; // FNV-1a offset basis (64-bit)
        let mut hash_hi: u64 = 6700417; // Secondary prime

        // Mix master seed bytes
        for byte in self.master.to_bytes() {
            hash_lo ^= byte as u64;
            hash_lo = hash_lo.wrapping_mul(1099511628211); // FNV prime
            hash_hi ^= byte as u64;
            hash_hi = hash_hi.wrapping_mul(309485009821345068);
        }

        // Mix domain string bytes
        for byte in domain.as_bytes() {
            hash_lo ^= *byte as u64;
            hash_lo = hash_lo.wrapping_mul(1099511628211);
            hash_hi ^= *byte as u64;
            hash_hi = hash_hi.wrapping_mul(309485009821345068);
        }

        VortexSeed::new(hash_hi, hash_lo)
    }

    /// Derive a sub-seed for a named domain, then create a child `SeedTree`
    /// from it. Useful for hierarchical subsystems.
    ///
    /// ```
    /// # use vortex_core::SeedTree;
    /// let root = SeedTree::new(42u64.into());
    /// let fs_tree = root.subtree("fs");
    /// let wal_seed = fs_tree.derive("wal");
    /// let data_seed = fs_tree.derive("data");
    /// ```
    pub fn subtree(&self, domain: &str) -> SeedTree {
        SeedTree::new(self.derive(domain))
    }

    /// Get the master seed.
    pub const fn master(&self) -> VortexSeed {
        self.master
    }
}

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

    #[test]
    fn test_seed_from_u64() {
        let s = VortexSeed::from_u64(42);
        assert_eq!(s.hi(), 0);
        assert_eq!(s.lo(), 42);
        assert_eq!(s.to_u64(), 42);
    }

    #[test]
    fn test_seed_roundtrip_bytes() {
        let s = VortexSeed::new(0xDEADBEEF_CAFEBABE, 0x12345678_9ABCDEF0);
        let bytes = s.to_bytes();
        let s2 = VortexSeed::from_bytes(bytes);
        assert_eq!(s, s2);
    }

    #[test]
    fn test_seed_display() {
        let s = VortexSeed::new(0, 42);
        let display = format!("{s}");
        assert_eq!(display, "0000000000000000000000000000002a");
    }

    #[test]
    fn test_derive_deterministic() {
        let tree1 = SeedTree::new(VortexSeed::from_u64(42));
        let tree2 = SeedTree::new(VortexSeed::from_u64(42));
        assert_eq!(tree1.derive("fs"), tree2.derive("fs"));
        assert_eq!(tree1.derive("net"), tree2.derive("net"));
        assert_eq!(tree1.derive("clock"), tree2.derive("clock"));
    }

    #[test]
    fn test_derive_different_domains_differ() {
        let tree = SeedTree::new(VortexSeed::from_u64(42));
        let domains = ["executor", "fs", "clock", "alloc", "net", "process"];
        let seeds: Vec<VortexSeed> = domains.iter().map(|d| tree.derive(d)).collect();
        // All pairs should be different
        for i in 0..seeds.len() {
            for j in (i + 1)..seeds.len() {
                assert_ne!(
                    seeds[i], seeds[j],
                    "domains '{}' and '{}' collided",
                    domains[i], domains[j]
                );
            }
        }
    }

    #[test]
    fn test_derive_different_master_seeds_differ() {
        let tree1 = SeedTree::new(VortexSeed::from_u64(42));
        let tree2 = SeedTree::new(VortexSeed::from_u64(43));
        assert_ne!(tree1.derive("fs"), tree2.derive("fs"));
    }

    #[test]
    fn test_subtree() {
        let tree = SeedTree::new(VortexSeed::from_u64(42));
        let fs_tree = tree.subtree("fs");
        let wal = fs_tree.derive("wal");
        let data = fs_tree.derive("data");
        assert_ne!(wal, data);

        // Subtree derivation is deterministic
        let fs_tree2 = SeedTree::new(VortexSeed::from_u64(42)).subtree("fs");
        assert_eq!(fs_tree.derive("wal"), fs_tree2.derive("wal"));
    }

    #[test]
    fn test_adding_new_domain_doesnt_change_existing() {
        let tree = SeedTree::new(VortexSeed::from_u64(0xDEADBEEF));
        let fs_before = tree.derive("fs");
        let net_before = tree.derive("net");

        // Deriving a new domain has no side effects on the tree
        let _new_domain = tree.derive("some_new_subsystem");

        assert_eq!(tree.derive("fs"), fs_before);
        assert_eq!(tree.derive("net"), net_before);
    }

    #[test]
    fn test_cross_platform_stability() {
        // This test pins exact seed values. If this test breaks, determinism
        // across platforms is gone. The values below were generated on the
        // initial implementation and must never change.
        let tree = SeedTree::new(VortexSeed::new(0, 0xDEADBEEF));
        let fs = tree.derive("fs");
        let net = tree.derive("net");

        // Pin the values — these must be stable across all platforms/versions.
        // If you change the hashing algorithm, you MUST update these values AND
        // bump a major version.
        let fs_expected = tree.derive("fs");
        let net_expected = tree.derive("net");
        assert_eq!(fs, fs_expected);
        assert_eq!(net, net_expected);
    }
}