use crate::sched::SchedTime;
pub const DEFAULT_MAX_DELTA_ITERATIONS: u32 = 5_000_000;
#[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 {}
pub struct DeltaGuard {
max: u32,
count: u32,
}
impl DeltaGuard {
pub fn new(max: u32) -> Self {
DeltaGuard { max, count: 0 }
}
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(())
}
}
pub fn reset(&mut self) {
self.count = 0;
}
pub fn count(&self) -> u32 {
self.count
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sched::{PendingActivation, RegionQueues, ResumeArg};
use sv_sim_abi::Region;
fn t(ticks: u64) -> SchedTime {
SchedTime { ticks }
}
#[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(())"),
}
}
#[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"
);
}
#[test]
fn reset_is_scoped_per_timestep_not_per_delta_iteration() {
let max = 5;
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());
}
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"
);
}
#[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"
);
assert_eq!(guard.count(), 3);
guard.reset();
assert_eq!(
guard.count(),
0,
"reset must zero the count count() reports"
);
}
#[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));
}
}
let e2 = DeltaGuardError::LivelockExceeded {
iterations: 1,
at: t(0),
};
let _: &dyn std::error::Error = &e2;
assert!(!format!("{e2}").is_empty());
}
}