vj-sim 0.2.0

Netlist, scheduler and process model of the vj SystemVerilog simulator.
Documentation
//! SCHED::GUARD — delta-loop livelock guard & policy (ledger SC-079, SC-080,
//! DT-050).
//!
//! The outer driver's Active/Inactive/NBA convergence loop (Task 21's
//! `run_until`) must call [`DeltaGuard::tick`] on every delta-loop iteration
//! within a timestep, hard-bounding how many convergence iterations a single
//! timestep may take (SC-079).
//!
//! **Product decision (SC-080/DT-050) — this doc comment is the "design doc
//! or config that states the chosen policy" acceptance requires:** the
//! livelock policy is a **hard error, never warn-and-truncate**, with a
//! default threshold of [`DEFAULT_MAX_DELTA_ITERATIONS`]. A silently
//! truncated delta loop under a determinism gate would non-deterministically
//! vary observed behavior by exactly how much of the oscillation got
//! truncated (dependent on where `max` happens to land relative to the
//! oscillation's own period) — strictly worse than a loud, reproducible
//! abort that fails the same way every time for the same design, and it also
//! gates GVT advancement under a speculative controller (an unbounded loop
//! both hangs the simulation and starves GVT, deadlocking rollback on top of
//! the ordinary hang — DT-050). There is no code path anywhere in this
//! module that logs-and-continues past `max`: [`DeltaGuardError`] is a real
//! `std::error::Error`, not a `Result<T, ()>`-swallowing warn path.
//!
//! `reset` is scoped to once per *timestep* (called by the outer driver when
//! `now` advances), never once per delta iteration — a long-but-legitimately-
//! converging multi-timestep run must not be penalized by a guard that never
//! resets.

use crate::sched::SchedTime;

/// Default max Active/Inactive/NBA delta-loop iterations per timestep before
/// [`DeltaGuard::tick`] hard-errors. See this module's own doc comment for
/// the hard-error-vs-warn-and-truncate policy record (SC-080/DT-050). Large
/// enough for a finite process that spends millions of `#0` steps at one time
/// (ivtest `pow_ca_signed` does ~1M), small enough to report a true livelock
/// in seconds.
pub const DEFAULT_MAX_DELTA_ITERATIONS: u32 = 5_000_000;

/// [`DeltaGuard::tick`]'s one error variant: the delta loop exceeded its
/// configured bound within the current timestep (SC-079). A hard error by
/// construction — there is no `Ok`-with-a-warning path past `max`.
#[derive(Debug)]
pub enum DeltaGuardError {
    LivelockExceeded { iterations: u32, at: SchedTime },
}

impl std::fmt::Display for DeltaGuardError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            DeltaGuardError::LivelockExceeded { iterations, at } => write!(
                f,
                "delta-loop livelock guard exceeded after {iterations} iterations at {at:?} \
                 (hard error per SC-080/DT-050 policy — this guard never warns-and-truncates)"
            ),
        }
    }
}

impl std::error::Error for DeltaGuardError {}

/// Bounds the Active/Inactive/NBA convergence loop within one timestep
/// (SC-079). The outer driver must call [`Self::tick`] on every delta-loop
/// iteration, and [`Self::reset`] exactly once per timestep (never per delta
/// iteration — DT-050).
pub struct DeltaGuard {
    max: u32,
    count: u32,
}

impl DeltaGuard {
    pub fn new(max: u32) -> Self {
        DeltaGuard { max, count: 0 }
    }

    /// Call once per delta-loop iteration. Increments the iteration count
    /// and returns `Err(DeltaGuardError::LivelockExceeded { .. })` once that
    /// count exceeds `max` — never truncates, never logs-and-continues
    /// (SC-080/DT-050's hard-error policy).
    pub fn tick(&mut self, at: SchedTime) -> Result<(), DeltaGuardError> {
        self.count += 1;
        if self.count > self.max {
            Err(DeltaGuardError::LivelockExceeded {
                iterations: self.count,
                at,
            })
        } else {
            Ok(())
        }
    }

    /// Call once per *timestep* — when the outer driver's `now` advances —
    /// never once per delta-loop iteration. Resetting mid-timestep would let
    /// a genuine livelock hide behind an unrelated reset call, which is
    /// exactly what this guard exists to prevent.
    pub fn reset(&mut self) {
        self.count = 0;
    }

