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 {
71 path: String,
72 field: String,
73 stored: &'static str,
74 requested: &'static str,
75 },
76}
77
78impl ConfigError {
79 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
111struct Entry {
116 value: Rc<dyn Any>,
117 type_name: &'static str,
118 type_id: TypeId,
119 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
139type 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 static IN_BUILD: Cell<bool> = const { Cell::new(false) };
151}
152
153fn 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
167fn more_specific(a: &str, b: &str) -> bool {
170 glob_match(a, b)
171}
172
173fn 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
192pub struct ConfigDb;
195
196impl ConfigDb {
197 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 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 #[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 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 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 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 pub fn print() {
318 for line in ConfigDb::dump().lines() {
319 rustdv_sim::log::info(line);
320 }
321 }
322
323 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 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 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
372fn depth_of(path: &str) -> i32 {
375 if path.is_empty() {
376 0
377 } else {
378 path.matches('.').count() as i32
379 }
380}
381
382pub(crate) fn set_in_build(active: bool) {
385 IN_BUILD.with(|b| b.set(active));
386}
387
388#[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 #[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 #[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 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 #[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 #[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}