vj-sim 0.1.0

Netlist, scheduler and process model of the vj SystemVerilog simulator.
Documentation
//! Task 17 — The STATE-owned frame arena for `process::ActivationRecord`
//! (ledger ST-103, ST-104, ST-110).
//!
//! Three separate flat backing stores, one per [`crate::process::ArenaRange`]
//! -typed field `process::ActivationRecord` embeds (`frame`,
//! `WaitSpecStorage::EdgeList`, `ForkFrame::children` respectively —
//! `process/mod.rs`'s own doc comment on `ArenaRange` names all three): this
//! is where each of the three actually gets STATE-owned backing storage, not
//! just the pointer-free reference *shape* `process/mod.rs` already defines.
//! Same bump-allocation discipline as [`super::arena::Arena`] (Task 2/7):
//! append-only, never shrink/reuse — a range, once handed out, stays valid
//! and non-aliased for the life of the arena.
//!
//! `new_activation_record` (ST-103, ST-110) is `StateEngine`'s one entry
//! point for minting a real `process::ActivationRecord`: STATE owns both the
//! frame-word allocation *and* the PC storage (`ActivationRecord.pc` is
//! itself the resume-point storage ST-110 asks for — re-reading `sched`/
//! `process`'s own contract types confirms no separate scheduler-owned PC
//! field exists anywhere in this crate, so this method, not a new field
//! elsewhere, is the concrete fulfillment).

use crate::process::{ArenaRange, PredicateFn};
use crate::sched::ProcessId;

use super::EdgeKind;
use super::StateCellId;
use super::instance::StateEngine;

/// The STATE-owned backing storage for every `process::ActivationRecord`
/// this crate hands out (ledger ST-103, ST-104, ST-110). Exactly three flat
/// `Vec`s, one per `ArenaRange`-typed field an `ActivationRecord` can embed —
/// see this module's doc comment for why there are exactly three and not,
/// say, one shared buffer.
pub struct FrameArena {
    words: Vec<u64>,
    edge_terms: Vec<(EdgeKind, StateCellId, Option<PredicateFn>)>,
    fork_children: Vec<ProcessId>,
}

impl Default for FrameArena {
    fn default() -> Self {
        Self::new()
    }
}

impl FrameArena {
    pub fn new() -> Self {
        FrameArena {
            words: Vec::new(),
            edge_terms: Vec::new(),
            fork_children: Vec::new(),
        }
    }

    /// Bump-allocates `n` new frame words, returning the shared start offset
    /// as an [`ArenaRange`] — the backing storage for one activation's
    /// `ActivationRecord.frame`. `n == 0` is a legal no-op that still returns
    /// a well-defined (empty) range at the current length, matching
    /// `Arena::alloc_inline_words`' own zero-is-a-no-op contract.
    pub fn alloc_frame_words(&mut self, n: u32) -> ArenaRange {
        let base = self.words.len() as u32;
        self.words.resize(self.words.len() + n as usize, 0);
        ArenaRange { base, count: n }
    }

    /// Bump-allocates a copy of `terms` into the edge-term table, returning
    /// the shared start offset as an [`ArenaRange`] — the backing storage for
    /// `WaitSpecStorage::EdgeList`. The concrete `StateCellId`s for a given
    /// suspend point are fixed once elaboration resolves the instance's
    /// signals and never change for the life of the run, so this table is a
    /// read-only lookup once written, never a rollback hazard (see this
    /// module's doc comment / `WaitSpecStorage::EdgeList`'s own doc comment).
    pub fn alloc_edge_terms(
        &mut self,
        terms: &[(EdgeKind, StateCellId, Option<PredicateFn>)],
    ) -> ArenaRange {
        let base = self.edge_terms.len() as u32;
        self.edge_terms.extend_from_slice(terms);
        ArenaRange {
            base,
            count: terms.len() as u32,
        }
    }

    /// Bump-allocates `n` reserved fork-child slots, returning the shared
    /// start offset as an [`ArenaRange`] — the backing storage for
    /// `ForkFrame::children`. Reserved, not populated: per Resolved design
    /// decision D1 (`docs/CONTRACTS.md`), concrete child `ProcessId`s are
    /// assigned dynamically by the scheduler as children are actually
    /// spawned, so this only reserves the range; the placeholder `0` values
    /// written here are overwritten by the caller once each child's real
    /// `ProcessId` is known.
    pub fn alloc_fork_children(&mut self, n: u32) -> ArenaRange {
        let base = self.fork_children.len() as u32;
        self.fork_children
            .resize(self.fork_children.len() + n as usize, 0);
        ArenaRange { base, count: n }
    }
}

