Skip to main content

hydro_lang/compile/ir/
backtrace.rs

1//! Platform-independent interface for collecting backtraces, used in the Hydro IR to
2//! trace the origin of each node.
3
4#[cfg(feature = "build")]
5use std::cell::RefCell;
6#[cfg(feature = "build")]
7use std::fmt::Debug;
8#[cfg(feature = "build")]
9use std::sync::OnceLock;
10
11#[cfg(feature = "build")]
12use backtrace::BacktraceFrame;
13
14/// Strips `[hash]` patterns from nightly compiler symbol names.
15#[cfg(feature = "build")]
16fn strip_hash_brackets(s: &str) -> String {
17    let mut result = String::with_capacity(s.len());
18    let mut chars = s.chars().peekable();
19    while let Some(c) = chars.next() {
20        if c == '[' {
21            let bracket_content: String = chars.by_ref().take_while(|&ch| ch != ']').collect();
22            if !bracket_content.chars().all(|ch| ch.is_ascii_hexdigit()) {
23                result.push('[');
24                result.push_str(&bracket_content);
25                result.push(']');
26            }
27        } else {
28            result.push(c);
29        }
30    }
31    result
32}
33
34#[cfg(not(feature = "build"))]
35/// A dummy backtrace element with no data. Enable the `build` feature to collect backtraces.
36#[derive(Clone)]
37pub struct Backtrace;
38
39#[cfg(feature = "build")]
40/// Captures an entire backtrace, whose elements will be lazily resolved. See
41/// [`Backtrace::elements`] for more information.
42#[derive(Clone)]
43pub struct Backtrace {
44    skip_count: usize,
45    col_offset: usize, // whether this is from `sliced!` which requires an offset
46    frames: Vec<(RefCell<Option<BacktraceFrame>>, OnceLock<BacktraceFrame>)>,
47}
48
49#[cfg(stageleft_runtime)]
50#[cfg(feature = "build")]
51#[doc(hidden)]
52pub fn __macro_get_backtrace(col_offset: usize) -> Backtrace {
53    let mut out = Backtrace::get_backtrace(1);
54    out.col_offset = col_offset;
55    out
56}
57
58#[cfg(not(feature = "build"))]
59#[doc(hidden)]
60pub fn __macro_get_backtrace(_col_offset: usize) -> Backtrace {
61    panic!();
62}
63
64impl Backtrace {
65    #[cfg(feature = "build")]
66    #[inline(never)]
67    pub(crate) fn get_backtrace(skip_count: usize) -> Backtrace {
68        let backtrace = backtrace::Backtrace::new_unresolved();
69        let frames_vec: Vec<_> = backtrace.into();
70        Backtrace {
71            skip_count,
72            col_offset: 0,
73            frames: frames_vec
74                .into_iter()
75                .map(|f| (RefCell::new(Some(f)), OnceLock::new()))
76                .collect(),
77        }
78    }
79
80    #[cfg(not(feature = "build"))]
81    pub(crate) fn get_backtrace(_skip_count: usize) -> Backtrace {
82        panic!();
83    }
84
85    #[cfg(feature = "build")]
86    /// Gets the elements of the backtrace including inlined frames.
87    ///
88    /// Excludes all backtrace elements up to the original `get_backtrace` call as
89    /// well as additional skipped frames from that call. Also drops the suffix
90    /// of frames from `__rust_begin_short_backtrace` onwards.
91    pub fn elements(&self) -> impl Iterator<Item = BacktraceElement> + '_ {
92        self.frames
93            .iter()
94            .map(|(frame_refcell, resolved_frame)| {
95                resolved_frame.get_or_init(|| {
96                    let mut gotten_frame = frame_refcell.borrow_mut().take().unwrap();
97                    gotten_frame.resolve();
98                    gotten_frame
99                })
100            })
101            .skip_while(|f| {
102                !(std::ptr::eq(f.symbol_address(), Backtrace::get_backtrace as _)
103                    || f.symbols()
104                        .first()
105                        .and_then(|s| s.name())
106                        .and_then(|n| n.as_str())
107                        .is_some_and(|n| n.contains("get_backtrace")))
108            })
109            .skip(1)
110            .take_while(|f| {
111                !f.symbols()
112                    .last()
113                    .and_then(|s| s.name())
114                    .and_then(|n| n.as_str())
115                    .is_some_and(|n| n.contains("__rust_begin_short_backtrace"))
116            })
117            .flat_map(move |frame| frame.symbols())
118            .skip(self.skip_count)
119            .enumerate()
120            .map(|(idx, symbol)| {
121                let full_fn_name = strip_hash_brackets(&symbol.name().unwrap().to_string());
122                let mut element = BacktraceElement {
123                    fn_name: full_fn_name
124                        .rfind("::")
125                        .map(|idx| full_fn_name.split_at(idx).0.to_owned())
126                        .unwrap_or(full_fn_name),
127                    filename: symbol.filename().map(|f| f.display().to_string()),
128                    lineno: symbol.lineno(),
129                    colno: symbol.colno(),
130                    addr: symbol.addr().map(|a| a as usize),
131                };
132
133                if self.col_offset > 0 && idx == 0 {
134                    element.colno = element
135                        .colno
136                        .map(|c| c.saturating_sub(self.col_offset as u32));
137                }
138
139                element
140            })
141    }
142
143    #[cfg(feature = "build")]
144    /// Format the first user-code frame as `"file:line"`, or `None` if unavailable.
145    pub fn format_span(&self) -> Option<String> {
146        let elem = self.elements().next()?;
147        let file = elem.filename.as_ref()?;
148        let line = elem.lineno?;
149        Some(format!("{file}:{line}"))
150    }
151
152    #[cfg(not(feature = "build"))]
153    /// Format the first user-code frame as `"file:line"`, or `None` if unavailable.
154    pub fn format_span(&self) -> Option<String> {
155        None
156    }
157}
158
159#[cfg(feature = "build")]
160/// A single frame of a backtrace, corresponding to a single function call.
161#[derive(Clone)]
162pub struct BacktraceElement {
163    /// The name of the function that was called.
164    pub fn_name: String,
165    /// The path to the file where this call occured.
166    pub filename: Option<String>,
167    /// The line number of the function call.
168    pub lineno: Option<u32>,
169    /// The column number of the function call.
170    pub colno: Option<u32>,
171    /// The address of the instruction corresponding to this function call.
172    pub addr: Option<usize>,
173}
174
175#[cfg(feature = "build")]
176impl Debug for BacktraceElement {
177    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178        // filename / addr is unstable across platforms so we drop it
179        f.debug_struct("BacktraceElement")
180            .field("fn_name", &self.fn_name)
181            .field("lineno", &self.lineno)
182            .field("colno", &self.colno)
183            .finish()
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    #[cfg(feature = "build")]
190    #[test]
191    #[cfg_attr(not(target_os = "linux"), ignore)]
192    fn test_backtrace() {
193        use super::*;
194
195        let backtrace = Backtrace::get_backtrace(0);
196        let elements = backtrace.elements();
197
198        hydro_build_utils::assert_debug_snapshot!(elements.collect::<Vec<_>>());
199    }
200}