nichlink_run_method/runtime/trace/frames/
frames.rs1use std::fmt::Write as _;
5
6use super::*;
7
8pub struct FramePath<'a> {
15 trace: &'a CallTrace,
16 target: u64,
17 next_depth: usize,
18 depth: usize,
19}
20
21impl<'a> Iterator for FramePath<'a> {
22 type Item = &'a CallSite;
23
24 fn next(&mut self) -> Option<Self::Item> {
25 if self.next_depth > self.depth {
26 return None;
27 }
28 let mut frame_id = self.target;
29 for _ in 0..(self.depth - self.next_depth) {
30 frame_id = self.trace.frame(frame_id)?.parent?;
31 }
32 self.next_depth += 1;
33 self.trace.frame(frame_id).map(|frame| &frame.call)
34 }
35}
36
37impl CallTrace {
38 #[track_caller]
47 pub fn with<R>(
48 &mut self,
49 node: NodeId,
50 function: &'static str,
51 operation: impl FnOnce(&mut Self) -> R,
52 ) -> R {
53 let caller = std::panic::Location::caller();
54 self.with_at(
55 node,
56 function,
57 SourceLocation {
58 file: caller.file(),
59 line: caller.line(),
60 column: caller.column(),
61 function,
62 },
63 operation,
64 )
65 }
66
67 pub fn with_at<R>(
70 &mut self,
71 node: NodeId,
72 function: &'static str,
73 source: SourceLocation,
74 operation: impl FnOnce(&mut Self) -> R,
75 ) -> R {
76 self.with_source(node, function, Some(source), operation)
77 }
78
79 fn with_source<R>(
80 &mut self,
81 node: NodeId,
82 function: &'static str,
83 source: Option<SourceLocation>,
84 operation: impl FnOnce(&mut Self) -> R,
85 ) -> R {
86 if matches!(self.mode, TraceMode::Off) {
87 return operation(self);
88 }
89 let frame_id = self.next_frame_id;
90 self.next_frame_id = self.next_frame_id.wrapping_add(1);
91 let call = CallSite {
92 node,
93 function,
94 frame_id,
95 source,
96 };
97 let parent = self.current.last().copied();
98 self.frames.push(FrameRecord { call, parent });
99 self.frame_index.insert(frame_id, self.frames.len() - 1);
100 self.current.push(frame_id);
101 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| operation(self)));
105 self.current.pop();
106 match result {
107 Ok(value) => value,
108 Err(payload) => std::panic::resume_unwind(payload),
109 }
110 }
111
112 pub(super) fn frame(&self, frame_id: u64) -> Option<&FrameRecord> {
113 self.frame_index
114 .get(&frame_id)
115 .and_then(|index| self.frames.get(*index))
116 }
117
118 pub fn frame_depth(&self, frame_id: u64) -> usize {
121 let mut depth = 0;
122 let mut next = self.frame(frame_id).and_then(|frame| frame.parent);
123 while let Some(id) = next {
124 depth += 1;
125 next = self.frame(id).and_then(|frame| frame.parent);
126 }
127 depth
128 }
129
130 pub(super) fn frame_chain_matches(&self, frame_id: u64, query: &str) -> bool {
131 let mut next = Some(frame_id);
132 while let Some(id) = next {
133 let Some(frame) = self.frame(id) else { break };
134 if frame.call.function.to_ascii_lowercase().contains(query)
135 || frame
136 .call
137 .source
138 .is_some_and(|source| source_file_matches(source.file, query))
139 {
140 return true;
141 }
142 next = frame.parent;
143 }
144 false
145 }
146
147 pub(super) fn path_for(&self, frame_id: u64) -> Vec<CallSite> {
148 let mut path = Vec::new();
149 let mut next = Some(frame_id);
150 while let Some(id) = next {
151 let Some(frame) = self.frame(id) else { break };
152 path.push(frame.call.clone());
153 next = frame.parent;
154 }
155 path.reverse();
156 path
157 }
158
159 pub fn frame_view(&self, frame_id: u64) -> Option<FrameView<'_>> {
162 self.frame(frame_id).map(|frame| FrameView {
163 call: &frame.call,
164 parent: frame.parent,
165 })
166 }
167
168 pub fn frame_ids(&self) -> impl Iterator<Item = u64> + '_ {
171 self.frames.iter().map(|frame| frame.call.frame_id)
172 }
173
174 pub fn path_for_frame(&self, frame_id: u64) -> Option<Vec<CallSite>> {
177 self.frame(frame_id).map(|_| self.path_for(frame_id))
178 }
179
180 pub fn path_iter(&self, frame_id: u64) -> Option<FramePath<'_>> {
183 self.frame(frame_id).map(|_| FramePath {
184 trace: self,
185 target: frame_id,
186 next_depth: 0,
187 depth: self.frame_depth(frame_id),
188 })
189 }
190
191 pub fn current_path_iter(&self) -> Option<FramePath<'_>> {
194 self.current
195 .last()
196 .and_then(|frame_id| self.path_iter(*frame_id))
197 }
198
199 pub fn visit_path<F>(&self, frame_id: u64, mut visit: F) -> bool
207 where
208 F: FnMut(&CallSite) -> bool,
209 {
210 if self.frame(frame_id).is_none() {
211 return false;
212 }
213 self.visit_path_inner(frame_id, &mut visit)
214 }
215
216 fn visit_path_inner<F>(&self, frame_id: u64, visit: &mut F) -> bool
217 where
218 F: FnMut(&CallSite) -> bool,
219 {
220 let Some(frame) = self.frame(frame_id) else {
221 return true;
222 };
223 if let Some(parent) = frame.parent
224 && !self.visit_path_inner(parent, visit)
225 {
226 return false;
227 }
228 visit(&frame.call)
229 }
230
231 pub fn current_path(&self) -> Vec<CallSite> {
234 self.current
235 .iter()
236 .filter_map(|id| self.frame(*id).map(|frame| frame.call.clone()))
237 .collect()
238 }
239
240 pub fn paths(&self) -> Vec<Vec<CallSite>> {
243 self.frames
244 .iter()
245 .map(|frame| self.path_for(frame.call.frame_id))
246 .collect()
247 }
248
249 pub fn paths_iter(&self) -> impl Iterator<Item = Vec<CallSite>> + '_ {
252 self.frames
253 .iter()
254 .map(|frame| self.path_for(frame.call.frame_id))
255 }
256
257 pub fn borrowed_paths_iter(&self) -> impl Iterator<Item = FramePath<'_>> + '_ {
260 self.frames
261 .iter()
262 .filter_map(|frame| self.path_iter(frame.call.frame_id))
263 }
264
265 pub fn frame_count(&self) -> usize {
268 self.frames.len()
269 }
270
271 pub fn matching_paths(&self, query: &str) -> Vec<Vec<CallSite>> {
274 self.matching_paths_iter(query).collect()
275 }
276
277 pub fn matching_paths_iter(&self, query: &str) -> impl Iterator<Item = Vec<CallSite>> + '_ {
280 let query = query.trim().to_ascii_lowercase();
281 self.frames
282 .iter()
283 .filter(move |frame| {
284 query.is_empty()
285 || frame.call.function.to_ascii_lowercase().contains(&query)
286 || frame
287 .call
288 .source
289 .is_some_and(|source| source_file_matches(source.file, &query))
290 })
291 .map(|frame| self.path_for(frame.call.frame_id))
292 }
293
294 pub fn render_tree(&self) -> String {
297 let mut output = String::new();
298 for frame in &self.frames {
299 let call = &frame.call;
300 writeln!(
301 output,
302 "{}- {} {}#{}{}",
303 " ".repeat(self.frame_depth(call.frame_id)),
304 call.node,
305 call.function,
306 call.frame_id,
307 call.source
308 .map_or_else(String::new, |source| format!(" @ {source}")),
309 )
310 .unwrap();
311 }
312 output
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 #[test]
321 fn visit_path_reads_the_frame_arena_without_materializing_a_path() {
322 let mut trace = CallTrace::full();
323 let root = NodeId::from_raw([1; 16]);
324 let child = NodeId::from_raw([2; 16]);
325 trace.with(root, "root", |trace| {
326 trace.with(child, "child", |trace| trace.with(child, "leaf", |_| {}))
327 });
328 let target = trace.frame_ids().last().expect("leaf frame");
329 let mut names = Vec::new();
330 assert!(trace.visit_path(target, |call| {
331 names.push(call.function);
332 true
333 }));
334 assert_eq!(names, ["root", "child", "leaf"]);
335 let borrowed = trace
336 .path_iter(target)
337 .expect("leaf path")
338 .map(|call| call.function)
339 .collect::<Vec<_>>();
340 assert_eq!(borrowed, ["root", "child", "leaf"]);
341 }
342
343 #[test]
344 fn disabled_mode_does_not_retain_frames_locals_or_edges() {
345 let mut trace = CallTrace::disabled();
346 let node = NodeId::from_raw([7; 16]);
347 let result = trace.with(node, "work", |trace| {
348 let input = trace.local("input", "u32", 7, LocalKind::Input);
349 trace.transform(input, "output", "u32", 8)
350 });
351 assert_eq!(result, LocalId(0));
352 assert_eq!(trace.frame_count(), 0);
353 assert!(trace.locals().is_empty());
354 assert!(trace.data_edges().is_empty());
355 }
356
357 #[test]
358 fn errors_only_discards_success_and_keeps_failure_evidence() {
359 let node = NodeId::from_raw([8; 16]);
360 let mut trace = CallTrace::errors_only();
361 let success: Result<(), &str> = trace.with_result(|trace| {
362 trace.with(node, "successful", |trace| {
363 trace.local("value", "u32", 1, LocalKind::Binding);
364 });
365 Ok(())
366 });
367 assert!(success.is_ok());
368 assert_eq!(trace.frame_count(), 0);
369 assert!(trace.locals().is_empty());
370
371 let failure: Result<(), &str> = trace.with_result(|trace| {
372 trace.with(node, "failed", |trace| {
373 trace.local("value", "u32", 2, LocalKind::Binding);
374 });
375 Err("broken")
376 });
377 assert_eq!(failure, Err("broken"));
378 assert_eq!(trace.frame_count(), 1);
379 assert_eq!(trace.locals().len(), 1);
380 assert_eq!(trace.locals()[0].value, "2");
381 }
382
383 #[test]
386 fn an_id_from_a_discarded_scope_is_never_reused() {
387 let mut trace = CallTrace::errors_only();
388 let escaped: Result<LocalId, &str> =
389 trace.with_result(|trace| Ok(trace.local("discarded", "u32", 1, LocalKind::Binding)));
390 let escaped = escaped.expect("a successful scope returns its local id");
391
392 let mut kept = None;
393 let failed: Result<(), &str> = trace.with_result(|trace| {
394 kept = Some(trace.local("kept", "u32", 2, LocalKind::Binding));
395 Err("broken")
396 });
397 assert_eq!(failed, Err("broken"));
398 let kept = kept.expect("the failing scope keeps its local");
399 assert_ne!(escaped, kept, "the discarded scope's id came back");
400 assert!(
401 trace.find_local(escaped).is_none(),
402 "an id from discarded evidence must resolve to nothing"
403 );
404 assert_eq!(
405 trace.find_local(kept).map(|local| local.value.as_str()),
406 Some("2")
407 );
408 }
409
410 #[test]
411 fn resetting_the_mode_inside_a_scope_does_not_break_the_pairing() {
412 let node = NodeId::from_raw([9; 16]);
413 let mut trace = CallTrace::errors_only();
414 let cleared: Result<(), &str> = trace.with_result(|trace| {
415 trace.clear();
416 Ok(())
417 });
418 assert_eq!(cleared, Ok(()));
419 let changed: Result<(), &str> = trace.with_result(|trace| {
420 trace.set_mode(TraceMode::Full);
421 Ok(())
422 });
423 assert_eq!(changed, Ok(()));
424 let failure: Result<(), &str> = trace.with_result(|trace| {
426 trace.with(node, "after-reset", |trace| {
427 trace.local("value", "u32", 3, LocalKind::Binding);
428 });
429 Err("broken")
430 });
431 assert_eq!(failure, Err("broken"));
432 assert_eq!(trace.locals().len(), 1);
433 }
434
435 #[test]
436 fn runtime_default_matches_the_build_profile() {
437 let trace = CallTrace::runtime();
438 if cfg!(debug_assertions) {
439 assert_eq!(trace.mode(), TraceMode::ErrorsOnly);
440 } else {
441 assert_eq!(trace.mode(), TraceMode::Off);
442 }
443 }
444
445 #[test]
446 fn trace_mode_accepts_human_facing_names() {
447 assert_eq!(TraceMode::parse("off"), Some(TraceMode::Off));
448 assert_eq!(TraceMode::parse("errors_only"), Some(TraceMode::ErrorsOnly));
449 assert_eq!(TraceMode::parse("FULL"), Some(TraceMode::Full));
450 assert_eq!(TraceMode::parse("verbose"), None);
451 }
452}