vj-sim 0.3.0

Netlist, scheduler and process model of the vj SystemVerilog simulator.
Documentation
//! Task 7 (NL-034): `ReadSetLattice` — the `sv_lang::dataflow::Lattice`
//! implementation used to compute a continuous assign's RHS read-set via
//! slang's own CFG walker (netlist-analysis.md Decision 1 — the single
//! biggest scope-reduction move: this crate never hand-recurses the
//! ~20k-line Expression/Statement AST surface itself). `write` events are
//! recorded too (used starting Task 8's driver graph), but this task only
//! consumes `reads`.
//!
//! **Deviation from the plan's literal field types, documented here
//! (binding for this whole module).** The plan's own illustrative shape is
//! `pub reads: BTreeSet<crate::netlist::SymbolKey>` — but `SymbolKey` is a
//! position-based index INTO ONE PARTICULAR SCOPE's `members()`
//! enumeration, and `Lattice::top()` (the entry state `run_dataflow`
//! constructs before ever calling `transfer`) takes no arguments at all:
//! there is no way for a per-scope `Symbol -> SymbolKey` table to reach the
//! lattice's own state at construction time, so `transfer` can never itself
//! produce a `SymbolKey` for the `Symbol` an event carries. `sv_lang::
//! SymbolId` (`Symbol::id()`, `ast.rs:5216`) is the context-free, `Hash +
//! Eq + Clone` stable identity `sv-lang` itself publishes for exactly this
//! kind of "remember a symbol without a live table" need (already used by
//! `dedup::DefinitionTable` for the same reason) — this lattice accumulates
//! `SymbolId`s during the run, and [`cont_assign_read_set`] (the caller,
//! `elaborate.rs`) converts the finished set to `SymbolKey`s via a table
//! built from the SAME scope the procedure symbol itself belongs to, once
//! `run_dataflow` has already returned.

use crate::netlist::{ElabError, SymbolKey};
use crate::symid::SymbolUid;
use std::collections::{HashMap, HashSet};

/// **Review fix (r1, blocking finding on `build_own_cont_assigns`).**
/// `reads` used to be a `HashSet<SymbolId>`: a symbol that is both the
/// LHS lvalue-address-evaluation artifact `run_dataflow` reports for a
/// continuous assign's own target AND a genuine RHS operand (`assign y =
/// y | a;` — a legal LRM continuous-assign feedback/latch idiom that
/// compiles and simulates under iverilog) collapsed to one set entry, so
/// the old blanket `read_set.retain(|&key| key != target)` in
/// `elaborate.rs` silently dropped the real dependency along with the
/// artifact. Every `Read` event is now kept as its own entry, paired with
/// the expression node it occurred at (`DfaEvent::node`'s `source_range`),
/// so the caller can tell the two apart by whether the node lies inside
/// the assignment's RHS span (see [`cont_assign_read_set`]).
#[derive(Clone, Default)]
pub(crate) struct ReadSetLattice {
    pub(crate) reads: Vec<(crate::symid::SymbolId, Option<sv_lang::SourceSpan>)>,
    pub(crate) writes: HashSet<crate::symid::SymbolId>,
}

impl sv_lang::dataflow::Lattice for ReadSetLattice {
    fn top() -> Self {
        Self::default()
    }

    fn join(&mut self, other: &Self) {
        self.reads.extend(other.reads.iter().cloned());
        self.writes.extend(other.writes.iter().cloned());
    }

    fn transfer(&mut self, ev: sv_lang::dataflow::DfaEvent<'_>) {
        use sv_lang::dataflow::DfaEventKind;
        match ev.kind {
            DfaEventKind::Read => {
                if let Some(sym) = ev.symbol {
                    self.reads
                        .push((sym.uid(), ev.node.map(|n| n.source_range())));
                }
            }
            DfaEventKind::Write => {
                if let Some(sym) = ev.symbol {
                    self.writes.insert(sym.uid());
                }
            }
            DfaEventKind::Call => {
                // Tagged separately by Task 9's DPI/VPI classification pass
                // (out of this task's 3 ledger IDs) — not a read or a write
                // of a value symbol, so this Lattice records nothing for it.
            }
        }
    }
}

