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