1use 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
51const DEFAULT_PRECEDENCE: i32 = 1000;
55
56#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum ConfigError {
64 NotFound { path: String, field: String },
68 TypeMismatch { path: String, field: String, stored: &'static str, requested: &'static str },
71}
72
73impl ConfigError {
74 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
101struct Entry {
106 value: Rc<dyn Any>,
107 type_name: &'static str,
108 type_id: TypeId,
109 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
129type 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 static IN_BUILD: Cell<bool> = const { Cell::new(false) };
141}
142
143fn 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
157fn more_specific(a: &str, b: &str) -> bool {
160 glob_match(a, b)
161}
162
163fn 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
182pub struct ConfigDb;
185
186impl ConfigDb {
187 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 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 #[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 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 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 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 pub fn print() {
302 for line in ConfigDb::dump().lines() {
303 rustdv_sim::log::info(line);
304 }
305 }
306
307 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 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 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
356fn depth_of(path: &str) -> i32 {
359 if path.is_empty() {
360 0
361 } else {
362 path.matches('.').count() as i32
363 }
364}
365
366pub(crate) fn set_in_build(active: bool) {
369 IN_BUILD.with(|b| b.set(active));
370}
371
372#[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 #[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 #[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 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 #[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 #[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}