Skip to main content

miniextendr_api/
condition.rs

1//! Condition macros and signal enum for the Rust→R condition pipeline.
2//!
3//! This module provides two things:
4//!
5//! 1. **[`RCondition`] enum** — the internal panic payload used by `error!()`,
6//!    `warning!()`, `message!()`, and `condition!()` macros. Caught by
7//!    `with_r_unwind_protect` before the generic panic→error path, then
8//!    forwarded to R as a structured condition with `rust_*` class layering.
9//!
10//! 2. **[`AsRError`] struct** — wraps any `E: std::error::Error` and
11//!    preserves the full error chain (cause/source) when converting to an R
12//!    error message. Use as the `Err` type in `Result` returns.
13//!
14//! # Condition macros
15//!
16//! The four macros are the user-facing API for raising non-panic conditions from
17//! Rust. They ride the tagged-condition transport that every `#[miniextendr]`
18//! function uses:
19//!
20//! ```ignore
21//! use miniextendr_api::{error, warning, message, condition};
22//!
23//! #[miniextendr]
24//! fn demo_error() {
25//!     error!("something went wrong: {}", 42);
26//! }
27//!
28//! #[miniextendr]
29//! fn demo_warning() {
30//!     warning!("something looks suspicious");
31//! }
32//!
33//! #[miniextendr]
34//! fn demo_message() {
35//!     message!("progress: {} of {}", 1, 10);
36//! }
37//!
38//! #[miniextendr]
39//! fn demo_condition() {
40//!     condition!("a signallable condition");
41//! }
42//! ```
43//!
44//! Optional `class =` extension for programmatic catching:
45//!
46//! ```ignore
47//! #[miniextendr]
48//! fn typed_error(name: &str) {
49//!     error!(class = "my_error", "missing field: {name}");
50//! }
51//! ```
52//!
53//! ```r
54//! tryCatch(typed_error("x"), my_error = function(e) "caught!")
55//! # [1] "caught!"
56//! ```
57//!
58//! # `AsRError`
59//!
60//! ```ignore
61//! use miniextendr_api::condition::AsRError;
62//!
63//! #[miniextendr]
64//! fn parse_config(path: &str) -> Result<i32, AsRError<std::io::Error>> {
65//!     let content = std::fs::read_to_string(path).map_err(AsRError)?;
66//!     Ok(content.len() as i32)
67//! }
68//! ```
69
70// region: RCondition enum — internal panic payload
71
72/// Internal panic payload for structured R conditions.
73///
74/// Raised by the `error!()`, `warning!()`, `message!()`, and `condition!()`
75/// macros via `std::panic::panic_any`. Caught by `with_r_unwind_protect`
76/// before the generic panic→string path and forwarded to R as a tagged SEXP
77/// with `rust_*` class layering.
78///
79/// This type is `#[doc(hidden)]` because users interact with the macros,
80/// not the enum directly.
81#[doc(hidden)]
82#[derive(Debug)]
83pub enum RCondition {
84    /// Raised by `error!(...)` / `error!(class = "...", ...)`.
85    Error {
86        message: String,
87        class: Option<String>,
88    },
89    /// Raised by `warning!(...)` / `warning!(class = "...", ...)`.
90    Warning {
91        message: String,
92        class: Option<String>,
93    },
94    /// Raised by `message!(...)`.
95    Message { message: String },
96    /// Raised by `condition!(...)` / `condition!(class = "...", ...)`.
97    Condition {
98        message: String,
99        class: Option<String>,
100    },
101}
102
103// endregion
104
105// region: Macros
106
107/// Raise an R error from Rust with `rust_error` class layering.
108///
109/// Rides the tagged-condition transport that every `#[miniextendr]` function uses.
110/// The raised condition has class `c("rust_error", "simpleError", "error", "condition")`.
111///
112/// An optional `class = "name"` form prepends a custom class for programmatic catching:
113/// `c("name", "rust_error", "simpleError", "error", "condition")`.
114///
115/// # Examples
116///
117/// ```ignore
118/// use miniextendr_api::error;
119///
120/// #[miniextendr]
121/// fn fail() {
122///     error!("something went wrong: {}", 42);
123/// }
124///
125/// // With a custom class for tryCatch:
126/// #[miniextendr]
127/// fn typed_fail(name: &str) {
128///     error!(class = "my_error", "missing field: {name}");
129/// }
130/// ```
131///
132/// ```r
133/// tryCatch(fail(), rust_error = function(e) conditionMessage(e))
134/// # [1] "something went wrong: 42"
135///
136/// tryCatch(typed_fail("x"), my_error = function(e) "caught!")
137/// # [1] "caught!"
138/// ```
139#[macro_export]
140macro_rules! error {
141    (class = $class:expr, $($arg:tt)*) => {
142        ::std::panic::panic_any($crate::condition::RCondition::Error {
143            message: ::std::format!($($arg)*),
144            class: ::std::option::Option::Some($class.to_string()),
145        })
146    };
147    ($($arg:tt)*) => {
148        ::std::panic::panic_any($crate::condition::RCondition::Error {
149            message: ::std::format!($($arg)*),
150            class: ::std::option::Option::None,
151        })
152    };
153}
154
155/// Raise an R warning from Rust with `rust_warning` class layering.
156///
157/// Rides the tagged-condition transport that every `#[miniextendr]` function uses.
158/// Unlike `panic!`, execution continues after `warning!` is caught by a handler.
159/// The raised condition has class `c("rust_warning", "simpleWarning", "warning", "condition")`.
160///
161/// An optional `class = "name"` form prepends a custom class.
162///
163/// # Example
164///
165/// ```ignore
166/// use miniextendr_api::warning;
167///
168/// #[miniextendr]
169/// fn maybe_warn(x: i32) -> i32 {
170///     if x > 100 {
171///         warning!("x is large: {x}");
172///     }
173///     x * 2
174/// }
175/// ```
176///
177/// ```r
178/// withCallingHandlers(
179///   maybe_warn(200L),
180///   warning = function(w) { cat("saw:", conditionMessage(w)); invokeRestart("muffleWarning") }
181/// )
182/// # saw: x is large: 200
183/// # [1] 400
184/// ```
185#[macro_export]
186macro_rules! warning {
187    (class = $class:expr, $($arg:tt)*) => {
188        ::std::panic::panic_any($crate::condition::RCondition::Warning {
189            message: ::std::format!($($arg)*),
190            class: ::std::option::Option::Some($class.to_string()),
191        })
192    };
193    ($($arg:tt)*) => {
194        ::std::panic::panic_any($crate::condition::RCondition::Warning {
195            message: ::std::format!($($arg)*),
196            class: ::std::option::Option::None,
197        })
198    };
199}
200
201/// Emit an R message from Rust with `rust_message` class layering.
202///
203/// Rides the tagged-condition transport that every `#[miniextendr]` function uses.
204/// The raised condition has class `c("rust_message", "simpleMessage", "message", "condition")`.
205/// Muffled by `suppressMessages()` automatically (standard R restart mechanism).
206///
207/// # Example
208///
209/// ```ignore
210/// use miniextendr_api::message;
211///
212/// #[miniextendr]
213/// fn log_step(step: i32) {
214///     message!("step {} complete", step);
215/// }
216/// ```
217///
218/// ```r
219/// log_step(3L)
220/// # step 3 complete
221///
222/// suppressMessages(log_step(3L))  # no output
223/// ```
224#[macro_export]
225macro_rules! message {
226    ($($arg:tt)*) => {
227        ::std::panic::panic_any($crate::condition::RCondition::Message {
228            message: ::std::format!($($arg)*),
229        })
230    };
231}
232
233/// Signal a generic R condition from Rust with `rust_condition` class layering.
234///
235/// Rides the tagged-condition transport that every `#[miniextendr]` function uses.
236/// Unlike `error!`, a bare condition is a silent no-op if there is no handler.
237/// The raised condition has class `c("rust_condition", "simpleCondition", "condition")`.
238///
239/// An optional `class = "name"` form prepends a custom class.
240///
241/// # Example
242///
243/// ```ignore
244/// use miniextendr_api::condition;
245///
246/// #[miniextendr]
247/// fn signal_progress(n: i32) {
248///     condition!(class = "my_progress", "processed {n} items");
249/// }
250/// ```
251///
252/// ```r
253/// withCallingHandlers(
254///   signal_progress(42L),
255///   my_progress = function(c) cat("progress:", conditionMessage(c), "\n")
256/// )
257/// # progress: processed 42 items
258/// ```
259#[macro_export]
260macro_rules! condition {
261    (class = $class:expr, $($arg:tt)*) => {
262        ::std::panic::panic_any($crate::condition::RCondition::Condition {
263            message: ::std::format!($($arg)*),
264            class: ::std::option::Option::Some($class.to_string()),
265        })
266    };
267    ($($arg:tt)*) => {
268        ::std::panic::panic_any($crate::condition::RCondition::Condition {
269            message: ::std::format!($($arg)*),
270            class: ::std::option::Option::None,
271        })
272    };
273}
274
275// endregion
276
277// region: from_tagged_sexp + repanic_if_rust_error — shim re-panic helpers
278
279impl RCondition {
280    /// Reconstruct an [`RCondition::Error`] from a tagged SEXP produced by
281    /// [`crate::error_value::make_rust_condition_value`].
282    ///
283    /// Returns `Some(RCondition)` when `sexp` has class `"rust_condition_value"` AND
284    /// the `"__rust_condition__"` attribute is `TRUE`. Returns `None` for all other
285    /// SEXPs (normal return values, `R_NilValue`, etc.).
286    ///
287    /// Reconstructs the matching variant for each kind: `"error"`/`"panic"`/
288    /// `"result_err"`/`"none_err"`/`"other_rust_error"` → [`RCondition::Error`];
289    /// `"warning"` → [`RCondition::Warning`]; `"message"` → [`RCondition::Message`];
290    /// `"condition"` → [`RCondition::Condition`]. Unknown kinds degrade to
291    /// [`RCondition::Error`] with the kind string prefixed to the message.
292    ///
293    /// # Safety
294    ///
295    /// Must be called from R's main thread.
296    pub unsafe fn from_tagged_sexp(sexp: crate::ffi::SEXP) -> Option<Self> {
297        use crate::ffi::SexpExt;
298
299        // Use SexpExt::inherits_class — wraps Rf_inherits, already main-thread.
300        if !sexp.inherits_class(c"rust_condition_value") {
301            return None;
302        }
303
304        // Belt-and-suspenders PROTECT across the full inspection window. The reads
305        // below are nominally non-allocating, but R-devel's GC is aggressive enough
306        // (see MEMORY.md "Common gotchas") that a defensive guard is cheap and
307        // closes the door on subtle regressions if the read path ever changes.
308        let _guard = unsafe { crate::gc_protect::OwnedProtect::new(sexp) };
309
310        // Verify the __rust_condition__ marker attribute is TRUE (a length-1 LGLSXP
311        // with value 1). This guards against coincidental class attribute collisions.
312        let attr_sym = crate::cached_class::rust_condition_attr_symbol();
313        let marker = sexp.get_attr(attr_sym);
314        // marker should be a scalar logical TRUE: is_logical() and logical_elt(0) == 1
315        if !marker.is_logical() || marker.logical_elt(0) != 1 {
316            return None;
317        }
318
319        // It's a tagged SEXP. Read the elements.
320        // Both 3-element (legacy) and 4-element (condition) forms have:
321        //   [0] = error message (STRSXP)
322        //   [1] = kind string (STRSXP)
323        //   [2] = class name or NULL (only in 4-element form; absent in legacy)
324
325        let len = sexp.len();
326
327        // Defense-in-depth: a tagged SEXP must have at least the message and kind
328        // slots. inherits_class + __rust_condition__ marker should already imply this,
329        // but a corrupted/spoofed SEXP that satisfies both checks shouldn't OOB
330        // the vector_elt reads below.
331        if len < 2 {
332            return None;
333        }
334
335        let msg_sexp = sexp.vector_elt(0);
336        let msg: String = msg_sexp
337            .string_elt_str(0)
338            .unwrap_or("<invalid error message>")
339            .to_string();
340
341        let kind_sexp = sexp.vector_elt(1);
342        let kind: &str = kind_sexp
343            .string_elt_str(0)
344            .unwrap_or(crate::error_value::kind::PANIC);
345
346        // Class slot is element [2] in the 4-element form (NULL in legacy form)
347        let class: Option<String> = if len >= 4 {
348            let class_sexp = sexp.vector_elt(2);
349            if class_sexp.is_nil() {
350                None
351            } else {
352                class_sexp.string_elt_str(0).map(|s| s.to_string())
353            }
354        } else {
355            None
356        };
357
358        use crate::error_value::kind as kind_const;
359        let cond = match kind {
360            kind_const::ERROR
361            | kind_const::PANIC
362            | kind_const::RESULT_ERR
363            | kind_const::NONE_ERR
364            | kind_const::OTHER_RUST_ERROR => RCondition::Error {
365                message: msg,
366                class,
367            },
368            kind_const::WARNING => RCondition::Warning {
369                message: msg,
370                class,
371            },
372            kind_const::MESSAGE => RCondition::Message { message: msg },
373            kind_const::CONDITION => RCondition::Condition {
374                message: msg,
375                class,
376            },
377            other => {
378                // Unknown kind — degrade to error
379                RCondition::Error {
380                    message: format!("[{other}] {msg}"),
381                    class,
382                }
383            }
384        };
385        Some(cond)
386    }
387}
388
389/// Inspect a SEXP returned by a trait-ABI vtable shim and, if it is a tagged
390/// error value, re-panic with the reconstructed [`RCondition`].
391///
392/// This is the "re-panic at the View boundary" step of Approach 1 from the
393/// issue-345 plan. The caller (a generated View method wrapper) does:
394///
395/// ```ignore
396/// let result = { vtable_call };
397/// ::miniextendr_api::trait_abi::repanic_if_rust_error(result);
398/// // ... convert result normally if we reach here
399/// ```
400///
401/// When `sexp` is a tagged error value:
402/// - `RCondition::Error` / `RCondition::Warning` / etc. → `panic_any!(cond)`.
403///   The outer `with_r_unwind_protect` in the consumer's C entry point will
404///   catch this and produce a tagged SEXP for the consumer's R wrapper.
405///
406/// When `sexp` is a normal value: this is a no-op.
407///
408/// # Safety
409///
410/// Must be called from R's main thread. `sexp` must be a valid (possibly
411/// tagged) SEXP.
412pub unsafe fn repanic_if_rust_error(sexp: crate::ffi::SEXP) {
413    if let Some(cond) = unsafe { RCondition::from_tagged_sexp(sexp) } {
414        std::panic::panic_any(cond);
415    }
416}
417
418// endregion
419
420// region: AsRError struct — wraps std::error::Error for Result returns
421
422/// Structured error wrapper that preserves the `std::error::Error` cause chain.
423///
424/// When displayed, formats the error message with its full source chain:
425/// ```text
426/// top-level message
427///   caused by: middle error
428///   caused by: root cause
429/// ```
430///
431/// Implements `From<E>` so it works with `?` and `.map_err(AsRError)`.
432///
433/// # Example
434///
435/// ```ignore
436/// use miniextendr_api::condition::AsRError;
437/// use std::num::ParseIntError;
438///
439/// #[miniextendr]
440/// fn parse_number(s: &str) -> Result<i32, AsRError<ParseIntError>> {
441///     s.parse::<i32>().map_err(AsRError)
442/// }
443/// ```
444pub struct AsRError<E: std::error::Error>(pub E);
445
446impl<E: std::error::Error> From<E> for AsRError<E> {
447    #[inline]
448    fn from(err: E) -> Self {
449        AsRError(err)
450    }
451}
452
453impl<E: std::error::Error> std::fmt::Display for AsRError<E> {
454    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
455        // Write the top-level message
456        write!(f, "{}", self.0)?;
457
458        // Walk the cause chain
459        let mut current: &dyn std::error::Error = &self.0;
460        while let Some(source) = current.source() {
461            write!(f, "\n  caused by: {source}")?;
462            current = source;
463        }
464
465        Ok(())
466    }
467}
468
469impl<E: std::error::Error> std::fmt::Debug for AsRError<E> {
470    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
471        write!(f, "AsRError<{}>({})", std::any::type_name::<E>(), self)
472    }
473}
474
475impl<E: std::error::Error> AsRError<E> {
476    /// Get the inner error.
477    #[inline]
478    pub fn into_inner(self) -> E {
479        self.0
480    }
481
482    /// Get the Rust type name of the wrapped error (for programmatic matching).
483    #[inline]
484    pub fn rust_type_name(&self) -> &'static str {
485        std::any::type_name::<E>()
486    }
487
488    /// Collect the full cause chain as a `Vec<String>`.
489    pub fn cause_chain(&self) -> Vec<String> {
490        let mut chain = vec![self.0.to_string()];
491        let mut current: &dyn std::error::Error = &self.0;
492        while let Some(source) = current.source() {
493            chain.push(source.to_string());
494            current = source;
495        }
496        chain
497    }
498}
499
500// endregion