Skip to main content

rustdv_methodology/
analysis.rs

1//! Analysis: one write, every subscriber hears it (D86–D88; pyuvm
2//! `uvm_analysis_port`, `uvm_subscriber`, `uvm_tlm_analysis_fifo`).
3//!
4//! # Analysis is not a queue
5//!
6//! [`AnalysisBus`] and [`TlmFifo`](crate::TlmFifo) share three letters and
7//! nothing else. A `TlmFifo` is a *queue*: one consumer takes each item, the
8//! producer blocks when it is full, and the item is gone once taken. An
9//! `AnalysisBus` is a *broadcast*, and **it has no queue at all**: `write`
10//! calls every subscriber and returns. Nothing is stored, so a write with no
11//! subscribers is not buffered for later — it is simply gone, which is legal
12//! and is what a monitor nobody listens to should cost. Do not reach for one
13//! expecting the other.
14//!
15//! To keep the traffic, *subscribe and keep it*: a subscriber's `write` puts
16//! the item wherever that component wants it — a `Vec`, an unbounded
17//! `TlmFifo`, a comparison against a prediction. The hub is not the memory;
18//! the subscriber is.
19//!
20//! # Why delivery is synchronous
21//!
22//! A monitor writes a transaction and moves on within the same simulation
23//! instant — the time wheel must not turn because a scoreboard was listening.
24//! So `write` is not `async`: the publisher's call runs every subscriber's
25//! handler and returns.
26//!
27//! That is only possible because a subscriber shares its **state** rather than
28//! itself. A handler needs `&mut` its data, and no component can hand out
29//! `&mut self` to a sibling — so the data lives in a
30//! [`RustdvShared`](crate::RustdvShared), the component keeps one handle, and
31//! the port gets another. An earlier design queued items and delivered them
32//! later; "later" is exactly what analysis must not do.
33//!
34//! # One connection idiom (Ray, 2026-07-24)
35//!
36//! The UVM broadcasts straight from a source's analysis port to subscribers.
37//! rustdv's components are erased, so neither side can reach the other, and
38//! analysis gets a **hub** for the same reason put/get has a FIFO: a concrete
39//! `#[component]` child that the parent owns and can wire.
40//!
41//! ```ignore
42//! self.bus.pub_export().connect(&self.mon, Monitor::AP);
43//! self.bus.sub_export().connect(&self.sb, Scoreboard::INPUT);
44//! self.bus.sub_export().connect(&self.cov, Coverage::INPUT);
45//! ```
46//!
47//! Several subscribers on one `sub_export()` is what makes it a broadcast.
48//! This is a deliberate divergence from IEEE 1800.2 — which we are not
49//! implementing — and one idiom to learn beats two.
50
51use std::cell::RefCell;
52use std::rc::Rc;
53
54use crate::component::{Component, ComponentNode};
55use crate::port::{PortName, PortOwner, PublishIf, SinkHandle, bind_or_panic, sink_of};
56
57// ===========================================================================
58// The hub
59// ===========================================================================
60
61/// What every handle to one hub points at. A subscriber list, and nothing
62/// else — there is no queue here by design (D90).
63struct HubInner<T: 'static> {
64    subs: RefCell<Vec<Rc<dyn SinkHandle<T>>>>,
65}
66
67impl<T: 'static> HubInner<T> {
68    /// Hand the item to every subscriber, in connection order, and return.
69    ///
70    /// No queue, no clone of the item, no yield: subscribers see a `&T` and
71    /// take from it what they want to keep.
72    fn broadcast(&self, item: &T) {
73        // Cloned out of the RefCell first: a subscriber's handler is free to
74        // do anything, and this loop must not hold a borrow while it runs.
75        let subs: Vec<Rc<dyn SinkHandle<T>>> = self.subs.borrow().clone();
76        for sub in subs {
77            sub.deliver(item);
78        }
79    }
80}
81
82impl<T: 'static> PublishIf<T> for HubInner<T> {
83    fn write(&self, item: &T) {
84        self.broadcast(item);
85    }
86}
87
88/// The publish side of a hub: connect it to a source's
89/// [`PublishPort`](crate::PublishPort).
90pub struct PublishExport<T: 'static> {
91    inner: Rc<HubInner<T>>,
92}
93
94impl<T: 'static> PublishExport<T> {
95    pub fn connect(&self, owner: &dyn PortOwner, name: PortName<dyn PublishIf<T>>) {
96        bind_or_panic(owner, name, self.inner.clone() as Rc<dyn PublishIf<T>>);
97    }
98}
99
100/// The subscribe side of a hub. Connect as many subscribers to it as you
101/// like — that is what makes the write a broadcast.
102pub struct SubscribeExport<T: 'static> {
103    inner: Rc<HubInner<T>>,
104}
105
106impl<T: 'static> SubscribeExport<T> {
107    /// Take the component's subscriber and add it to the broadcast list.
108    ///
109    /// Unlike a put/get connect, nothing is written *into* the port: the
110    /// component already put its subscriber there with `subscribe`, and the
111    /// hub collects it. Broadcast runs the other way, so the wiring does too.
112    pub fn connect(&self, owner: &dyn PortOwner, name: PortName<dyn SinkHandle<T>>) {
113        match sink_of(owner, name) {
114            Ok(sink) => self.inner.subs.borrow_mut().push(sink),
115            Err(e) => panic!("{e}"),
116        }
117    }
118}
119
120/// A broadcast hub: one publisher in, every subscriber out.
121///
122/// **It holds no items.** `write` calls each subscriber and returns; if nobody
123/// is subscribed the datum is gone (D90). A component that needs to keep the
124/// traffic subscribes and keeps it — in a `Vec`, or in an unbounded `TlmFifo`
125/// it owns, if it wants to pull on its own schedule.
126///
127/// Declare it as a child with `#[component]`, like a `TlmFifo`, then hand
128/// out its exports in `connect`: [`pub_export`](Self::pub_export) for the
129/// source, [`sub_export`](Self::sub_export) for each listener.
130pub struct AnalysisBus<T: 'static> {
131    inner: Rc<HubInner<T>>,
132}
133
134impl<T: 'static> Clone for AnalysisBus<T> {
135    /// Another handle to the *same* hub.
136    fn clone(&self) -> Self {
137        AnalysisBus {
138            inner: self.inner.clone(),
139        }
140    }
141}
142
143impl<T: 'static> Default for AnalysisBus<T> {
144    fn default() -> Self {
145        AnalysisBus::new()
146    }
147}
148
149impl<T: 'static> AnalysisBus<T> {
150    pub fn new() -> AnalysisBus<T> {
151        AnalysisBus {
152            inner: Rc::new(HubInner {
153                subs: RefCell::new(Vec::new()),
154            }),
155        }
156    }
157
158    /// The publish side, for a source's `PublishPort`.
159    pub fn pub_export(&self) -> PublishExport<T> {
160        PublishExport {
161            inner: self.inner.clone(),
162        }
163    }
164
165    /// The subscribe side, for a subscriber's `SubscribePort`. Connect
166    /// several; each one sees every item.
167    pub fn sub_export(&self) -> SubscribeExport<T> {
168        SubscribeExport {
169            inner: self.inner.clone(),
170        }
171    }
172
173    /// Broadcast an item, as the owner of the hub rather than through a port.
174    pub fn write(&self, item: &T) {
175        self.inner.broadcast(item);
176    }
177
178    /// How many subscribers are listening. Zero is legal.
179    pub fn subscriber_count(&self) -> usize {
180        self.inner.subs.borrow().len()
181    }
182}
183
184// A hub is a component: it appears in the hierarchy and its phases are no-ops.
185impl<T: 'static> Component for AnalysisBus<T> {}
186
187impl<T: 'static> ComponentNode for AnalysisBus<T> {
188    fn node_name(&self) -> &'static str {
189        "AnalysisBus"
190    }
191    fn children_mut(&mut self) -> Vec<(String, &mut (dyn ComponentNode + 'static))> {
192        Vec::new()
193    }
194}
195
196// ===========================================================================
197// Tests — no simulator. The broadcast is synchronous by design (D87).
198// ===========================================================================
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use crate::port::{
204        PortField, PortName, PortOwner, PublishPort, SinkHandle, SubscribePort, Subscriber,
205    };
206    use crate::shared::RustdvShared;
207    use std::any::Any;
208
209    #[derive(Default)]
210    struct Tally {
211        seen: Vec<u8>,
212    }
213    impl Subscriber<u8> for Tally {
214        fn write(&mut self, item: &u8) {
215            self.seen.push(*item);
216        }
217    }
218
219    struct Source {
220        ap: PublishPort<u8>,
221    }
222    impl Source {
223        const AP: PortName<dyn PublishIf<u8>> = PortName::new("ap");
224    }
225    impl PortOwner for Source {
226        fn owner_port_slot(&self, name: &str) -> Option<Rc<dyn Any>> {
227            (name == "ap").then(|| self.ap.slot_any())
228        }
229        fn owner_label(&self) -> &'static str {
230            "Source"
231        }
232    }
233
234    struct Listener {
235        input: SubscribePort<u8>,
236        tally: RustdvShared<Tally>,
237    }
238    impl Listener {
239        const INPUT: PortName<dyn SinkHandle<u8>> = PortName::new("input");
240        fn new() -> Listener {
241            let l = Listener {
242                input: SubscribePort::default(),
243                tally: RustdvShared::default(),
244            };
245            l.input.subscribe(l.tally.clone());
246            l
247        }
248    }
249    impl PortOwner for Listener {
250        fn owner_port_slot(&self, name: &str) -> Option<Rc<dyn Any>> {
251            (name == "input").then(|| self.input.slot_any())
252        }
253        fn owner_label(&self) -> &'static str {
254            "Listener"
255        }
256    }
257
258    #[test]
259    fn one_write_reaches_every_subscriber() {
260        let bus: AnalysisBus<u8> = AnalysisBus::new();
261        let src = Source {
262            ap: PublishPort::default(),
263        };
264        let a = Listener::new();
265        let b = Listener::new();
266
267        bus.pub_export().connect(&src, Source::AP);
268        bus.sub_export().connect(&a, Listener::INPUT);
269        bus.sub_export().connect(&b, Listener::INPUT);
270        assert_eq!(bus.subscriber_count(), 2);
271
272        src.ap.write(&7);
273        assert_eq!(a.tally.get().seen, vec![7]);
274        assert_eq!(
275            b.tally.get().seen,
276            vec![7],
277            "several subscribers is what makes it a broadcast"
278        );
279    }
280
281    #[test]
282    fn subscribers_are_called_in_connection_order() {
283        let bus: AnalysisBus<u8> = AnalysisBus::new();
284        let src = Source {
285            ap: PublishPort::default(),
286        };
287        let first = Listener::new();
288        let second = Listener::new();
289        bus.pub_export().connect(&src, Source::AP);
290        bus.sub_export().connect(&first, Listener::INPUT);
291        bus.sub_export().connect(&second, Listener::INPUT);
292
293        for n in 1..=3u8 {
294            src.ap.write(&n);
295        }
296        assert_eq!(first.tally.get().seen, vec![1, 2, 3]);
297        assert_eq!(second.tally.get().seen, vec![1, 2, 3]);
298    }
299
300    /// D90: the hub holds nothing. A datum broadcast to nobody is gone, and a
301    /// subscriber connected afterwards does not receive it.
302    #[test]
303    fn the_bus_stores_nothing() {
304        let bus: AnalysisBus<u8> = AnalysisBus::new();
305        let src = Source {
306            ap: PublishPort::default(),
307        };
308        bus.pub_export().connect(&src, Source::AP);
309
310        src.ap.write(&1); // nobody is listening
311        src.ap.write(&2);
312
313        let late = Listener::new();
314        bus.sub_export().connect(&late, Listener::INPUT);
315        assert!(
316            late.tally.get().seen.is_empty(),
317            "nothing was buffered for a late subscriber"
318        );
319
320        src.ap.write(&3);
321        assert_eq!(
322            late.tally.get().seen,
323            vec![3],
324            "only what arrives after it connects"
325        );
326    }
327
328    /// D85: analysis has min cardinality 0 — a monitor nobody listens to is a
329    /// legitimate testbench, and `write` on an unconnected port is legal.
330    #[test]
331    fn writing_with_no_subscribers_is_legal() {
332        let bus: AnalysisBus<u8> = AnalysisBus::new();
333        let src = Source {
334            ap: PublishPort::default(),
335        };
336        bus.pub_export().connect(&src, Source::AP);
337        assert_eq!(bus.subscriber_count(), 0);
338        src.ap.write(&1); // must not panic
339    }
340
341    #[test]
342    fn an_unconnected_publish_port_does_not_panic() {
343        let src = Source {
344            ap: PublishPort::default(),
345        };
346        assert!(!src.ap.has_subscribers());
347        src.ap.write(&1); // a source nobody wired is still a valid testbench
348    }
349
350    /// D87: delivery is synchronous — the handler has already run by the time
351    /// `write` returns, with no `await` anywhere in the path.
352    #[test]
353    fn delivery_happens_before_write_returns() {
354        let bus: AnalysisBus<u8> = AnalysisBus::new();
355        let src = Source {
356            ap: PublishPort::default(),
357        };
358        let sub = Listener::new();
359        bus.pub_export().connect(&src, Source::AP);
360        bus.sub_export().connect(&sub, Listener::INPUT);
361
362        src.ap.write(&5);
363        assert_eq!(
364            sub.tally.get().seen,
365            vec![5],
366            "already delivered, no scheduling in between"
367        );
368    }
369
370    /// A component that never called `subscribe` has nothing to receive with,
371    /// and connecting it says so by name rather than dropping items silently.
372    #[test]
373    #[should_panic(expected = "has no subscriber")]
374    fn connecting_a_subscriber_with_no_sink_is_a_named_error() {
375        let bus: AnalysisBus<u8> = AnalysisBus::new();
376        let bare = Listener {
377            input: SubscribePort::default(),
378            tally: RustdvShared::default(),
379        };
380        bus.sub_export().connect(&bare, Listener::INPUT);
381    }
382
383    #[test]
384    fn a_clone_is_the_same_bus() {
385        let bus: AnalysisBus<u8> = AnalysisBus::new();
386        let other = bus.clone();
387        let sub = Listener::new();
388        other.sub_export().connect(&sub, Listener::INPUT);
389        assert_eq!(
390            bus.subscriber_count(),
391            1,
392            "two handles, one subscriber list"
393        );
394        bus.write(&4);
395        assert_eq!(sub.tally.get().seen, vec![4]);
396    }
397}