use std::any::TypeId;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::future::Future;
use crate::registry::DependencyScope;
const MAX_RESOLUTION_DEPTH: usize = 100;
struct CycleDetectionState {
resolution_set: HashSet<TypeId>,
resolution_depth: usize,
type_names: HashMap<TypeId, &'static str>,
resolution_path: Vec<(TypeId, &'static str)>,
scope_stack: Vec<DependencyScope>,
}
impl CycleDetectionState {
fn new() -> Self {
Self {
resolution_set: HashSet::new(),
resolution_depth: 0,
type_names: HashMap::new(),
resolution_path: Vec::new(),
scope_stack: Vec::new(),
}
}
}
tokio::task_local! {
static CYCLE_STATE: RefCell<CycleDetectionState>;
}
pub async fn with_cycle_detection_scope<F, T>(f: F) -> T
where
F: Future<Output = T>,
{
let already_scoped = CYCLE_STATE.try_with(|_| ()).is_ok();
if already_scoped {
f.await
} else {
CYCLE_STATE
.scope(RefCell::new(CycleDetectionState::new()), f)
.await
}
}
fn with_state<R>(f: impl FnOnce(&RefCell<CycleDetectionState>) -> R) -> Result<R, CycleError> {
CYCLE_STATE.try_with(f).map_err(|_| CycleError::NoScope)
}
fn check_circular_dependency(type_id: TypeId) -> Result<(), CycleError> {
with_state(|state| {
let state_ref = state.borrow();
if state_ref.resolution_set.contains(&type_id) {
let type_name = state_ref
.type_names
.get(&type_id)
.copied()
.unwrap_or("<unknown>");
let cycle_path = build_cycle_path_inner(&state_ref, type_id);
return Err(CycleError::CircularDependency {
type_name: type_name.to_string(),
path: cycle_path,
});
}
Ok(())
})?
}
pub fn begin_resolution(
type_id: TypeId,
type_name: &'static str,
) -> Result<ResolutionGuard, CycleError> {
let depth = with_state(|state| {
let mut s = state.borrow_mut();
s.resolution_depth += 1;
s.resolution_depth
})?;
if depth > MAX_RESOLUTION_DEPTH {
let _ = with_state(|state| {
let mut s = state.borrow_mut();
s.resolution_depth -= 1;
});
return Err(CycleError::MaxDepthExceeded(depth));
}
if let Err(e) = check_circular_dependency(type_id) {
let _ = with_state(|state| {
let mut s = state.borrow_mut();
s.resolution_depth -= 1;
});
return Err(e);
}
with_state(|state| {
let mut s = state.borrow_mut();
s.resolution_set.insert(type_id);
s.resolution_path.push((type_id, type_name));
})?;
Ok(ResolutionGuard::Tracked(type_id))
}
#[derive(Debug)]
pub enum ResolutionGuard {
Tracked(TypeId),
TrackedWithScope(TypeId),
}
impl Drop for ResolutionGuard {
fn drop(&mut self) {
let (type_id, pop_scope) = match self {
ResolutionGuard::Tracked(id) => (id, false),
ResolutionGuard::TrackedWithScope(id) => (id, true),
};
let _ = CYCLE_STATE.try_with(|state| {
let mut s = state.borrow_mut();
s.resolution_set.remove(type_id);
if let Some(pos) = s.resolution_path.iter().rposition(|(id, _)| id == type_id) {
s.resolution_path.remove(pos);
}
s.resolution_depth = s.resolution_depth.saturating_sub(1);
if pop_scope {
s.scope_stack.pop();
}
});
}
}
pub fn register_type_name<T: 'static>(name: &'static str) {
let _ = CYCLE_STATE.try_with(|state| {
state
.borrow_mut()
.type_names
.insert(TypeId::of::<T>(), name);
});
}
fn build_cycle_path_inner(state: &CycleDetectionState, current_type_id: TypeId) -> String {
let type_name = state
.type_names
.get(¤t_type_id)
.copied()
.unwrap_or("<unknown>");
if let Some(cycle_start) = state
.resolution_path
.iter()
.position(|(id, _)| *id == current_type_id)
{
let cycle: Vec<&str> = state.resolution_path[cycle_start..]
.iter()
.map(|(_, name)| *name)
.collect();
format!("{} -> {}", cycle.join(" -> "), type_name)
} else {
format!("Unknown cycle involving {}", type_name)
}
}
pub fn current_dependent_scope() -> DependencyScope {
CYCLE_STATE
.try_with(|state| {
state
.borrow()
.scope_stack
.last()
.copied()
.unwrap_or(DependencyScope::Transient)
})
.unwrap_or(DependencyScope::Transient)
}
pub fn current_dependent_type_name() -> String {
CYCLE_STATE
.try_with(|state| {
state
.borrow()
.resolution_path
.last()
.map(|(_, name)| name.to_string())
})
.ok()
.flatten()
.unwrap_or_else(|| "<root>".to_string())
}
pub fn begin_scoped_resolution(
type_id: TypeId,
type_name: &'static str,
dependency_scope: DependencyScope,
) -> Result<ResolutionGuard, CycleError> {
let dependent_scope = current_dependent_scope();
if !dependency_scope.outlives(dependent_scope) {
let dependent_type = CYCLE_STATE
.try_with(|state| {
state
.borrow()
.resolution_path
.last()
.map(|(_, name)| name.to_string())
})
.ok()
.flatten()
.unwrap_or_else(|| "<root>".to_string());
return Err(CycleError::ScopeViolation {
dependent_type,
dependent_scope,
dependency_type: type_name.to_string(),
dependency_scope,
});
}
let guard = begin_resolution(type_id, type_name)?;
with_state(|state| {
state.borrow_mut().scope_stack.push(dependency_scope);
})?;
std::mem::forget(guard);
Ok(ResolutionGuard::TrackedWithScope(type_id))
}
#[derive(Debug, thiserror::Error)]
pub enum CycleError {
#[error(
"Circular dependency detected: {type_name}\n Path: {path}\nThis forms a cycle that cannot be resolved."
)]
CircularDependency {
type_name: String,
path: String,
},
#[error(
"Maximum resolution depth exceeded: {0}\nThis likely indicates an extremely deep or circular dependency chain."
)]
MaxDepthExceeded(usize),
#[error(
"Cycle detection called outside of a task-local scope. Use `with_cycle_detection_scope` to initialize."
)]
NoScope,
#[error(
"Scope violation: {dependent_scope:?}-scoped '{dependent_type}' cannot resolve \
{dependency_scope:?}-scoped '{dependency_type}'; \
the dependency would be captured with a shorter lifetime"
)]
ScopeViolation {
dependent_type: String,
dependent_scope: DependencyScope,
dependency_type: String,
dependency_scope: DependencyScope,
},
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
struct TypeA;
struct TypeB;
struct TypeC;
#[rstest]
#[tokio::test]
async fn test_simple_cycle_detection() {
with_cycle_detection_scope(async {
let type_a = TypeId::of::<TypeA>();
register_type_name::<TypeA>("TypeA");
let guard_a = begin_resolution(type_a, "TypeA").unwrap();
let result = begin_resolution(type_a, "TypeA");
assert!(matches!(result, Err(CycleError::CircularDependency { .. })));
drop(guard_a);
let result = begin_resolution(type_a, "TypeA");
assert!(result.is_ok());
})
.await;
}
#[rstest]
#[tokio::test]
async fn test_depth_limit() {
with_cycle_detection_scope(async {
use std::marker::PhantomData;
let type1 = TypeId::of::<PhantomData<[u8; 0]>>();
let type2 = TypeId::of::<PhantomData<[u8; 1]>>();
let type3 = TypeId::of::<PhantomData<[u8; 2]>>();
let guard1 = begin_resolution(type1, "Type1").unwrap();
let depth1 = CYCLE_STATE.with(|state| state.borrow().resolution_depth);
assert_eq!(depth1, 1, "Depth should be 1 after first resolution");
let guard2 = begin_resolution(type2, "Type2").unwrap();
let depth2 = CYCLE_STATE.with(|state| state.borrow().resolution_depth);
assert_eq!(depth2, 2, "Depth should be 2 after second resolution");
let guard3 = begin_resolution(type3, "Type3").unwrap();
let depth3 = CYCLE_STATE.with(|state| state.borrow().resolution_depth);
assert_eq!(depth3, 3, "Depth should be 3 after third resolution");
drop(guard3);
let depth_after_drop3 = CYCLE_STATE.with(|state| state.borrow().resolution_depth);
assert_eq!(
depth_after_drop3, 2,
"Depth should be 2 after dropping guard3"
);
drop(guard2);
let depth_after_drop2 = CYCLE_STATE.with(|state| state.borrow().resolution_depth);
assert_eq!(
depth_after_drop2, 1,
"Depth should be 1 after dropping guard2"
);
drop(guard1);
let depth_after_drop1 = CYCLE_STATE.with(|state| state.borrow().resolution_depth);
assert_eq!(
depth_after_drop1, 0,
"Depth should be 0 after dropping all guards"
);
})
.await;
}
#[rstest]
#[tokio::test]
async fn test_no_scope_returns_error() {
let type_a = TypeId::of::<TypeA>();
let result = begin_resolution(type_a, "TypeA");
assert!(matches!(result, Err(CycleError::NoScope)));
}
#[rstest]
#[tokio::test]
async fn test_nested_scope_reuses_existing() {
with_cycle_detection_scope(async {
let type_a = TypeId::of::<TypeA>();
register_type_name::<TypeA>("TypeA");
let _guard_a = begin_resolution(type_a, "TypeA").unwrap();
with_cycle_detection_scope(async {
let result = begin_resolution(type_a, "TypeA");
assert!(
matches!(result, Err(CycleError::CircularDependency { .. })),
"Nested scope should share state with outer scope"
);
})
.await;
})
.await;
}
#[rstest]
#[tokio::test]
async fn test_deterministic_detection_at_deep_depth() {
with_cycle_detection_scope(async {
let mut guards = Vec::new();
macro_rules! push_guard {
($($idx:literal),*) => {
$(
{
use std::marker::PhantomData;
let type_id = TypeId::of::<PhantomData<[u8; $idx]>>();
let name: &'static str = concat!("Type", stringify!($idx));
guards.push(begin_resolution(type_id, name).unwrap());
}
)*
};
}
push_guard!(
100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115,
116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131,
132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147,
148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159
);
let current_depth = CYCLE_STATE.with(|state| state.borrow().resolution_depth);
assert_eq!(current_depth, 60, "Depth should be 60");
use std::marker::PhantomData;
let type_at_depth_55 = TypeId::of::<PhantomData<[u8; 155]>>();
let result = begin_resolution(type_at_depth_55, "Type155");
assert!(
matches!(result, Err(CycleError::CircularDependency { .. })),
"Cycle must be detected deterministically at depth > 50"
);
})
.await;
}
#[rstest]
#[tokio::test]
async fn test_cycle_path_display() {
with_cycle_detection_scope(async {
let type_a = TypeId::of::<TypeA>();
let type_b = TypeId::of::<TypeB>();
let type_c = TypeId::of::<TypeC>();
register_type_name::<TypeA>("TypeA");
register_type_name::<TypeB>("TypeB");
register_type_name::<TypeC>("TypeC");
let _guard_a = begin_resolution(type_a, "TypeA").unwrap();
let _guard_b = begin_resolution(type_b, "TypeB").unwrap();
let _guard_c = begin_resolution(type_c, "TypeC").unwrap();
let result = begin_resolution(type_a, "TypeA");
match result {
Err(CycleError::CircularDependency { path, .. }) => {
assert_eq!(path, "TypeA -> TypeB -> TypeC -> TypeA");
}
other => panic!("Expected CircularDependency, got {:?}", other),
}
})
.await;
}
}