use std::collections::{HashMap, HashSet};
use std::hash::Hash;
use std::sync::{OnceLock, RwLock};
use crate::analysis::{get_file_analysis, FileAnalysis};
use crate::callsite::{current_source_info, module_path_for_line};
pub trait RegistryKey: AsRef<str> + Clone + Eq + Hash {
fn from_module_path(module_path: String) -> Self;
fn is_unknown(&self) -> bool {
self.as_ref() == "unknown"
}
}
pub trait RegistryValue: Clone {
fn file_path(&self) -> Option<&str>;
fn set_file_path(&mut self, file_path: String);
}
pub trait RegistryDomain {
type Key: RegistryKey;
type Value: RegistryValue;
type Entry;
fn entries(analysis: &FileAnalysis) -> &[Self::Entry];
fn entry_line(entry: &Self::Entry) -> usize;
fn build_value(entry: &Self::Entry, module_path: &Self::Key) -> Option<Self::Value>;
fn matches_entry(_entry: &Self::Entry) -> bool {
true
}
fn entry_hint(_entry: &Self::Entry) -> Option<String> {
None
}
}
pub trait NamedRegistryDomain: RegistryDomain {
fn entry_name(entry: &Self::Entry) -> String;
fn value_name(value: &Self::Value) -> String;
}
pub struct StaticRegistry<K, V> {
inner: OnceLock<RwLock<HashMap<K, V>>>,
}
impl<K, V> StaticRegistry<K, V> {
pub const fn new() -> Self {
Self {
inner: OnceLock::new(),
}
}
pub fn map(&self) -> &RwLock<HashMap<K, V>> {
self.inner.get_or_init(|| RwLock::new(HashMap::new()))
}
}
impl<K, V> Default for StaticRegistry<K, V> {
fn default() -> Self {
Self::new()
}
}
impl<K, V> StaticRegistry<K, V>
where
K: Eq + Hash,
V: Clone,
{
pub fn get_cloned(&self, key: &K) -> Option<V> {
self.map().read().ok()?.get(key).cloned()
}
pub fn insert(&self, key: K, value: V) {
if let Ok(mut map) = self.map().write() {
map.insert(key, value);
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SourceContext {
pub file_path: String,
pub line_number: usize,
}
fn tracked_file_matches(tracked_file_path: Option<&str>, file_path: &str) -> bool {
tracked_file_path == Some(file_path)
}
impl SourceContext {
pub fn new(file_path: impl Into<String>, line_number: usize) -> Self {
Self {
file_path: file_path.into(),
line_number,
}
}
pub fn current() -> Option<Self> {
current_source_info().map(|(file_path, line_number)| Self::new(file_path, line_number))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum LookupMode<K> {
Exact(K),
AnyModule,
}
impl<K> LookupMode<K>
where
K: RegistryKey,
{
pub fn from_key(requested_key: &K) -> Self {
if requested_key.is_unknown() {
Self::AnyModule
} else {
Self::Exact(requested_key.clone())
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LookupCandidate<K> {
pub canonical_key: K,
pub item_hint: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct LookupMatch<K, V> {
pub canonical_key: K,
pub value: V,
pub cache_hit: bool,
}
pub type LookupResult<K, V> = Result<LookupMatch<K, V>, LookupFailure<K>>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum LookupFailure<K> {
SourceUnavailable,
AnalysisUnavailable {
file_path: String,
},
NotFound {
requested: LookupMode<K>,
item_name: Option<String>,
},
Ambiguous {
requested: LookupMode<K>,
item_name: Option<String>,
matches: Vec<LookupCandidate<K>>,
},
}
pub fn ensure_loaded<D>(
registry: &StaticRegistry<D::Key, D::Value>,
requested_key: &D::Key,
) -> Option<D::Value>
where
D: RegistryDomain,
{
let requested = LookupMode::from_key(requested_key);
if let Some(source) = SourceContext::current() {
return try_ensure_loaded_from_source::<D>(registry, requested, &source)
.ok()
.map(|loaded| loaded.value);
}
match requested {
LookupMode::Exact(_) => registry.get_cloned(requested_key),
LookupMode::AnyModule => None,
}
}
pub fn ensure_loaded_by_name<D>(
registry: &StaticRegistry<D::Key, D::Value>,
requested_key: &D::Key,
item_name: &str,
) -> Option<D::Value>
where
D: NamedRegistryDomain,
{
let requested = LookupMode::from_key(requested_key);
if let Some(source) = SourceContext::current() {
return try_ensure_loaded_by_name_from_source::<D>(registry, requested, item_name, &source)
.ok()
.map(|loaded| loaded.value);
}
match requested {
LookupMode::Exact(_) => registry
.get_cloned(requested_key)
.filter(|value| D::value_name(value) == item_name),
LookupMode::AnyModule => None,
}
}
pub fn try_ensure_loaded<D>(
registry: &StaticRegistry<D::Key, D::Value>,
requested: LookupMode<D::Key>,
) -> LookupResult<D::Key, D::Value>
where
D: RegistryDomain,
{
let Some(source) = SourceContext::current() else {
return Err(LookupFailure::SourceUnavailable);
};
try_ensure_loaded_from_source::<D>(registry, requested, &source)
}
pub fn try_ensure_loaded_from_source<D>(
registry: &StaticRegistry<D::Key, D::Value>,
requested: LookupMode<D::Key>,
source: &SourceContext,
) -> LookupResult<D::Key, D::Value>
where
D: RegistryDomain,
{
resolve_lookup::<D, _, _>(registry, requested, None, source, |_, _| true, |_, _| true)
}
pub fn try_ensure_loaded_by_name<D>(
registry: &StaticRegistry<D::Key, D::Value>,
requested: LookupMode<D::Key>,
item_name: &str,
) -> LookupResult<D::Key, D::Value>
where
D: NamedRegistryDomain,
{
let Some(source) = SourceContext::current() else {
return Err(LookupFailure::SourceUnavailable);
};
try_ensure_loaded_by_name_from_source::<D>(registry, requested, item_name, &source)
}
pub fn try_ensure_loaded_by_name_from_source<D>(
registry: &StaticRegistry<D::Key, D::Value>,
requested: LookupMode<D::Key>,
item_name: &str,
source: &SourceContext,
) -> LookupResult<D::Key, D::Value>
where
D: NamedRegistryDomain,
{
resolve_lookup::<D, _, _>(
registry,
requested,
Some(item_name),
source,
|entry, expected| D::entry_name(entry) == expected,
|value, expected| D::value_name(value) == expected,
)
}
fn resolve_lookup<D, EntryNameMatch, ValueNameMatch>(
registry: &StaticRegistry<D::Key, D::Value>,
requested: LookupMode<D::Key>,
item_name: Option<&str>,
source: &SourceContext,
entry_name_matches: EntryNameMatch,
value_name_matches: ValueNameMatch,
) -> LookupResult<D::Key, D::Value>
where
D: RegistryDomain,
EntryNameMatch: Fn(&D::Entry, &str) -> bool,
ValueNameMatch: Fn(&D::Value, &str) -> bool,
{
if let LookupMode::Exact(requested_key) = &requested {
if let Some(cached) = registry.get_cloned(requested_key) {
let name_matches = item_name.is_none_or(|name| value_name_matches(&cached, name));
if tracked_file_matches(cached.file_path(), &source.file_path) && name_matches {
return Ok(LookupMatch {
canonical_key: requested_key.clone(),
value: cached,
cache_hit: true,
});
}
}
}
let Some(analysis) = get_file_analysis(&source.file_path) else {
return Err(LookupFailure::AnalysisUnavailable {
file_path: source.file_path.clone(),
});
};
let mut matches = Vec::new();
let mut line_module_cache: HashMap<usize, Option<String>> = HashMap::new();
for entry in D::entries(&analysis) {
if !D::matches_entry(entry) || !item_name.is_none_or(|name| entry_name_matches(entry, name))
{
continue;
}
let line_number = D::entry_line(entry);
let resolved_module_path = if let Some(cached) = line_module_cache.get(&line_number) {
cached.clone()
} else {
let resolved = module_path_for_line(&source.file_path, line_number);
line_module_cache.insert(line_number, resolved.clone());
resolved
};
let Some(resolved_module_path) = resolved_module_path else {
continue;
};
if !matches_lookup_mode(&requested, &resolved_module_path) {
continue;
}
let canonical_key = D::Key::from_module_path(resolved_module_path);
let Some(mut value) = D::build_value(entry, &canonical_key) else {
continue;
};
value.set_file_path(source.file_path.clone());
matches.push((
LookupCandidate {
canonical_key,
item_hint: D::entry_hint(entry),
},
value,
));
}
dedup_matches(&mut matches);
if matches.is_empty() {
return Err(LookupFailure::NotFound {
requested,
item_name: item_name.map(str::to_string),
});
}
if matches.len() > 1 {
return Err(LookupFailure::Ambiguous {
requested,
item_name: item_name.map(str::to_string),
matches: matches
.into_iter()
.map(|(candidate, _)| candidate)
.collect(),
});
}
let (candidate, value) = matches.pop().expect("single match");
store_lookup_result(registry, &requested, &candidate.canonical_key, &value);
Ok(LookupMatch {
canonical_key: candidate.canonical_key,
value,
cache_hit: false,
})
}
fn matches_lookup_mode<K: RegistryKey>(
requested: &LookupMode<K>,
resolved_module_path: &str,
) -> bool {
match requested {
LookupMode::Exact(requested_key) => requested_key.as_ref() == resolved_module_path,
LookupMode::AnyModule => true,
}
}
fn dedup_matches<K, V>(matches: &mut Vec<(LookupCandidate<K>, V)>)
where
K: Clone + Eq + Hash,
V: Clone,
{
let mut seen = HashSet::new();
matches.retain(|(candidate, _)| {
seen.insert((candidate.canonical_key.clone(), candidate.item_hint.clone()))
});
}
fn store_lookup_result<K, V>(
registry: &StaticRegistry<K, V>,
requested: &LookupMode<K>,
canonical_key: &K,
value: &V,
) where
K: AsRef<str> + Clone + Eq + Hash,
V: Clone,
{
registry.insert(canonical_key.clone(), value.clone());
if let LookupMode::Exact(requested_key) = requested {
if canonical_key.as_ref() != requested_key.as_ref() {
registry.insert(requested_key.clone(), value.clone());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analysis::EnumEntry;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct TestKey(String);
impl AsRef<str> for TestKey {
fn as_ref(&self) -> &str {
&self.0
}
}
impl RegistryKey for TestKey {
fn from_module_path(module_path: String) -> Self {
Self(module_path)
}
}
#[derive(Clone, Debug, PartialEq)]
struct TestValue {
name: String,
file_path: Option<String>,
}
impl RegistryValue for TestValue {
fn file_path(&self) -> Option<&str> {
self.file_path.as_deref()
}
fn set_file_path(&mut self, file_path: String) {
self.file_path = Some(file_path);
}
}
struct TestDomain;
impl RegistryDomain for TestDomain {
type Key = TestKey;
type Value = TestValue;
type Entry = EnumEntry;
fn entries(analysis: &FileAnalysis) -> &[Self::Entry] {
&analysis.enums
}
fn entry_line(entry: &Self::Entry) -> usize {
entry.line_number
}
fn build_value(entry: &Self::Entry, _module_path: &Self::Key) -> Option<Self::Value> {
Some(TestValue {
name: entry.item.ident.to_string(),
file_path: None,
})
}
fn matches_entry(entry: &Self::Entry) -> bool {
entry.attrs.iter().any(|attr| attr == "state")
}
fn entry_hint(entry: &Self::Entry) -> Option<String> {
Some(entry.item.ident.to_string())
}
}
impl NamedRegistryDomain for TestDomain {
fn entry_name(entry: &Self::Entry) -> String {
entry.item.ident.to_string()
}
fn value_name(value: &Self::Value) -> String {
value.name.clone()
}
}
fn write_temp_rust_file(contents: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock")
.as_nanos();
let crate_dir = std::env::temp_dir().join(format!("statum_registry_{nanos}"));
let src_dir = crate_dir.join("src");
fs::create_dir_all(&src_dir).expect("create temp crate");
let path = src_dir.join("lib.rs");
fs::write(&path, contents).expect("write temp file");
path
}
#[test]
fn lookup_mode_from_key_uses_any_module_for_unknown() {
assert_eq!(
LookupMode::from_key(&TestKey("unknown".into())),
LookupMode::AnyModule
);
assert_eq!(
LookupMode::from_key(&TestKey("crate::foo".into())),
LookupMode::Exact(TestKey("crate::foo".into()))
);
}
#[test]
fn store_lookup_result_only_aliases_exact_requests() {
let registry: StaticRegistry<TestKey, TestValue> = StaticRegistry::new();
let value = TestValue {
name: "State".into(),
file_path: Some("/tmp/a.rs".into()),
};
store_lookup_result(
®istry,
&LookupMode::Exact(TestKey("crate::workflow".into())),
&TestKey("crate::workflow".into()),
&value,
);
store_lookup_result(
®istry,
&LookupMode::AnyModule,
&TestKey("crate::other".into()),
&value,
);
let map = registry.map().read().expect("lock");
assert!(map.get(&TestKey("crate::workflow".into())).is_some());
assert!(map.get(&TestKey("crate::other".into())).is_some());
assert_eq!(map.len(), 2);
}
#[test]
fn try_ensure_loaded_from_source_reports_ambiguous_module() {
let path = write_temp_rust_file(
r#"
mod workflow {
#[state]
enum TaskState {
Draft,
}
#[state]
enum ReviewState {
Review,
}
}
"#,
);
let registry: StaticRegistry<TestKey, TestValue> = StaticRegistry::new();
let source = SourceContext::new(path.to_string_lossy(), 2);
let result = try_ensure_loaded_from_source::<TestDomain>(
®istry,
LookupMode::Exact(TestKey("workflow".into())),
&source,
);
match result {
Err(LookupFailure::Ambiguous { matches, .. }) => {
assert_eq!(matches.len(), 2);
assert_eq!(matches[0].item_hint.as_deref(), Some("TaskState"));
assert_eq!(matches[1].item_hint.as_deref(), Some("ReviewState"));
}
other => panic!("expected ambiguity, got {other:?}"),
}
let _ = fs::remove_dir_all(path.parent().expect("src").parent().expect("crate"));
}
#[test]
fn try_ensure_loaded_by_name_from_source_finds_named_match() {
let path = write_temp_rust_file(
r#"
mod workflow {
#[state]
enum TaskState {
Draft,
}
#[state]
enum ReviewState {
Review,
}
}
"#,
);
let registry: StaticRegistry<TestKey, TestValue> = StaticRegistry::new();
let source = SourceContext::new(path.to_string_lossy(), 2);
let loaded = try_ensure_loaded_by_name_from_source::<TestDomain>(
®istry,
LookupMode::Exact(TestKey("workflow".into())),
"ReviewState",
&source,
)
.expect("named lookup");
assert_eq!(loaded.canonical_key, TestKey("workflow".into()));
assert_eq!(loaded.value.name, "ReviewState");
assert!(!loaded.cache_hit);
let _ = fs::remove_dir_all(path.parent().expect("src").parent().expect("crate"));
}
#[test]
fn try_ensure_loaded_without_source_reports_unavailable() {
let registry: StaticRegistry<TestKey, TestValue> = StaticRegistry::new();
let result = try_ensure_loaded::<TestDomain>(
®istry,
LookupMode::Exact(TestKey("workflow".into())),
);
assert_eq!(result, Err(LookupFailure::SourceUnavailable));
}
#[test]
fn cache_match_checks_current_file() {
let value = TestValue {
name: "State".into(),
file_path: Some("/tmp/a.rs".into()),
};
assert!(tracked_file_matches(value.file_path(), "/tmp/a.rs"));
assert!(!tracked_file_matches(value.file_path(), "/tmp/b.rs"));
}
#[test]
fn cache_match_requires_tracked_file_path() {
let value = TestValue {
name: "State".into(),
file_path: None,
};
assert!(!tracked_file_matches(value.file_path(), "/tmp/a.rs"));
}
}