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}