vj-sim 0.3.0

Netlist, scheduler and process model of the vj SystemVerilog simulator.
Documentation
//! Task 18 — Hierarchical read-back API and dedup-invisibility above the
//! introspection boundary (ledger ST-118, ST-119).
//!
//! [`read_by_hier_path`] is the one entry point ARCHITECTURE §2's "Signal
//! read/write/subscribe: STATE owns the one canonical primitive" promises to
//! introspection consumers (Gate-2/Gate-3 harness corpus assertions, FST/
//! VPI read-back per state.md/verification.md) that address a signal by a
//! *hierarchical instance path string* rather than by an already-resolved
//! [`super::StateCellId`]. It composes two contract seams this crate already
//! owns/consumes:
//!
//! - [`crate::netlist::HierPathResolver::resolve_hier_path`] — owned by the
//!   `netlist` track, consumed here read-only (mocked by a hand-written test
//!   double in this file's own tests, per this plan's established pattern
//!   for not-yet-built sibling tracks; `netlist` has no concrete
//!   implementation yet this wave).
//! - [`StateEngine`]'s own `hier_cells: HashMap<InstantiatedSignalId,
//!   StateCellId>` map (this task's own addition to that struct, populated
//!   by [`StateEngine::allocate_instance`]'s existing per-slot loop — see
//!   that function's doc comment for the retrofit).
//!
//! **ST-119 (dedup-invisibility):** the map above is keyed by
//! [`crate::netlist::InstantiatedSignalId`] — `{instance: InstanceId, member:
//! SymbolKey}` — never by `member: SymbolKey` alone. Two instances of one
//! deduped template share exactly one `StateLayout`/one set of `SymbolKey`s,
//! but `allocate_instance` mints each instance its own `InstanceId` (an
//! internal, monotonically-increasing counter — see that function's own
//! `next_instance_id` field), so the same `SymbolKey` produces two distinct
//! map keys (and therefore two independently-readable `StateCellId`s) once
//! and only once it is combined with each instance's own identity. This is
//! what makes FRONTEND's compiled-body dedup invisible above this boundary
//! *by construction*: nothing above this map ever observes a shared
//! template identity, only per-instance signal identity.

use super::{FourStateValue, SignalAccess, StateEngine};
use crate::netlist::HierPathResolver;

