vj-sim 0.3.0

Netlist, scheduler and process model of the vj SystemVerilog simulator.
Documentation
//! NETLIST (`vj-sim::netlist`) — the identity/dedup ID types and the
//! `Netlist` hand-off surface (netlist-analysis.md §Key Data Structures /
//! Public Interfaces) that CODEGEN, STATE, SCHED, RUNTIME and the debugger
//! all address signals/instances through, plus (as of this wave's Task 1)
//! the real, `sv-lang`-backed elaboration pipeline: a deterministic DFS over
//! `Design::top_instances()` producing a `Netlist` that implements
//! `NetlistView`. See `docs/superpowers/plans/2026-09-23-wave-A-netlist.md`.

// Elaboration over the sv-lang `Design` is frontend-only (this crate's
// `frontend` feature); the plain ID/key types below stay available without it.
#[cfg(feature = "frontend")]
mod dedup;
// `dedup_algorithm` (Task 31, abi track, XC-045 partial closure) is
// deliberately NOT behind `frontend`, unlike `dedup` just above: it is a
// generic, sv-lang-free hash-then-verify algorithm (see its own module doc).
mod dedup_algorithm;
// Classes plan Task 2 (`docs/superpowers/plans/2026-10-03-classes.md`):
// class-template dedup + member classification. Depends on `sv_lang::Type`/
// `Symbol`, so `#[cfg(feature = "frontend")]`-gated like `dedup`/`elaborate`/
// `template` above, not unconditional like `dedup_algorithm` just above.
#[cfg(feature = "frontend")]
mod classes;
#[cfg(feature = "frontend")]
mod elaborate;
#[cfg(feature = "frontend")]
mod error;
#[cfg(feature = "frontend")]
mod exprwalk;
#[cfg(feature = "frontend")]
mod instance;
#[cfg(feature = "frontend")]
mod items;
#[cfg(feature = "frontend")]
mod lattice;
/// `docs/superpowers/plans/2026-10-03-sdf.md` Task 2: the specify-block/
/// timing-check/specparam timing table (`TimingModel`). `pub`, not
/// `pub(crate)` like the sibling modules above re-exported through this
/// file's own flat `pub use` list below, so `vj`'s `specify`/
/// `timingcheck`/`specify_eval` modules (Track A/B/C, this plan's later
/// tasks) can name `netlist::specify::{TimingModel, PathEntry, CheckEntry}`
/// directly.
#[cfg(feature = "frontend")]
pub mod specify;

/// Whether `sym` heads code that never runs: an instance of a definition
/// slang elaborated only as uninstantiated (not a top, never instantiated),
/// or a generate block whose branch is not taken. Design-wide walks skip it.
#[cfg(feature = "frontend")]
pub fn is_uninstantiated_scope(sym: &sv_lang::Symbol<'_>) -> bool {
    use sv_lang_kinds::SymbolKind;
    // An accept-list test, not a classification of every kind.
    if sym.kind() == SymbolKind::Instance {
        sym.instance_body().is_some_and(|b| b.is_uninstantiated())
    } else {
        sym.kind() == SymbolKind::GenerateBlock && sym.generate_block_is_uninstantiated()
    }
}
#[cfg(feature = "frontend")]
pub mod itf;
#[cfg(feature = "frontend")]
mod template;
#[cfg(feature = "frontend")]
pub mod vif;

#[cfg(feature = "frontend")]
pub use classes::{
    ClassDefinitionTable, ClassMembers, assign_class_template_ids, base_chain,
    check_class_constraints, class_method_symbols, class_param_binding_hash,
    class_specialization_key, class_template_key_of, class_templates_with_statics,
    classes_with_dynamic_static_init, classify_members, collect_new_class_types,
    flattened_interfaces, same_specialization,
};
#[cfg(feature = "frontend")]
pub use dedup::{
    ConfirmedMembers, MemberShape, confirm_and_assign_symbol_keys, merge_groups_within_bucket,
};
/// Task 31 (abi track, XC-045 partial closure): the generic, sv-lang-free
/// hash-then-verify dedup algorithm — always available, not gated behind
/// `frontend` (see `dedup_algorithm`'s own module doc for why it lives
/// alongside, not inside, the `netlist`-track-owned `dedup` module above).
pub use dedup_algorithm::hash_then_verify_dedup;
/// Task 8 (`analysis::graph`'s own containing-symbol/read-set resolution):
/// re-exported crate-internal, not part of this module's public surface,
/// so `analysis::graph` reuses the SAME position-based `SymbolId ->
/// SymbolKey` table `elaborate()`'s own dedup/cont-assign passes build,
/// never a second, independently-reasoned re-derivation of that numbering.
#[cfg(feature = "frontend")]
pub(crate) use elaborate::symbol_key_map;
#[cfg(feature = "frontend")]
pub use elaborate::{Netlist, elaborate};
#[cfg(feature = "frontend")]
pub use error::ElabError;
#[cfg(feature = "frontend")]
pub use instance::{InstanceNode, InstanceTree};
#[cfg(feature = "frontend")]
pub use items::{
    CheckerOutput, FreeVarKind, ProceduralChecker, checker_instance_body, checker_instances,
    checker_output_of, checker_outputs, enclosing_instance_body, enclosing_package,
    free_var_groups, free_var_kind, free_var_seed, generate_scopes, is_in_checker, is_in_program,
    is_static_procedural_var, module_items, procedural_checkers, state_items,
    static_procedural_vars, variable_initializer,
};
#[cfg(feature = "frontend")]
pub use template::{
    ContAssignTemplate, DriverEntry, MemoryDecl, PortDecl, PortDirection, ProcessTemplate,
    SignalDecl, SignalKind, StaticSensitivity, SubInstanceSlot, Template, ValueDomain,
};

