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_string())
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
144#[cfg(feature = "build")]
145/// A single frame of a backtrace, corresponding to a single function call.
146#[derive(Clone)]
147pub struct BacktraceElement {
148    /// The name of the function that was called.
149    pub fn_name: String,
150    /// The path to the file where this call occured.
151    pub filename: Option<String>,
152    /// The line number of the function call.
153    pub lineno: Option<u32>,
154    /// The column number of the function call.
155    pub colno: Option<u32>,
156    /// The address of the instruction corresponding to this function call.
157    pub addr: Option<usize>,
158}
159
160#[cfg(feature = "build")]
161impl Debug for BacktraceElement {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        // filename / addr is unstable across platforms so we drop it
164        f.debug_struct("BacktraceElement")
165            .field("fn_name", &self.fn_name)
166            .field("lineno", &self.lineno)
167            .field("colno", &self.colno)
168            .finish()
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    #[cfg(feature = "build")]
175    #[test]
176    fn test_backtrace() {
177        use super::*;
178
179        if cfg!(not(target_os = "linux")) && std::env::var_os("GITHUB_ACTIONS").is_some() {
180            eprintln!("Backtrace tests fail on non-linux Github Actions runners, skipping.");
181            return;
182        }
183
184        let backtrace = Backtrace::get_backtrace(0);
185        let elements = backtrace.elements();
186
187        hydro_build_utils::assert_debug_snapshot!(elements.collect::<Vec<_>>());
188    }
189}