Skip to main content

rustdv_methodology/
component.rs

1//! Component lifecycle and hierarchy traversal (design-doc §5.2/§5.3).
2//!
3//! > **Superseded, being replaced — do not build on it.** What this file
4//! > still implements is review-memo R3: build/connect as *constructor
5//! > conventions* rather than phases, a component's `new(config, ...)`
6//! > constructing its children (`// build:`) and taking channel endpoints
7//! > as arguments (`// connect:`), with only the runtime lifecycle left as
8//! > a trait.
9//! >
10//! > **D5 and D6 reverse that, and the reversal has begun.** `build`
11//! > (top-down) and `connect` (bottom-up) are real phase methods again
12//! > (D51), because the gap between "a component exists" and "its children
13//! > exist" is where all late binding lives — path-addressed configuration,
14//! > factory overrides, TLM connection. The [`Component`] trait below now
15//! > carries all nine phases and [`run_component_test`] drives them.
16//! > `new(config, ...)`-style construction survives only in not-yet-
17//! > converted testbenches (`tinyalu_tb`), not as the design.
18//!
19//! **What has landed.** Step 4 (D46–D49): a test is a component with an
20//! `async fn run`, receiving the one universal [`RustdvCtx`]. Ch24 first
21//! half (D51/D52): the nine phases, the phaser, path-aware phase logging.
22//! Ch24 second half: two-stage construction (a parent creates children as
23//! `Option<T>`/`Vec<T>` in its own `build`) and the bottom-up [`run_all`]
24//! traversal firing every component's run.
25
26use std::future::Future;
27use std::pin::Pin;
28
29use rustdv_sim::handle::HierarchyHandle;
30use rustdv_sim::log::{Level, Logger};
31use rustdv_sim::rng::Rng;
32
33use crate::error::TestError;
34use crate::objection::{ObjectionGuard, ObjectionRegistry};
35
36/// Agent activity (pyuvm's ConfigDB `is_active` int becomes an enum —
37/// mapping row 42; illegal values are unrepresentable).
38#[derive(Copy, Clone, Debug, PartialEq, Eq)]
39pub enum Active {
40    Active,
41    Passive,
42}
43
44/// Collector for `check`-phase failures (design-doc §5.3 signature).
45#[derive(Default)]
46pub struct CheckSink {
47    errors: Vec<String>,
48}
49
50impl CheckSink {
51    pub fn new() -> CheckSink {
52        CheckSink::default()
53    }
54    pub fn error(&mut self, msg: impl Into<String>) {
55        let msg = msg.into();
56        rustdv_sim::log::error(&msg);
57        self.errors.push(msg);
58    }
59    pub fn is_ok(&self) -> bool {
60        self.errors.is_empty()
61    }
62    pub fn errors(&self) -> &[String] {
63        &self.errors
64    }
65    pub fn into_result(self) -> Result<(), String> {
66        if self.errors.is_empty() {
67            Ok(())
68        } else {
69            Err(format!("{} check failure(s): {}", self.errors.len(), self.errors.join("; ")))
70        }
71    }
72}
73
74// ===========================================================================
75// RustdvCtx — the one context (D47, which strikes D8)
76// ===========================================================================
77
78/// Everything a running testbench is handed: the DUT, randomization, the
79/// objection registry, and the component's path.
80///
81/// **One type, not one per phase.** D8 wanted `BuildCtx`/`ConnectCtx`/
82/// `RunCtx` so that `build_child` during run would fail to *compile*. D47
83/// gives that up: Part II teaches a testbench with no components and
84/// therefore no phases, and naming the type after a phase names a concept
85/// the reader has not met. Phase-illegal operations are caught at run time,
86/// as UVM catches them.
87///
88/// `Clone` is deliberate — the objection registry is `Rc`-shared, so a
89/// clone objects to the same test. D9's per-node context, when the build
90/// phase arrives, is that clone with the path extended.
91#[derive(Clone)]
92pub struct RustdvCtx {
93    dut: HierarchyHandle,
94    seed: u64,
95    objections: ObjectionRegistry,
96    logger: Logger,
97}
98
99impl RustdvCtx {
100    /// A context with a path and no simulator, for unit tests.
101    ///
102    /// `dut()` will panic if called, which is the point: a test that reaches
103    /// for the DUT needs a simulator and belongs in a `sim-*` case.
104    #[cfg(test)]
105    pub(crate) fn for_test(path: &str) -> RustdvCtx {
106        RustdvCtx {
107            dut: HierarchyHandle::null_for_test(),
108            seed: 1,
109            objections: ObjectionRegistry::new(),
110            logger: Logger::new(path),
111        }
112    }
113
114    /// Built by the runner, once per test, with `path` the test's
115    /// registered name (D49 — UVM's fixed `uvm_test_top` is not ported).
116    pub fn new(path: &str, dut: HierarchyHandle, seed: u64) -> RustdvCtx {
117        RustdvCtx { dut, seed, objections: ObjectionRegistry::new(), logger: Logger::new(path) }
118    }
119
120    /// A child context: same services, path extended by `name` (D9). The
121    /// phase traversals hand each child its own context so a component's
122    /// `ctx.info()` logs the path the walk derived, never a stored string.
123    pub fn child(&self, name: &str) -> RustdvCtx {
124        RustdvCtx {
125            dut: self.dut,
126            seed: self.seed,
127            objections: self.objections.clone(),
128            // Segment extension, not string concatenation: the child's path is
129            // the parent's plus one name, so it cannot be malformed (D7).
130            logger: Logger::at(self.logger.rustdv_path().child(name)),
131        }
132    }
133
134    pub fn dut(&self) -> HierarchyHandle {
135        self.dut
136    }
137
138    pub fn seed(&self) -> u64 {
139        self.seed
140    }
141
142    /// A deterministic RNG seeded from RUSTDV_RANDOM_SEED + test index.
143    pub fn rng(&self) -> Rng {
144        Rng::new(self.seed)
145    }
146
147    /// This component's path — derived by the walk, never stored on the
148    /// component itself (D7).
149    pub fn path(&self) -> &str {
150        self.logger.path()
151    }
152
153    /// This component's path as segments. The connection registry (D83)
154    /// addresses components by this, so a port key cannot be hand-typed.
155    pub fn rustdv_path(&self) -> &rustdv_sim::RustdvPath {
156        self.logger.rustdv_path()
157    }
158
159    // --- Path-aware logging (D7's first real appearance) ------------------
160    //
161    // `log::info(...)` reaches a global sink with no idea who called it, and
162    // a hand-typed `Logger::new("env.loga")` silently lies the moment a
163    // component moves. These do not, because the path came from the walk.
164
165    pub fn debug(&self, msg: &str) {
166        self.logger.debug(msg);
167    }
168    pub fn info(&self, msg: &str) {
169        self.logger.info(msg);
170    }
171    pub fn warning(&self, msg: &str) {
172        self.logger.warning(msg);
173    }
174    pub fn error(&self, msg: &str) {
175        self.logger.error(msg);
176    }
177    pub fn critical(&self, msg: &str) {
178        self.logger.critical(msg);
179    }
180
181    /// The logger itself, for code that wants to hold one.
182    pub fn logger(&self) -> &Logger {
183        &self.logger
184    }
185
186    // --- Hierarchical logging control (pyuvm's *_hier methods) ------------
187    //
188    // Each applies to this component and everything below it. Note what is
189    // *missing* from every signature: a path. pyuvm's `set_logging_level_hier`
190    // is a method on the component and knows its own name; ours knows it
191    // because the walk handed the context its path (D7). The alternative —
192    // `set_level_for("uvm_test_top.comp", ..)` typed by hand — is a string
193    // nobody checks, that silently addresses the wrong subtree the moment a
194    // component is renamed or moved.
195
196    /// Port of `set_logging_level_hier(level)`.
197    pub fn set_logging_level_hier(&self, level: Level) {
198        rustdv_sim::log::set_level_for(self.path(), level);
199    }
200
201    /// Port of `disable_logging_hier()`.
202    pub fn disable_logging_hier(&self) {
203        rustdv_sim::log::set_level_for(self.path(), Level::Off);
204    }
205
206    /// Port of `add_logging_handler_hier(logging.FileHandler(path))` —
207    /// this subtree's messages are also written to `file`.
208    pub fn add_file_handler_hier(&self, file: &str, append: bool) -> std::io::Result<()> {
209        rustdv_sim::log::add_file_for(self.path(), file, append)
210    }
211
212    /// Port of `remove_streaming_handler_hier()` — stop printing this
213    /// subtree to the console (file handlers keep receiving it).
214    pub fn remove_console_hier(&self) {
215        rustdv_sim::log::set_console_for(self.path(), false);
216    }
217
218    // --- Objections -------------------------------------------------------
219
220    /// Port of raise_objection, returning a guard whose Drop is
221    /// drop_objection (pyuvm: uvm_component.objection()).
222    pub fn raise_objection(&self, description: &str) -> ObjectionGuard {
223        self.objections.raise(description)
224    }
225
226    pub fn objections(&self) -> &ObjectionRegistry {
227        &self.objections
228    }
229
230    /// Wait until every raised objection has been dropped. Logs the pyuvm
231    /// "you never objected" warning if nothing was ever raised.
232    pub async fn all_objections_dropped(&self) {
233        self.objections.wait_all_dropped().await;
234    }
235}
236
237// ===========================================================================
238// The lifecycle
239// ===========================================================================
240
241/// The UVM phase lifecycle (design-doc §5.3, D51), restored in full. Nine
242/// phases, each a method with a default no-op body — override only what you
243/// use, exactly as pyuvm's `uvm_component` does. **`build` and `connect`
244/// are real phases again**, not the "constructor conventions" R3 collapsed
245/// them into; restoring them is the point of this chapter.
246///
247/// Every phase receives the context so it can log with the component's
248/// derived path (D52/D7). The runner drives the whole sequence over the
249/// tree (see [`run_component_test`]), so a component author never calls a
250/// phase by hand.
251pub trait Component {
252    /// 1. `build` — top-down. Where a component constructs its children
253    /// (D6); the gap between "a component exists" and "its children exist"
254    /// that all late binding lives in.
255    fn build(&mut self, ctx: &mut RustdvCtx) {
256        let _ = ctx;
257    }
258    /// 2. `connect` — bottom-up. Wire children together once they exist.
259    fn connect(&mut self, ctx: &mut RustdvCtx) {
260        let _ = ctx;
261    }
262    /// 3. `end_of_elaboration` — top-down. The hierarchy is final.
263    fn end_of_elaboration(&mut self, ctx: &mut RustdvCtx) {
264        let _ = ctx;
265    }
266    /// 4. `start_of_simulation` — top-down. Last chance before time moves.
267    fn start_of_simulation(&mut self, ctx: &mut RustdvCtx) {
268        let _ = ctx;
269    }
270    /// 5. `run` — bottom-up, async, objection-gated. The test body; `Err`
271    /// fails the test.
272    ///
273    /// `async fn` in a trait costs dyn-compatibility, which is why the sync
274    /// phases are mirrored onto [`DynPhases`] for traversal (D48).
275    #[allow(async_fn_in_trait)]
276    async fn run(&mut self, ctx: &mut RustdvCtx) -> Result<(), TestError> {
277        let _ = ctx;
278        Ok(())
279    }
280    /// 6. `extract` — top-down, post-run.
281    fn extract(&mut self, ctx: &mut RustdvCtx) {
282        let _ = ctx;
283    }
284    /// 7. `check` — top-down. Report failures into the sink.
285    fn check(&mut self, ctx: &mut RustdvCtx, errors: &mut CheckSink) {
286        let _ = (ctx, errors);
287    }
288    /// 8. `report` — top-down.
289    fn report(&mut self, ctx: &mut RustdvCtx) {
290        let _ = ctx;
291    }
292    /// 9. `final_phase` — top-down. (`final` is a Rust keyword.)
293    fn final_phase(&mut self, ctx: &mut RustdvCtx) {
294        let _ = ctx;
295    }
296
297    /// **Transitional spawn hook**, pre-dating the restored `run`
298    /// traversal. `tinyalu_tb` and the not-yet-converted chapters still
299    /// spawn free-running behavior here; it folds into `run` as each
300    /// converts. Not part of the nine-phase lifecycle.
301    fn start(&mut self, ctx: &mut RustdvCtx) {
302        let _ = ctx;
303    }
304
305    // --- Factory (D75) ----------------------------------------------------
306
307    /// This type's registered short name, used as the factory override key.
308    /// The last path segment of the type name (`Foo` from
309    /// `crate::mod::Foo`), which matches the name `#[derive(Component)]`
310    /// registers.
311    fn comp_name() -> &'static str
312    where
313        Self: Sized,
314    {
315        let full = std::any::type_name::<Self>();
316        full.rsplit("::").next().unwrap_or(full)
317    }
318
319    /// Build this component the normal way and drop it in an [`crate::factory::RustdvComp`]
320    /// slot — the analogue of UVM's `new` (D75). Not overridable.
321    fn new_comp() -> crate::factory::RustdvComp
322    where
323        Self: Sized + Default + ComponentNode + 'static,
324    {
325        crate::factory::RustdvComp::fixed(Box::new(Self::default()))
326    }
327
328    /// Build this component through the factory — the analogue of UVM's
329    /// `create` (D75). The default is built now and the slot is flagged; the
330    /// build walk swaps in an override if one applies.
331    fn create_comp() -> crate::factory::RustdvComp
332    where
333        Self: Sized + Default + ComponentNode + 'static,
334    {
335        crate::factory::RustdvComp::overridable(Box::new(Self::default()), Self::comp_name())
336    }
337}
338
339/// Dyn-safe mirror of [`Component`]'s non-async phases (D48).
340///
341/// `Component` stopped being dyn-compatible the moment `run` became an
342/// `async fn`, and `ComponentNode` needs a dyn-safe supertrait to walk a
343/// tree of `&mut dyn` children. The blanket impl means users never write
344/// this: they override the phases on `Component`, and the distinct method
345/// names keep `component.extract(..)` unambiguous.
346pub trait DynPhases {
347    fn dyn_build(&mut self, ctx: &mut RustdvCtx);
348    fn dyn_connect(&mut self, ctx: &mut RustdvCtx);
349    fn dyn_end_of_elaboration(&mut self, ctx: &mut RustdvCtx);
350    fn dyn_start_of_simulation(&mut self, ctx: &mut RustdvCtx);
351    /// The async `run`, boxed so it can be awaited behind `dyn` (D48). This
352    /// is the object-safe shim `run_all` needs to fire each component's run.
353    fn dyn_run<'a>(
354        &'a mut self,
355        ctx: &'a mut RustdvCtx,
356    ) -> Pin<Box<dyn Future<Output = Result<(), TestError>> + 'a>>;
357    fn dyn_extract(&mut self, ctx: &mut RustdvCtx);
358    fn dyn_check(&mut self, ctx: &mut RustdvCtx, errors: &mut CheckSink);
359    fn dyn_report(&mut self, ctx: &mut RustdvCtx);
360    fn dyn_final(&mut self, ctx: &mut RustdvCtx);
361    fn dyn_start(&mut self, ctx: &mut RustdvCtx);
362}
363
364impl<T: Component> DynPhases for T {
365    fn dyn_build(&mut self, ctx: &mut RustdvCtx) {
366        Component::build(self, ctx)
367    }
368    fn dyn_run<'a>(
369        &'a mut self,
370        ctx: &'a mut RustdvCtx,
371    ) -> Pin<Box<dyn Future<Output = Result<(), TestError>> + 'a>> {
372        Box::pin(Component::run(self, ctx))
373    }
374    fn dyn_connect(&mut self, ctx: &mut RustdvCtx) {
375        Component::connect(self, ctx)
376    }
377    fn dyn_end_of_elaboration(&mut self, ctx: &mut RustdvCtx) {
378        Component::end_of_elaboration(self, ctx)
379    }
380    fn dyn_start_of_simulation(&mut self, ctx: &mut RustdvCtx) {
381        Component::start_of_simulation(self, ctx)
382    }
383    fn dyn_extract(&mut self, ctx: &mut RustdvCtx) {
384        Component::extract(self, ctx)
385    }
386    fn dyn_check(&mut self, ctx: &mut RustdvCtx, errors: &mut CheckSink) {
387        Component::check(self, ctx, errors)
388    }
389    fn dyn_report(&mut self, ctx: &mut RustdvCtx) {
390        Component::report(self, ctx)
391    }
392    fn dyn_final(&mut self, ctx: &mut RustdvCtx) {
393        Component::final_phase(self, ctx)
394    }
395    fn dyn_start(&mut self, ctx: &mut RustdvCtx) {
396        Component::start(self, ctx)
397    }
398}
399
400/// Structural traversal over the ownership tree (design-doc D5.2).
401/// Generated by `#[derive(Component)]` for structs whose children are
402/// fields marked `#[component]`; hand-implementable by design
403/// (OQ-15: the derive is convenience, not requirement).
404pub trait ComponentNode: DynPhases {
405    /// The component's type-level name (hierarchical path is synthesized
406    /// from field names during traversal).
407    fn node_name(&self) -> &'static str;
408
409    /// Direct children as (field-derived name, node) pairs, in declaration
410    /// order. An **owned Vec**, not a `visit_children`-style sync callback:
411    /// [`run_all`] awaits inside the walk, and a higher-ranked closure
412    /// cannot yield a child borrow that outlives the call, so the borrows
413    /// have to come back in a value the caller holds. An `Option<T>` child
414    /// created during `build` (D6) appears here only once it is `Some`.
415    fn children_mut(&mut self) -> Vec<(String, &mut (dyn ComponentNode + 'static))>;
416
417    /// This node's port binding cell of the given name, erased (D83).
418    ///
419    /// **This method is the cast Rust does not have.** A parent holds its
420    /// children as `dyn ComponentNode` and cannot recover their concrete
421    /// types, but it does not need to: it needs one port, by name, and a trait
422    /// method reaches through erasure by definition. `#[derive(Component)]`
423    /// generates the match arm for each `#[port(..)]` field; the default is
424    /// `None`, for components that declare no ports.
425    fn port_slot(&self, name: &str) -> Option<std::rc::Rc<dyn std::any::Any>> {
426        let _ = name;
427        None
428    }
429
430    /// Every port this node declares, for the elaboration report.
431    fn port_infos(&self) -> Vec<crate::port::PortInfo> {
432        Vec::new()
433    }
434
435    /// Resolve factory overrides for this node's `RustdvComp` fields (D75).
436    /// The derive generates this to call `field.resolve(ctx, "field")` for
437    /// each `RustdvComp` field; the default is a no-op for nodes with none.
438    /// Called by [`build_all`] after `build`, before descending — so a
439    /// swapped-out default's own phases never run.
440    fn resolve_children(&mut self, ctx: &RustdvCtx) {
441        let _ = ctx;
442    }
443
444    /// Move this node's `RustdvComp` children **out**, for the run phase (D82b).
445    ///
446    /// Returns owned boxes with no borrow relationship to `self`, which is what
447    /// lets [`run_all`] drive a parent's own `run` concurrently with its
448    /// children's. The derive generates this for `RustdvComp` fields; other
449    /// child shapes (`Option<T>`, `Vec<T>`, plain `T`) stay in place and are
450    /// reached through [`ComponentNode::children_mut`] as before.
451    ///
452    /// The default returns nothing, so a hand-written `ComponentNode` keeps the
453    /// old behaviour and still compiles.
454    fn take_children(&mut self) -> Vec<(String, Box<dyn ComponentNode>)> {
455        Vec::new()
456    }
457
458    /// Put back what [`ComponentNode::take_children`] removed, in the same order.
459    /// Called unconditionally after the run phase — including on error — so the
460    /// post-run phases walk a whole tree.
461    fn restore_children(&mut self, taken: Vec<(String, Box<dyn ComponentNode>)>) {
462        let _ = taken;
463    }
464}
465
466// ---------------------------------------------------------------------------
467// Phase traversals (pyuvm order, D34): build top-down, connect bottom-up,
468// run bottom-up, the elaboration and post-run phases top-down.
469//
470// Each child is walked with its own context (path extended, D9), so a
471// component always logs under the path the walk gave it. Top-down = the
472// node acts, then its children; bottom-up = children first, then the node.
473// ---------------------------------------------------------------------------
474
475/// Top-down: `build` a node, then build the children it just created (D6).
476/// Reading `children_mut` *after* `build` is what lets a parent construct
477/// them in its own build phase and have the walk descend into them.
478pub fn build_all(node: &mut dyn ComponentNode, ctx: &mut RustdvCtx) {
479    node.dyn_build(ctx);
480    // Swap in factory overrides for this node's `RustdvComp` children now, while
481    // the node is accessible as its concrete type, and *before* descending —
482    // so a replaced default's own build never runs (D75).
483    node.resolve_children(ctx);
484    for (name, child) in node.children_mut() {
485        let mut cctx = ctx.child(&name);
486        build_all(child, &mut cctx);
487    }
488}
489
490/// Bottom-up: children `connect` before parents.
491pub fn connect_all(node: &mut dyn ComponentNode, ctx: &mut RustdvCtx) {
492    for (name, child) in node.children_mut() {
493        let mut cctx = ctx.child(&name);
494        connect_all(child, &mut cctx);
495    }
496    node.dyn_connect(ctx);
497}
498
499/// Every required port in the tree that nobody connected, as
500/// `path.name (kind)`.
501///
502/// The whole tree is swept and **all** the misses are reported at once (D85),
503/// which is the point of declaring ports rather than reaching for handles:
504/// pyuvm discovers a missing connection lazily, at first use, as an attribute
505/// error deep inside a run phase. Analysis ports are exempt — a monitor that
506/// nobody subscribes to is a legitimate testbench.
507pub fn unconnected_ports(node: &mut dyn ComponentNode, ctx: &mut RustdvCtx) -> Vec<String> {
508    let mut out = Vec::new();
509    let path = ctx.path().to_string();
510    for info in node.port_infos() {
511        if info.required && !info.connected {
512            let owner = if path.is_empty() { String::from("(top)") } else { path.clone() };
513            out.push(format!("{owner}.{} ({})", info.name, info.kind));
514        }
515    }
516    for (name, child) in node.children_mut() {
517        let mut cctx = ctx.child(&name);
518        out.extend(unconnected_ports(child, &mut cctx));
519    }
520    out
521}
522
523/// Run the connection sweep and turn any misses into one error listing them
524/// all. Called by the runner between `connect` and `end_of_elaboration`.
525pub fn check_connections(node: &mut dyn ComponentNode, ctx: &mut RustdvCtx) -> Result<(), TestError> {
526    let missing = unconnected_ports(node, ctx);
527    if missing.is_empty() {
528        return Ok(());
529    }
530    let mut msg = String::from("these TLM ports were declared but never connected:");
531    for m in &missing {
532        msg.push_str("\n  ");
533        msg.push_str(m);
534    }
535    // A classified failure, so a test can assert it failed for *this* reason:
536    // `#[rustdv::test(expect_error = "tlm_unconnected_port")]`.
537    Err(TestError::with_kind(msg, "tlm_unconnected_port"))
538}
539
540/// Top-down.
541pub fn end_of_elaboration_all(node: &mut dyn ComponentNode, ctx: &mut RustdvCtx) {
542    node.dyn_end_of_elaboration(ctx);
543    for (name, child) in node.children_mut() {
544        let mut cctx = ctx.child(&name);
545        end_of_elaboration_all(child, &mut cctx);
546    }
547}
548
549/// Top-down.
550pub fn start_of_simulation_all(node: &mut dyn ComponentNode, ctx: &mut RustdvCtx) {
551    node.dyn_start_of_simulation(ctx);
552    for (name, child) in node.children_mut() {
553        let mut cctx = ctx.child(&name);
554        start_of_simulation_all(child, &mut cctx);
555    }
556}
557
558/// One component's own `run`, ended by the objection consensus.
559///
560/// **Every run body races the drained event here, at the leaf, rather than the
561/// whole tree racing it at the top.** The difference matters because losing a
562/// race means being *dropped*: a top-level race drops the entire `run_all`
563/// future, and with it the children [`take_children`] moved out — so
564/// extract/check/report would walk a tree whose components had been destroyed
565/// mid-phase, silently, and a scoreboard's `check` would never run. Racing per
566/// component instead lets every level of the walk return normally and put its
567/// children back.
568///
569/// A run body that loops forever (a driver, a monitor) is dropped when the
570/// consensus is reached, which is what UVM does to its forked `run_phase`
571/// processes.
572async fn run_one<'a>(
573    node: &'a mut dyn ComponentNode,
574    ctx: &'a mut RustdvCtx,
575) -> Result<(), TestError> {
576    let objections = ctx.objections().clone();
577    match rustdv_sim::first2(node.dyn_run(ctx), objections.wait_drained_event()).await {
578        rustdv_sim::Either::First(r) => r,
579        // The consensus ended the phase: this component's run did not fail,
580        // it was stopped.
581        rustdv_sim::Either::Second(()) => Ok(()),
582    }
583}
584
585/// Bottom-up: every component's `run` fires, children first (D48). The walk
586/// is boxed-recursive because `dyn_run` yields a boxed future we await, and
587/// the children's borrows are held across those awaits.
588///
589/// **Concurrent (D82).** Every component's `run` makes progress together —
590/// the analog of UVM forking each `run_phase`. The children's runs are joined
591/// (SystemVerilog's `fork...join`), and the node's own `run` joins them, so a
592/// producer that blocks on a full FIFO and a consumer that drains it can both
593/// proceed. Sequential awaiting was the earlier behaviour and deadlocked on
594/// exactly that shape.
595///
596/// The futures **borrow** the tree rather than being spawned: `spawn` is
597/// `'static`-bound and would force the component tree into `Rc`/`RefCell`,
598/// whereas this scope already owns it. `spawn` stays for work that must
599/// outlive the phase (BFM loops, monitor collectors — D59/D61).
600///
601/// **Scope: siblings are concurrent; a node's own `run` follows its subtree.**
602/// All of a node's children (and their subtrees) run joined together, then the
603/// node's own `run` body executes. That is what Rust's borrow rules allow:
604/// `Component::run` takes `&mut self`, which *includes* the child fields, so
605/// one `&mut node` cannot be split into "this node's own state" and "its
606/// children" — a parent's run future and its children's run futures cannot
607/// coexist.
608///
609/// **D82b lifts that limit for `RustdvComp` children.** A `RustdvComp` slot
610/// holds a `Box`, so the box can be *moved out* of the parent for the duration
611/// of the run phase. Once out, it has no borrow relationship to the parent, and
612/// the two futures can be driven together. The boxes go back before the
613/// post-run phases walk the tree.
614///
615/// So a node's run proceeds in two steps:
616///
617/// 1. **In-place children first** — `Option<T>`, `Vec<T>` and plain `T` fields
618///    cannot be moved out of their parent, so they keep the earlier behaviour:
619///    joined with each other, completing before the parent's own run begins.
620///    Legacy chapters (ch24, ch25, `tinyalu_tb`) are all of this shape and hold
621///    parents with no run body, so nothing changes for them.
622/// 2. **Taken children joined *with* the parent's own run** — the shape D78
623///    prescribes for all new code, and the one the sequence chapters need.
624pub fn run_all<'a>(
625    node: &'a mut dyn ComponentNode,
626    ctx: &'a mut RustdvCtx,
627) -> Pin<Box<dyn Future<Output = Result<(), TestError>> + 'a>> {
628    Box::pin(async move {
629        // Step 1: move the factory children out **first**, so this node's own
630        // run can be concurrent with theirs (D82b) — and so the in-place walk
631        // below does not see them and run them to completion instead.
632        let mut taken = node.take_children();
633
634        // Step 2: in-place children (legacy shapes that cannot be moved out).
635        // The block scopes their borrow of `node` so it ends before anything
636        // below reborrows. Each child's context clone carries its derived path
637        // (D9) and is moved into the future that uses it, so no borrows overlap.
638        {
639            let children: Vec<Pin<Box<dyn Future<Output = Result<(), TestError>> + '_>>> = node
640                .children_mut()
641                .into_iter()
642                .map(|(name, child)| {
643                    let cctx = ctx.child(&name);
644                    Box::pin(async move {
645                        let mut cctx = cctx;
646                        run_all(child, &mut cctx).await
647                    }) as Pin<Box<dyn Future<Output = Result<(), TestError>> + '_>>
648                })
649                .collect();
650
651            // First error wins; the others keep running until the join is done.
652            if !children.is_empty() {
653                for r in rustdv_sim::join_all(children).await {
654                    if r.is_err() {
655                        node.restore_children(taken);
656                        return r;
657                    }
658                }
659            }
660        }
661
662        // Step 3: the taken children join this node's own run.
663        if taken.is_empty() {
664            return run_one(node, ctx).await;
665        }
666
667        let outcome = {
668            let mut futs: Vec<Pin<Box<dyn Future<Output = Result<(), TestError>> + '_>>> =
669                Vec::new();
670            for (name, child) in taken.iter_mut() {
671                let cctx = ctx.child(name);
672                futs.push(Box::pin(async move {
673                    let mut cctx = cctx;
674                    run_all(&mut **child, &mut cctx).await
675                }));
676            }
677            // `taken` and `node` are now separate values, so the parent's own
678            // run joins its children's instead of following them.
679            futs.push(Box::pin(run_one(node, ctx)));
680
681            let mut first_err = Ok(());
682            for r in rustdv_sim::join_all(futs).await {
683                if first_err.is_ok() {
684                    first_err = r;
685                }
686            }
687            first_err
688        };
689
690        // Unconditional — including on error — so extract/check/report walk a
691        // whole tree.
692        node.restore_children(taken);
693        outcome
694    })
695}
696
697/// Bottom-up: children start before parents (transitional spawn hook).
698pub fn start_all(node: &mut dyn ComponentNode, ctx: &mut RustdvCtx) {
699    for (name, child) in node.children_mut() {
700        let mut cctx = ctx.child(&name);
701        start_all(child, &mut cctx);
702    }
703    node.dyn_start(ctx);
704}
705
706/// Top-down.
707pub fn extract_all(node: &mut dyn ComponentNode, ctx: &mut RustdvCtx) {
708    node.dyn_extract(ctx);
709    for (name, child) in node.children_mut() {
710        let mut cctx = ctx.child(&name);
711        extract_all(child, &mut cctx);
712    }
713}
714
715/// Top-down.
716pub fn check_all(node: &mut dyn ComponentNode, ctx: &mut RustdvCtx, sink: &mut CheckSink) {
717    node.dyn_check(ctx, sink);
718    for (name, child) in node.children_mut() {
719        let mut cctx = ctx.child(&name);
720        check_all(child, &mut cctx, sink);
721    }
722}
723
724/// Top-down.
725pub fn report_all(node: &mut dyn ComponentNode, ctx: &mut RustdvCtx) {
726    node.dyn_report(ctx);
727    for (name, child) in node.children_mut() {
728        let mut cctx = ctx.child(&name);
729        report_all(child, &mut cctx);
730    }
731}
732
733/// Top-down.
734pub fn final_all(node: &mut dyn ComponentNode, ctx: &mut RustdvCtx) {
735    node.dyn_final(ctx);
736    for (name, child) in node.children_mut() {
737        let mut cctx = ctx.child(&name);
738        final_all(child, &mut cctx);
739    }
740}
741
742/// The standard post-run tail: extract → check → report → final, returning
743/// `Err` if any check failed (an `Err` fails the test, design-doc §0.6).
744pub fn run_extract_check_report(
745    node: &mut dyn ComponentNode,
746    ctx: &mut RustdvCtx,
747) -> Result<(), String> {
748    extract_all(node, ctx);
749    let mut sink = CheckSink::new();
750    check_all(node, ctx, &mut sink);
751    report_all(node, ctx);
752    final_all(node, ctx);
753    sink.into_result()
754}
755
756/// The full phaser: drive every UVM phase over a component tree, in order
757/// (D51) — the analog of pyuvm handing the test class to its phaser. The
758/// runner calls this for a `#[rustdv::test]` struct, so the test body is
759/// just the phase methods; no hand-rolled `start_all`.
760pub async fn run_component_test<T: Component + ComponentNode>(
761    test: &mut T,
762    ctx: &mut RustdvCtx,
763) -> Result<(), TestError> {
764    // Writes made during build take depth-scaled precedence, so a parent
765    // outranks a child even though build is top-down and the parent
766    // therefore writes first (D13, tier 2).
767    crate::config::set_in_build(true);
768    build_all(test, ctx);
769    crate::config::set_in_build(false);
770
771    connect_all(test, ctx);
772
773    // Elaboration check: every declared port must be wired before anything
774    // runs (D22/D85). The whole tree is swept and every miss is named at once,
775    // where pyuvm finds the first one lazily, at use, deep inside a run phase.
776    check_connections(test, ctx)?;
777
778    end_of_elaboration_all(test, ctx);
779    start_of_simulation_all(test, ctx);
780
781    // The run phase ends when the last objection drops (UVM), or when every
782    // run body has returned — whichever comes first (D82). Racing the two is
783    // what lets a responder loop (a driver or monitor that never returns) end
784    // with the phase instead of hanging the test. Unfinished runs are dropped
785    // silently, as UVM kills its forked run processes.
786    //
787    // A test that never objected is not made to wait for consensus (D46), so
788    // in that case the run tree alone decides.
789    // The run phase ends when the objection consensus is reached or when every
790    // run body has returned, whichever comes first — but the race is run *per
791    // component*, inside `run_all` (see `run_one`), not around the whole tree.
792    // Racing the whole tree here would drop it mid-phase and destroy the
793    // components before extract/check/report could walk them.
794    let run_result = run_all(test, ctx).await;
795
796    let post = run_extract_check_report(test, ctx).map_err(TestError::from);
797    run_result.and(post)
798}
799
800/// Debug printer: the child walker serving pyuvm's hierarchy print
801/// (design-doc §5.2).
802pub fn print_hierarchy(node: &mut dyn ComponentNode) {
803    fn rec(node: &mut dyn ComponentNode, path: &str) {
804        rustdv_sim::log::info(&format!("{path} ({})", node.node_name()));
805        let parent = path.to_string();
806        for (name, child) in node.children_mut() {
807            rec(child, &format!("{parent}.{name}"));
808        }
809    }
810    rec(node, "top");
811}
812
813// ===========================================================================
814// Tests — no simulator. The walk is ordinary tree traversal; only `run`
815// awaits, and these run bodies return immediately.
816// ===========================================================================
817
818#[cfg(test)]
819mod tests {
820    use super::*;
821    use crate::factory::RustdvComp;
822    use rustdv_sim::testing::block_on;
823    use std::cell::RefCell;
824    use std::rc::Rc;
825
826    type Trace = Rc<RefCell<Vec<String>>>;
827
828    thread_local! {
829        static TRACE: Trace = Rc::new(RefCell::new(Vec::new()));
830    }
831
832    fn note(s: String) {
833        TRACE.with(|t| t.borrow_mut().push(s));
834    }
835    fn trace() -> Vec<String> {
836        TRACE.with(|t| t.borrow().clone())
837    }
838    fn reset() {
839        TRACE.with(|t| t.borrow_mut().clear());
840    }
841
842    /// A leaf that records every phase it is given, with its own path — so
843    /// the test can assert both the order *and* that the path was derived by
844    /// the walk (D7) rather than stored.
845    #[derive(Default)]
846    struct Leaf;
847
848    impl Component for Leaf {
849        fn build(&mut self, ctx: &mut RustdvCtx) {
850            note(format!("build {}", ctx.path()));
851        }
852        fn connect(&mut self, ctx: &mut RustdvCtx) {
853            note(format!("connect {}", ctx.path()));
854        }
855        fn check(&mut self, ctx: &mut RustdvCtx, _e: &mut CheckSink) {
856            note(format!("check {}", ctx.path()));
857        }
858        async fn run(&mut self, ctx: &mut RustdvCtx) -> Result<(), TestError> {
859            note(format!("run {}", ctx.path()));
860            Ok(())
861        }
862    }
863
864    impl ComponentNode for Leaf {
865        fn node_name(&self) -> &'static str {
866            "Leaf"
867        }
868        fn children_mut(&mut self) -> Vec<(String, &mut (dyn ComponentNode + 'static))> {
869            Vec::new()
870        }
871    }
872
873    /// A parent that creates its children in `build` — two-stage construction
874    /// (D6). The walk must descend into what build just made.
875    #[derive(Default)]
876    struct Parent {
877        first: Option<Leaf>,
878        second: Option<Leaf>,
879    }
880
881    impl Component for Parent {
882        fn build(&mut self, ctx: &mut RustdvCtx) {
883            note(format!("build {}", ctx.path()));
884            self.first = Some(Leaf);
885            self.second = Some(Leaf);
886        }
887        fn connect(&mut self, ctx: &mut RustdvCtx) {
888            note(format!("connect {}", ctx.path()));
889        }
890    }
891
892    impl ComponentNode for Parent {
893        fn node_name(&self) -> &'static str {
894            "Parent"
895        }
896        fn children_mut(&mut self) -> Vec<(String, &mut (dyn ComponentNode + 'static))> {
897            let mut out: Vec<(String, &mut (dyn ComponentNode + 'static))> = Vec::new();
898            if let Some(c) = self.first.as_mut() {
899                out.push((String::from("first"), c));
900            }
901            if let Some(c) = self.second.as_mut() {
902                out.push((String::from("second"), c));
903            }
904            out
905        }
906    }
907
908    /// Build is **top-down**: a parent acts, then the children it just made.
909    /// That gap is where every late-binding mechanism lives (D5).
910    #[test]
911    fn build_is_top_down_and_descends_into_what_it_created() {
912        reset();
913        let mut root = Parent::default();
914        let mut ctx = RustdvCtx::for_test("top");
915        build_all(&mut root, &mut ctx);
916        assert_eq!(
917            trace(),
918            vec!["build top", "build top.first", "build top.second"],
919            "parent first, then the children it created in its own build"
920        );
921    }
922
923    /// Connect is **bottom-up**: children are wired before their parent.
924    #[test]
925    fn connect_is_bottom_up() {
926        reset();
927        let mut root = Parent::default();
928        let mut ctx = RustdvCtx::for_test("top");
929        build_all(&mut root, &mut ctx);
930        reset();
931        connect_all(&mut root, &mut ctx);
932        assert_eq!(trace(), vec!["connect top.first", "connect top.second", "connect top"]);
933    }
934
935    /// D7: the path comes from the field name via the walk. Rename the field
936    /// and the path follows — which a hand-typed `Logger::new("top.first")`
937    /// would not.
938    #[test]
939    fn paths_are_derived_from_field_names() {
940        reset();
941        let mut root = Parent::default();
942        let mut ctx = RustdvCtx::for_test("alu_test");
943        build_all(&mut root, &mut ctx);
944        assert!(trace().contains(&String::from("build alu_test.first")));
945        assert!(trace().contains(&String::from("build alu_test.second")));
946    }
947
948    #[test]
949    fn an_option_child_appears_only_once_some() {
950        let mut root = Parent::default();
951        assert!(root.children_mut().is_empty(), "declared but not yet built (D6)");
952        let mut ctx = RustdvCtx::for_test("top");
953        build_all(&mut root, &mut ctx);
954        assert_eq!(root.children_mut().len(), 2);
955    }
956
957    #[test]
958    fn every_component_runs() {
959        reset();
960        block_on(async {
961            let mut root = Parent::default();
962            let mut ctx = RustdvCtx::for_test("top");
963            build_all(&mut root, &mut ctx);
964            reset();
965            run_all(&mut root, &mut ctx).await.unwrap();
966        });
967        let t = trace();
968        assert!(t.contains(&String::from("run top.first")));
969        assert!(t.contains(&String::from("run top.second")));
970    }
971
972    #[test]
973    fn check_visits_the_whole_tree() {
974        reset();
975        let mut root = Parent::default();
976        let mut ctx = RustdvCtx::for_test("top");
977        build_all(&mut root, &mut ctx);
978        reset();
979        let mut sink = CheckSink::new();
980        check_all(&mut root, &mut ctx, &mut sink);
981        assert_eq!(trace().len(), 2, "both leaves were checked");
982        assert!(sink.is_ok());
983    }
984
985    // --- D82b: children move out for the run phase, and come back ---------
986
987    #[derive(Default)]
988    struct FactoryParent {
989        child: RustdvComp,
990    }
991
992    impl Component for FactoryParent {}
993
994    impl ComponentNode for FactoryParent {
995        fn node_name(&self) -> &'static str {
996            "FactoryParent"
997        }
998        fn children_mut(&mut self) -> Vec<(String, &mut (dyn ComponentNode + 'static))> {
999            let mut out: Vec<(String, &mut (dyn ComponentNode + 'static))> = Vec::new();
1000            if let Some(n) = self.child.as_node_mut() {
1001                out.push((String::from("child"), n));
1002            }
1003            out
1004        }
1005        fn take_children(&mut self) -> Vec<(String, Box<dyn ComponentNode>)> {
1006            let mut out = Vec::new();
1007            if let Some(n) = self.child.take_node() {
1008                out.push((String::from("child"), n));
1009            }
1010            out
1011        }
1012        fn restore_children(&mut self, taken: Vec<(String, Box<dyn ComponentNode>)>) {
1013            for (_, node) in taken {
1014                self.child.put_node(node);
1015            }
1016        }
1017    }
1018
1019    #[test]
1020    fn take_children_empties_the_slot_and_restore_refills_it() {
1021        let mut p = FactoryParent { child: RustdvComp::fixed(Box::new(Leaf)) };
1022        let taken = p.take_children();
1023        assert_eq!(taken.len(), 1);
1024        assert!(p.children_mut().is_empty(), "the slot is empty during the run phase");
1025        p.restore_children(taken);
1026        assert_eq!(p.children_mut().len(), 1, "and full again for check/report");
1027    }
1028
1029    /// D82c: restoration is unconditional, so the post-run phases always walk
1030    /// a whole tree — including when a run returned an error. A tree missing
1031    /// its children is how a scoreboard silently never runs.
1032    #[test]
1033    fn children_are_restored_even_when_a_run_fails() {
1034        #[derive(Default)]
1035        struct Failing;
1036        impl Component for Failing {
1037            async fn run(&mut self, _c: &mut RustdvCtx) -> Result<(), TestError> {
1038                Err(TestError::from(String::from("deliberate")))
1039            }
1040        }
1041        impl ComponentNode for Failing {
1042            fn node_name(&self) -> &'static str {
1043                "Failing"
1044            }
1045            fn children_mut(&mut self) -> Vec<(String, &mut (dyn ComponentNode + 'static))> {
1046                Vec::new()
1047            }
1048        }
1049
1050        block_on(async {
1051            let mut p = FactoryParent { child: RustdvComp::fixed(Box::new(Failing)) };
1052            let mut ctx = RustdvCtx::for_test("top");
1053            let outcome = run_all(&mut p, &mut ctx).await;
1054            assert!(outcome.is_err(), "the child's run failed");
1055            assert_eq!(p.children_mut().len(), 1, "and its child came back anyway");
1056        });
1057    }
1058
1059    #[test]
1060    fn a_component_with_no_children_walks_cleanly() {
1061        reset();
1062        let mut leaf = Leaf;
1063        let mut ctx = RustdvCtx::for_test("solo");
1064        build_all(&mut leaf, &mut ctx);
1065        connect_all(&mut leaf, &mut ctx);
1066        assert_eq!(trace(), vec!["build solo", "connect solo"]);
1067    }
1068}