/// Opaque, dense, deterministic index into a design-wide symbol table (ledger
/// ST-141, ST-143, ST-153, NL-012, NL-016, NL-017). Two symbols share a key iff
/// they refer to the same deduplicated Template member.
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
pub struct SymbolKey(pub u32);

/// Undeduped instance identity — one per elaborated instance, distinct even when
/// two instances share a compiled `Template`. This is what DEBUGGER/RUNTIME/VPI
/// address (netlist-analysis.md Decision 3; ledger ST-115, ST-116, PR-075..
/// PR-077).
///
/// `#[repr(transparent)]` (B-runtime Task 14 deviation, minimal and
/// additive, documented in `docs/CONTRACTS.md`): RT-097's `svScope` ABI
/// symbols (`vj_runtime::dpi::scope::sv_rt_get_scope`/`sv_rt_set_scope`)
/// are real `extern "C" fn`s that take/return this type by value. Without an
/// explicit repr, `rustc`'s `improper_ctypes_definitions` lint (deny-by-
/// default under this workspace's `-D warnings` gate) rejects any `extern
/// "C"` signature naming it. `repr(transparent)` is a pure, backward-
/// compatible layout GUARANTEE for this single-field tuple struct (adds a
/// promise, changes no existing behavior, breaks no existing caller) — never
/// a semantic change to `InstanceId` itself.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, serde::Serialize, serde::Deserialize)]
#[repr(transparent)]
pub struct InstanceId(pub u32);

/// A member as seen through one particular instance — what STATE's arena
/// actually allocates storage for.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct InstantiatedSignalId {
    pub instance: InstanceId,
    pub member: SymbolKey,
}

#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct TemplateId(pub u32);

/// Canonicalization key = (Definition identity, ordered resolved parameter
/// values/types) (netlist-analysis.md Decision 2: hash-then-verify, not pointer
/// identity, because `DisableInstanceCaching` denies slang's normal body
/// sharing; ledger CG-143, CG-145, PR-029, PR-031, PR-037, PR-117). `params_hash`
/// stands in for the real `Vec<ResolvedParam>` the netlist track builds from
/// `sv_lang::Symbol::parameters()`; kept opaque here since this crate does not
/// depend on `sv-lang`.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct TemplateKey {
    pub definition: u32,
    pub params_hash: u64,
}

/// Classes plan (`docs/superpowers/plans/2026-10-03-classes.md`) Task 2's
/// dense id for a class template (one per distinct `(generic class or plain
/// class definition, resolved parameter binding)` — see `classes.rs`'s own
/// doc comment for the full rationale). Defined here, unconditionally,
/// rather than inside `classes.rs` itself (which is entirely
/// `#[cfg(feature = "frontend")]`, like `dedup`/`template` above) for
/// exactly the reason [`TemplateId`]/[`TemplateKey`] already live here
/// instead of inside `dedup.rs`/`template.rs`: Task 3's object-heap runtime
/// (`vj-sim::objects`) stores a `ClassTemplateId` in every live object's
/// entry and must stay usable from `vj-runtime`, which depends on this
/// crate with `default-features = false` and therefore never reaches
/// `sv-lang` (see this crate's `Cargo.toml` comment on that dependency
/// edge). A plain newtype has no sv-lang content to gate.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct ClassTemplateId(pub u32);

/// Classes plan Task 2's candidate-bucketing key for class-template dedup —
/// moved here alongside [`ClassTemplateId`] for the same frontend-independence
/// reason (see that type's doc comment); the sv-lang-backed construction
/// (`class_template_key_of`, `same_specialization`) stays in `classes.rs`,
/// `#[cfg(feature = "frontend")]`-gated, same split as [`TemplateKey`]/
/// `dedup.rs`.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct ClassTemplateKey {
    pub definition: u32,
    pub params_hash: u64,
}

