Skip to main content

rustdv_methodology/
objection.rs

1//! Objections: distributed end-of-test consensus with RAII guards
2//! (design-doc §5.3; pyuvm: ObjectionHandler, uvm_component.objection()).
3//! Forgetting to drop is impossible; diagnostics (description, raise site)
4//! are captured in the guard.
5
6use std::cell::{Cell, RefCell};
7use std::rc::Rc;
8
9use rustdv_sim::sync::Event;
10
11struct ObjInner {
12    count: Cell<usize>,
13    drained: Event,
14    raised_ever: Cell<bool>,
15    active: RefCell<Vec<String>>,
16}
17
18#[derive(Clone)]
19pub struct ObjectionRegistry {
20    inner: Rc<ObjInner>,
21}
22
23impl ObjectionRegistry {
24    #[allow(clippy::new_without_default)]
25    pub fn new() -> ObjectionRegistry {
26        ObjectionRegistry {
27            inner: Rc::new(ObjInner {
28                count: Cell::new(0),
29                drained: Event::new(),
30                raised_ever: Cell::new(false),
31                active: RefCell::new(Vec::new()),
32            }),
33        }
34    }
35
36    pub fn raise(&self, description: &str) -> ObjectionGuard {
37        let inner = self.inner.clone();
38        inner.count.set(inner.count.get() + 1);
39        inner.raised_ever.set(true);
40        inner.drained.clear();
41        inner.active.borrow_mut().push(description.to_string());
42        ObjectionGuard { inner, description: description.to_string() }
43    }
44
45    pub fn count(&self) -> usize {
46        self.inner.count.get()
47    }
48
49    /// Was an objection ever raised? The runner asks before awaiting
50    /// consensus, so a Part II test that never objects is not scolded by
51    /// `wait_all_dropped`'s pyuvm warning (D46: both front doors, one path).
52    pub fn ever_raised(&self) -> bool {
53        self.inner.raised_ever.get()
54    }
55
56    /// Wait for the run phase to end by objection consensus (D82/D82b).
57    ///
58    /// Unlike [`ObjectionRegistry::wait_all_dropped`], this takes no shortcut: it waits on the
59    /// `drained` event, which is set only when a raised objection count falls
60    /// back to zero. That is exactly the semantics the phaser needs to *race*
61    /// against the run tree:
62    ///
63    /// - objections raised and later dropped → the event fires and the phase
64    ///   ends, cancelling responder loops that never return;
65    /// - no objection ever raised → the event never fires, so the run tree
66    ///   decides when the phase ends (D46's second front door).
67    ///
68    /// The runner cannot ask `ever_raised()` up front, because at that moment
69    /// no run body has executed and nothing has been raised yet.
70    pub async fn wait_drained_event(&self) {
71        self.inner.drained.wait().await;
72    }
73
74    /// Objection report for timeout diagnostics (pyuvm ObjectionHandler).
75    pub fn active(&self) -> Vec<String> {
76        self.inner.active.borrow().clone()
77    }
78
79    pub async fn wait_all_dropped(&self) {
80        if !self.inner.raised_ever.get() {
81            // pyuvm's run_phase_complete warning path, ported as-is.
82            rustdv_sim::log::warning(
83                "all_objections_dropped awaited but no objection was ever raised",
84            );
85            return;
86        }
87        if self.inner.count.get() == 0 {
88            return;
89        }
90        self.inner.drained.wait().await;
91    }
92}
93
94/// RAII objection. Drop = drop_objection (mapping row 32).
95pub struct ObjectionGuard {
96    inner: Rc<ObjInner>,
97    description: String,
98}
99
100impl Drop for ObjectionGuard {
101    fn drop(&mut self) {
102        let mut active = self.inner.active.borrow_mut();
103        if let Some(pos) = active.iter().position(|d| *d == self.description) {
104            active.remove(pos);
105        }
106        drop(active);
107        let n = self.inner.count.get().saturating_sub(1);
108        self.inner.count.set(n);
109        if n == 0 {
110            self.inner.drained.set();
111        }
112    }
113}
114
115// ===========================================================================
116// Tests — no simulator.
117// ===========================================================================
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use rustdv_sim::testing::{assert_pending, block_on};
123
124    #[test]
125    fn a_guard_raises_and_dropping_it_drops() {
126        let reg = ObjectionRegistry::new();
127        assert_eq!(reg.count(), 0);
128        {
129            let _g = reg.raise("stimulus");
130            assert_eq!(reg.count(), 1);
131        }
132        assert_eq!(reg.count(), 0, "the guard dropped it");
133    }
134
135    #[test]
136    fn nested_objections_end_only_at_the_last_drop() {
137        let reg = ObjectionRegistry::new();
138        let a = reg.raise("a");
139        let b = reg.raise("b");
140        assert_eq!(reg.count(), 2);
141        drop(a);
142        assert_eq!(reg.count(), 1, "one left");
143        drop(b);
144        assert_eq!(reg.count(), 0);
145    }
146
147    #[test]
148    fn drained_fires_when_a_raised_count_returns_to_zero() {
149        block_on(async {
150            let reg = ObjectionRegistry::new();
151            let g = reg.raise("work");
152            let waiter = reg.clone();
153            rustdv_sim::executor::spawn(async move {
154                waiter.wait_drained_event().await;
155            });
156            drop(g);
157            // The waiter completes; if it did not, block_on would time out.
158            reg.wait_drained_event().await;
159        });
160    }
161
162    /// The D82b bug, as a regression test. Arming the race on "has anything
163    /// ever objected?" answered `false` before any run body had executed, so
164    /// the race was never armed and every responder-style testbench hung.
165    /// The event's own semantics carry D46's rule instead: never objecting
166    /// simply never fires.
167    #[test]
168    fn never_objecting_never_fires_the_drained_event() {
169        let reg = ObjectionRegistry::new();
170        assert_eq!(reg.count(), 0);
171        assert_pending(async move { reg.wait_drained_event().await });
172    }
173}