use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use sv_sim_abi::Region;
use super::{ActivationId, CanonicalEventKey, DeterminismController, ResolvedBatch, WorkItem};
use crate::sched::{PendingActivation, SchedTime, region_ordinal};
use crate::state::{FourStateValue, StateCellId};
pub struct SequentialController {
gvt: Mutex<SchedTime>,
next_seq: AtomicU64,
rollback_count: Arc<AtomicU64>,
}
impl SequentialController {
pub fn new() -> Self {
Self {
gvt: Mutex::new(SchedTime { ticks: 0 }),
next_seq: AtomicU64::new(0),
rollback_count: Arc::new(AtomicU64::new(0)),
}
}
pub fn default_boxed() -> Box<dyn DeterminismController> {
Box::new(Self::new())
}
pub fn rollback_count_handle(&self) -> Arc<AtomicU64> {
self.rollback_count.clone()
}
pub fn record_rollback(&self) {
self.rollback_count.fetch_add(1, Ordering::SeqCst);
}
}
impl Default for SequentialController {
fn default() -> Self {
Self::new()
}
}
impl DeterminismController for SequentialController {
fn partition(
&self,
ready: &[PendingActivation],
region: Region,
now: SchedTime,
) -> Vec<WorkItem> {
*self
.gvt
.lock()
.expect("SequentialController gvt mutex poisoned") = now;
ready
.iter()
.map(|activation| {
let seq = self.next_seq.fetch_add(1, Ordering::SeqCst);
let key = CanonicalEventKey {
vtime: activation.logical_order.0,
region_ordinal: region_ordinal(region),
reconv_iter: activation.reconv_iter,
canonical_process_id: activation.proc,
local_send_seq: seq,
};
WorkItem {
activation: *activation,
key,
}
})
.collect()
}
fn on_read(&self, _act: ActivationId, _cell: StateCellId, _version: u64) {}
fn on_write(
&self,
_act: ActivationId,
_cell: StateCellId,
_prior: FourStateValue,
_version: u64,
) {
}
fn gvt(&self) -> SchedTime {
*self
.gvt
.lock()
.expect("SequentialController gvt mutex poisoned")
}
fn drain_and_resolve(&self, work: Vec<WorkItem>) -> ResolvedBatch {
ResolvedBatch {
completed: work.iter().map(|w| w.activation.id).collect(),
}
}
fn kind(&self) -> &'static str {
"sequential"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sched::{ProcessId, ResumeArg, canonical_key_of, sort_ready_set};
fn activation(
id: ActivationId,
proc: ProcessId,
ticks: u64,
reconv_iter: u32,
registration_order: u64,
) -> PendingActivation {
PendingActivation {
id,
proc,
arg: ResumeArg::Start,
reconv_iter,
logical_order: (SchedTime { ticks }, Region::Active, registration_order),
}
}
#[test]
fn sequential_controller_gvt_equals_now_after_partition() {
let controller = SequentialController::new();
let out = controller.partition(&[], Region::Active, SchedTime { ticks: 9 });
assert!(out.is_empty());
assert_eq!(controller.gvt(), SchedTime { ticks: 9 });
}
#[test]
fn sequential_controller_never_reorders_the_given_ready_set() {
let mut ready = vec![
activation(3, 30, 5, 0, 2),
activation(2, 20, 5, 0, 0),
activation(1, 10, 5, 0, 1),
];
sort_ready_set(&mut ready);
assert_eq!(
ready.iter().map(|a| a.proc).collect::<Vec<_>>(),
vec![20, 10, 30]
);
let controller = SequentialController::new();
let out = controller.partition(&ready, Region::Active, SchedTime { ticks: 5 });
let procs: Vec<ProcessId> = out.iter().map(|w| w.activation.proc).collect();
assert_eq!(
procs,
vec![20, 10, 30],
"partition must preserve the caller's given order, never reorder itself \
(this order is neither ascending nor descending in ProcessId, so a \
reorder-by-ProcessId bug in either direction is caught)"
);
for (item, act) in out.iter().zip(ready.iter()) {
let (vtime, region_ordinal, reconv_iter, proc) = canonical_key_of(act);
assert_eq!(item.key.vtime, vtime);
assert_eq!(item.key.region_ordinal, region_ordinal);
assert_eq!(item.key.reconv_iter, reconv_iter);
assert_eq!(item.key.canonical_process_id, proc);
}
}
#[test]
fn sequential_controller_assigns_strictly_increasing_local_send_seq() {
let controller = SequentialController::new();
let ready1 = vec![activation(1, 1, 1, 0, 0), activation(2, 2, 1, 0, 1)];
let ready2 = vec![activation(3, 3, 2, 0, 0)];
let out1 = controller.partition(&ready1, Region::Active, SchedTime { ticks: 1 });
let out2 = controller.partition(&ready2, Region::Active, SchedTime { ticks: 2 });
let seqs: Vec<u64> = out1
.iter()
.chain(out2.iter())
.map(|w| w.key.local_send_seq)
.collect();
let mut sorted = seqs.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(
seqs.len(),
sorted.len(),
"every local_send_seq across both partition calls must be unique: {seqs:?}"
);
assert!(
seqs.windows(2).all(|w| w[0] < w[1]),
"local_send_seq must be strictly increasing across successive partition \
calls, not reset per call: {seqs:?}"
);
}
#[test]
fn rollback_count_handle_is_zero_until_record_rollback_is_called() {
let controller = SequentialController::new();
let handle = controller.rollback_count_handle();
assert_eq!(handle.load(Ordering::SeqCst), 0);
controller.record_rollback();
assert_eq!(
handle.load(Ordering::SeqCst),
1,
"record_rollback must increment the same counter rollback_count_handle \
returns a clone of"
);
controller.record_rollback();
assert_eq!(handle.load(Ordering::SeqCst), 2);
let fresh = SequentialController::new();
assert_eq!(
fresh.rollback_count_handle().load(Ordering::SeqCst),
0,
"a new SequentialController's rollback counter must start at 0, not \
inherit a prior instance's count"
);
}
#[test]
fn sequential_controller_is_the_default_when_none_configured() {
let controller = SequentialController::default_boxed();
let out = controller.partition(&[], Region::Active, SchedTime { ticks: 3 });
assert!(out.is_empty());
assert_eq!(controller.gvt(), SchedTime { ticks: 3 });
}
#[test]
fn dt006_no_thread_spawn_or_rollback_code_outside_determinism_module() {
let sched_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/sched");
let mut scanned_files = 0usize;
let mut violations = Vec::new();
scan_dir_for_thread_spawn(&sched_dir, &mut scanned_files, &mut violations);
assert!(
scanned_files > 0,
"scan matched no files under {sched_dir:?} — the scan itself is broken"
);
assert!(
violations.is_empty(),
"found `thread::spawn` in production (non-#[cfg(test)]) code under \
sched/: {violations:?} — scheduler production code must never spawn a \
thread; that is exclusively the DeterminismController's job (DT-006)"
);
}
fn scan_dir_for_thread_spawn(
dir: &std::path::Path,
scanned_files: &mut usize,
violations: &mut Vec<String>,
) {
for entry in std::fs::read_dir(dir).expect("read sched/ dir for DT-006 scan") {
let entry = entry.expect("dir entry for DT-006 scan");
let path = entry.path();
if path.is_dir() {
scan_dir_for_thread_spawn(&path, scanned_files, violations);
continue;
}
if path.extension().is_none_or(|e| e != "rs") {
continue;
}
*scanned_files += 1;
let src = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read {path:?} for DT-006 scan: {e}"));
let production_only = crate::determinism::scan_support::strip_cfg_test_blocks(&src);
if production_only.contains("thread::spawn") {
violations.push(path.display().to_string());
}
}
}
}