/// The hand-off surface `netlist::elaborate()` produces (netlist-analysis.md's
/// `impl<'d> Netlist<'d>`), reframed as a trait so the `netlist` track's
/// concrete, `sv-lang`-backed type can implement it without this contracts crate
/// needing the `sv-lang` dependency. `registration_order()`'s DFS order **is**
/// SCHED's `registration_order` tie-break (no separate sort; ledger
/// `registration_order`: SC-013, SC-063, SC-078, PR-118, NL-010, NL-024;
/// `hierarchical_path`: CG-070, CG-071, CG-172, NL-021, NL-022, NL-024;
/// `tick_ns_per_unit`: NL-025, NL-027).
/// The one netlist query `vj-runtime` needs (`%m`'s scope name), kept
/// sv-lang-free and outside the `frontend` feature (CONTRACTS D4): the runtime
/// depends on `vj-sim` with `default-features = false`, where
/// [`NetlistView`] (which exposes frontend-derived `InstanceNode`/`Template`)
/// does not exist. Every `NetlistView` provides it via the blanket impl below.
pub trait HierarchicalNames {
    fn hierarchical_path(&self, id: InstanceId) -> &str;
}

/// Name -> signal resolution (VPI `vpi_handle_by_name`, debugger watch paths,
/// STATE's `introspect::read_by_hier_path`), sv-lang-free and always built for
/// the same reason as [`HierarchicalNames`]. Every `NetlistView` provides it
/// via the blanket impl below.
pub trait HierPathResolver {
    fn resolve_hier_path(&self, path: &str) -> Option<InstantiatedSignalId>;
}

#[cfg(feature = "frontend")]
impl<T: NetlistView + ?Sized> HierPathResolver for T {
    fn resolve_hier_path(&self, path: &str) -> Option<InstantiatedSignalId> {
        NetlistView::resolve_hier_path(self, path)
    }
}

#[cfg(feature = "frontend")]
impl<T: NetlistView + ?Sized> HierarchicalNames for T {
    fn hierarchical_path(&self, id: InstanceId) -> &str {
        NetlistView::hierarchical_path(self, id)
    }
}

#[cfg(feature = "frontend")]
pub trait NetlistView {
    fn registration_order(&self) -> Vec<InstanceId>;
    fn hierarchical_path(&self, id: InstanceId) -> &str;
    fn resolve_hier_path(&self, path: &str) -> Option<InstantiatedSignalId>;
    fn dedup_key(&self, tmpl: TemplateId) -> &TemplateKey;
    /// The single source of truth for the scheduler's time precision
    /// (netlist-analysis.md Decision 8).
    fn tick_ns_per_unit(&self) -> u64;
    /// NL-024 (Task 4): the instance-level query half of the public surface
    /// netlist-analysis.md's `impl<'d> Netlist<'d>` specifies. Purely
    /// additive to this trait (Deviation #1) — every existing method keeps
    /// its exact signature.
    fn instance(&self, id: InstanceId) -> &InstanceNode;
    /// NL-024 (Task 4): the template-level query half.
    fn template(&self, id: TemplateId) -> &Template;
}

#[cfg(test)]
mod tests {
    use super::{InstanceId, InstantiatedSignalId, SymbolKey};

    /// XC-039 (**partial** — see `docs/CONTRACTS.md`'s "Partial closures
    /// (wave A)" entry for the real scoping). This proves only the
    /// type-level precondition Gate 3's per-instance FST diff depends on:
    /// two instances elaborated from the same deduped `Template` (sharing a
    /// dedup key, hence sharing the same member's `SymbolKey`) must never
    /// collapse to one `InstantiatedSignalId` — the type system cannot
    /// accidentally alias two deduped-template instances into one signal
    /// identity. It does **not** exercise the FST/Gate-3 differential
    /// behavior XC-039's literal acceptance names (no FST writer or Gate-3
    /// harness exists anywhere in the workspace yet), so it cannot catch a
    /// real merged-or-misattributed-signal bug once one becomes possible to
    /// produce — only a regression in `InstantiatedSignalId`'s own equality.
    #[test]
    fn dedup_collapses_template_but_preserves_per_instance_identity() {
        // Two instances of one deduped Template share a `TemplateKey`, so
        // codegen emits one shared logic body/lowered unit for both — but
        // STATE still needs two *distinct* signal identities for the same
        // named member (`member: SymbolKey(7)`), one per instance.
        let shared_member = SymbolKey(7);
        let a = InstantiatedSignalId {
            instance: InstanceId(1),
            member: shared_member,
        };
        let b = InstantiatedSignalId {
            instance: InstanceId(2),
            member: shared_member,
        };

        assert_eq!(
            a.member, b.member,
            "both instances resolve to the same deduped Template member"
        );
        assert_ne!(
            a, b,
            "two instances of a deduped template must never alias to one \
             InstantiatedSignalId — equality must also compare `instance`, \
             never `member` alone"
        );
    }
}