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}