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();
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/// Strips the trailing `::h<16 hex digits>` disambiguator that legacy symbol mangling appends
35/// to demangled names. Symbols demangled from the v0 mangling scheme (the default since the
36/// toolchain moved to it) have no such suffix, so the name is returned unchanged — blindly
37/// stripping the last `::` segment would chop off the actual function name.
38#[cfg(feature = "build")]
39fn strip_legacy_hash_suffix(full_fn_name: &str) -> String {
40    if let Some(idx) = full_fn_name.rfind("::") {
41        let last_segment = &full_fn_name[idx + 2..];
42        if last_segment.len() == 17
43            && last_segment.starts_with('h')
44            && last_segment[1..].bytes().all(|b| b.is_ascii_hexdigit())
45        {
46            return full_fn_name[..idx].to_owned();
47        }
48    }
49    full_fn_name.to_owned()
50}
51
52#[cfg(not(feature = "build"))]
53/// A dummy backtrace element with no data. Enable the `build` feature to collect backtraces.
54#[derive(Clone)]
55pub struct Backtrace;
56
57#[cfg(feature = "build")]
58/// Captures an entire backtrace, whose elements will be lazily resolved. See
59/// [`Backtrace::elements`] for more information.
60#[derive(Clone)]
61pub struct Backtrace {
62    skip_count: usize,
63    col_offset: usize, // whether this is from `sliced!` which requires an offset
64    frames: Vec<(RefCell<Option<BacktraceFrame>>, OnceLock<BacktraceFrame>)>,
65}
66
67#[cfg(stageleft_runtime)]
68#[cfg(feature = "build")]
69#[doc(hidden)]
70pub fn __macro_get_backtrace(col_offset: usize) -> Backtrace {
71    let mut out = Backtrace::get_backtrace(1);
72    out.col_offset = col_offset;
73    out
74}
75
76#[cfg(not(feature = "build"))]
77#[doc(hidden)]
78pub fn __macro_get_backtrace(_col_offset: usize) -> Backtrace {
79    panic!();
80}
81
82impl Backtrace {
83    #[cfg(feature = "build")]
84    #[inline(never)]
85    pub(crate) fn get_backtrace(skip_count: usize) -> Backtrace {
86        let backtrace = backtrace::Backtrace::new_unresolved();
87        let frames_vec: Vec<_> = backtrace.into();
88        Backtrace {
89            skip_count,
90            col_offset: 0,
91            frames: frames_vec
92                .into_iter()
93                .map(|f| (RefCell::new(Some(f)), OnceLock::new()))
94                .collect(),
95        }
96    }
97
98    #[cfg(not(feature = "build"))]
99    pub(crate) fn get_backtrace(_skip_count: usize) -> Backtrace {
100        panic!();
101    }
102
103    #[cfg(feature = "build")]
104    /// Gets the elements of the backtrace including inlined frames.
105    ///
106    /// Excludes all backtrace elements up to the original `get_backtrace` call as
107    /// well as additional skipped frames from that call. Also drops the suffix
108    /// of frames from `__rust_begin_short_backtrace` onwards.
109    pub fn elements(&self) -> impl Iterator<Item = BacktraceElement> + '_ {
110        self.frames
111            .iter()
112            .map(|(frame_refcell, resolved_frame)| {
113                resolved_frame.get_or_init(|| {
114                    let mut gotten_frame = frame_refcell.borrow_mut().take().unwrap();
115                    gotten_frame.resolve();
116                    gotten_frame
117                })
118            })
119            .skip_while(|f| {
120                !(std::ptr::eq(f.symbol_address(), Backtrace::get_backtrace as _)
121                    || f.symbols()
122                        .first()
123                        .and_then(|s| s.name())
124                        .and_then(|n| n.as_str())
125                        .is_some_and(|n| n.contains("get_backtrace")))
126            })
127            .skip(1)
128            .take_while(|f| {
129                !f.symbols()
130                    .last()
131                    .and_then(|s| s.name())
132                    .and_then(|n| n.as_str())
133                    .is_some_and(|n| n.contains("__rust_begin_short_backtrace"))
134            })
135            .flat_map(move |frame| frame.symbols())
136            .skip(self.skip_count)
137            .enumerate()
138            .map(|(idx, symbol)| {
139                let full_fn_name = strip_hash_brackets(&symbol.name().unwrap().to_string());
140                let mut element = BacktraceElement {
141                    fn_name: strip_legacy_hash_suffix(&full_fn_name),
142                    filename: symbol.filename().map(|f| f.display().to_string()),
143                    lineno: symbol.lineno(),
144                    colno: symbol.colno(),
145                    addr: symbol.addr().map(|a| a as usize),
146                };
147
148                if self.col_offset > 0 && idx == 0 {
149                    element.colno = element
150                        .colno
151                        .map(|c| c.saturating_sub(self.col_offset as u32));
152                }
153
154                element
155            })
156    }
157
158    #[cfg(feature = "build")]
159    /// Format the first user-code frame as `"file:line"`, or `None` if unavailable.
160    pub fn format_span(&self) -> Option<String> {
161        let elem = self.elements().next()?;
162        let file = elem.filename.as_ref()?;
163        let line = elem.lineno?;
164        Some(format!("{file}:{line}"))
165    }
166
167    #[cfg(not(feature = "build"))]
168    /// Format the first user-code frame as `"file:line"`, or `None` if unavailable.
169    pub fn format_span(&self) -> Option<String> {
170        None
171    }
172}
173
174#[cfg(feature = "build")]
175/// A single frame of a backtrace, corresponding to a single function call.
176#[derive(Clone)]
177pub struct BacktraceElement {
178    /// The name of the function that was called.
179    pub fn_name: String,
180    /// The path to the file where this call occured.
181    pub filename: Option<String>,
182    /// The line number of the function call.
183    pub lineno: Option<u32>,
184    /// The column number of the function call.
185    pub colno: Option<u32>,
186    /// The address of the instruction corresponding to this function call.
187    pub addr: Option<usize>,
188}
189
190#[cfg(feature = "build")]
191impl Debug for BacktraceElement {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        // filename / addr is unstable across platforms so we drop it
194        f.debug_struct("BacktraceElement")
195            .field("fn_name", &self.fn_name)
196            .field("lineno", &self.lineno)
197            .field("colno", &self.colno)
198            .finish()
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    #[cfg(feature = "build")]
205    #[test]
206    #[cfg_attr(not(target_os = "linux"), ignore = "backtraces only stable on Linux")]
207    fn test_backtrace() {
208        use super::*;
209
210        let backtrace = Backtrace::get_backtrace(0);
211        let elements = backtrace.elements();
212
213        hydro_build_utils::assert_debug_snapshot_unfiltered!(elements.collect::<Vec<_>>());
214    }
215}