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