vj-sim 0.1.0

Netlist, scheduler and process model of the vj SystemVerilog simulator.
Documentation
//! DETERMINISM::POLICY (Task 17, ledger DT-001/DT-013) — the v1 conservative
//! default and the fail-closed DPI speculation gate.
//!
//! ARCHITECTURE §2/§4 item 1: "ship v1 with `LOOKAHEAD_CYCLES = 0` globally
//! ... functionally a conservative deterministic partitioned engine
//! (bit-reproducible, no rollback)" — optimistic speculation is fully
//! disabled at first release. This module is the one place that constant and
//! the DPI fail-closed classification live; nothing here waits on WOCB.

use sv_sim_abi::Region;

use crate::analysis::DpiVpiTag;

/// DT-001: "a global `LOOKAHEAD_CYCLES` config defaults to 0 for every
/// region." One constant, one place — every region-specific hook
/// ([`lookahead_cycles_for_region`]) reads through it rather than each
/// inlining its own `0`, so a later wave can turn per-region optimism on
/// (ARCHITECTURE §4 item 2) without an API break.
pub const LOOKAHEAD_CYCLES: u32 = 0;

/// Whether a speculative window may run past a given point without waiting
/// for a conservative barrier. v1 never produces `Allow` from an unproven or
/// unclassified DPI call — see [`dpi_speculation_policy`].
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SpeculationGate {
    Allow,
    Barrier,
}

/// DT-013: DPI purity classification for speculation is fail-closed —
/// anything not *provably* pure/safe defaults to [`SpeculationGate::Barrier`].
/// `None` means unclassified (`analyze()` hasn't run, or doesn't exist yet in
/// this wave) and is treated exactly like a proven-unsafe tag, never like a
/// proven-safe one. The `match` inside the `Some` arm is exhaustive (no
/// wildcard): adding a 5th `DpiVpiTag` variant later without updating this
/// function fails to compile instead of silently defaulting that new variant
/// to `Allow`.
pub fn dpi_speculation_policy(tag: Option<DpiVpiTag>) -> SpeculationGate {
    match tag {
        None => SpeculationGate::Barrier,
        Some(t) => match t {
            DpiVpiTag::Pure => SpeculationGate::Allow,
            DpiVpiTag::DeferredSafe => SpeculationGate::Allow,
            DpiVpiTag::Barrier => SpeculationGate::Barrier,
            DpiVpiTag::SpeculationUnsafe => SpeculationGate::Barrier,
        },
    }
}

/// DT-001's per-region hook: today every region reads the same global
/// [`LOOKAHEAD_CYCLES`], but the hook's existence (rather than every call
/// site inlining the constant) is what lets a later wave enable per-region
/// optimism (ARCHITECTURE §4 item 2) without an API break — ties to SC-130's
/// review in Task 22.
pub fn lookahead_cycles_for_region(_region: Region) -> u32 {
    LOOKAHEAD_CYCLES
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::determinism::SequentialController;
    use crate::sched::SchedTime;

    #[test]
    fn unclassified_dpi_defaults_to_barrier_never_pure() {
        assert!(matches!(
            dpi_speculation_policy(None),
            SpeculationGate::Barrier
        ));
    }

    #[test]
    fn every_dpi_vpi_tag_variant_maps_correctly_fail_closed() {
        let cases = [
            (DpiVpiTag::Pure, SpeculationGate::Allow),
            (DpiVpiTag::DeferredSafe, SpeculationGate::Allow),
            (DpiVpiTag::Barrier, SpeculationGate::Barrier),
            (DpiVpiTag::SpeculationUnsafe, SpeculationGate::Barrier),
        ];
        for (tag, expected) in cases {
            assert_eq!(
                dpi_speculation_policy(Some(tag)),
                expected,
                "DpiVpiTag::{tag:?} must map to {expected:?}"
            );
        }
    }

    #[test]
    fn lookahead_cycles_is_zero_for_every_region_in_v1() {
        for r in [
            Region::Active,
            Region::Inactive,
            Region::Nba,
            Region::Reactive,
            Region::Postponed,
        ] {
            assert_eq!(
                lookahead_cycles_for_region(r),
                0,
                "region {r:?} must have LOOKAHEAD_CYCLES == 0 in v1"
            );
        }
    }

    /// DT-001: "0 lookahead" and "the degenerate sequential controller" are
    /// the same statement viewed two ways (ARCHITECTURE §4 item 1) — the
    /// only `DeterminismController` reachable at default config is
    /// `SequentialController`, whose own `gvt() == now` behavior is
    /// consistent with a globally-zero lookahead (nothing ever runs ahead
    /// of `now`, so there is nothing to roll back).
    #[test]
    fn the_only_determinism_controller_reachable_at_default_config_is_sequential() {
        assert_eq!(lookahead_cycles_for_region(Region::Active), 0);

        let controller = SequentialController::default_boxed();
        let now = SchedTime { ticks: 11 };
        let out = controller.partition(&[], Region::Active, now);
        assert!(out.is_empty());
        assert_eq!(
            controller.gvt(),
            now,
            "the default controller's gvt() must equal now immediately after \
             partition() — consistent with LOOKAHEAD_CYCLES == 0 (nothing ever \
             runs ahead of now)"
        );
    }
}