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::{bind_or_panic, sink_of, PortName, PortOwner, PublishIf, SinkHandle};
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 { inner: self.inner.clone() }
138    }
139}
140
141impl<T: 'static> Default for AnalysisBus<T> {
142    fn default() -> Self {
143        AnalysisBus::new()
144    }
145}
146
147impl<T: 'static> AnalysisBus<T> {
148    pub fn new() -> AnalysisBus<T> {
149        AnalysisBus { inner: Rc::new(HubInner { subs: RefCell::new(Vec::new()) }) }
150    }
151
152    /// The publish side, for a source's `PublishPort`.
153    pub fn pub_export(&self) -> PublishExport<T> {
154        PublishExport { inner: self.inner.clone() }
155    }
156
157    /// The subscribe side, for a subscriber's `SubscribePort`. Connect
158    /// several; each one sees every item.
159    pub fn sub_export(&self) -> SubscribeExport<T> {
160        SubscribeExport { inner: self.inner.clone() }
161    }
162
163    /// Broadcast an item, as the owner of the hub rather than through a port.
164    pub fn write(&self, item: &T) {
165        self.inner.broadcast(item);
166    }
167
168    /// How many subscribers are listening. Zero is legal.
169    pub fn subscriber_count(&self) -> usize {
170        self.inner.subs.borrow().len()
171    }
172}
173
174// A hub is a component: it appears in the hierarchy and its phases are no-ops.
175impl<T: 'static> Component for AnalysisBus<T> {}
176
177impl<T: 'static> ComponentNode for AnalysisBus<T> {
178    fn node_name(&self) -> &'static str {
179        "AnalysisBus"
180    }
181    fn children_mut(&mut self) -> Vec<(String, &mut (dyn ComponentNode + 'static))> {
182        Vec::new()
183    }
184}
185
186// ===========================================================================
187// Tests — no simulator. The broadcast is synchronous by design (D87).
188// ===========================================================================
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use crate::port::{
194        PortField, PortName, PortOwner, PublishPort, SinkHandle, SubscribePort, Subscriber,
195    };
196    use crate::shared::RustdvShared;
197    use std::any::Any;
198
199    #[derive(Default)]
200    struct Tally {
201        seen: Vec<u8>,
202    }
203    impl Subscriber<u8> for Tally {
204        fn write(&mut self, item: &u8) {
205            self.seen.push(*item);
206        }
207    }
208
209    struct Source {
210        ap: PublishPort<u8>,
211    }
212    impl Source {
213        const AP: PortName<dyn PublishIf<u8>> = PortName::new("ap");
214    }
215    impl PortOwner for Source {
216        fn owner_port_slot(&self, name: &str) -> Option<Rc<dyn Any>> {
217            (name == "ap").then(|| self.ap.slot_any())
218        }
219        fn owner_label(&self) -> &'static str {
220            "Source"
221        }
222    }
223
224    struct Listener {
225        input: SubscribePort<u8>,
226        tally: RustdvShared<Tally>,
227    }
228    impl Listener {
229        const INPUT: PortName<dyn SinkHandle<u8>> = PortName::new("input");
230        fn new() -> Listener {
231            let l = Listener { input: SubscribePort::default(), tally: RustdvShared::default() };
232            l.input.subscribe(l.tally.clone());
233            l
234        }
235    }
236    impl PortOwner for Listener {
237        fn owner_port_slot(&self, name: &str) -> Option<Rc<dyn Any>> {
238            (name == "input").then(|| self.input.slot_any())
239        }
240        fn owner_label(&self) -> &'static str {
241            "Listener"
242        }
243    }
244
245    #[test]
246    fn one_write_reaches_every_subscriber() {
247        let bus: AnalysisBus<u8> = AnalysisBus::new();
248        let src = Source { ap: PublishPort::default() };
249        let a = Listener::new();
250        let b = Listener::new();
251
252        bus.pub_export().connect(&src, Source::AP);
253        bus.sub_export().connect(&a, Listener::INPUT);
254        bus.sub_export().connect(&b, Listener::INPUT);
255        assert_eq!(bus.subscriber_count(), 2);
256
257        src.ap.write(&7);
258        assert_eq!(a.tally.get().seen, vec![7]);
259        assert_eq!(b.tally.get().seen, vec![7], "several subscribers is what makes it a broadcast");
260    }
261
262    #[test]
263    fn subscribers_are_called_in_connection_order() {
264        let bus: AnalysisBus<u8> = AnalysisBus::new();
265        let src = Source { ap: PublishPort::default() };
266        let first = Listener::new();
267        let second = Listener::new();
268        bus.pub_export().connect(&src, Source::AP);
269        bus.sub_export().connect(&first, Listener::INPUT);
270        bus.sub_export().connect(&second, Listener::INPUT);
271
272        for n in 1..=3u8 {
273            src.ap.write(&n);
274        }
275        assert_eq!(first.tally.get().seen, vec![1, 2, 3]);
276        assert_eq!(second.tally.get().seen, vec![1, 2, 3]);
277    }
278
279    /// D90: the hub holds nothing. A datum broadcast to nobody is gone, and a
280    /// subscriber connected afterwards does not receive it.
281    #[test]
282    fn the_bus_stores_nothing() {
283        let bus: AnalysisBus<u8> = AnalysisBus::new();
284        let src = Source { ap: PublishPort::default() };
285        bus.pub_export().connect(&src, Source::AP);
286
287        src.ap.write(&1); // nobody is listening
288        src.ap.write(&2);
289
290        let late = Listener::new();
291        bus.sub_export().connect(&late, Listener::INPUT);
292        assert!(late.tally.get().seen.is_empty(), "nothing was buffered for a late subscriber");
293
294        src.ap.write(&3);
295        assert_eq!(late.tally.get().seen, vec![3], "only what arrives after it connects");
296    }
297
298    /// D85: analysis has min cardinality 0 — a monitor nobody listens to is a
299    /// legitimate testbench, and `write` on an unconnected port is legal.
300    #[test]
301    fn writing_with_no_subscribers_is_legal() {
302        let bus: AnalysisBus<u8> = AnalysisBus::new();
303        let src = Source { ap: PublishPort::default() };
304        bus.pub_export().connect(&src, Source::AP);
305        assert_eq!(bus.subscriber_count(), 0);
306        src.ap.write(&1); // must not panic
307    }
308
309    #[test]
310    fn an_unconnected_publish_port_does_not_panic() {
311        let src = Source { ap: PublishPort::default() };
312        assert!(!src.ap.has_subscribers());
313        src.ap.write(&1); // a source nobody wired is still a valid testbench
314    }
315
316    /// D87: delivery is synchronous — the handler has already run by the time
317    /// `write` returns, with no `await` anywhere in the path.
318    #[test]
319    fn delivery_happens_before_write_returns() {
320        let bus: AnalysisBus<u8> = AnalysisBus::new();
321        let src = Source { ap: PublishPort::default() };
322        let sub = Listener::new();
323        bus.pub_export().connect(&src, Source::AP);
324        bus.sub_export().connect(&sub, Listener::INPUT);
325
326        src.ap.write(&5);
327        assert_eq!(sub.tally.get().seen, vec![5], "already delivered, no scheduling in between");
328    }
329
330    /// A component that never called `subscribe` has nothing to receive with,
331    /// and connecting it says so by name rather than dropping items silently.
332    #[test]
333    #[should_panic(expected = "has no subscriber")]
334    fn connecting_a_subscriber_with_no_sink_is_a_named_error() {
335        let bus: AnalysisBus<u8> = AnalysisBus::new();
336        let bare = Listener { input: SubscribePort::default(), tally: RustdvShared::default() };
337        bus.sub_export().connect(&bare, Listener::INPUT);
338    }
339
340    #[test]
341    fn a_clone_is_the_same_bus() {
342        let bus: AnalysisBus<u8> = AnalysisBus::new();
343        let other = bus.clone();
344        let sub = Listener::new();
345        other.sub_export().connect(&sub, Listener::INPUT);
346        assert_eq!(bus.subscriber_count(), 1, "two handles, one subscriber list");
347        bus.write(&4);
348        assert_eq!(sub.tally.get().seen, vec![4]);
349    }
350}