1use crate::sched::{SchedTime, infer_edge};
19use crate::state::{ChangeDelta, FourStateValue, SignalAccess, StateCellId, ValueChangeSink};
20use sv_sim_abi::Region;
21
22pub fn commit_blocking_write(
30 cell: StateCellId,
31 value: FourStateValue,
32 arena: &mut dyn SignalAccess,
33 at: SchedTime,
34 sink: &mut dyn ValueChangeSink,
35) -> bool {
36 let old = arena.read(cell);
37 let new_for_edge = value.clone();
38 let changed = arena.write(cell, value);
39 if changed {
40 if sink.wants_delta(cell) {
41 sink.note_delta(cell, ChangeDelta::between(&old, &new_for_edge));
42 }
43 notify_blocking_change(cell, infer_edge(&old, &new_for_edge), at, sink);
44 }
45 notify_alias_changes(arena, at, sink);
46 changed
47}
48
49pub fn notify_alias_changes(
52 arena: &mut dyn SignalAccess,
53 at: SchedTime,
54 sink: &mut dyn ValueChangeSink,
55) {
56 for change in arena.take_alias_changes() {
57 let edge = if change.no_edge {
58 crate::state::EdgeKind::None
59 } else {
60 infer_edge(&change.old, &change.new)
61 };
62 notify_blocking_change(change.cell, edge, at, sink);
63 }
64}
65
66pub fn notify_blocking_change(
72 cell: StateCellId,
73 edge: crate::state::EdgeKind,
74 at: SchedTime,
75 sink: &mut dyn ValueChangeSink,
76) {
77 sink.notify_change(cell, edge, at.to_abi_display(0), Region::Active);
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use crate::sched::{DeferredIoCallback, NbaLog, ProcessId, SchedulerOps};
84 use crate::state::{EdgeKind, ListenerRole};
85 use std::cell::RefCell;
86 use std::collections::HashMap;
87 use std::rc::Rc;
88 use sv_sim_abi::SimTime;
89
90 struct FakeArenaState {
99 cells: HashMap<StateCellId, FourStateValue>,
100 notifications: Vec<(StateCellId, EdgeKind, SimTime, Region)>,
101 }
102
103 #[derive(Clone)]
104 struct FakeArena(Rc<RefCell<FakeArenaState>>);
105
106 struct ArenaHandle(Rc<RefCell<FakeArenaState>>);
107 struct SinkHandle(Rc<RefCell<FakeArenaState>>);
108
109 fn four_state_eq(a: &FourStateValue, b: &FourStateValue) -> bool {
110 match (a, b) {
111 (FourStateValue::Narrow(x), FourStateValue::Narrow(y)) => x == y,
112 (
113 FourStateValue::Wide { aval: a1, bval: b1 },
114 FourStateValue::Wide { aval: a2, bval: b2 },
115 ) => a1 == a2 && b1 == b2,
116 _ => false,
117 }
118 }
119
120 impl FakeArena {
121 fn new() -> Self {
122 Self(Rc::new(RefCell::new(FakeArenaState {
123 cells: HashMap::new(),
124 notifications: Vec::new(),
125 })))
126 }
127
128 fn seed(&self, cell: StateCellId, value: FourStateValue) {
129 self.0.borrow_mut().cells.insert(cell, value);
130 }
131
132 fn notifications(&self) -> Vec<(StateCellId, EdgeKind, SimTime, Region)> {
133 self.0.borrow().notifications.clone()
134 }
135
136 fn arena_handle(&self) -> ArenaHandle {
137 ArenaHandle(self.0.clone())
138 }
139
140 fn sink_handle(&self) -> SinkHandle {
141 SinkHandle(self.0.clone())
142 }
143 }
144
145 impl SignalAccess for ArenaHandle {
146 fn read(&self, cell: StateCellId) -> FourStateValue {
147 self.0
148 .borrow()
149 .cells
150 .get(&cell)
151 .cloned()
152 .unwrap_or(FourStateValue::Narrow(0))
153 }
154
155 fn write(&mut self, cell: StateCellId, value: FourStateValue) -> bool {
156 let mut state = self.0.borrow_mut();
157 let changed = match state.cells.get(&cell) {
158 Some(existing) => !four_state_eq(existing, &value),
159 None => true,
160 };
161 state.cells.insert(cell, value);
162 changed
163 }
164
165 fn subscribe(
166 &mut self,
167 _cell: StateCellId,
168 _role: ListenerRole,
169 _sink: Box<dyn ValueChangeSink + Send>,
170 ) {
171 }
174 }
175
176 impl ValueChangeSink for SinkHandle {
177 fn notify_change(
178 &mut self,
179 cell: StateCellId,
180 edge: EdgeKind,
181 at: SimTime,
182 region: Region,
183 ) {
184 self.0
185 .borrow_mut()
186 .notifications
187 .push((cell, edge, at, region));
188 }
189 }
190
191 const PROC_A: ProcessId = 1;
192
193 #[test]
196 fn commit_blocking_write_is_visible_immediately_same_activation() {
197 let fake = FakeArena::new();
198 let cell = StateCellId(1);
199 fake.seed(cell, FourStateValue::Narrow(0));
200 let mut arena = fake.arena_handle();
201 let mut sink = fake.sink_handle();
202
203 let changed = commit_blocking_write(
204 cell,
205 FourStateValue::Narrow(1),
206 &mut arena,
207 SchedTime { ticks: 0 },
208 &mut sink,
209 );
210
211 assert!(changed);
212 assert!(matches!(arena.read(cell), FourStateValue::Narrow(1)));
217 }
218
219 #[test]
220 fn commit_blocking_write_triggers_notify_within_the_same_active_pass() {
221 let fake = FakeArena::new();
222 let cell = StateCellId(2);
223 fake.seed(cell, FourStateValue::Narrow(0));
224 let mut arena = fake.arena_handle();
225 let mut sink = fake.sink_handle();
226
227 commit_blocking_write(
228 cell,
229 FourStateValue::Narrow(1),
230 &mut arena,
231 SchedTime { ticks: 5 },
232 &mut sink,
233 );
234
235 let notes = fake.notifications();
236 assert_eq!(
237 notes.len(),
238 1,
239 "exactly one notify_change call, with no deferral to a later region"
240 );
241 assert_eq!(notes[0].0, cell);
242 assert_eq!(
243 notes[0].3,
244 Region::Active,
245 "commit_blocking_write must tag its notify_change call Region::Active"
246 );
247 }
248
249 #[test]
257 fn commit_blocking_write_does_not_notify_when_the_value_is_unchanged() {
258 let fake = FakeArena::new();
259 let cell = StateCellId(9);
260 fake.seed(cell, FourStateValue::Narrow(5));
261 let mut arena = fake.arena_handle();
262 let mut sink = fake.sink_handle();
263
264 let changed = commit_blocking_write(
265 cell,
266 FourStateValue::Narrow(5),
267 &mut arena,
268 SchedTime { ticks: 0 },
269 &mut sink,
270 );
271
272 assert!(
273 !changed,
274 "writing the cell's own current value must report no change"
275 );
276 assert!(
277 fake.notifications().is_empty(),
278 "a write that does not change the stored value must never call notify_change"
279 );
280 }
281
282 #[test]
283 fn commit_blocking_write_behaves_identically_for_a_net_backed_or_var_backed_cell() {
284 let net_labeled_cell = StateCellId(10);
291 let var_labeled_cell = StateCellId(11);
292
293 let fake = FakeArena::new();
294 fake.seed(net_labeled_cell, FourStateValue::Narrow(0));
295 fake.seed(var_labeled_cell, FourStateValue::Narrow(0));
296 let mut arena = fake.arena_handle();
297 let mut sink = fake.sink_handle();
298
299 let changed_a = commit_blocking_write(
300 net_labeled_cell,
301 FourStateValue::Narrow(1),
302 &mut arena,
303 SchedTime { ticks: 0 },
304 &mut sink,
305 );
306 let changed_b = commit_blocking_write(
307 var_labeled_cell,
308 FourStateValue::Narrow(1),
309 &mut arena,
310 SchedTime { ticks: 0 },
311 &mut sink,
312 );
313
314 assert_eq!(changed_a, changed_b);
315 let notes = fake.notifications();
316 assert_eq!(notes.len(), 2);
317 assert_eq!(
318 notes[0].1, notes[1].1,
319 "both transitions must classify to the same edge"
320 );
321 assert_eq!(
322 notes[0].3, notes[1].3,
323 "both calls must tag the same region"
324 );
325 }
326
327 #[test]
336 fn ops_rs_production_code_has_no_signal_origin_vocabulary() {
337 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/sched/ops.rs");
338 let src = std::fs::read_to_string(&path)
339 .expect("read sched/ops.rs for the SC-043 source-text scan");
340 let production_only = crate::determinism::scan_support::strip_cfg_test_blocks(&src);
341 let lower = production_only.to_lowercase();
342 let banned = ["net", "strength", "driver"];
343 let found: Vec<&str> = banned
344 .iter()
345 .copied()
346 .filter(|needle| lower.contains(needle))
347 .collect();
348 assert!(
349 found.is_empty(),
350 "sched/ops.rs production code must draw no net-vs-variable \
351 distinction (SC-043), but found banned substring(s) {found:?}"
352 );
353 }
354
355 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
358 enum MockOpsCall {
359 Now,
360 CurrentRegion,
361 StageNba,
362 CommitBlockingWrite,
363 RequestFinish,
364 DeferIo,
365 RegisterRegionCallback,
366 }
367
368 struct MockSchedulerOps {
374 arena: RefCell<FakeArena>,
375 nba: RefCell<NbaLog>,
380 calls: RefCell<Vec<MockOpsCall>>,
381 region: Region,
382 now: SchedTime,
383 }
384
385 impl MockSchedulerOps {
386 fn new(arena: FakeArena, region: Region, now: SchedTime) -> Self {
387 Self {
388 arena: RefCell::new(arena),
389 nba: RefCell::new(NbaLog::new()),
390 calls: RefCell::new(Vec::new()),
391 region,
392 now,
393 }
394 }
395 }
396
397 impl SchedulerOps for MockSchedulerOps {
398 fn now(&self) -> SchedTime {
399 self.calls.borrow_mut().push(MockOpsCall::Now);
400 self.now
401 }
402
403 fn current_region(&self) -> Region {
404 self.calls.borrow_mut().push(MockOpsCall::CurrentRegion);
405 self.region
406 }
407
408 fn stage_nba(&self, cell: StateCellId, value: FourStateValue, source: ProcessId) {
409 self.calls.borrow_mut().push(MockOpsCall::StageNba);
410 self.nba.borrow_mut().stage(cell, value, source);
411 }
412
413 fn commit_blocking_write(&self, cell: StateCellId, value: FourStateValue) {
414 self.calls
415 .borrow_mut()
416 .push(MockOpsCall::CommitBlockingWrite);
417 let fake = self.arena.borrow();
418 let mut arena_handle = fake.arena_handle();
419 let mut sink_handle = fake.sink_handle();
420 super::commit_blocking_write(
421 cell,
422 value,
423 &mut arena_handle,
424 self.now,
425 &mut sink_handle,
426 );
427 }
428
429 fn request_finish(&self, _code: i32) {
430 self.calls.borrow_mut().push(MockOpsCall::RequestFinish);
431 }
432
433 fn defer_io(&self, _at: SchedTime, _region: Region, _cb: DeferredIoCallback) {
434 self.calls.borrow_mut().push(MockOpsCall::DeferIo);
435 }
436
437 fn register_region_callback(
438 &self,
439 _region: Region,
440 _at: SchedTime,
441 _cb: Box<dyn crate::sched::RegionCallback>,
442 ) {
443 self.calls
444 .borrow_mut()
445 .push(MockOpsCall::RegisterRegionCallback);
446 }
447 }
448
449 struct NoopRegionCallback;
454 impl crate::sched::RegionCallback for NoopRegionCallback {
455 fn invoke(&mut self, _ctx: &dyn crate::sched::SchedulerCtx) {}
456 }
457
458 #[test]
459 fn mock_scheduler_ops_exercises_all_seven_methods() {
460 let fake = FakeArena::new();
461 let cell = StateCellId(20);
462 fake.seed(cell, FourStateValue::Narrow(0));
463 let ops = MockSchedulerOps::new(fake, Region::Active, SchedTime { ticks: 0 });
464
465 let resume_body = |ops: &MockSchedulerOps| {
468 let _ = ops.now();
469 let _ = ops.current_region();
470 ops.stage_nba(cell, FourStateValue::Narrow(1), PROC_A);
471 ops.commit_blocking_write(cell, FourStateValue::Narrow(2));
472 ops.request_finish(0);
473 ops.defer_io(SchedTime { ticks: 0 }, Region::Active, Box::new(|_ctx| {}));
474 ops.register_region_callback(
475 Region::Postponed,
476 SchedTime { ticks: 0 },
477 Box::new(NoopRegionCallback),
478 );
479 };
480 resume_body(&ops);
481
482 let calls = ops.calls.borrow();
483 assert_eq!(calls.len(), 7, "the trait has exactly seven methods");
484 assert_eq!(
485 *calls,
486 vec![
487 MockOpsCall::Now,
488 MockOpsCall::CurrentRegion,
489 MockOpsCall::StageNba,
490 MockOpsCall::CommitBlockingWrite,
491 MockOpsCall::RequestFinish,
492 MockOpsCall::DeferIo,
493 MockOpsCall::RegisterRegionCallback,
494 ],
495 "each of the seven methods must appear exactly once, in call order"
496 );
497 }
498}