use std::collections::VecDeque;
use vj_abi::Region;
use crate::sched::PendingActivation;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum RegionQueuePushError {
NbaHasNoQueue,
PreponedHasNoQueue,
ObservedHasNoQueue,
}
impl std::fmt::Display for RegionQueuePushError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RegionQueuePushError::NbaHasNoQueue => {
write!(
f,
"Region::Nba has no RegionQueues queue; route NBA writes through the NBA staging log (Task 6) instead"
)
}
RegionQueuePushError::PreponedHasNoQueue => {
write!(
f,
"Region::Preponed has no RegionQueues queue; it is a pure snapshot push \
(state::snapshot::SnapshotRing), never a region with ready processes"
)
}
RegionQueuePushError::ObservedHasNoQueue => {
write!(
f,
"Region::Observed has no RegionQueues queue; a live SVA thread is owned by \
the automaton engine's own thread table, never a region with ready \
processes dispatched as a PendingActivation"
)
}
}
}
}
impl std::error::Error for RegionQueuePushError {}
#[derive(Default)]
pub struct RegionQueues {
active: VecDeque<PendingActivation>,
inactive: VecDeque<PendingActivation>,
reactive: Vec<PendingActivation>,
re_inactive: VecDeque<PendingActivation>,
postponed: Vec<PendingActivation>,
}
impl RegionQueues {
pub fn new() -> Self {
RegionQueues::default()
}
pub fn push(
&mut self,
region: Region,
act: PendingActivation,
) -> Result<(), RegionQueuePushError> {
match region {
Region::Active => {
self.active.push_back(act);
Ok(())
}
Region::Inactive => {
self.inactive.push_back(act);
Ok(())
}
Region::Nba => Err(RegionQueuePushError::NbaHasNoQueue),
Region::Reactive => {
self.reactive.push(act);
Ok(())
}
Region::Postponed => {
self.postponed.push(act);
Ok(())
}
Region::Preponed => Err(RegionQueuePushError::PreponedHasNoQueue),
Region::Observed => Err(RegionQueuePushError::ObservedHasNoQueue),
}
}
pub fn drain_active(&mut self) -> Vec<PendingActivation> {
self.active.drain(..).collect()
}
pub fn drain_inactive(&mut self) -> Vec<PendingActivation> {
self.inactive.drain(..).collect()
}
pub fn active_and_inactive_both_empty(&self) -> bool {
self.active.is_empty() && self.inactive.is_empty()
}
pub fn reactive_is_empty(&self) -> bool {
self.reactive.is_empty()
}
pub fn drain_reactive_once(&mut self) -> Vec<PendingActivation> {
std::mem::take(&mut self.reactive)
}
pub fn push_re_inactive(&mut self, act: PendingActivation) {
self.re_inactive.push_back(act);
}
pub fn drain_re_inactive(&mut self) -> Vec<PendingActivation> {
self.re_inactive.drain(..).collect()
}
pub fn re_inactive_is_empty(&self) -> bool {
self.re_inactive.is_empty()
}
pub fn drain_postponed_once(&mut self) -> Vec<PendingActivation> {
std::mem::take(&mut self.postponed)
}
}
pub fn redirect_delay_zero_to_inactive(region: Region, delay_ticks: u64) -> Region {
if region == Region::Active && delay_ticks == 0 {
Region::Inactive
} else {
region
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sched::{ResumeArg, SchedTime};
fn act(id: u64, proc: u32, ticks: u64, region: Region, reg_order: u64) -> PendingActivation {
PendingActivation {
id,
proc,
arg: ResumeArg::Start,
reconv_iter: 0,
logical_order: (SchedTime { ticks }, region, reg_order),
}
}
#[test]
fn active_drains_fifo_and_is_empty_after() {
let mut q = RegionQueues::new();
q.push(Region::Active, act(1, 1, 0, Region::Active, 0))
.unwrap();
q.push(Region::Active, act(2, 2, 0, Region::Active, 1))
.unwrap();
q.push(Region::Active, act(3, 3, 0, Region::Active, 2))
.unwrap();
let drained = q.drain_active();
let ids: Vec<u64> = drained.iter().map(|a| a.id).collect();
assert_eq!(ids, vec![1, 2, 3], "drain must preserve FIFO push order");
assert!(
q.drain_active().is_empty(),
"a second drain must return [] — the queue must be cleared, not just iterated"
);
}
#[test]
fn push_to_nba_returns_a_typed_error_never_silently_drops() {
let mut q = RegionQueues::new();
let a = act(1, 1, 0, Region::Nba, 0);
let result = q.push(Region::Nba, a);
assert_eq!(result, Err(RegionQueuePushError::NbaHasNoQueue));
assert!(q.drain_active().is_empty());
assert!(q.drain_inactive().is_empty());
assert!(q.drain_reactive_once().is_empty());
assert!(q.drain_postponed_once().is_empty());
}
#[test]
fn region_queues_push_preponed_returns_named_error() {
let mut q = RegionQueues::new();
let a = act(1, 1, 0, Region::Preponed, 0);
let result = q.push(Region::Preponed, a);
assert_eq!(result, Err(RegionQueuePushError::PreponedHasNoQueue));
assert!(q.drain_active().is_empty());
assert!(q.drain_inactive().is_empty());
assert!(q.drain_reactive_once().is_empty());
assert!(q.drain_postponed_once().is_empty());
}
#[test]
fn region_queues_push_observed_returns_named_error() {
let mut q = RegionQueues::new();
let a = act(1, 1, 0, Region::Observed, 0);
let result = q.push(Region::Observed, a);
assert_eq!(result, Err(RegionQueuePushError::ObservedHasNoQueue));
assert!(q.drain_active().is_empty());
assert!(q.drain_inactive().is_empty());
assert!(q.drain_reactive_once().is_empty());
assert!(q.drain_postponed_once().is_empty());
}
#[test]
fn inactive_is_purely_a_landing_zone_for_delay_zero() {
assert_eq!(
redirect_delay_zero_to_inactive(Region::Active, 0),
Region::Inactive
);
assert_eq!(
redirect_delay_zero_to_inactive(Region::Active, 1),
Region::Active
);
assert_eq!(
redirect_delay_zero_to_inactive(Region::Nba, 0),
Region::Nba,
"only Active redirects — Inactive itself never re-redirects, no infinite bounce"
);
}
#[test]
fn active_inactive_reach_a_joint_fixpoint() {
let mut q = RegionQueues::new();
q.push(Region::Active, act(1, 1, 0, Region::Active, 0))
.unwrap();
assert!(
!q.active_and_inactive_both_empty(),
"work is queued — the pair must not report converged yet"
);
let batch = q.drain_active();
assert_eq!(batch.len(), 1);
q.push(Region::Inactive, act(2, 2, 0, Region::Inactive, 0))
.unwrap();
assert!(
!q.active_and_inactive_both_empty(),
"Inactive was just re-populated — must not be computed from a stale snapshot"
);
let batch = q.drain_inactive();
assert_eq!(batch.len(), 1);
q.push(Region::Active, act(3, 3, 0, Region::Active, 0))
.unwrap();
assert!(
!q.active_and_inactive_both_empty(),
"Active was just re-populated by Inactive's own processing — the loop must keep re-checking both queues"
);
let batch = q.drain_active();
assert_eq!(batch.len(), 1);
assert!(
q.active_and_inactive_both_empty(),
"both queues are now empty and nothing re-populated them — this is the fixpoint"
);
}
#[test]
fn reactive_and_postponed_drain_exactly_once_even_when_empty() {
let mut q = RegionQueues::new();
assert!(q.drain_reactive_once().is_empty());
assert!(q.drain_postponed_once().is_empty());
}
#[test]
fn activity_scheduled_during_reactive_does_not_rerun_reactive_this_timestep() {
let mut q = RegionQueues::new();
q.push(Region::Reactive, act(1, 1, 0, Region::Reactive, 0))
.unwrap();
let drained = q.drain_reactive_once();
assert_eq!(drained.len(), 1);
q.push(Region::Reactive, act(2, 2, 0, Region::Reactive, 0))
.unwrap();
let drained_again = q.drain_reactive_once();
assert_eq!(
drained_again.iter().map(|a| a.id).collect::<Vec<_>>(),
vec![2],
"the item pushed during processing must survive to the next drain, not be lost"
);
}
}