Skip to main content

rustdv_methodology/
config.rs

1//! The ConfigDb — path-addressed configuration (D11–D16, D65–D68).
2//!
3//! A component is configured by code that never touches it, through a path
4//! named as a string. That is the capability the first rustdv pass removed
5//! when it replaced this with typed structs passed to constructors, and
6//! restoring it is most of the point of this branch.
7//!
8//! **Shape of the API (D66).** `get` and `set` take a *context*, an
9//! *offset*, and a *field*:
10//!
11//! ```ignore
12//! ConfigDb::set(Some(ctx), "env.loga", "MSG", String::from("LOG A msg"));
13//! ConfigDb::set(None,      "*",        "MSG", String::from("GLOBAL"));
14//!
15//! let msg: String = ConfigDb::get(Some(ctx), "",     "MSG")?;  // me
16//! let msg: String = ConfigDb::get(Some(ctx), "loga", "MSG")?;  // a child
17//! let seqr: Rc<Sequencer> = ConfigDb::get(None, "", "SEQR")?;  // no context
18//! ```
19//!
20//! The offset is *relative to the context*, so a lookup is
21//! position-independent — an absolute path would be the hand-typed string
22//! D7 forbids. `None` means no context: the offset is absolute. That form
23//! is not an edge case; a `Sequence` is not a component and has no path, so
24//! it is the only way a sequence can read at all.
25//!
26//! **Namespacing (D65).** One namespace per (path, field). The type is
27//! *not* part of the key, which is where SystemVerilog differs: it passes
28//! `uvm_resource#(T)::get_type()` into the lookup, so a `set` and a `get`
29//! that disagree on type simply never meet and you get a silent `return 0`.
30//! Here the entry is found and the type checked, so a mismatch is its own
31//! error. The cost, stated rather than hidden: two different types can no
32//! longer share a field name at one path.
33//!
34//! **Storage (D67).** Values must be `Clone`. Store a plain value and every
35//! reader gets a copy; wrap it in an `Rc` and every reader shares one
36//! object.
37//!
38//! **Debugging is built in, not bolted on (D68).** [`ConfigDb::print`] dumps
39//! every entry *with its precedences*, because a resolved value tells you
40//! who won but not who was competing; [`ConfigDb::set_tracing`] logs every
41//! operation with the context, the offset, and the path they resolved to.
42
43use std::any::{Any, TypeId};
44use std::cell::{Cell, RefCell};
45use std::collections::BTreeMap;
46use std::fmt;
47use std::rc::Rc;
48
49use crate::component::RustdvCtx;
50
51/// Build-phase writes get `DEFAULT_PRECEDENCE - depth`, so the shallowest
52/// setter wins regardless of write order (D13). Writes after build use the
53/// full default and outrank every build-time write.
54const DEFAULT_PRECEDENCE: i32 = 1000;
55
56// ===========================================================================
57// Errors (D14)
58// ===========================================================================
59
60/// Why a `get` failed. SystemVerilog collapses all of these into `return 0`
61/// and leaves your variable untouched; naming them is the point.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum ConfigError {
64    /// No entry matched the resolved path and field. Covers "never set",
65    /// "path does not match" and "field misspelled" — indistinguishable to
66    /// any database addressed by strings, which is why ch28 exists.
67    NotFound { path: String, field: String },
68    /// An entry was found, but it holds another type. Only reportable
69    /// because the type is not part of the key (D65).
70    TypeMismatch { path: String, field: String, stored: &'static str, requested: &'static str },
71}
72
73impl ConfigError {
74    /// Stable, machine-readable cause for `#[rustdv::test(expect_error=…)]`.
75    pub fn kind(&self) -> &'static str {
76        match self {
77            ConfigError::NotFound { .. } => "config_not_found",
78            ConfigError::TypeMismatch { .. } => "config_type_mismatch",
79        }
80    }
81}
82
83impl fmt::Display for ConfigError {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        match self {
86            ConfigError::NotFound { path, field } => write!(
87                f,
88                "ConfigDb: no value for \"{field}\" at \"{path}\" \
89                 (never set, path does not match, or the key is misspelled)"
90            ),
91            ConfigError::TypeMismatch { path, field, stored, requested } => write!(
92                f,
93                "ConfigDb: \"{field}\" at \"{path}\" holds a {stored}, but a {requested} was requested"
94            ),
95        }
96    }
97}
98
99impl std::error::Error for ConfigError {}
100
101// ===========================================================================
102// The store
103// ===========================================================================
104
105struct Entry {
106    value: Rc<dyn Any>,
107    type_name: &'static str,
108    type_id: TypeId,
109    /// The value rendered at `set` time. Stored because the dump and the
110    /// tracer must show *what* was configured, not merely its type — the
111    /// parent/child conflict is diagnosed by reading "PARENT RULES!" beside
112    /// "CHILD RULES!". Python gets this free from `repr()`; in Rust it costs
113    /// a `Debug` bound on `set`, which is the price of a database you can
114    /// actually debug (D68).
115    rendered: String,
116}
117
118impl Clone for Entry {
119    fn clone(&self) -> Entry {
120        Entry {
121            value: self.value.clone(),
122            type_name: self.type_name,
123            type_id: self.type_id,
124            rendered: self.rendered.clone(),
125        }
126    }
127}
128
129/// path glob -> field -> precedence -> entry. The precedence map is kept
130/// (rather than collapsing to a winner) so [`ConfigDb::print`] can show a
131/// losing entry beside the one that beat it — which is how you debug a
132/// parent/child conflict (D68).
133type Store = BTreeMap<String, BTreeMap<String, BTreeMap<i32, Entry>>>;
134
135thread_local! {
136    static STORE: RefCell<Store> = RefCell::new(Store::new());
137    static TRACING: Cell<bool> = const { Cell::new(false) };
138    /// True while the build phase is walking, so writes take depth-scaled
139    /// precedence (D13, tier 2).
140    static IN_BUILD: Cell<bool> = const { Cell::new(false) };
141}
142
143/// Does a concrete path match a stored glob? Only `*` is supported, which
144/// is what both source books use.
145fn glob_match(path: &str, pattern: &str) -> bool {
146    fn inner(p: &[u8], g: &[u8]) -> bool {
147        match (p.first(), g.first()) {
148            (_, Some(b'*')) => inner(p, &g[1..]) || (!p.is_empty() && inner(&p[1..], g)),
149            (Some(a), Some(b)) if a == b => inner(&p[1..], &g[1..]),
150            (None, None) => true,
151            _ => false,
152        }
153    }
154    inner(path.as_bytes(), pattern.as_bytes())
155}
156
157/// Is `a` at least as specific as `b`? `a.b.c` before `a.b.*` before `*`
158/// (D13, tier 1 — pyuvm's rule, which SystemVerilog does not have).
159fn more_specific(a: &str, b: &str) -> bool {
160    glob_match(a, b)
161}
162
163/// Resolve context + offset into the path this operation addresses.
164fn resolve(ctx: Option<&RustdvCtx>, offset: &str) -> String {
165    match ctx {
166        None => offset.to_string(),
167        Some(c) if offset.is_empty() => c.path().to_string(),
168        Some(c) if c.path().is_empty() => offset.to_string(),
169        Some(c) => format!("{}.{}", c.path(), offset),
170    }
171}
172
173fn trace(op: &str, ctx: Option<&RustdvCtx>, offset: &str, path: &str, field: &str, value: &str) {
174    if TRACING.with(|t| t.get()) {
175        let context = ctx.map(|c| c.path()).unwrap_or("<none>");
176        rustdv_sim::log::info(&format!(
177            "CFGDB/{op} context={context} offset=\"{offset}\" -> {path} {field}={value}"
178        ));
179    }
180}
181
182/// The configuration database. All methods are associated functions over an
183/// ambient per-test store (D11, D16).
184pub struct ConfigDb;
185
186impl ConfigDb {
187    /// Store `value` for every component whose path matches `offset`
188    /// (a glob), resolved against `ctx`.
189    pub fn set<T: Clone + fmt::Debug + 'static>(
190        ctx: Option<&RustdvCtx>,
191        offset: &str,
192        field: &str,
193        value: T,
194    ) {
195        let path = resolve(ctx, offset);
196        // Precedence is scaled by the depth of the **setter**, not of the
197        // path it wrote to (pyuvm: `default_precedence - context.get_depth()`).
198        // That distinction is the whole of D13 tier 2: in a parent/child
199        // conflict both writers resolve to the *same* path, so scaling by the
200        // target would tie and let recency decide — and since build is
201        // top-down, the child always writes last and would always win.
202        let setter_depth = ctx.map(|c| depth_of(c.path())).unwrap_or(0);
203        let precedence = if IN_BUILD.with(|b| b.get()) {
204            DEFAULT_PRECEDENCE - setter_depth
205        } else {
206            DEFAULT_PRECEDENCE
207        };
208        let entry = Entry {
209            rendered: format!("{value:?}"),
210            value: Rc::new(value),
211            type_name: std::any::type_name::<T>(),
212            type_id: TypeId::of::<T>(),
213        };
214        trace("SET", ctx, offset, &path, field, &entry.rendered);
215        STORE.with(|s| {
216            s.borrow_mut()
217                .entry(path)
218                .or_default()
219                .entry(field.to_string())
220                .or_default()
221                .insert(precedence, entry);
222        });
223    }
224
225    /// Read `field` for the component at `ctx` + `offset`.
226    ///
227    /// The offset must be concrete — globs are legal only when storing
228    /// (D12, pyuvm's rule). Resolution is most-specific path first, then
229    /// highest precedence, then most recent write (D13).
230    #[must_use = "a ConfigDb miss is a real failure; SystemVerilog's silent \
231                  zero is what this Result exists to prevent"]
232    pub fn get<T: Clone + 'static>(
233        ctx: Option<&RustdvCtx>,
234        offset: &str,
235        field: &str,
236    ) -> Result<T, ConfigError> {
237        let path = resolve(ctx, offset);
238
239        let found = STORE.with(|s| {
240            let store = s.borrow();
241            let mut matches: Vec<(&String, &Entry)> = store
242                .iter()
243                .filter(|(pattern, _)| glob_match(&path, pattern))
244                .filter_map(|(pattern, fields)| {
245                    fields
246                        .get(field)
247                        .and_then(|by_prec| by_prec.iter().next_back())
248                        .map(|(_, entry)| (pattern, entry))
249                })
250                .collect();
251            // Most specific first; ties keep insertion order.
252            matches.sort_by(|(a, _), (b, _)| {
253                more_specific(a, b).cmp(&more_specific(b, a)).reverse()
254            });
255            matches.first().map(|(_, e)| (*e).clone())
256        });
257
258        let Some(entry) = found else {
259            trace("GET", ctx, offset, &path, field, "<not found>");
260            return Err(ConfigError::NotFound { path, field: field.to_string() });
261        };
262
263        if entry.type_id != TypeId::of::<T>() {
264            trace("GET", ctx, offset, &path, field, "<type mismatch>");
265            return Err(ConfigError::TypeMismatch {
266                path,
267                field: field.to_string(),
268                stored: entry.type_name,
269                requested: std::any::type_name::<T>(),
270            });
271        }
272
273        trace("GET", ctx, offset, &path, field, &entry.rendered);
274        Ok(entry.value.downcast_ref::<T>().expect("type id checked above").clone())
275    }
276
277    /// Is there a value for this field, without producing an error?
278    pub fn exists(ctx: Option<&RustdvCtx>, offset: &str, field: &str) -> bool {
279        let path = resolve(ctx, offset);
280        STORE.with(|s| {
281            s.borrow().iter().any(|(pattern, fields)| {
282                glob_match(&path, pattern) && fields.contains_key(field)
283            })
284        })
285    }
286
287    /// Log every `set` and `get` as it happens: the context, the offset, and
288    /// the path they resolved to — which is what you actually got wrong when
289    /// a lookup misses. Port of pyuvm's `ConfigDB().is_tracing`.
290    pub fn set_tracing(on: bool) {
291        TRACING.with(|t| t.set(on));
292    }
293
294    pub fn is_tracing() -> bool {
295        TRACING.with(|t| t.get())
296    }
297
298    /// Print the whole database, **including precedences**. A resolved value
299    /// tells you who won; this tells you who else was competing, which is
300    /// what a parent/child conflict needs (D68).
301    pub fn print() {
302        for line in ConfigDb::dump().lines() {
303            rustdv_sim::log::info(line);
304        }
305    }
306
307    /// The dump as a string, for tests and for callers that want to route it
308    /// somewhere other than the log.
309    pub fn dump() -> String {
310        let mut out = format!("{:<28}: {:<10}: {}", "PATH", "KEY", "DATA");
311        STORE.with(|s| {
312            for (path, fields) in s.borrow().iter() {
313                for (field, by_prec) in fields.iter() {
314                    let data = by_prec
315                        .iter()
316                        .rev()
317                        .map(|(p, e)| format!("{p}: {}", e.rendered))
318                        .collect::<Vec<_>>()
319                        .join(", ");
320                    out.push_str(&format!("\n{path:<28}: {field:<10}: {{{data}}}"));
321                }
322            }
323        });
324        out
325    }
326
327    /// The factory overrides in force, as `(path, from, to)` — the entries
328    /// the factory stores here under a reserved key prefix (D75). Used by
329    /// `Factory::print`.
330    pub fn factory_overrides() -> Vec<(String, String, String)> {
331        let mut out = Vec::new();
332        STORE.with(|s| {
333            for (path, fields) in s.borrow().iter() {
334                for (field, by_prec) in fields.iter() {
335                    if let Some(from) = field.strip_prefix("__factory_override__") {
336                        if let Some((_, e)) = by_prec.iter().next_back() {
337                            let to = e.rendered.trim_start_matches("-> ").to_string();
338                            out.push((path.clone(), from.to_string(), to));
339                        }
340                    }
341                }
342            }
343        });
344        out
345    }
346
347    /// Drop every entry. The runner calls this between tests (D16), so a
348    /// test never inherits another's configuration.
349    pub fn clear() {
350        STORE.with(|s| s.borrow_mut().clear());
351        TRACING.with(|t| t.set(false));
352        IN_BUILD.with(|b| b.set(false));
353    }
354}
355
356/// How deep is this path? The root test is 0. Used for build-phase
357/// precedence, so an ancestor outranks a descendant (D13).
358fn depth_of(path: &str) -> i32 {
359    if path.is_empty() {
360        0
361    } else {
362        path.matches('.').count() as i32
363    }
364}
365
366/// The phaser brackets the build walk with this, so writes made during
367/// `build` take depth-scaled precedence.
368pub(crate) fn set_in_build(active: bool) {
369    IN_BUILD.with(|b| b.set(active));
370}
371
372// ===========================================================================
373// Tests — no simulator. The ConfigDb is a path-keyed map; nothing here waits.
374// ===========================================================================
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379    use crate::component::RustdvCtx;
380
381    fn fresh() {
382        ConfigDb::clear();
383    }
384
385    #[test]
386    fn set_and_get_round_trip() {
387        fresh();
388        ConfigDb::set(None, "env.tester", "COUNT", 7u32);
389        let ctx = RustdvCtx::for_test("env.tester");
390        assert_eq!(ConfigDb::get::<u32>(Some(&ctx), "", "COUNT").unwrap(), 7);
391    }
392
393    /// D14, and the reason `get` returns a `Result` at all: SystemVerilog's
394    /// `get()` collapses never-set, path mismatch, field typo and type
395    /// mismatch into a silent `return 0`, and you find out much later.
396    #[test]
397    fn a_miss_is_an_error_not_a_default() {
398        fresh();
399        let ctx = RustdvCtx::for_test("env");
400        let got = ConfigDb::get::<u32>(Some(&ctx), "", "NOPE");
401        match got {
402            Err(ConfigError::NotFound { field, .. }) => assert_eq!(field, "NOPE"),
403            other => panic!("expected NotFound, got {other:?}"),
404        }
405    }
406
407    #[test]
408    fn a_wrong_type_names_both_types() {
409        fresh();
410        ConfigDb::set(None, "env", "N", 1u32);
411        let ctx = RustdvCtx::for_test("env");
412        match ConfigDb::get::<String>(Some(&ctx), "", "N") {
413            Err(ConfigError::TypeMismatch { stored, requested, .. }) => {
414                assert!(stored.contains("u32"), "stored type named: {stored}");
415                assert!(requested.contains("String"), "requested type named: {requested}");
416            }
417            other => panic!("expected TypeMismatch, got {other:?}"),
418        }
419    }
420
421    #[test]
422    fn a_wildcard_reaches_every_component_below() {
423        fresh();
424        ConfigDb::set(None, "*", "BFM", 99u32);
425        for path in ["env", "env.tester", "env.agent.driver"] {
426            let ctx = RustdvCtx::for_test(path);
427            assert_eq!(
428                ConfigDb::get::<u32>(Some(&ctx), "", "BFM").unwrap(),
429                99,
430                "`*` should reach {path}"
431            );
432        }
433    }
434
435    #[test]
436    fn a_more_specific_path_wins_over_a_wildcard() {
437        fresh();
438        ConfigDb::set(None, "*", "MSG", String::from("everyone"));
439        ConfigDb::set(None, "env.loga", "MSG", String::from("just me"));
440        let loga = RustdvCtx::for_test("env.loga");
441        let logb = RustdvCtx::for_test("env.logb");
442        assert_eq!(ConfigDb::get::<String>(Some(&loga), "", "MSG").unwrap(), "just me");
443        assert_eq!(ConfigDb::get::<String>(Some(&logb), "", "MSG").unwrap(), "everyone");
444    }
445
446    /// The `ab`-under-`a` case: a glob must not match a longer sibling name.
447    #[test]
448    fn a_prefix_glob_does_not_match_a_longer_sibling() {
449        fresh();
450        ConfigDb::set(None, "env.t*", "MSG", String::from("t-things"));
451        let tester = RustdvCtx::for_test("env.tester");
452        let logger = RustdvCtx::for_test("env.logger");
453        assert!(ConfigDb::get::<String>(Some(&tester), "", "MSG").is_ok());
454        assert!(
455            ConfigDb::get::<String>(Some(&logger), "", "MSG").is_err(),
456            "env.t* must not reach env.logger"
457        );
458    }
459
460    #[test]
461    fn the_most_recent_write_wins_at_equal_precedence() {
462        fresh();
463        ConfigDb::set(None, "env", "N", 1u32);
464        ConfigDb::set(None, "env", "N", 2u32);
465        let ctx = RustdvCtx::for_test("env");
466        assert_eq!(ConfigDb::get::<u32>(Some(&ctx), "", "N").unwrap(), 2);
467    }
468
469    #[test]
470    fn an_offset_resolves_against_the_context() {
471        fresh();
472        ConfigDb::set(None, "env.loga", "MSG", String::from("hello"));
473        let env = RustdvCtx::for_test("env");
474        // The env asks what its child will see.
475        assert_eq!(ConfigDb::get::<String>(Some(&env), "loga", "MSG").unwrap(), "hello");
476    }
477
478    #[test]
479    fn a_null_context_addresses_from_the_top() {
480        fresh();
481        ConfigDb::set(None, "env.loga", "MSG", String::from("hello"));
482        assert_eq!(ConfigDb::get::<String>(None, "env.loga", "MSG").unwrap(), "hello");
483    }
484
485    /// The per-test guarantee the runner relies on — and, since D101, the one
486    /// that keeps a test from inheriting the previous test's BFM.
487    #[test]
488    fn clear_empties_it() {
489        fresh();
490        ConfigDb::set(None, "env", "N", 1u32);
491        ConfigDb::clear();
492        let ctx = RustdvCtx::for_test("env");
493        assert!(ConfigDb::get::<u32>(Some(&ctx), "", "N").is_err());
494    }
495
496    /// D101: the ConfigDb holds *handles* now, not just config values.
497    #[test]
498    fn it_holds_a_shared_handle() {
499        use std::rc::Rc;
500        fresh();
501        #[derive(Debug)]
502        struct Bfm(u32);
503        let bfm = Rc::new(Bfm(7));
504        ConfigDb::set(None, "*", "BFM", bfm.clone());
505        let ctx = RustdvCtx::for_test("env.driver");
506        let got: Rc<Bfm> = ConfigDb::get(Some(&ctx), "", "BFM").unwrap();
507        assert_eq!(got.0, 7);
508        assert!(Rc::ptr_eq(&got, &bfm), "the same object, not a copy");
509    }
510
511    #[test]
512    fn dump_renders_every_entry() {
513        fresh();
514        ConfigDb::set(None, "env", "A", 1u32);
515        ConfigDb::set(None, "env.x", "B", String::from("two"));
516        let dumped = ConfigDb::dump();
517        assert!(dumped.contains("env"), "dump names the paths: {dumped}");
518        assert!(dumped.contains('A') && dumped.contains('B'), "and the fields");
519    }
520}