Skip to main content

ruff_db/
panic.rs

1use std::any::Any;
2use std::backtrace::BacktraceStatus;
3use std::cell::Cell;
4use std::panic::Location;
5use std::sync::OnceLock;
6
7#[derive(Debug)]
8pub struct PanicError {
9    pub location: Option<String>,
10    pub payload: Payload,
11    pub backtrace: Option<std::backtrace::Backtrace>,
12    pub salsa_backtrace: Option<salsa::Backtrace>,
13}
14
15#[derive(Debug)]
16pub struct Payload(Box<dyn std::any::Any + Send>);
17
18impl Payload {
19    pub fn downcast_ref<R: Any>(&self) -> Option<&R> {
20        self.0.downcast_ref::<R>()
21    }
22}
23
24impl std::fmt::Display for Payload {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        if let Some(s) = self.0.downcast_ref::<String>() {
27            f.write_str(s)
28        } else if let Some(s) = self.0.downcast_ref::<&str>() {
29            f.write_str(s)
30        } else if let Some(s) = self.0.downcast_ref::<salsa::Cancelled>() {
31            write!(f, "{s}")
32        } else {
33            f.write_str("Box<dyn Any>")
34        }
35    }
36}
37
38impl PanicError {
39    pub fn resume_unwind(self) -> ! {
40        std::panic::resume_unwind(self.payload.0)
41    }
42
43    pub fn to_diagnostic_message(&self, path: Option<impl std::fmt::Display>) -> String {
44        use std::fmt::Write;
45
46        let mut message = String::new();
47        message.push_str("Panicked");
48
49        if let Some(location) = &self.location {
50            let _ = write!(&mut message, " at {location}");
51        }
52
53        if let Some(path) = path {
54            let _ = write!(&mut message, " when checking `{path}`");
55        }
56
57        let _ = write!(&mut message, ": `{payload}`", payload = self.payload);
58
59        message
60    }
61}
62
63impl std::fmt::Display for PanicError {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        write!(f, "panicked at")?;
66        if let Some(location) = &self.location {
67            write!(f, " {location}")?;
68        }
69
70        write!(f, ":\n{payload}", payload = self.payload)?;
71
72        if let Some(query_trace) = self.salsa_backtrace.as_ref() {
73            let _ = writeln!(f, "{query_trace}");
74        }
75
76        if let Some(backtrace) = &self.backtrace {
77            match backtrace.status() {
78                BacktraceStatus::Disabled => {
79                    writeln!(
80                        f,
81                        "\nrun with `RUST_BACKTRACE=1` environment variable to display a backtrace"
82                    )?;
83                }
84                BacktraceStatus::Captured => {
85                    writeln!(f, "\nBacktrace: {backtrace}")?;
86                }
87                _ => {}
88            }
89        }
90
91        Ok(())
92    }
93}
94
95#[derive(Default)]
96struct CapturedPanicInfo {
97    backtrace: Option<std::backtrace::Backtrace>,
98    location: Option<String>,
99    salsa_backtrace: Option<salsa::Backtrace>,
100}
101
102thread_local! {
103    static CAPTURE_PANIC_INFO: Cell<bool> = const { Cell::new(false) };
104    static LAST_BACKTRACE: Cell<CapturedPanicInfo> = const {
105        Cell::new(CapturedPanicInfo { backtrace: None, location: None, salsa_backtrace: None })
106    };
107}
108
109fn install_hook() {
110    static ONCE: OnceLock<()> = OnceLock::new();
111    ONCE.get_or_init(|| {
112        let prev = std::panic::take_hook();
113        std::panic::set_hook(Box::new(move |info| {
114            let should_capture = CAPTURE_PANIC_INFO.with(Cell::get);
115            if !should_capture {
116                return (*prev)(info);
117            }
118
119            let location = info.location().map(Location::to_string);
120            let backtrace = Some(std::backtrace::Backtrace::capture());
121
122            LAST_BACKTRACE.set(CapturedPanicInfo {
123                backtrace,
124                location,
125                salsa_backtrace: salsa::Backtrace::capture(),
126            });
127        }));
128    });
129}
130
131/// Invokes a closure, capturing and returning the cause of an unwinding panic if one occurs.
132///
133/// ### Thread safety
134///
135/// This is implemented by installing a custom [panic hook](std::panic::set_hook).  This panic hook
136/// is a global resource.  The hook that we install captures panic info in a thread-safe manner,
137/// and also ensures that any threads that are _not_ currently using this `catch_unwind` wrapper
138/// still use the previous hook (typically the default hook, which prints out panic information to
139/// stderr).
140///
141/// We assume that there is nothing else running in this process that needs to install a competing
142/// panic hook. We are careful to install our custom hook only once, and we do not ever restore
143/// the previous hook (since you can always retain the previous hook's behavior by not calling this
144/// wrapper).
145pub fn catch_unwind<F, R>(f: F) -> Result<R, PanicError>
146where
147    F: FnOnce() -> R + std::panic::UnwindSafe,
148{
149    install_hook();
150    let prev_should_capture = CAPTURE_PANIC_INFO.replace(true);
151    let result = std::panic::catch_unwind(f).map_err(|payload| {
152        // Try to get the backtrace and location from our custom panic hook.
153        // The custom panic hook only runs once when `panic!` is called (or similar). It doesn't
154        // run when the panic is propagated with `std::panic::resume_unwind`. The panic hook
155        // is also not called when the panic is raised with `std::panic::resume_unwind` as is the
156        // case for salsa unwinds (see the ignored test below).
157        // Because of that, always take the payload from `catch_unwind` because it may have been transformed
158        // by an inner `std::panic::catch_unwind` handlers and only use the information
159        // from the custom handler to enrich the error with the backtrace and location.
160        let CapturedPanicInfo {
161            location,
162            backtrace,
163            salsa_backtrace,
164        } = LAST_BACKTRACE.with(Cell::take);
165
166        PanicError {
167            location,
168            payload: Payload(payload),
169            backtrace,
170            salsa_backtrace,
171        }
172    });
173    CAPTURE_PANIC_INFO.set(prev_should_capture);
174    result
175}
176
177#[cfg(test)]
178mod tests {
179    use salsa::{Database, Durability};
180
181    #[test]
182    #[ignore = "super::catch_unwind installs a custom panic handler, which could effect test isolation"]
183    fn no_backtrace_for_salsa_cancelled() {
184        #[salsa::input]
185        struct Input {
186            #[returns(copy)]
187            value: u32,
188        }
189
190        #[salsa::tracked(returns(copy))]
191        fn test_query(db: &dyn Database, input: Input) -> u32 {
192            loop {
193                // This should throw a cancelled error
194                let _ = input.value(db);
195            }
196        }
197
198        let db = salsa::DatabaseImpl::new();
199
200        let input = Input::new(&db, 42);
201
202        let result = std::thread::scope(move |scope| {
203            {
204                let mut db = db.clone();
205                scope.spawn(move || {
206                    // This will cancel the other thread by throwing a `salsa::Cancelled` error.
207                    db.synthetic_write(Durability::MEDIUM);
208                });
209            }
210
211            {
212                scope.spawn(move || {
213                    super::catch_unwind(|| {
214                        test_query(&db, input);
215                    })
216                })
217            }
218            .join()
219            .unwrap()
220        });
221
222        match result {
223            Ok(_) => panic!("Expected query to panic"),
224            Err(err) => {
225                // Panics triggered with `resume_unwind` have no backtrace.
226                assert!(err.backtrace.is_none());
227            }
228        }
229    }
230}