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}