use crate::sched::{SchedTime, infer_edge};
use crate::state::{ChangeDelta, FourStateValue, SignalAccess, StateCellId, ValueChangeSink};
use vj_abi::Region;
pub fn commit_blocking_write(
cell: StateCellId,
value: FourStateValue,
arena: &mut dyn SignalAccess,
at: SchedTime,
sink: &mut dyn ValueChangeSink,
) -> bool {
let old = arena.read(cell);
let new_for_edge = value.clone();
let changed = arena.write(cell, value);
if changed {
if sink.wants_delta(cell) {
sink.note_delta(cell, ChangeDelta::between(&old, &new_for_edge));
}
notify_blocking_change(cell, infer_edge(&old, &new_for_edge), at, sink);
}
notify_alias_changes(arena, at, sink);
changed
}
pub fn notify_alias_changes(
arena: &mut dyn SignalAccess,
at: SchedTime,
sink: &mut dyn ValueChangeSink,
) {
for change in arena.take_alias_changes() {
let edge = if change.no_edge {
crate::state::EdgeKind::None
} else {
infer_edge(&change.old, &change.new)
};
notify_blocking_change(change.cell, edge, at, sink);
}
}
pub fn notify_blocking_change(
cell: StateCellId,
edge: crate::state::EdgeKind,
at: SchedTime,
sink: &mut dyn ValueChangeSink,
) {
sink.notify_change(cell, edge, at.to_abi_display(0), Region::Active);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sched::{DeferredIoCallback, NbaLog, ProcessId, SchedulerOps};
use crate::state::{EdgeKind, ListenerRole};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use vj_abi::SimTime;
struct FakeArenaState {
cells: HashMap<StateCellId, FourStateValue>,
notifications: Vec<(StateCellId, EdgeKind, SimTime, Region)>,
}
#[derive(Clone)]
struct FakeArena(Rc<RefCell<FakeArenaState>>);
struct ArenaHandle(Rc<RefCell<FakeArenaState>>);
struct SinkHandle(Rc<RefCell<FakeArenaState>>);
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,
}
}
impl FakeArena {
fn new() -> Self {
Self(Rc::new(RefCell::new(FakeArenaState {
cells: HashMap::new(),
notifications: Vec::new(),
})))
}
fn seed(&self, cell: StateCellId, value: FourStateValue) {
self.0.borrow_mut().cells.insert(cell, value);
}
fn notifications(&self) -> Vec<(StateCellId, EdgeKind, SimTime, Region)> {
self.0.borrow().notifications.clone()
}
fn arena_handle(&self) -> ArenaHandle {
ArenaHandle(self.0.clone())
}
fn sink_handle(&self) -> SinkHandle {
SinkHandle(self.0.clone())
}
}
impl SignalAccess for ArenaHandle {
fn read(&self, cell: StateCellId) -> FourStateValue {
self.0
.borrow()
.cells
.get(&cell)
.cloned()
.unwrap_or(FourStateValue::Narrow(0))
}
fn write(&mut self, cell: StateCellId, value: FourStateValue) -> bool {
let mut state = self.0.borrow_mut();
let changed = match state.cells.get(&cell) {
Some(existing) => !four_state_eq(existing, &value),
None => true,
};
state.cells.insert(cell, value);
changed
}
fn subscribe(
&mut self,
_cell: StateCellId,
_role: ListenerRole,
_sink: Box<dyn ValueChangeSink + Send>,
) {
}
}
impl ValueChangeSink for SinkHandle {
fn notify_change(
&mut self,
cell: StateCellId,
edge: EdgeKind,
at: SimTime,
region: Region,
) {
self.0
.borrow_mut()
.notifications
.push((cell, edge, at, region));
}
}
const PROC_A: ProcessId = 1;
#[test]
fn commit_blocking_write_is_visible_immediately_same_activation() {
let fake = FakeArena::new();
let cell = StateCellId(1);
fake.seed(cell, FourStateValue::Narrow(0));
let mut arena = fake.arena_handle();
let mut sink = fake.sink_handle();
let changed = commit_blocking_write(
cell,
FourStateValue::Narrow(1),
&mut arena,
SchedTime { ticks: 0 },
&mut sink,
);
assert!(changed);
assert!(matches!(arena.read(cell), FourStateValue::Narrow(1)));
}
#[test]
fn commit_blocking_write_triggers_notify_within_the_same_active_pass() {
let fake = FakeArena::new();
let cell = StateCellId(2);
fake.seed(cell, FourStateValue::Narrow(0));
let mut arena = fake.arena_handle();
let mut sink = fake.sink_handle();
commit_blocking_write(
cell,
FourStateValue::Narrow(1),
&mut arena,
SchedTime { ticks: 5 },
&mut sink,
);
let notes = fake.notifications();
assert_eq!(
notes.len(),
1,
"exactly one notify_change call, with no deferral to a later region"
);
assert_eq!(notes[0].0, cell);
assert_eq!(
notes[0].3,
Region::Active,
"commit_blocking_write must tag its notify_change call Region::Active"
);
}
#[test]
fn commit_blocking_write_does_not_notify_when_the_value_is_unchanged() {
let fake = FakeArena::new();
let cell = StateCellId(9);
fake.seed(cell, FourStateValue::Narrow(5));
let mut arena = fake.arena_handle();
let mut sink = fake.sink_handle();
let changed = commit_blocking_write(
cell,
FourStateValue::Narrow(5),
&mut arena,
SchedTime { ticks: 0 },
&mut sink,
);
assert!(
!changed,
"writing the cell's own current value must report no change"
);
assert!(
fake.notifications().is_empty(),
"a write that does not change the stored value must never call notify_change"
);
}
#[test]
fn commit_blocking_write_behaves_identically_for_a_net_backed_or_var_backed_cell() {
let net_labeled_cell = StateCellId(10);
let var_labeled_cell = StateCellId(11);
let fake = FakeArena::new();
fake.seed(net_labeled_cell, FourStateValue::Narrow(0));
fake.seed(var_labeled_cell, FourStateValue::Narrow(0));
let mut arena = fake.arena_handle();
let mut sink = fake.sink_handle();
let changed_a = commit_blocking_write(
net_labeled_cell,
FourStateValue::Narrow(1),
&mut arena,
SchedTime { ticks: 0 },
&mut sink,
);
let changed_b = commit_blocking_write(
var_labeled_cell,
FourStateValue::Narrow(1),
&mut arena,
SchedTime { ticks: 0 },
&mut sink,
);
assert_eq!(changed_a, changed_b);
let notes = fake.notifications();
assert_eq!(notes.len(), 2);
assert_eq!(
notes[0].1, notes[1].1,
"both transitions must classify to the same edge"
);
assert_eq!(
notes[0].3, notes[1].3,
"both calls must tag the same region"
);
}
#[test]
fn ops_rs_production_code_has_no_signal_origin_vocabulary() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/sched/ops.rs");
let src = std::fs::read_to_string(&path)
.expect("read sched/ops.rs for the SC-043 source-text scan");
let production_only = crate::determinism::scan_support::strip_cfg_test_blocks(&src);
let lower = production_only.to_lowercase();
let banned = ["net", "strength", "driver"];
let found: Vec<&str> = banned
.iter()
.copied()
.filter(|needle| lower.contains(needle))
.collect();
assert!(
found.is_empty(),
"sched/ops.rs production code must draw no net-vs-variable \
distinction (SC-043), but found banned substring(s) {found:?}"
);
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum MockOpsCall {
Now,
CurrentRegion,
StageNba,
CommitBlockingWrite,
RequestFinish,
DeferIo,
RegisterRegionCallback,
}
struct MockSchedulerOps {
arena: RefCell<FakeArena>,
nba: RefCell<NbaLog>,
calls: RefCell<Vec<MockOpsCall>>,
region: Region,
now: SchedTime,
}
impl MockSchedulerOps {
fn new(arena: FakeArena, region: Region, now: SchedTime) -> Self {
Self {
arena: RefCell::new(arena),
nba: RefCell::new(NbaLog::new()),
calls: RefCell::new(Vec::new()),
region,
now,
}
}
}
impl SchedulerOps for MockSchedulerOps {
fn now(&self) -> SchedTime {
self.calls.borrow_mut().push(MockOpsCall::Now);
self.now
}
fn current_region(&self) -> Region {
self.calls.borrow_mut().push(MockOpsCall::CurrentRegion);
self.region
}
fn stage_nba(&self, cell: StateCellId, value: FourStateValue, source: ProcessId) {
self.calls.borrow_mut().push(MockOpsCall::StageNba);
self.nba.borrow_mut().stage(cell, value, source);
}
fn commit_blocking_write(&self, cell: StateCellId, value: FourStateValue) {
self.calls
.borrow_mut()
.push(MockOpsCall::CommitBlockingWrite);
let fake = self.arena.borrow();
let mut arena_handle = fake.arena_handle();
let mut sink_handle = fake.sink_handle();
super::commit_blocking_write(
cell,
value,
&mut arena_handle,
self.now,
&mut sink_handle,
);
}
fn request_finish(&self, _code: i32) {
self.calls.borrow_mut().push(MockOpsCall::RequestFinish);
}
fn defer_io(&self, _at: SchedTime, _region: Region, _cb: DeferredIoCallback) {
self.calls.borrow_mut().push(MockOpsCall::DeferIo);
}
fn register_region_callback(
&self,
_region: Region,
_at: SchedTime,
_cb: Box<dyn crate::sched::RegionCallback>,
) {
self.calls
.borrow_mut()
.push(MockOpsCall::RegisterRegionCallback);
}
}
struct NoopRegionCallback;
impl crate::sched::RegionCallback for NoopRegionCallback {
fn invoke(&mut self, _ctx: &dyn crate::sched::SchedulerCtx) {}
}
#[test]
fn mock_scheduler_ops_exercises_all_seven_methods() {
let fake = FakeArena::new();
let cell = StateCellId(20);
fake.seed(cell, FourStateValue::Narrow(0));
let ops = MockSchedulerOps::new(fake, Region::Active, SchedTime { ticks: 0 });
let resume_body = |ops: &MockSchedulerOps| {
let _ = ops.now();
let _ = ops.current_region();
ops.stage_nba(cell, FourStateValue::Narrow(1), PROC_A);
ops.commit_blocking_write(cell, FourStateValue::Narrow(2));
ops.request_finish(0);
ops.defer_io(SchedTime { ticks: 0 }, Region::Active, Box::new(|_ctx| {}));
ops.register_region_callback(
Region::Postponed,
SchedTime { ticks: 0 },
Box::new(NoopRegionCallback),
);
};
resume_body(&ops);
let calls = ops.calls.borrow();
assert_eq!(calls.len(), 7, "the trait has exactly seven methods");
assert_eq!(
*calls,
vec![
MockOpsCall::Now,
MockOpsCall::CurrentRegion,
MockOpsCall::StageNba,
MockOpsCall::CommitBlockingWrite,
MockOpsCall::RequestFinish,
MockOpsCall::DeferIo,
MockOpsCall::RegisterRegionCallback,
],
"each of the seven methods must appear exactly once, in call order"
);
}
}