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