/// ST-118: resolves `path` through `netlist`'s own hierarchical-path table,
/// then STATE's own `InstantiatedSignalId -> StateCellId` map, then reads the
/// live value through [`SignalAccess::read`] — the exact same primitive
/// every other STATE consumer (the accessor ABI, `driver::resolve_net_step`)
/// reads through (ST-152's "no second, independently-populated store"
/// discipline applied to introspection too). Returns `None`, never a
/// fabricated/zero value or a panic, for a path `netlist` cannot resolve, or
/// (structurally impossible in practice once `allocate_instance` is the only
/// producer of `InstantiatedSignalId`s this map ever sees, but checked
/// anyway rather than `.unwrap()`ed) for one `netlist` resolves to a signal
/// this `StateEngine` never allocated storage for.
pub fn read_by_hier_path<N: HierPathResolver + ?Sized>(
    engine: &StateEngine,
    netlist: &N,
    path: &str,
) -> Option<FourStateValue> {
    let sig = netlist.resolve_hier_path(path)?;
    let cell = engine.cell_for_instantiated_signal(sig)?;
    Some(engine.read(cell))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::netlist::{InstanceId, InstantiatedSignalId, SymbolKey, TemplateKey};
    use crate::state::{SlotClass, SlotDescriptor, SlotOffset, StateLayout, TemplateRegistry};
    use std::collections::HashMap;

    /// Test-local stand-in for the not-yet-built `netlist` track's concrete
    /// `NetlistView`, per this plan's established mocking pattern for
    /// sibling tracks that don't exist yet this wave.
    struct MockNetlistView {
        paths: HashMap<String, InstantiatedSignalId>,
    }

    impl MockNetlistView {
        fn new() -> Self {
            MockNetlistView {
                paths: HashMap::new(),
            }
        }

        fn map(&mut self, path: &str, sig: InstantiatedSignalId) {
            self.paths.insert(path.to_string(), sig);
        }
    }

    impl HierPathResolver for MockNetlistView {
        fn resolve_hier_path(&self, path: &str) -> Option<InstantiatedSignalId> {
            self.paths.get(path).copied()
        }
    }

    fn key(definition: u32, params_hash: u64) -> TemplateKey {
        TemplateKey {
            definition,
            params_hash,
        }
    }

    fn inline_slot(sym: u32, word_offset: u32, bit_width: u32) -> SlotDescriptor {
        SlotDescriptor {
            key: SymbolKey(sym),
            offset: SlotOffset {
                class: SlotClass::Inline,
                word_offset,
                bit_width,
                elem_word_count: 1,
                row_stride_words: 0,
                num_rows: 0,
            },
            initial: None,
            net_info: None,
        }
    }

    /// Matches every other `state::*::tests` module's own local copy (no
    /// shared test-util module exists in this crate) — `val` in the low 64
    /// bits, `unk` (all-known ⇒ 0) in the high 64, per `FourStateValue::
    /// Narrow`'s own doc comment.
    fn narrow_word(val: u64, unk: u64) -> u128 {
        (val as u128) | ((unk as u128) << 64)
    }

    fn assert_narrow_eq(v: FourStateValue, expected: u128) {
        match v {
            FourStateValue::Narrow(w) => assert_eq!(w, expected),
            FourStateValue::Wide { .. } => panic!("expected Narrow, got Wide"),
        }
    }

    /// ST-118: a mock resolves `"top.leaf.x"`; after `allocate_instance` +
    /// a `commit_write` on that signal's cell, `read_by_hier_path` returns
    /// the written value. A path the mock cannot resolve returns `None`
    /// (never a panic or a fabricated zero). Fails if `read_by_hier_path`
    /// doesn't actually thread the resolved `InstantiatedSignalId` through
    /// to a real, written `StateCellId` (e.g. always returning `None`, or
    /// always returning the all-X default regardless of what was written).
    #[test]
    fn read_by_hier_path_resolves_a_real_signal_through_a_mock_netlist_view() {
        let templates = TemplateRegistry::new();
        let tmpl = key(0, 0);
        let member = SymbolKey(0);
        // SAFETY: `templates` is a local, single-owned value with no other
        // live borrow and no concurrent caller.
        unsafe {
            templates.register(
                tmpl,
                StateLayout {
                    slots: vec![inline_slot(member.0, 0, 8)],
                    inline_words: 1,
                    wide_words: 0,
                },
            );
        }
        let mut engine = StateEngine::new(templates);
        let handle = engine
            .allocate_instance(tmpl)
            .expect("template is registered");

        let sig = InstantiatedSignalId {
            instance: InstanceId(0),
            member,
        };
        let cell = engine
            .cell_for_instantiated_signal(sig)
            .expect("allocate_instance populates the hier map for every slot");
        engine.commit_write(cell, FourStateValue::Narrow(narrow_word(0x2A, 0)), &mut ());
        drop(handle);

        let mut netlist = MockNetlistView::new();
        netlist.map("top.leaf.x", sig);

        match read_by_hier_path(&engine, &netlist, "top.leaf.x") {
            Some(v) => assert_narrow_eq(v, narrow_word(0x2A, 0)),
            None => panic!("expected a resolved value for a mapped path"),
        }
        assert!(
            read_by_hier_path(&engine, &netlist, "top.leaf.unmapped").is_none(),
            "an unresolvable path must return None, never a panic or a fabricated value"
        );
    }

    /// ST-119: two instances of a deduped template (`allocate_instance`
    /// called twice for the same `TemplateKey`) get two distinct
    /// `InstanceId`s from `StateEngine`'s own internal counter, so the same
    /// `member: SymbolKey` resolves to two different, independently
    /// writable/readable cells. Fails if instance identity were collapsed
    /// to template identity anywhere in the map (both instances' hier-path
    /// reads would alias onto one cell and observe the same, last-written
    /// value).
    #[test]
    fn two_instances_of_a_deduped_template_have_independently_readable_identity() {
        let templates = TemplateRegistry::new();
        let tmpl = key(1, 0);
        let member = SymbolKey(0);
        // SAFETY: `templates` is a local, single-owned value with no other
        // live borrow and no concurrent caller.
        unsafe {
            templates.register(
                tmpl,
                StateLayout {
                    slots: vec![inline_slot(member.0, 0, 8)],
                    inline_words: 1,
                    wide_words: 0,
                },
            );
        }
        let mut engine = StateEngine::new(templates);
        let handle_a = engine
            .allocate_instance(tmpl)
            .expect("template is registered");
        let handle_b = engine
            .allocate_instance(tmpl)
            .expect("template is registered");
        drop(handle_a);
        drop(handle_b);

        let sig_a = InstantiatedSignalId {
            instance: InstanceId(0),
            member,
        };
        let sig_b = InstantiatedSignalId {
            instance: InstanceId(1),
            member,
        };
        let cell_a = engine
            .cell_for_instantiated_signal(sig_a)
            .expect("instance 0's hier entry exists");
        let cell_b = engine
            .cell_for_instantiated_signal(sig_b)
            .expect("instance 1's hier entry exists");
        assert_ne!(
            cell_a, cell_b,
            "two instances must never share one StateCellId"
        );

        engine.commit_write(
            cell_a,
            FourStateValue::Narrow(narrow_word(0x11, 0)),
            &mut (),
        );
        engine.commit_write(
            cell_b,
            FourStateValue::Narrow(narrow_word(0x22, 0)),
            &mut (),
        );

        let mut netlist = MockNetlistView::new();
        netlist.map("top.a.x", sig_a);
        netlist.map("top.b.x", sig_b);

        match read_by_hier_path(&engine, &netlist, "top.a.x") {
            Some(v) => assert_narrow_eq(v, narrow_word(0x11, 0)),
            None => panic!("expected a resolved value for top.a.x"),
        }
        match read_by_hier_path(&engine, &netlist, "top.b.x") {
            Some(v) => assert_narrow_eq(v, narrow_word(0x22, 0)),
            None => panic!("expected a resolved value for top.b.x"),
        }
    }

    /// ST-119, structural: a template with 2 signals, allocated 3 times,
    /// must grow the internal hier-path map by exactly `2 * 3 = 6` entries
    /// — proving no accidental sharing/collapsing at the data-structure
    /// level (not merely at the observable-read level the tests above
    /// already cover). Fails if e.g. the map were keyed by `member` alone
    /// (capped at 2 entries regardless of instance count) or if a later
    /// `allocate_instance` call ever overwrote an earlier instance's entry.
    #[test]
    fn hier_path_map_grows_by_exactly_one_entry_per_signal_per_instance() {
        let templates = TemplateRegistry::new();
        let tmpl = key(2, 0);
        // SAFETY: `templates` is a local, single-owned value with no other
        // live borrow and no concurrent caller.
        unsafe {
            templates.register(
                tmpl,
                StateLayout {
                    slots: vec![inline_slot(0, 0, 8), inline_slot(1, 1, 8)],
                    inline_words: 2,
                    wide_words: 0,
                },
            );
        }
        let mut engine = StateEngine::new(templates);
        assert_eq!(engine.hier_cells_len(), 0);

        for _ in 0..3 {
            let _ = engine
                .allocate_instance(tmpl)
                .expect("template is registered");
        }

        assert_eq!(engine.hier_cells_len(), 6);
    }
}