impl StateEngine {
    /// STATE's one constructor for a real (non-hand-literal)
    /// `process::ActivationRecord` (ST-103, ST-110): allocates
    /// `frame_word_count` words from this engine's own `FrameArena` and
    /// returns a fresh record whose `pc`/`frame` are both STATE-owned
    /// storage. `pending_wait`/`fork_state` start at their empty defaults
    /// (`WaitSpecStorage::None`/`None`) — populating them (via
    /// `alloc_edge_terms`/`alloc_fork_children` above) is the caller's job
    /// once a suspend point or fork actually occurs, not this constructor's.
    pub fn new_activation_record(
        &mut self,
        pc: u32,
        frame_word_count: u32,
    ) -> crate::process::ActivationRecord {
        let frame = self.frame.alloc_frame_words(frame_word_count);
        crate::process::ActivationRecord {
            pc,
            frame,
            pending_wait: crate::process::WaitSpecStorage::None,
            fork_state: None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::super::instance::StateEngine;
    use super::super::instance::TemplateRegistry;
    use super::*;
    use crate::process::ArenaRange;

    #[test]
    fn activation_records_are_allocated_by_state_not_constructed_ad_hoc() {
        let mut engine = StateEngine::new(TemplateRegistry::new());
        let ar1 = engine.new_activation_record(0, 4);
        let ar2 = engine.new_activation_record(0, 4);
        assert_eq!(ar2.frame.base, ar1.frame.base + 4);
        assert_eq!(ar1.frame.count, 4);
        assert_eq!(ar2.frame.count, 4);
    }

    #[test]
    fn activation_record_is_still_pod_after_going_through_state_allocation() {
        let mut engine = StateEngine::new(TemplateRegistry::new());
        let ar = engine.new_activation_record(0, 4);
        let copy = ar;
        engine.frame.words[ar.frame.base as usize] = 0xDEAD_BEEF;
        assert_eq!(copy.frame.base, ar.frame.base);
        assert_eq!(copy.frame.count, ar.frame.count);
        assert_eq!(engine.frame.words[copy.frame.base as usize], 0xDEAD_BEEF);
        assert_eq!(engine.frame.words[ar.frame.base as usize], 0xDEAD_BEEF);
    }

    #[test]
    fn edge_list_and_fork_children_get_independent_arena_ranges() {
        use crate::state::{EdgeKind, StateCellId};
        let mut fa = FrameArena::new();
        let edge_range = fa.alloc_edge_terms(&[
            (EdgeKind::Posedge, StateCellId(0), None),
            (EdgeKind::Negedge, StateCellId(1), None),
            (EdgeKind::BothEdges, StateCellId(2), None),
        ]);
        let fork_range = fa.alloc_fork_children(2);

        assert_eq!(edge_range, ArenaRange { base: 0, count: 3 });
        assert_eq!(fork_range, ArenaRange { base: 0, count: 2 });

        // Independent backing stores: writing/reading through each does not
        // alias the other.
        fa.fork_children[fork_range.base as usize] = 42;
        fa.fork_children[fork_range.base as usize + 1] = 43;
        assert_eq!(fa.edge_terms.len(), 3);
        assert_eq!(fa.edge_terms[0].1, StateCellId(0));
        assert_eq!(fa.fork_children[0], 42);
        assert_eq!(fa.fork_children[1], 43);
    }

    #[test]
    fn resume_pc_lives_on_the_activation_record_state_owns_no_separate_pc_table() {
        let mut engine = StateEngine::new(TemplateRegistry::new());
        let mut ar1 = engine.new_activation_record(10, 0);
        let ar2 = engine.new_activation_record(20, 0);
        assert_eq!(ar1.pc, 10);
        assert_eq!(ar2.pc, 20);

        ar1.pc = 99;
        assert_eq!(ar1.pc, 99);
        assert_eq!(
            ar2.pc, 20,
            "mutating one record's pc must never affect another's"
        );
    }
}