    /// **NEW (Task 21, `wA-sched` plan; ledger SC-007) — the current
    /// iteration count since the last [`Self::reset`]**, read-only and
    /// side-effect-free (unlike [`Self::tick`], never increments, never
    /// errors). Lets the outer driver (`sched::control::Engine::run_until`)
    /// capture a per-timestep high-water mark for black-box integration
    /// tests to observe (`Engine::max_delta_iterations_in_a_timestep`) —
    /// SC-007's acceptance requires proving the delta loop actually
    /// converges (a bounded number of passes), not just that it eventually
    /// does *something*; with `count` private and no getter, no test outside
    /// this crate's own `#[cfg(test)]` unit tests could observe that bound
    /// at all.
    pub fn count(&self) -> u32 {
        self.count
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::sched::{PendingActivation, RegionQueues, ResumeArg};
    use vj_abi::Region;

    fn t(ticks: u64) -> SchedTime {
        SchedTime { ticks }
    }

    /// SC-079: exactly `max` ticks succeed; the `max + 1`th errors, and the
    /// error's `iterations` is exactly `max + 1` (fails on an off-by-one in
    /// either direction).
    #[test]
    fn guard_allows_exactly_max_iterations_then_errors() {
        let mut guard = DeltaGuard::new(3);
        assert!(guard.tick(t(0)).is_ok());
        assert!(guard.tick(t(0)).is_ok());
        assert!(guard.tick(t(0)).is_ok());
        match guard.tick(t(0)) {
            Err(DeltaGuardError::LivelockExceeded { iterations, .. }) => {
                assert_eq!(iterations, 4);
            }
            Ok(()) => panic!("expected the 4th tick past max=3 to error, got Ok(())"),
        }
    }

    /// SC-079: models `always @* x = ~x;` — a process that re-arms itself
    /// into `Region::Active` every delta iteration, forever, unless
    /// something bounds the loop. If `DeltaGuard::tick` never errors, this
    /// test hangs against its own bounded `for` wrapper rather than silently
    /// passing — the strongest "terminates rather than hangs" assertion a
    /// unit test can make of a broken guard.
    #[test]
    fn adversarial_zero_delay_oscillation_terminates_via_the_guard_not_a_hang() {
        let mut queues = RegionQueues::new();
        let mut guard = DeltaGuard::new(DEFAULT_MAX_DELTA_ITERATIONS);
        let now = t(0);

        let mut stopped_via_guard = false;
        for i in 0..=(DEFAULT_MAX_DELTA_ITERATIONS as u64 + 1) {
            let self_re_arming_activation = PendingActivation {
                id: i,
                proc: 0,
                arg: ResumeArg::Start,
                reconv_iter: 0,
                logical_order: (now, Region::Active, i),
            };
            queues
                .push(Region::Active, self_re_arming_activation)
                .expect("Region::Active always has a queue");
            if guard.tick(now).is_err() {
                stopped_via_guard = true;
                break;
            }
        }
        assert!(
            stopped_via_guard,
            "guard never errored within DEFAULT_MAX_DELTA_ITERATIONS + 1 iterations — \
             a broken guard would have hung this test instead of reaching this assertion"
        );
    }

    /// DT-050: `reset` is scoped per timestep, not per delta iteration.
    #[test]
    fn reset_is_scoped_per_timestep_not_per_delta_iteration() {
        let max = 5;

        // A long-but-legitimately-converging run across two timesteps must
        // not be penalized: max-1 ticks, reset (models the outer driver's
        // `now` advancing to a new timestep), then max-1 more ticks — neither
        // burst alone reaches max, so neither should error.
        let mut guard = DeltaGuard::new(max);
        for _ in 0..max - 1 {
            assert!(guard.tick(t(0)).is_ok());
        }
        guard.reset();
        for _ in 0..max - 1 {
            assert!(guard.tick(t(1)).is_ok());
        }

        // Without an intervening reset, two max-sized bursts must compound:
        // the second burst errors immediately, proving reset is load-bearing
        // and not a no-op that happens to make the first sub-test pass.
        let mut guard2 = DeltaGuard::new(max);
        for _ in 0..max {
            assert!(guard2.tick(t(0)).is_ok());
        }
        assert!(
            guard2.tick(t(1)).is_err(),
            "a second max-sized burst without an intervening reset must error immediately"
        );
    }

    /// SC-007 (Task 21) — `count()` tracks ticks since the last `reset()`,
    /// is read-only (repeated calls don't change it), and `reset()` zeroes
    /// it — the exact contract `Engine::max_delta_iterations_in_a_timestep`
    /// depends on.
    #[test]
    fn count_reflects_ticks_since_last_reset_and_reset_zeroes_it() {
        let mut guard = DeltaGuard::new(10);
        assert_eq!(guard.count(), 0);
        guard.tick(t(0)).unwrap();
        assert_eq!(guard.count(), 1);
        guard.tick(t(0)).unwrap();
        guard.tick(t(0)).unwrap();
        assert_eq!(
            guard.count(),
            3,
            "count must reflect every tick, not just the last one"
        );
        // Read-only: calling it again doesn't itself mutate anything.
        assert_eq!(guard.count(), 3);
        guard.reset();
        assert_eq!(
            guard.count(),
            0,
            "reset must zero the count count() reports"
        );
    }

    /// SC-080: this test itself is the "design doc or config states the
    /// chosen policy" acceptance check — assert the documented default
    /// matches the constant, and that `DeltaGuardError` is a real hard-error
    /// type (not a `Result<T, ()>`-swallowing warn path).
    #[test]
    fn default_threshold_is_documented_and_matches_the_constant() {
        assert_eq!(DEFAULT_MAX_DELTA_ITERATIONS, 5_000_000);

        let err = DeltaGuardError::LivelockExceeded {
            iterations: DEFAULT_MAX_DELTA_ITERATIONS + 1,
            at: t(0),
        };
        match err {
            DeltaGuardError::LivelockExceeded { iterations, at } => {
                assert_eq!(iterations, DEFAULT_MAX_DELTA_ITERATIONS + 1);
                assert_eq!(at, t(0));
            }
        }
        // Compile-level witness that this is a real `std::error::Error`,
        // not a bare marker type.
        let e2 = DeltaGuardError::LivelockExceeded {
            iterations: 1,
            at: t(0),
        };
        let _: &dyn std::error::Error = &e2;
        assert!(!format!("{e2}").is_empty());
    }
}