1use std::cell::{Cell, RefCell};
17use std::collections::VecDeque;
18use std::path::{Path, PathBuf};
19use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
20
21#[derive(Debug, Clone)]
25pub struct ForceFrame {
26 pub defined_in: Option<PathBuf>,
28 pub description: String,
30 pub thunk_id: usize,
32}
33
34#[derive(Debug, Clone)]
36pub struct ForceChain(pub Vec<ForceFrame>);
37
38thread_local! {
39 static FORCE_STACK: RefCell<Vec<ForceFrame>> = RefCell::new(Vec::new());
40}
41
42pub fn push_force(frame: ForceFrame) {
44 FORCE_STACK.with(|s| {
45 s.borrow_mut().push(frame);
46 let depth = s.borrow().len();
48 THUNK_MAX_FORCE_DEPTH.with(|m| {
49 if depth > m.get() as usize {
50 m.set(depth as u32);
51 }
52 });
53 THUNK_CURRENT_FORCE_DEPTH.with(|c| c.set(depth as u32));
54 });
55}
56
57pub fn pop_force() {
59 FORCE_STACK.with(|s| {
60 s.borrow_mut().pop();
61 let depth = s.borrow().len();
62 THUNK_CURRENT_FORCE_DEPTH.with(|c| c.set(depth as u32));
63 });
64}
65
66pub fn force_stack_contains(thunk_id: usize) -> bool {
71 FORCE_STACK.with(|s| s.borrow().iter().any(|f| f.thunk_id == thunk_id))
72}
73
74pub fn dump_force_stack_ids() {
76 FORCE_STACK.with(|s| {
77 let stack = s.borrow();
78 eprintln!("[SUI_DEBUG_CYCLE] force stack depth={}", stack.len());
79 for (i, f) in stack.iter().enumerate() {
80 let loc = f
81 .defined_in
82 .as_ref()
83 .map(|p| p.display().to_string())
84 .unwrap_or_else(|| "<eval>".into());
85 let d: String = f.description.chars().take(50).collect();
86 let d = d.replace('\n', " ");
87 eprintln!("[SUI_DEBUG_CYCLE] [{i}] id={:#x} {loc} :: {d}", f.thunk_id);
88 }
89 });
90}
91
92pub fn capture_cycle(thunk_id: usize) -> ForceChain {
95 FORCE_STACK.with(|s| {
96 let stack = s.borrow();
97 let start = stack.iter().position(|f| f.thunk_id == thunk_id);
98 match start {
99 Some(idx) => ForceChain(stack[idx..].to_vec()),
100 None => ForceChain(stack.clone()),
101 }
102 })
103}
104
105impl std::fmt::Display for ForceChain {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 writeln!(f, "infinite recursion detected")?;
108 writeln!(f, "force chain ({} frames):", self.0.len())?;
109 let mut prev_desc: Option<&str> = None;
117 let mut repeat = 0u32;
118 for (i, frame) in self.0.iter().enumerate() {
119 let has_desc = !frame.description.is_empty();
120 if has_desc && prev_desc == Some(&frame.description) {
121 repeat += 1;
122 continue;
123 }
124 if repeat > 0 {
125 writeln!(f, " ... repeated {repeat} more times")?;
126 repeat = 0;
127 }
128 let loc = frame
129 .defined_in
130 .as_ref()
131 .map(|p| p.display().to_string())
132 .unwrap_or_else(|| "<eval>".into());
133 let arrow = if i == 0 { "\u{2192}" } else { "\u{2192}" };
134 let desc = if has_desc {
135 frame.description.as_str()
136 } else {
137 "<thunk>"
138 };
139 writeln!(f, " {arrow} {desc} ({loc})")?;
140 prev_desc = if has_desc { Some(&frame.description) } else { None };
141 }
142 if repeat > 0 {
143 writeln!(f, " ... repeated {repeat} more times")?;
144 }
145 if self.0.iter().any(|fr| fr.description.is_empty()) {
146 writeln!(
147 f,
148 " hint: set SUI_TRACE_EVAL=verbose for per-frame source text"
149 )?;
150 }
151 Ok(())
152 }
153}
154
155static TRACE_ENABLED: AtomicBool = AtomicBool::new(false);
158
159static TRACE_VERBOSE: AtomicBool = AtomicBool::new(false);
162
163pub fn init_trace() {
169 let mode = std::env::var("SUI_TRACE_EVAL").unwrap_or_default();
170 if mode.is_empty() {
171 TRACE_ENABLED.store(false, Ordering::Relaxed);
172 TRACE_VERBOSE.store(false, Ordering::Relaxed);
173 } else {
174 TRACE_ENABLED.store(true, Ordering::Relaxed);
175 TRACE_VERBOSE.store(mode == "1" || mode == "verbose", Ordering::Relaxed);
176 }
177}
178
179#[inline(always)]
181pub fn trace_enabled() -> bool {
182 TRACE_ENABLED.load(Ordering::Relaxed)
183}
184
185thread_local! {
186 static TRACE_DEPTH: Cell<u32> = const { Cell::new(0) };
187 static RING_BUFFER: RefCell<VecDeque<String>> =
189 RefCell::new(VecDeque::with_capacity(256));
190}
191
192pub fn trace_force_enter(file: Option<&Path>, desc: &str) {
195 if !trace_enabled() {
196 return;
197 }
198 let depth = TRACE_DEPTH.with(|d| {
199 let v = d.get();
200 d.set(v + 1);
201 v
202 });
203 let indent = " ".repeat(depth as usize);
204 let loc = file
205 .map(|f| f.display().to_string())
206 .unwrap_or_default();
207 let msg = format!("[trace] {indent}force {loc} ({desc})");
208 if TRACE_VERBOSE.load(Ordering::Relaxed) {
209 eprintln!("{msg}");
210 }
211 RING_BUFFER.with(|rb| {
212 let mut rb = rb.borrow_mut();
213 if rb.len() >= 256 {
214 rb.pop_front();
215 }
216 rb.push_back(msg);
217 });
218}
219
220pub fn trace_force_exit() {
222 if !trace_enabled() {
223 return;
224 }
225 TRACE_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
226}
227
228pub fn dump_ring_tail(n: usize) {
230 RING_BUFFER.with(|rb| {
231 let rb = rb.borrow();
232 let start = rb.len().saturating_sub(n);
233 for line in rb.iter().skip(start) {
234 eprintln!("{line}");
235 }
236 });
237}
238
239pub fn dump_trace_on_error() {
241 if !trace_enabled() {
242 return;
243 }
244 RING_BUFFER.with(|rb| {
245 let rb = rb.borrow();
246 if rb.is_empty() {
247 return;
248 }
249 eprintln!("[trace] last {} force operations:", rb.len());
250 for line in rb.iter() {
251 eprintln!("{line}");
252 }
253 });
254}
255
256static MAX_FORCE_DEPTH: AtomicUsize = AtomicUsize::new(0);
259
260pub fn set_max_force_depth(limit: usize) {
262 MAX_FORCE_DEPTH.store(limit, Ordering::Relaxed);
263}
264
265pub fn check_force_depth() -> Result<(), String> {
268 let limit = MAX_FORCE_DEPTH.load(Ordering::Relaxed);
269 if limit == 0 {
270 return Ok(());
271 }
272 let depth = FORCE_STACK.with(|s| s.borrow().len());
273 if depth > limit {
274 Err(format!("force depth exceeded ({depth}/{limit})"))
275 } else {
276 Ok(())
277 }
278}
279
280thread_local! {
283 static THUNKS_CREATED: Cell<u64> = const { Cell::new(0) };
284 static THUNKS_FORCED_UNIQUE: Cell<u64> = const { Cell::new(0) };
285 static THUNK_MAX_FORCE_DEPTH: Cell<u32> = const { Cell::new(0) };
286 static THUNK_CURRENT_FORCE_DEPTH: Cell<u32> = const { Cell::new(0) };
287 static OVERLAY_FLATTEN_NANOS: Cell<u128> = const { Cell::new(0) };
291 static SORTED_ENTRIES_NANOS: Cell<u128> = const { Cell::new(0) };
294 static SELF_REC_WALK_NANOS: Cell<u128> = const { Cell::new(0) };
298}
299
300#[inline(always)]
302pub fn add_overlay_flatten_nanos(nanos: u128) {
303 if crate::perf::enabled() {
304 OVERLAY_FLATTEN_NANOS.with(|c| c.set(c.get() + nanos));
305 }
306}
307
308pub fn get_overlay_flatten_nanos() -> u128 {
310 OVERLAY_FLATTEN_NANOS.with(Cell::get)
311}
312
313#[inline(always)]
315pub fn add_sorted_entries_nanos(nanos: u128) {
316 if crate::perf::enabled() {
317 SORTED_ENTRIES_NANOS.with(|c| c.set(c.get() + nanos));
318 }
319}
320
321pub fn get_sorted_entries_nanos() -> u128 {
323 SORTED_ENTRIES_NANOS.with(Cell::get)
324}
325
326#[inline(always)]
328pub fn add_self_rec_walk_nanos(nanos: u128) {
329 if crate::perf::enabled() {
330 SELF_REC_WALK_NANOS.with(|c| c.set(c.get() + nanos));
331 }
332}
333
334pub fn get_self_rec_walk_nanos() -> u128 {
336 SELF_REC_WALK_NANOS.with(Cell::get)
337}
338
339thread_local! {
346 static MAYBE_OTHER_KINDS: RefCell<std::collections::BTreeMap<&'static str, u64>> =
347 RefCell::new(std::collections::BTreeMap::new());
348}
349
350#[inline(always)]
351pub fn inc_maybe_other_kind(kind: &'static str) {
352 if crate::perf::enabled() {
353 MAYBE_OTHER_KINDS.with(|m| *m.borrow_mut().entry(kind).or_insert(0) += 1);
354 }
355}
356
357pub fn report_maybe_other_kinds() {
358 if !crate::perf::enabled() {
359 return;
360 }
361 MAYBE_OTHER_KINDS.with(|m| {
362 let m = m.borrow();
363 if m.is_empty() {
364 return;
365 }
366 let mut rows: Vec<(&&'static str, &u64)> = m.iter().collect();
367 rows.sort_by(|a, b| b.1.cmp(a.1));
368 eprintln!("--- maybe_thunk `_`-arm by expr kind ---");
369 for (k, v) in rows {
370 eprintln!(" {k:<20} {v}");
371 }
372 });
373}
374
375#[inline(always)]
377pub fn inc_thunks_created() {
378 if crate::perf::enabled() {
379 THUNKS_CREATED.with(|c| c.set(c.get() + 1));
380 }
381}
382
383#[inline(always)]
385pub fn inc_thunks_forced_unique() {
386 if crate::perf::enabled() {
387 THUNKS_FORCED_UNIQUE.with(|c| c.set(c.get() + 1));
388 }
389}
390
391pub fn current_force_depth() -> u32 {
393 THUNK_CURRENT_FORCE_DEPTH.with(Cell::get)
394}
395
396pub fn get_thunks_created() -> u64 {
398 THUNKS_CREATED.with(Cell::get)
399}
400
401pub fn get_thunks_forced() -> u64 {
403 THUNKS_FORCED_UNIQUE.with(Cell::get)
404}
405
406pub fn reset_thunk_stats() {
409 THUNKS_CREATED.with(|c| c.set(0));
410 THUNKS_FORCED_UNIQUE.with(|c| c.set(0));
411 THUNK_MAX_FORCE_DEPTH.with(|c| c.set(0));
412 OVERLAY_FLATTEN_NANOS.with(|c| c.set(0));
413 SORTED_ENTRIES_NANOS.with(|c| c.set(0));
414 SELF_REC_WALK_NANOS.with(|c| c.set(0));
415}
416
417pub fn report_thunk_stats() {
419 if !crate::perf::enabled() {
420 return;
421 }
422 let created = THUNKS_CREATED.with(Cell::get);
423 let forced = THUNKS_FORCED_UNIQUE.with(Cell::get);
424 let max_depth = THUNK_MAX_FORCE_DEPTH.with(Cell::get);
425 eprintln!("thunks_created: {created}");
426 eprintln!("thunks_forced: {forced}");
427 eprintln!("max_force_depth: {max_depth}");
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433
434 #[test]
437 fn force_chain_display_empty() {
438 let chain = ForceChain(vec![]);
439 let s = chain.to_string();
440 assert!(s.contains("0 frames"));
441 }
442
443 #[test]
444 fn force_chain_display_single() {
445 let chain = ForceChain(vec![ForceFrame {
446 defined_in: Some(PathBuf::from("/test.nix")),
447 description: "x".into(),
448 thunk_id: 1,
449 }]);
450 let s = chain.to_string();
451 assert!(s.contains("1 frames"));
452 assert!(s.contains("/test.nix"));
453 assert!(s.contains("x"));
454 }
455
456 #[test]
457 fn force_chain_display_empty_descriptions_show_one_per_frame() {
458 let frames: Vec<ForceFrame> = (0..3)
461 .map(|i| ForceFrame {
462 defined_in: Some(PathBuf::from(format!("/m{i}.nix"))),
463 description: String::new(),
464 thunk_id: i,
465 })
466 .collect();
467 let s = ForceChain(frames).to_string();
468 assert!(s.contains("3 frames"));
469 assert_eq!(s.matches("<thunk>").count(), 3);
470 assert!(s.contains("SUI_TRACE_EVAL=verbose"));
471 }
472
473 #[test]
474 fn force_chain_display_repeated_frames() {
475 let chain = ForceChain(vec![
476 ForceFrame {
477 defined_in: None,
478 description: "x".into(),
479 thunk_id: 1,
480 },
481 ForceFrame {
482 defined_in: None,
483 description: "x".into(),
484 thunk_id: 2,
485 },
486 ForceFrame {
487 defined_in: None,
488 description: "x".into(),
489 thunk_id: 3,
490 },
491 ForceFrame {
492 defined_in: None,
493 description: "y".into(),
494 thunk_id: 4,
495 },
496 ]);
497 let s = chain.to_string();
498 assert!(s.contains("repeated 2 more times"));
499 assert!(s.contains("y"));
500 }
501
502 #[test]
503 fn force_chain_display_eval_location() {
504 let chain = ForceChain(vec![ForceFrame {
505 defined_in: None,
506 description: "z".into(),
507 thunk_id: 1,
508 }]);
509 let s = chain.to_string();
510 assert!(s.contains("<eval>"));
511 }
512
513 #[test]
514 fn push_pop_force_stack() {
515 FORCE_STACK.with(|s| s.borrow_mut().clear());
517 push_force(ForceFrame {
518 defined_in: None,
519 description: "a".into(),
520 thunk_id: 100,
521 });
522 push_force(ForceFrame {
523 defined_in: None,
524 description: "b".into(),
525 thunk_id: 200,
526 });
527 let chain = capture_cycle(100);
528 assert_eq!(chain.0.len(), 2);
529 assert_eq!(chain.0[0].thunk_id, 100);
530 pop_force();
531 pop_force();
532 }
533
534 #[test]
535 fn capture_cycle_with_unknown_id() {
536 FORCE_STACK.with(|s| s.borrow_mut().clear());
537 push_force(ForceFrame {
538 defined_in: None,
539 description: "a".into(),
540 thunk_id: 10,
541 });
542 let chain = capture_cycle(999);
544 assert_eq!(chain.0.len(), 1);
545 pop_force();
546 }
547
548 #[test]
551 fn trace_disabled_by_default() {
552 let _ = trace_enabled();
556 }
557
558 #[test]
559 fn trace_force_enter_exit_no_panic() {
560 trace_force_enter(None, "test");
562 trace_force_exit();
563 }
564
565 #[test]
568 fn check_force_depth_logic() {
569 FORCE_STACK.with(|s| s.borrow_mut().clear());
572
573 set_max_force_depth(0);
575 assert!(check_force_depth().is_ok());
576
577 set_max_force_depth(10);
579 push_force(ForceFrame {
580 defined_in: None,
581 description: "a".into(),
582 thunk_id: 1,
583 });
584 assert!(check_force_depth().is_ok());
585
586 set_max_force_depth(1);
588 push_force(ForceFrame {
589 defined_in: None,
590 description: "b".into(),
591 thunk_id: 2,
592 });
593 let result = check_force_depth();
594 assert!(result.is_err());
595 assert!(result.unwrap_err().contains("force depth exceeded"));
596
597 pop_force();
599 pop_force();
600 set_max_force_depth(0);
601 }
602
603 #[test]
606 fn thunk_stats_increment() {
607 inc_thunks_created();
609 inc_thunks_forced_unique();
610 }
611
612 #[test]
615 fn force_chain_captures_self_reference() {
616 let result = crate::eval::eval("let x = x; in x");
617 assert!(result.is_err());
618 let msg = result.unwrap_err().to_string();
619 assert!(
620 msg.contains("infinite recursion")
621 || msg.contains("force chain")
622 || msg.contains("blackhole"),
623 "expected infinite recursion error, got: {msg}"
624 );
625 }
626
627 #[test]
628 fn force_chain_captures_mutual_recursion() {
629 let result = crate::eval::eval("let a = b; b = a; in a");
630 assert!(result.is_err());
631 let msg = result.unwrap_err().to_string();
632 assert!(
633 msg.contains("infinite recursion")
634 || msg.contains("force chain")
635 || msg.contains("blackhole"),
636 "expected infinite recursion error, got: {msg}"
637 );
638 }
639
640 #[test]
641 fn force_chain_captures_rec_self_reference() {
642 let result = crate::eval::eval("rec { x = x; }.x");
643 assert!(result.is_err());
644 let msg = result.unwrap_err().to_string();
645 assert!(
646 msg.contains("infinite recursion")
647 || msg.contains("force chain")
648 || msg.contains("blackhole"),
649 "expected infinite recursion error, got: {msg}"
650 );
651 }
652}