use std::cell::{Cell, RefCell};
use std::collections::VecDeque;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
#[derive(Debug, Clone)]
pub struct ForceFrame {
pub defined_in: Option<PathBuf>,
pub description: String,
pub thunk_id: usize,
}
#[derive(Debug, Clone)]
pub struct ForceChain(pub Vec<ForceFrame>);
thread_local! {
static FORCE_STACK: RefCell<Vec<ForceFrame>> = RefCell::new(Vec::new());
}
pub fn push_force(frame: ForceFrame) {
FORCE_STACK.with(|s| {
s.borrow_mut().push(frame);
let depth = s.borrow().len();
THUNK_MAX_FORCE_DEPTH.with(|m| {
if depth > m.get() as usize {
m.set(depth as u32);
}
});
THUNK_CURRENT_FORCE_DEPTH.with(|c| c.set(depth as u32));
});
}
pub fn pop_force() {
FORCE_STACK.with(|s| {
s.borrow_mut().pop();
let depth = s.borrow().len();
THUNK_CURRENT_FORCE_DEPTH.with(|c| c.set(depth as u32));
});
}
pub fn force_stack_contains(thunk_id: usize) -> bool {
FORCE_STACK.with(|s| s.borrow().iter().any(|f| f.thunk_id == thunk_id))
}
pub fn dump_force_stack_ids() {
FORCE_STACK.with(|s| {
let stack = s.borrow();
eprintln!("[SUI_DEBUG_CYCLE] force stack depth={}", stack.len());
for (i, f) in stack.iter().enumerate() {
let loc = f
.defined_in
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "<eval>".into());
let d: String = f.description.chars().take(50).collect();
let d = d.replace('\n', " ");
eprintln!("[SUI_DEBUG_CYCLE] [{i}] id={:#x} {loc} :: {d}", f.thunk_id);
}
});
}
pub fn capture_cycle(thunk_id: usize) -> ForceChain {
FORCE_STACK.with(|s| {
let stack = s.borrow();
let start = stack.iter().position(|f| f.thunk_id == thunk_id);
match start {
Some(idx) => ForceChain(stack[idx..].to_vec()),
None => ForceChain(stack.clone()),
}
})
}
impl std::fmt::Display for ForceChain {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "infinite recursion detected")?;
writeln!(f, "force chain ({} frames):", self.0.len())?;
let mut prev_desc: Option<&str> = None;
let mut repeat = 0u32;
for (i, frame) in self.0.iter().enumerate() {
let has_desc = !frame.description.is_empty();
if has_desc && prev_desc == Some(&frame.description) {
repeat += 1;
continue;
}
if repeat > 0 {
writeln!(f, " ... repeated {repeat} more times")?;
repeat = 0;
}
let loc = frame
.defined_in
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "<eval>".into());
let arrow = if i == 0 { "\u{2192}" } else { "\u{2192}" };
let desc = if has_desc {
frame.description.as_str()
} else {
"<thunk>"
};
writeln!(f, " {arrow} {desc} ({loc})")?;
prev_desc = if has_desc { Some(&frame.description) } else { None };
}
if repeat > 0 {
writeln!(f, " ... repeated {repeat} more times")?;
}
if self.0.iter().any(|fr| fr.description.is_empty()) {
writeln!(
f,
" hint: set SUI_TRACE_EVAL=verbose for per-frame source text"
)?;
}
Ok(())
}
}
static TRACE_ENABLED: AtomicBool = AtomicBool::new(false);
static TRACE_VERBOSE: AtomicBool = AtomicBool::new(false);
pub fn init_trace() {
let mode = std::env::var("SUI_TRACE_EVAL").unwrap_or_default();
if mode.is_empty() {
TRACE_ENABLED.store(false, Ordering::Relaxed);
TRACE_VERBOSE.store(false, Ordering::Relaxed);
} else {
TRACE_ENABLED.store(true, Ordering::Relaxed);
TRACE_VERBOSE.store(mode == "1" || mode == "verbose", Ordering::Relaxed);
}
}
#[inline(always)]
pub fn trace_enabled() -> bool {
TRACE_ENABLED.load(Ordering::Relaxed)
}
thread_local! {
static TRACE_DEPTH: Cell<u32> = const { Cell::new(0) };
static RING_BUFFER: RefCell<VecDeque<String>> =
RefCell::new(VecDeque::with_capacity(256));
}
pub fn trace_force_enter(file: Option<&Path>, desc: &str) {
if !trace_enabled() {
return;
}
let depth = TRACE_DEPTH.with(|d| {
let v = d.get();
d.set(v + 1);
v
});
let indent = " ".repeat(depth as usize);
let loc = file
.map(|f| f.display().to_string())
.unwrap_or_default();
let msg = format!("[trace] {indent}force {loc} ({desc})");
if TRACE_VERBOSE.load(Ordering::Relaxed) {
eprintln!("{msg}");
}
RING_BUFFER.with(|rb| {
let mut rb = rb.borrow_mut();
if rb.len() >= 256 {
rb.pop_front();
}
rb.push_back(msg);
});
}
pub fn trace_force_exit() {
if !trace_enabled() {
return;
}
TRACE_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
}
pub fn dump_ring_tail(n: usize) {
RING_BUFFER.with(|rb| {
let rb = rb.borrow();
let start = rb.len().saturating_sub(n);
for line in rb.iter().skip(start) {
eprintln!("{line}");
}
});
}
pub fn dump_trace_on_error() {
if !trace_enabled() {
return;
}
RING_BUFFER.with(|rb| {
let rb = rb.borrow();
if rb.is_empty() {
return;
}
eprintln!("[trace] last {} force operations:", rb.len());
for line in rb.iter() {
eprintln!("{line}");
}
});
}
static MAX_FORCE_DEPTH: AtomicUsize = AtomicUsize::new(0);
pub fn set_max_force_depth(limit: usize) {
MAX_FORCE_DEPTH.store(limit, Ordering::Relaxed);
}
pub fn check_force_depth() -> Result<(), String> {
let limit = MAX_FORCE_DEPTH.load(Ordering::Relaxed);
if limit == 0 {
return Ok(());
}
let depth = FORCE_STACK.with(|s| s.borrow().len());
if depth > limit {
Err(format!("force depth exceeded ({depth}/{limit})"))
} else {
Ok(())
}
}
thread_local! {
static THUNKS_CREATED: Cell<u64> = const { Cell::new(0) };
static THUNKS_FORCED_UNIQUE: Cell<u64> = const { Cell::new(0) };
static THUNK_MAX_FORCE_DEPTH: Cell<u32> = const { Cell::new(0) };
static THUNK_CURRENT_FORCE_DEPTH: Cell<u32> = const { Cell::new(0) };
static OVERLAY_FLATTEN_NANOS: Cell<u128> = const { Cell::new(0) };
static SORTED_ENTRIES_NANOS: Cell<u128> = const { Cell::new(0) };
static SELF_REC_WALK_NANOS: Cell<u128> = const { Cell::new(0) };
}
#[inline(always)]
pub fn add_overlay_flatten_nanos(nanos: u128) {
if crate::perf::enabled() {
OVERLAY_FLATTEN_NANOS.with(|c| c.set(c.get() + nanos));
}
}
pub fn get_overlay_flatten_nanos() -> u128 {
OVERLAY_FLATTEN_NANOS.with(Cell::get)
}
#[inline(always)]
pub fn add_sorted_entries_nanos(nanos: u128) {
if crate::perf::enabled() {
SORTED_ENTRIES_NANOS.with(|c| c.set(c.get() + nanos));
}
}
pub fn get_sorted_entries_nanos() -> u128 {
SORTED_ENTRIES_NANOS.with(Cell::get)
}
#[inline(always)]
pub fn add_self_rec_walk_nanos(nanos: u128) {
if crate::perf::enabled() {
SELF_REC_WALK_NANOS.with(|c| c.set(c.get() + nanos));
}
}
pub fn get_self_rec_walk_nanos() -> u128 {
SELF_REC_WALK_NANOS.with(Cell::get)
}
thread_local! {
static MAYBE_OTHER_KINDS: RefCell<std::collections::BTreeMap<&'static str, u64>> =
RefCell::new(std::collections::BTreeMap::new());
}
#[inline(always)]
pub fn inc_maybe_other_kind(kind: &'static str) {
if crate::perf::enabled() {
MAYBE_OTHER_KINDS.with(|m| *m.borrow_mut().entry(kind).or_insert(0) += 1);
}
}
pub fn report_maybe_other_kinds() {
if !crate::perf::enabled() {
return;
}
MAYBE_OTHER_KINDS.with(|m| {
let m = m.borrow();
if m.is_empty() {
return;
}
let mut rows: Vec<(&&'static str, &u64)> = m.iter().collect();
rows.sort_by(|a, b| b.1.cmp(a.1));
eprintln!("--- maybe_thunk `_`-arm by expr kind ---");
for (k, v) in rows {
eprintln!(" {k:<20} {v}");
}
});
}
#[inline(always)]
pub fn inc_thunks_created() {
if crate::perf::enabled() {
THUNKS_CREATED.with(|c| c.set(c.get() + 1));
}
}
#[inline(always)]
pub fn inc_thunks_forced_unique() {
if crate::perf::enabled() {
THUNKS_FORCED_UNIQUE.with(|c| c.set(c.get() + 1));
}
}
pub fn current_force_depth() -> u32 {
THUNK_CURRENT_FORCE_DEPTH.with(Cell::get)
}
pub fn get_thunks_created() -> u64 {
THUNKS_CREATED.with(Cell::get)
}
pub fn get_thunks_forced() -> u64 {
THUNKS_FORCED_UNIQUE.with(Cell::get)
}
pub fn reset_thunk_stats() {
THUNKS_CREATED.with(|c| c.set(0));
THUNKS_FORCED_UNIQUE.with(|c| c.set(0));
THUNK_MAX_FORCE_DEPTH.with(|c| c.set(0));
OVERLAY_FLATTEN_NANOS.with(|c| c.set(0));
SORTED_ENTRIES_NANOS.with(|c| c.set(0));
SELF_REC_WALK_NANOS.with(|c| c.set(0));
}
pub fn report_thunk_stats() {
if !crate::perf::enabled() {
return;
}
let created = THUNKS_CREATED.with(Cell::get);
let forced = THUNKS_FORCED_UNIQUE.with(Cell::get);
let max_depth = THUNK_MAX_FORCE_DEPTH.with(Cell::get);
eprintln!("thunks_created: {created}");
eprintln!("thunks_forced: {forced}");
eprintln!("max_force_depth: {max_depth}");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn force_chain_display_empty() {
let chain = ForceChain(vec![]);
let s = chain.to_string();
assert!(s.contains("0 frames"));
}
#[test]
fn force_chain_display_single() {
let chain = ForceChain(vec![ForceFrame {
defined_in: Some(PathBuf::from("/test.nix")),
description: "x".into(),
thunk_id: 1,
}]);
let s = chain.to_string();
assert!(s.contains("1 frames"));
assert!(s.contains("/test.nix"));
assert!(s.contains("x"));
}
#[test]
fn force_chain_display_empty_descriptions_show_one_per_frame() {
let frames: Vec<ForceFrame> = (0..3)
.map(|i| ForceFrame {
defined_in: Some(PathBuf::from(format!("/m{i}.nix"))),
description: String::new(),
thunk_id: i,
})
.collect();
let s = ForceChain(frames).to_string();
assert!(s.contains("3 frames"));
assert_eq!(s.matches("<thunk>").count(), 3);
assert!(s.contains("SUI_TRACE_EVAL=verbose"));
}
#[test]
fn force_chain_display_repeated_frames() {
let chain = ForceChain(vec![
ForceFrame {
defined_in: None,
description: "x".into(),
thunk_id: 1,
},
ForceFrame {
defined_in: None,
description: "x".into(),
thunk_id: 2,
},
ForceFrame {
defined_in: None,
description: "x".into(),
thunk_id: 3,
},
ForceFrame {
defined_in: None,
description: "y".into(),
thunk_id: 4,
},
]);
let s = chain.to_string();
assert!(s.contains("repeated 2 more times"));
assert!(s.contains("y"));
}
#[test]
fn force_chain_display_eval_location() {
let chain = ForceChain(vec![ForceFrame {
defined_in: None,
description: "z".into(),
thunk_id: 1,
}]);
let s = chain.to_string();
assert!(s.contains("<eval>"));
}
#[test]
fn push_pop_force_stack() {
FORCE_STACK.with(|s| s.borrow_mut().clear());
push_force(ForceFrame {
defined_in: None,
description: "a".into(),
thunk_id: 100,
});
push_force(ForceFrame {
defined_in: None,
description: "b".into(),
thunk_id: 200,
});
let chain = capture_cycle(100);
assert_eq!(chain.0.len(), 2);
assert_eq!(chain.0[0].thunk_id, 100);
pop_force();
pop_force();
}
#[test]
fn capture_cycle_with_unknown_id() {
FORCE_STACK.with(|s| s.borrow_mut().clear());
push_force(ForceFrame {
defined_in: None,
description: "a".into(),
thunk_id: 10,
});
let chain = capture_cycle(999);
assert_eq!(chain.0.len(), 1);
pop_force();
}
#[test]
fn trace_disabled_by_default() {
let _ = trace_enabled();
}
#[test]
fn trace_force_enter_exit_no_panic() {
trace_force_enter(None, "test");
trace_force_exit();
}
#[test]
fn check_force_depth_logic() {
FORCE_STACK.with(|s| s.borrow_mut().clear());
set_max_force_depth(0);
assert!(check_force_depth().is_ok());
set_max_force_depth(10);
push_force(ForceFrame {
defined_in: None,
description: "a".into(),
thunk_id: 1,
});
assert!(check_force_depth().is_ok());
set_max_force_depth(1);
push_force(ForceFrame {
defined_in: None,
description: "b".into(),
thunk_id: 2,
});
let result = check_force_depth();
assert!(result.is_err());
assert!(result.unwrap_err().contains("force depth exceeded"));
pop_force();
pop_force();
set_max_force_depth(0);
}
#[test]
fn thunk_stats_increment() {
inc_thunks_created();
inc_thunks_forced_unique();
}
#[test]
fn force_chain_captures_self_reference() {
let result = crate::eval::eval("let x = x; in x");
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("infinite recursion")
|| msg.contains("force chain")
|| msg.contains("blackhole"),
"expected infinite recursion error, got: {msg}"
);
}
#[test]
fn force_chain_captures_mutual_recursion() {
let result = crate::eval::eval("let a = b; b = a; in a");
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("infinite recursion")
|| msg.contains("force chain")
|| msg.contains("blackhole"),
"expected infinite recursion error, got: {msg}"
);
}
#[test]
fn force_chain_captures_rec_self_reference() {
let result = crate::eval::eval("rec { x = x; }.x");
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("infinite recursion")
|| msg.contains("force chain")
|| msg.contains("blackhole"),
"expected infinite recursion error, got: {msg}"
);
}
}