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