use std::collections::HashMap;
use vj_abi::{Region, SimTime};
use crate::process::{SubscriptionSetId, WaitSpec};
use crate::sched::{
ActivationId, EventTerm, EventWaitSet, NbaLog, NbaUpdate, PendingActivation, ProcessId,
RegionQueues, ResumeArg, SchedTime, SensitivityTable, StaticSensitivity, TimeWheel, commit_nba,
};
use crate::state::{EdgeKind, SignalAccess, StateCellId, ValueChangeSink};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum RegisterWaitError {
UnknownSubscriptionSet(SubscriptionSetId),
UnexpectedWheelRegion(Region),
}
impl std::fmt::Display for RegisterWaitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RegisterWaitError::UnknownSubscriptionSet(id) => {
write!(f, "no StaticSensitivity registered for {id:?}")
}
RegisterWaitError::UnexpectedWheelRegion(r) => {
write!(f, "TimeWheel::arm_delay returned unexpected region {r:?}")
}
}
}
}
impl std::error::Error for RegisterWaitError {}
fn ticks_from(t: SimTime) -> u64 {
t.sim_units.max(0) as u64
}
pub fn register_wait(
table: &mut SensitivityTable,
wheel: &mut TimeWheel,
queues: &mut RegionQueues,
proc: ProcessId,
spec: &WaitSpec,
now: SchedTime,
static_sensitivity_by_set: &HashMap<SubscriptionSetId, StaticSensitivity>,
) -> Result<(), RegisterWaitError> {
match spec {
WaitSpec::Delay(t) => {
let delay_ticks = ticks_from(*t);
let act = PendingActivation {
id: proc as ActivationId,
proc,
arg: ResumeArg::TimeElapsed,
reconv_iter: 0,
logical_order: (
SchedTime {
ticks: now.ticks + delay_ticks,
},
Region::Active,
0,
),
};
match wheel.arm_delay(now.ticks, delay_ticks, act) {
Region::Inactive => {
let act = PendingActivation {
logical_order: (act.logical_order.0, Region::Inactive, act.logical_order.2),
..act
};
queues
.push(Region::Inactive, act)
.expect("Region::Inactive is always accepted by RegionQueues::push");
}
Region::Active => {
}
other @ (Region::Nba
| Region::Reactive
| Region::Postponed
| Region::Preponed
| Region::Observed) => {
return Err(RegisterWaitError::UnexpectedWheelRegion(other));
}
}
}
WaitSpec::Edge { edge, signal, iff } => {
let wait = EventWaitSet {
terms: vec![EventTerm::Edge {
cell: *signal,
edge: *edge,
iff: *iff,
}],
..Default::default()
};
table.install_dynamic(&wait, proc);
}
WaitSpec::EdgeList(terms) => {
let wait = EventWaitSet {
terms: terms
.iter()
.map(|(edge, signal, iff)| EventTerm::Edge {
cell: *signal,
edge: *edge,
iff: *iff,
})
.collect(),
..Default::default()
};
table.install_dynamic(&wait, proc);
}
WaitSpec::ImplicitComb(set_id) => {
let sensitivity = static_sensitivity_by_set
.get(set_id)
.ok_or(RegisterWaitError::UnknownSubscriptionSet(*set_id))?;
table.install_static(sensitivity, proc);
}
}
Ok(())
}
struct NbaWakeupSink<'a> {
sensitivity: &'a SensitivityTable,
region: Region,
now: SchedTime,
out: &'a mut Vec<PendingActivation>,
}
impl ValueChangeSink for NbaWakeupSink<'_> {
fn notify_change(&mut self, cell: StateCellId, edge: EdgeKind, _at: SimTime, _region: Region) {
for (proc, _, _, _) in self.sensitivity.matches(cell, edge) {
let seq = self.out.len() as u64;
self.out.push(PendingActivation {
id: seq,
proc,
arg: ResumeArg::EventFired { term_index: 0 },
reconv_iter: 0,
logical_order: (self.now, self.region, seq),
});
}
}
}
pub fn nba_drain_destination(
updates: Vec<NbaUpdate>,
arena: &mut dyn SignalAccess,
sensitivity: &SensitivityTable,
at: SchedTime,
) -> Vec<PendingActivation> {
let mut temp_log = NbaLog::new();
for u in updates {
temp_log.stage(u.cell, u.value, u.source);
}
let mut out: Vec<PendingActivation> = Vec::new();
{
let mut sink = NbaWakeupSink {
sensitivity,
region: Region::Active,
now: at,
out: &mut out,
};
commit_nba(&mut temp_log, arena, at, &mut sink);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sched::{ProcessId as SchedProcessId, StaticSensitivity, sort_ready_set};
use crate::state::{FourStateValue, ListenerRole};
use std::collections::HashMap;
fn four_state_eq(a: &FourStateValue, b: &FourStateValue) -> bool {
match (a, b) {
(FourStateValue::Narrow(x), FourStateValue::Narrow(y)) => x == y,
(
FourStateValue::Wide { aval: a1, bval: b1 },
FourStateValue::Wide { aval: a2, bval: b2 },
) => a1 == a2 && b1 == b2,
_ => false,
}
}
struct FakeArena {
cells: HashMap<StateCellId, FourStateValue>,
}
impl FakeArena {
fn new() -> Self {
Self {
cells: HashMap::new(),
}
}
fn seed(&mut self, cell: StateCellId, value: FourStateValue) {
self.cells.insert(cell, value);
}
}
impl SignalAccess for FakeArena {
fn read(&self, cell: StateCellId) -> FourStateValue {
self.cells
.get(&cell)
.cloned()
.unwrap_or(FourStateValue::Narrow(0))
}
fn write(&mut self, cell: StateCellId, value: FourStateValue) -> bool {
let changed = match self.cells.get(&cell) {
Some(existing) => !four_state_eq(existing, &value),
None => true,
};
self.cells.insert(cell, value);
changed
}
fn subscribe(
&mut self,
_cell: StateCellId,
_role: ListenerRole,
_sink: Box<dyn ValueChangeSink + Send>,
) {
}
}
#[test]
fn every_wait_spec_variant_has_a_registration_arm() {
let empty_lookup: HashMap<SubscriptionSetId, StaticSensitivity> = HashMap::new();
{
let mut table = SensitivityTable::new();
let mut wheel = TimeWheel::new();
let mut queues = RegionQueues::new();
let proc: SchedProcessId = 1;
let spec = WaitSpec::Delay(SimTime {
sim_units: 5,
delta: 0,
});
register_wait(
&mut table,
&mut wheel,
&mut queues,
proc,
&spec,
SchedTime { ticks: 10 },
&empty_lookup,
)
.expect("Delay registration must succeed");
assert_eq!(
wheel.next_tick(),
Some(15),
"Delay(5) registered at tick 10 must arm the wheel to fire at tick 15"
);
}
{
let mut table = SensitivityTable::new();
let mut wheel = TimeWheel::new();
let mut queues = RegionQueues::new();
let proc: SchedProcessId = 2;
let cell = StateCellId(100);
let spec = WaitSpec::Edge {
edge: EdgeKind::Posedge,
signal: cell,
iff: None,
};
register_wait(
&mut table,
&mut wheel,
&mut queues,
proc,
&spec,
SchedTime { ticks: 0 },
&empty_lookup,
)
.expect("Edge registration must succeed");
assert_eq!(
table
.matches(cell, EdgeKind::Posedge)
.map(|(p, _, _, _)| p)
.collect::<Vec<_>>(),
vec![proc],
"Edge registration must install a dynamic entry the table finds"
);
}
{
let mut table = SensitivityTable::new();
let mut wheel = TimeWheel::new();
let mut queues = RegionQueues::new();
let proc: SchedProcessId = 3;
let cell_a = StateCellId(101);
let cell_b = StateCellId(102);
let spec = WaitSpec::EdgeList(vec![
(EdgeKind::Posedge, cell_a, None),
(EdgeKind::Negedge, cell_b, None),
]);
register_wait(
&mut table,
&mut wheel,
&mut queues,
proc,
&spec,
SchedTime { ticks: 0 },
&empty_lookup,
)
.expect("EdgeList registration must succeed");
assert_eq!(
table
.matches(cell_a, EdgeKind::Posedge)
.map(|(p, _, _, _)| p)
.collect::<Vec<_>>(),
vec![proc],
"EdgeList must install a dynamic entry per (edge, signal) pair (first pair)"
);
assert_eq!(
table
.matches(cell_b, EdgeKind::Negedge)
.map(|(p, _, _, _)| p)
.collect::<Vec<_>>(),
vec![proc],
"EdgeList must install a dynamic entry per (edge, signal) pair (second pair)"
);
}
{
let mut table = SensitivityTable::new();
let mut wheel = TimeWheel::new();
let mut queues = RegionQueues::new();
let proc: SchedProcessId = 4;
let cell = StateCellId(103);
let set_id = SubscriptionSetId(7);
let mut lookup: HashMap<SubscriptionSetId, StaticSensitivity> = HashMap::new();
lookup.insert(
set_id,
StaticSensitivity {
edges: vec![(cell, None)],
},
);
let spec = WaitSpec::ImplicitComb(set_id);
register_wait(
&mut table,
&mut wheel,
&mut queues,
proc,
&spec,
SchedTime { ticks: 0 },
&lookup,
)
.expect("ImplicitComb registration must succeed via the fake lookup");
assert_eq!(
table
.matches(cell, EdgeKind::Negedge)
.map(|(p, _, _, _)| p)
.collect::<Vec<_>>(),
vec![proc],
"ImplicitComb must install a static any-change entry via the lookup"
);
}
}
#[test]
fn delay_zero_lands_directly_in_inactive_not_the_wheel() {
let mut table = SensitivityTable::new();
let mut wheel = TimeWheel::new();
let mut queues = RegionQueues::new();
let empty_lookup: HashMap<SubscriptionSetId, StaticSensitivity> = HashMap::new();
let proc: SchedProcessId = 9;
let spec = WaitSpec::Delay(SimTime {
sim_units: 0,
delta: 0,
});
register_wait(
&mut table,
&mut wheel,
&mut queues,
proc,
&spec,
SchedTime { ticks: 3 },
&empty_lookup,
)
.expect("Delay(0) registration must succeed");
assert_eq!(
wheel.next_tick(),
None,
"Delay(0) must never touch the wheel"
);
let drained = queues.drain_inactive();
assert_eq!(drained.len(), 1);
assert_eq!(drained[0].proc, proc);
assert_eq!(
drained[0].logical_order.1,
Region::Inactive,
"an activation landed in the Inactive queue must carry Region::Inactive as its \
own logical_order.1 tag, agreeing with the queue it actually lives in — \
canonical_key_of/sort_ready_set (order.rs) read logical_order.1 directly to \
build the canonical key that SC-010/DT-005/DT-010 depend on"
);
}
#[test]
fn register_wait_defensive_arm_still_exhaustive_with_preponed() {
let err = RegisterWaitError::UnexpectedWheelRegion(Region::Preponed);
assert_eq!(
err,
RegisterWaitError::UnexpectedWheelRegion(Region::Preponed)
);
assert!(
err.to_string().contains("Preponed"),
"the Display text must name the offending region: {err}"
);
}
#[test]
fn register_wait_defensive_arm_still_exhaustive_with_observed() {
let err = RegisterWaitError::UnexpectedWheelRegion(Region::Observed);
assert_eq!(
err,
RegisterWaitError::UnexpectedWheelRegion(Region::Observed)
);
assert!(
err.to_string().contains("Observed"),
"the Display text must name the offending region: {err}"
);
}
#[test]
fn implicit_comb_with_an_unresolvable_set_id_is_a_typed_error_not_a_panic() {
let mut table = SensitivityTable::new();
let mut wheel = TimeWheel::new();
let mut queues = RegionQueues::new();
let empty_lookup: HashMap<SubscriptionSetId, StaticSensitivity> = HashMap::new();
let spec = WaitSpec::ImplicitComb(SubscriptionSetId(999));
let result = register_wait(
&mut table,
&mut wheel,
&mut queues,
1,
&spec,
SchedTime { ticks: 0 },
&empty_lookup,
);
assert_eq!(
result,
Err(RegisterWaitError::UnknownSubscriptionSet(
SubscriptionSetId(999)
))
);
}
#[test]
fn nba_drain_produces_activations_only_for_changed_cells_in_canonical_form() {
let cell_changed = StateCellId(200);
let cell_unchanged = StateCellId(201);
let proc_changed: SchedProcessId = 10;
let proc_unchanged: SchedProcessId = 20;
let mut arena = FakeArena::new();
arena.seed(cell_changed, FourStateValue::Narrow(0));
arena.seed(cell_unchanged, FourStateValue::Narrow(7));
let mut sensitivity = SensitivityTable::new();
sensitivity.install_static(
&StaticSensitivity {
edges: vec![(cell_changed, None)],
},
proc_changed,
);
sensitivity.install_static(
&StaticSensitivity {
edges: vec![(cell_unchanged, None)],
},
proc_unchanged,
);
let updates = vec![
NbaUpdate {
cell: cell_changed,
value: FourStateValue::Narrow(1),
source: 1,
order: 0,
},
NbaUpdate {
cell: cell_unchanged,
value: FourStateValue::Narrow(7),
source: 1,
order: 1,
},
];
let activations =
nba_drain_destination(updates, &mut arena, &sensitivity, SchedTime { ticks: 5 });
assert_eq!(
activations.len(),
1,
"only the process sensitive to the genuinely-changed cell may be activated"
);
assert_eq!(activations[0].proc, proc_changed);
assert_eq!(
activations[0].logical_order.1,
Region::Active,
"NBA-triggered wakeups must land in Region::Active"
);
assert_eq!(
activations[0].logical_order.0,
SchedTime { ticks: 5 },
"an NBA-triggered wakeup's logical_order.0 must be the `at` scheduler time \
nba_drain_destination was called with, not a hardcoded tick"
);
}
#[test]
fn nba_drain_at_nonzero_time_stamps_activations_with_that_time_not_zero() {
let cell = StateCellId(230);
let proc: SchedProcessId = 40;
let mut arena = FakeArena::new();
arena.seed(cell, FourStateValue::Narrow(0));
let mut sensitivity = SensitivityTable::new();
sensitivity.install_static(
&StaticSensitivity {
edges: vec![(cell, None)],
},
proc,
);
let updates = vec![NbaUpdate {
cell,
value: FourStateValue::Narrow(1),
source: 1,
order: 0,
}];
let at = SchedTime { ticks: 7 };
let activations = nba_drain_destination(updates, &mut arena, &sensitivity, at);
assert_eq!(activations.len(), 1);
assert_eq!(
activations[0].logical_order.0, at,
"logical_order.0 must equal the `at` time nba_drain_destination was called \
with, not SchedTime {{ ticks: 0 }}"
);
}
#[test]
fn nba_drain_applies_exactly_the_queued_tuples_in_canonical_order() {
let cell_a = StateCellId(210);
let cell_b = StateCellId(211);
let cell_c = StateCellId(212);
let proc_a: SchedProcessId = 30;
let proc_b: SchedProcessId = 31;
let proc_c: SchedProcessId = 32;
let mut sensitivity = SensitivityTable::new();
sensitivity.install_static(
&StaticSensitivity {
edges: vec![(cell_a, None)],
},
proc_a,
);
sensitivity.install_static(
&StaticSensitivity {
edges: vec![(cell_b, None)],
},
proc_b,
);
sensitivity.install_static(
&StaticSensitivity {
edges: vec![(cell_c, None)],
},
proc_c,
);
let updates = vec![
NbaUpdate {
cell: cell_c,
value: FourStateValue::Narrow(1),
source: 1,
order: 0,
},
NbaUpdate {
cell: cell_a,
value: FourStateValue::Narrow(1),
source: 1,
order: 1,
},
NbaUpdate {
cell: cell_b,
value: FourStateValue::Narrow(1),
source: 1,
order: 2,
},
];
let mut arena1 = FakeArena::new();
let acts1 = nba_drain_destination(
updates.clone(),
&mut arena1,
&sensitivity,
SchedTime { ticks: 7 },
);
let updates2 = vec![updates[1].clone(), updates[2].clone(), updates[0].clone()];
let mut arena2 = FakeArena::new();
let acts2 =
nba_drain_destination(updates2, &mut arena2, &sensitivity, SchedTime { ticks: 7 });
let mut sorted1 = acts1;
let mut sorted2 = acts2;
sort_ready_set(&mut sorted1);
sort_ready_set(&mut sorted2);
let procs1: Vec<SchedProcessId> = sorted1.iter().map(|a| a.proc).collect();
let procs2: Vec<SchedProcessId> = sorted2.iter().map(|a| a.proc).collect();
assert_eq!(
procs1, procs2,
"canonical order must not depend on the original stage/arrival order"
);
assert_eq!(procs1.len(), 3);
}
#[test]
fn nba_drain_destination_delegates_to_commit_nba_not_a_second_diff_implementation() {
let cell = StateCellId(220);
let mut arena = FakeArena::new();
arena.seed(cell, FourStateValue::Narrow(5));
let sensitivity = SensitivityTable::new();
let updates = vec![NbaUpdate {
cell,
value: FourStateValue::Narrow(5),
source: 1,
order: 0,
}];
let activations =
nba_drain_destination(updates, &mut arena, &sensitivity, SchedTime { ticks: 0 });
assert!(
activations.is_empty(),
"an NBA write equal to the current value must produce no activation — a \
reimplementation that diffs against a stale/default value instead of \
genuinely calling commit_nba's arena-compare would produce a spurious one here"
);
let src = std::fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/sched/subscribe.rs"
))
.expect("must be able to read subscribe.rs's own source");
let production_only = crate::determinism::scan_support::strip_cfg_test_blocks(&src);
assert!(
production_only.contains("commit_nba("),
"nba_drain_destination must call Task 7's commit_nba"
);
assert!(
!production_only.contains(".notify_change("),
"subscribe.rs must not call .notify_change( itself — it must delegate to \
commit_nba, not reimplement NBA commit semantics"
);
}
}