/// Runs [`ReadSetLattice`] via `EvalSession::run_dataflow` over `procedure`
/// (this task's own use — a `ContinuousAssign` symbol; `run_dataflow`'s own
/// doc comment, `dataflow.rs:389-391`, also allows an `always`/`initial`/
/// `final` block or a subroutine, for a future task's reuse) and converts
/// the resulting read-set to `SymbolKey`s via `scope_keys` (built by the
/// caller from the SAME scope `procedure` is declared in — see this
/// module's own doc comment for why the conversion cannot happen inside the
/// `Lattice` itself). A read that resolves to a symbol NOT present in
/// `scope_keys` (e.g. a reference to a `parameter`/`localparam`, which this
/// wave's `Template.signals` never enumerates) is silently excluded from
/// the result — not an error: `scope_keys` is deliberately built from
/// `build_members`'s own SymbolKey-having member set, so "not in
/// scope_keys" means "not a signal this wave's `Template` tracks", not
/// "something went wrong".
///
/// **Review fix (r1, blocking finding on `build_own_cont_assigns`).**
/// `target` is the assignment's own LHS symbol id and `rhs_range` is
/// `assignment.right()`'s [`sv_lang::Expression::source_range`].
/// `run_dataflow` reports a `Read` for `target` BEFORE its `Write` as an
/// artifact of evaluating the LHS as an lvalue address (e.g. for `assign y
/// = a & b;` the raw event stream is `Read(y), Read(a), Read(b), Write(y)`)
/// — that artifact's `DfaEvent::node` is the LHS expression, outside
/// `rhs_range`, so it is dropped. Any OTHER read of `target` whose node
/// lies inside `rhs_range` is a genuine RHS self-reference (`assign y = y
/// | a;`, a legal LRM continuous-assign feedback/latch idiom) and is kept —
/// this is exactly what the old `HashSet<SymbolId>`-based
/// `read_set.retain(|&key| key != target)` in `elaborate.rs` could not
/// distinguish, since both the artifact and a genuine self-read collapsed
/// to the same set entry.
pub(crate) fn cont_assign_read_set<'d>(
    eval: &sv_lang::EvalSession<'d>,
    procedure: sv_lang::Symbol<'d>,
    target: crate::symid::SymbolId,
    rhs_range: sv_lang::SourceSpan,
    scope_keys: &HashMap<crate::symid::SymbolId, SymbolKey>,
) -> Result<Vec<SymbolKey>, ElabError> {
    let lattice: ReadSetLattice = eval.run_dataflow(procedure).map_err(|e| {
        ElabError::Frontend(format!(
            "run_dataflow failed for `{}`: {e}",
            procedure.hierarchical_path()
        ))
    })?;
    let mut ids: HashSet<crate::symid::SymbolId> = HashSet::new();
    for (id, node_range) in &lattice.reads {
        if *id == target {
            let is_genuine_rhs_read = node_range.is_some_and(|r| span_contains(rhs_range, r));
            if is_genuine_rhs_read {
                ids.insert(id.clone());
            }
        } else {
            ids.insert(id.clone());
        }
    }
    Ok(symbol_ids_to_keys(&ids, scope_keys))
}

/// True if `inner` lies fully within `outer` — same buffers, offsets
/// nested. Used to tell a genuine RHS read of a continuous assign's own
/// target apart from the LHS lvalue-address-evaluation artifact
/// `run_dataflow` also reports for it (see [`cont_assign_read_set`]).
fn span_contains(outer: sv_lang::SourceSpan, inner: sv_lang::SourceSpan) -> bool {
    inner.start.buffer == outer.start.buffer
        && inner.end.buffer == outer.end.buffer
        && inner.start.offset >= outer.start.offset
        && inner.end.offset <= outer.end.offset
}

pub(crate) fn symbol_ids_to_keys(
    ids: &HashSet<crate::symid::SymbolId>,
    scope_keys: &HashMap<crate::symid::SymbolId, SymbolKey>,
) -> Vec<SymbolKey> {
    let mut keys: Vec<SymbolKey> = ids
        .iter()
        .filter_map(|id| scope_keys.get(id).copied())
        .collect();
    keys.sort_by_key(|k| k.0);
    keys
}