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//!    [`crate::unwind_protect::with_r_unwind_protect`] before the generic
8//!    panic→error path, then forwarded to R as a structured condition with
9//!    `rust_*` class layering via
10//!    [`crate::error_value::make_rust_condition_value`].
11//!
12//! 2. **[`AsRError`] struct** — wraps any `E: std::error::Error` and
13//!    preserves the full error chain (cause/source) when converting to an R
14//!    error message. Use as the `Err` type in `Result` returns.
15//!
16//! # When to reach for what
17//!
18//! There are three Rust→R error-emission paths and they are not
19//! interchangeable. The crate-level rationale (why tagged-SEXP at all, what
20//! `error_in_r` defaults imply, and the `with_r_unwind_protect` leak) lives
21//! on [`crate::error_value`]; the practical picking-one summary:
22//!
23//! - **`panic!`** — escape hatch. Becomes class `c("rust_error",
24//!   "simpleError", "error", "condition")` with `kind = "panic"`. Use for
25//!   genuine bugs or impossible states. Cheapest in source; coarsest in R
26//!   (callers can only match `rust_error` / `error`, not a specific class).
27//! - **`error!` / `warning!` / `message!` / `condition!`** (this module) —
28//!   typed conditions. Same transport, but allow an optional `class =
29//!   "name"` so R-side `tryCatch` can route by class. `warning!` /
30//!   `message!` / `condition!` are the only way to emit non-error
31//!   conditions; `panic!` is always an error.
32//! - **`Result<T, E>` with [`AsRError<E>`]** — value-style propagation
33//!   through Rust code. Converts at the boundary; `kind = "result_err"`.
34//!   Best when the failure path is real-and-recoverable in Rust and the
35//!   error chain (`std::error::Error::source`) is worth preserving.
36//!
37//! `Rf_error` is *not* on this list. Direct `Rf_error` skips Rust
38//! destructors unconditionally and is forbidden by lint MXL300; see
39//! [`crate::error_value`] for the full reasoning.
40//!
41//! # Macro-vs-module name collision
42//!
43//! `#[macro_export]` puts each macro at the *crate root*, where `error!` and
44//! `condition!` collide with the same-named modules `pub mod error` and `pub
45//! mod condition`. The practical implication: `use miniextendr_api::{error,
46//! condition}` imports the *modules*, not the macros, and a subsequent
47//! `error!(...)` call fails to resolve.
48//!
49//! Workarounds, in rough order of ergonomics:
50//!
51//! 1. Invoke via fully-qualified path: `miniextendr_api::error!("...")`.
52//! 2. `use miniextendr_api as mx;` then `mx::error!("...")`.
53//! 3. `warning!` and `message!` have no module conflict — `use
54//!    miniextendr_api::{warning, message};` works directly.
55//!
56//! See the individual macro docs for the per-macro reminder.
57//!
58//! # Condition macros
59//!
60//! The four macros are the user-facing API for raising non-panic conditions from
61//! Rust. They ride the tagged-condition transport that every `#[miniextendr]`
62//! function uses:
63//!
64//! ```ignore
65//! use miniextendr_api::{error, warning, message, condition};
66//!
67//! #[miniextendr]
68//! fn demo_error() {
69//!     error!("something went wrong: {}", 42);
70//! }
71//!
72//! #[miniextendr]
73//! fn demo_warning() {
74//!     warning!("something looks suspicious");
75//! }
76//!
77//! #[miniextendr]
78//! fn demo_message() {
79//!     message!("progress: {} of {}", 1, 10);
80//! }
81//!
82//! #[miniextendr]
83//! fn demo_condition() {
84//!     condition!("a signallable condition");
85//! }
86//! ```
87//!
88//! Optional `class =` extension for programmatic catching:
89//!
90//! ```ignore
91//! #[miniextendr]
92//! fn typed_error(name: &str) {
93//!     error!(class = "my_error", "missing field: {name}");
94//! }
95//! ```
96//!
97//! ```r
98//! tryCatch(typed_error("x"), my_error = function(e) "caught!")
99//! # [1] "caught!"
100//! ```
101//!
102//! Optional `data =` extension attaches structured fields readable as
103//! `e$<name>` in handlers (rlang-`abort()`-style):
104//!
105//! ```ignore
106//! #[miniextendr]
107//! fn validate(value: i32) {
108//!     if !(0..=100).contains(&value) {
109//!         miniextendr_api::error!(
110//!             class = "validation_error",
111//!             data = [("value", value), ("min", 0), ("max", 100)],
112//!             "value {value} out of range"
113//!         );
114//!     }
115//! }
116//! ```
117//!
118//! ```r
119//! tryCatch(validate(150L), validation_error = function(e) c(e$value, e$min, e$max))
120//! # [1] 150   0 100
121//! ```
122//!
123//! Supported `data` value types (anything with `RValue: From<_>`): scalars and
124//! `Vec`s of `i32`, `f64`, `bool`, `String` / `&str`; their NA-aware `Option` /
125//! `Vec<Option<_>>` forms (`None` → R `NA`); the wide-integer ladder (`i64` /
126//! `u32`, narrowed to `integer(1)` when it fits, `double(1)` otherwise); and the
127//! [`RValue::debug`](crate::RValue::debug) escape hatch, which stringifies any
128//! `T: Debug`. For nested lists or complex/raw/NA-bearing values build an
129//! [`RValue`](crate::RValue) directly. The payload is built as a Send-safe owned
130//! value at the call site and materialised as R objects on the main thread — so
131//! `data =` works from worker-thread code too.
132//!
133//! Three `data =` grammars are accepted (see [`crate::error!`]):
134//! - single pair: `data = ("name", value)`
135//! - bracketed list: `data = [("a", v1), ("b", v2)]`
136//! - keyed builder sugar: `data = { value = 42, code = 7 }` (bare-ident keys)
137//!
138//! # `AsRError`
139//!
140//! ```ignore
141//! use miniextendr_api::condition::AsRError;
142//!
143//! #[miniextendr]
144//! fn parse_config(path: &str) -> Result<i32, AsRError<std::io::Error>> {
145//!     let content = std::fs::read_to_string(path).map_err(AsRError)?;
146//!     Ok(content.len() as i32)
147//! }
148//! ```
149
150// region: ConditionData — Send-safe owned condition-data payload
151
152/// Named condition-data payload: an ordered list of `(name, value)` pairs.
153///
154/// Produced by the macros' `data = ...` form and consumed by
155/// [`crate::error_value::make_rust_condition_value`]. Each value is an
156/// [`RValue`](crate::RValue) — an owned, `Send`, R-native value tree. Send-safe
157/// by construction (no live `SEXP`), so the payload can travel through
158/// `panic_any` and cross the worker→main thread boundary; the R objects are
159/// materialised on the main thread at the unwind boundary.
160///
161/// The macros accept any value with `RValue: From<_>` (scalars and `Vec`s of
162/// `i32` / `f64` / `bool` / `String` / `&str`; their NA-aware `Option` /
163/// `Vec<Option<_>>` forms; the `i64` / `u32` wide-integer ladder); a scalar
164/// `7i32` becomes `integer(1)` and a `Vec<i32>` becomes `integer(n)`. Any
165/// `T: Debug` rides along via [`RValue::debug`](crate::RValue::debug). For
166/// nested lists or complex/raw values build an [`RValue`](crate::RValue)
167/// directly.
168pub type ConditionData = Vec<(String, crate::RValue)>;
169
170// endregion
171
172// region: RCondition enum — internal panic payload
173
174/// Internal panic payload for structured R conditions.
175///
176/// Raised by the `error!()`, `warning!()`, `message!()`, and `condition!()`
177/// macros via `std::panic::panic_any`. Caught by `with_r_unwind_protect`
178/// before the generic panic→string path and forwarded to R as a tagged SEXP
179/// with `rust_*` class layering.
180///
181/// This type is `#[doc(hidden)]` because users interact with the macros,
182/// not the enum directly.
183#[doc(hidden)]
184#[derive(Debug)]
185pub enum RCondition {
186    /// Raised by `error!(...)` / `error!(class = "...", ...)`.
187    Error {
188        message: String,
189        class: Option<String>,
190        data: Option<ConditionData>,
191    },
192    /// Raised by `warning!(...)` / `warning!(class = "...", ...)`.
193    Warning {
194        message: String,
195        class: Option<String>,
196        data: Option<ConditionData>,
197    },
198    /// Raised by `message!(...)`.
199    Message {
200        message: String,
201        data: Option<ConditionData>,
202    },
203    /// Raised by `condition!(...)` / `condition!(class = "...", ...)`.
204    Condition {
205        message: String,
206        class: Option<String>,
207        data: Option<ConditionData>,
208    },
209}
210
211// endregion
212
213// region: Macros
214
215/// Internal: normalise a macro `data = ...` argument into
216/// `Option<ConditionData>`. Not part of the public API.
217///
218/// Three forms are accepted:
219/// - a single pair: `("name", value)`
220/// - a bracketed list of pairs: `[("a", v1), ("b", v2)]`
221/// - keyed builder sugar: `{ name = value, other = value }` — the field name is
222///   a bare identifier (stringified by the macro), so `{ value = 42, code = 7 }`
223///   is shorthand for `[("value", 42), ("code", 7)]`.
224///
225/// Each `value` is converted via `RValue::from`, so any type with an `RValue`
226/// `From` impl (the scalar/vector/`Option`/wide-integer set) works without
227/// ceremony.
228#[doc(hidden)]
229#[macro_export]
230macro_rules! __mx_condition_data {
231    (($name:expr, $value:expr) $(,)?) => {
232        ::std::option::Option::Some(::std::vec![(
233            ($name).to_string(),
234            $crate::RValue::from($value),
235        )])
236    };
237    ([ $(($name:expr, $value:expr)),* $(,)? ]) => {
238        ::std::option::Option::Some(::std::vec![
239            $(
240                (
241                    ($name).to_string(),
242                    $crate::RValue::from($value),
243                ),
244            )*
245        ])
246    };
247    ({ $($name:ident = $value:expr),* $(,)? }) => {
248        ::std::option::Option::Some(::std::vec![
249            $(
250                (
251                    ::std::stringify!($name).to_string(),
252                    $crate::RValue::from($value),
253                ),
254            )*
255        ])
256    };
257}
258
259/// Raise an R error from Rust with `rust_error` class layering.
260///
261/// Rides the tagged-condition transport that every `#[miniextendr]` function uses.
262/// The raised condition has class `c("rust_error", "simpleError", "error", "condition")`.
263///
264/// An optional `class = "name"` form prepends a custom class for programmatic catching:
265/// `c("name", "rust_error", "simpleError", "error", "condition")`.
266///
267/// # Structured `data = ...` payloads
268///
269/// An optional `data = ...` form (after `class`, before the message) attaches
270/// named fields to the condition object, rlang-`abort()`-style. Handlers read
271/// them as `e$<name>` instead of parsing the message string:
272///
273/// ```ignore
274/// // Single field:
275/// mx::error!(class = "range_error", data = ("value", value), "value {value} out of range");
276///
277/// // Multiple fields (bracketed list of pairs):
278/// mx::error!(
279///     class = "validation_error",
280///     data = [("value", value), ("min", 0), ("max", 100)],
281///     "value {value} out of range"
282/// );
283///
284/// // Keyed builder sugar (bare-ident keys, stringified by the macro):
285/// mx::error!(
286///     class = "validation_error",
287///     data = { value = value, min = 0, max = 100 },
288///     "value {value} out of range"
289/// );
290/// ```
291///
292/// ```r
293/// tryCatch(validate(150L), validation_error = function(e) c(e$value, e$min, e$max))
294/// # [1] 150   0 100
295/// ```
296///
297/// Argument order is fixed: `class = ...` (optional), then `data = ...`
298/// (optional), then the format message.
299///
300/// **Supported value types**: scalars and `Vec`s of `i32`, `f64`, `bool`, and
301/// `String` (plus `&str` / `Vec<&str>`, converted to owned); their NA-aware
302/// `Option` / `Vec<Option<_>>` forms (→ R `NA`); the wide-integer ladder (`i64`
303/// / `u32`); and the [`RValue::debug`](crate::RValue::debug) escape hatch for
304/// any `T: Debug`. The payload must be `Send` — it travels through `panic_any`
305/// and may cross the worker→main thread boundary, so live `SEXP`s cannot ride
306/// along; the R objects are materialised on the main thread at the unwind
307/// boundary. For nested lists or complex/raw values build an
308/// [`RValue`](crate::RValue) directly.
309///
310/// # See also
311///
312/// - [`crate::warning!`] / [`crate::message!`] / [`crate::condition!`] — the
313///   non-error sibling kinds (warning continues execution; message is muffled
314///   by `suppressMessages`; condition is silent without a handler).
315/// - [`std::panic!`] — escape hatch with the same `rust_error` class layering
316///   but no custom-class slot. Use for true bugs / impossible states; reach for
317///   `error!` when callers might want to route by class.
318/// - [`AsRError`] — wraps `Result<_, E: std::error::Error>` for value-style
319///   propagation through Rust code; converts at the boundary.
320/// - [`crate::error_value`] — module-level rationale for the tagged-SEXP
321///   transport and the `error_in_r` default.
322///
323/// **Name-collision note.** Because `pub mod error` exists at the crate root,
324/// `use miniextendr_api::error` imports the module rather than this macro, and
325/// glob imports (`use miniextendr_api::*;`) hit the same shadow. Prefer the
326/// collision-free alias [`crate::rust_error!`], which has the identical
327/// expansion; otherwise invoke via `miniextendr_api::error!(...)` (fully
328/// qualified) or `mx::error!(...)` after `use miniextendr_api as mx;`.
329///
330/// # Examples
331///
332/// ```ignore
333/// use miniextendr_api as mx;
334///
335/// #[miniextendr]
336/// fn fail() {
337///     mx::error!("something went wrong: {}", 42);
338/// }
339///
340/// // With a custom class for tryCatch:
341/// #[miniextendr]
342/// fn typed_fail(name: &str) {
343///     mx::error!(class = "my_error", "missing field: {name}");
344/// }
345/// ```
346///
347/// ```r
348/// tryCatch(fail(), rust_error = function(e) conditionMessage(e))
349/// # [1] "something went wrong: 42"
350///
351/// tryCatch(typed_fail("x"), my_error = function(e) "caught!")
352/// # [1] "caught!"
353/// ```
354#[macro_export]
355macro_rules! error {
356    (class = $class:expr, data = $data:tt, $($arg:tt)*) => {
357        ::std::panic::panic_any($crate::condition::RCondition::Error {
358            message: ::std::format!($($arg)*),
359            class: ::std::option::Option::Some($class.to_string()),
360            data: $crate::__mx_condition_data!($data),
361        })
362    };
363    (data = $data:tt, $($arg:tt)*) => {
364        ::std::panic::panic_any($crate::condition::RCondition::Error {
365            message: ::std::format!($($arg)*),
366            class: ::std::option::Option::None,
367            data: $crate::__mx_condition_data!($data),
368        })
369    };
370    (class = $class:expr, $($arg:tt)*) => {
371        ::std::panic::panic_any($crate::condition::RCondition::Error {
372            message: ::std::format!($($arg)*),
373            class: ::std::option::Option::Some($class.to_string()),
374            data: ::std::option::Option::None,
375        })
376    };
377    ($($arg:tt)*) => {
378        ::std::panic::panic_any($crate::condition::RCondition::Error {
379            message: ::std::format!($($arg)*),
380            class: ::std::option::Option::None,
381            data: ::std::option::Option::None,
382        })
383    };
384}
385
386/// Collision-free alias for [`crate::error!`].
387///
388/// Identical expansion and grammar (`class = …`, `data = …`, and the plain
389/// `format!` forms) — it exists so that `use miniextendr_api::*;` or
390/// `use miniextendr_api::rust_error;` gives you a usable macro name. The bare
391/// `error!` name is shadowed at the crate root by `pub mod error` (see the
392/// name-collision note on [`crate::error!`]), so a glob or direct import
393/// resolves to the module, not the macro. `rust_error!` has no such clash.
394///
395/// # Example
396///
397/// ```ignore
398/// use miniextendr_api::rust_error;
399///
400/// #[miniextendr]
401/// fn typed_fail(name: &str) {
402///     rust_error!(class = "my_error", "missing field: {name}");
403/// }
404/// ```
405#[macro_export]
406macro_rules! rust_error {
407    ($($t:tt)*) => { $crate::error!($($t)*) };
408}
409
410/// Raise an R warning from Rust with `rust_warning` class layering.
411///
412/// Rides the tagged-condition transport that every `#[miniextendr]` function uses.
413/// Unlike `panic!`, execution continues after `warning!` is caught by a handler.
414/// The raised condition has class `c("rust_warning", "simpleWarning", "warning", "condition")`.
415///
416/// An optional `class = "name"` form prepends a custom class. An optional
417/// `data = ...` form (after `class`, before the message) attaches named fields
418/// readable as `w$<name>` in handlers — same grammar and supported value types
419/// as [`crate::error!`] (see there for details):
420///
421/// ```ignore
422/// warning!(class = "truncation", data = ("dropped", n), "dropped {n} rows");
423/// ```
424///
425/// # See also
426///
427/// - [`crate::error!`] — fatal sibling; aborts the call instead of continuing.
428/// - [`crate::message!`] / [`crate::condition!`] — softer signal kinds (muffled
429///   by `suppressMessages` / silent without handler, respectively).
430/// - [`std::panic!`] — escape hatch when "continue after this" is not a sensible
431///   semantic.
432/// - [`crate::error_value`] — tagged-SEXP transport rationale.
433///
434/// No name-collision caveat: there is no `pub mod warning`, so
435/// `use miniextendr_api::warning;` then `warning!(...)` works directly.
436///
437/// # Example
438///
439/// ```ignore
440/// use miniextendr_api::warning;
441///
442/// #[miniextendr]
443/// fn maybe_warn(x: i32) -> i32 {
444///     if x > 100 {
445///         warning!("x is large: {x}");
446///     }
447///     x * 2
448/// }
449/// ```
450///
451/// ```r
452/// withCallingHandlers(
453///   maybe_warn(200L),
454///   warning = function(w) { cat("saw:", conditionMessage(w)); invokeRestart("muffleWarning") }
455/// )
456/// # saw: x is large: 200
457/// # [1] 400
458/// ```
459#[macro_export]
460macro_rules! warning {
461    (class = $class:expr, data = $data:tt, $($arg:tt)*) => {
462        ::std::panic::panic_any($crate::condition::RCondition::Warning {
463            message: ::std::format!($($arg)*),
464            class: ::std::option::Option::Some($class.to_string()),
465            data: $crate::__mx_condition_data!($data),
466        })
467    };
468    (data = $data:tt, $($arg:tt)*) => {
469        ::std::panic::panic_any($crate::condition::RCondition::Warning {
470            message: ::std::format!($($arg)*),
471            class: ::std::option::Option::None,
472            data: $crate::__mx_condition_data!($data),
473        })
474    };
475    (class = $class:expr, $($arg:tt)*) => {
476        ::std::panic::panic_any($crate::condition::RCondition::Warning {
477            message: ::std::format!($($arg)*),
478            class: ::std::option::Option::Some($class.to_string()),
479            data: ::std::option::Option::None,
480        })
481    };
482    ($($arg:tt)*) => {
483        ::std::panic::panic_any($crate::condition::RCondition::Warning {
484            message: ::std::format!($($arg)*),
485            class: ::std::option::Option::None,
486            data: ::std::option::Option::None,
487        })
488    };
489}
490
491/// Emit an R message from Rust with `rust_message` class layering.
492///
493/// Rides the tagged-condition transport that every `#[miniextendr]` function uses.
494/// The raised condition has class `c("rust_message", "simpleMessage", "message", "condition")`.
495/// Muffled by `suppressMessages()` automatically (standard R restart mechanism).
496///
497/// An optional `data = ...` form (before the message) attaches named fields
498/// readable as `m$<name>` in `withCallingHandlers` — same grammar and
499/// supported value types as [`crate::error!`] (see there for details). There
500/// is no `class =` form for `message!`.
501///
502/// # See also
503///
504/// - [`crate::warning!`] / [`crate::condition!`] — louder/quieter sibling kinds.
505/// - [`crate::error!`] — for fatal failures.
506/// - [`std::panic!`] — escape hatch.
507/// - [`crate::error_value`] — tagged-SEXP transport rationale.
508///
509/// No name-collision caveat: there is no `pub mod message`, so
510/// `use miniextendr_api::message;` then `message!(...)` works directly.
511///
512/// # Example
513///
514/// ```ignore
515/// use miniextendr_api::message;
516///
517/// #[miniextendr]
518/// fn log_step(step: i32) {
519///     message!("step {} complete", step);
520/// }
521/// ```
522///
523/// ```r
524/// log_step(3L)
525/// # step 3 complete
526///
527/// suppressMessages(log_step(3L))  # no output
528/// ```
529#[macro_export]
530macro_rules! message {
531    (data = $data:tt, $($arg:tt)*) => {
532        ::std::panic::panic_any($crate::condition::RCondition::Message {
533            message: ::std::format!($($arg)*),
534            data: $crate::__mx_condition_data!($data),
535        })
536    };
537    ($($arg:tt)*) => {
538        ::std::panic::panic_any($crate::condition::RCondition::Message {
539            message: ::std::format!($($arg)*),
540            data: ::std::option::Option::None,
541        })
542    };
543}
544
545/// Signal a generic R condition from Rust with `rust_condition` class layering.
546///
547/// Rides the tagged-condition transport that every `#[miniextendr]` function uses.
548/// Unlike `error!`, a bare condition is a silent no-op if there is no handler.
549/// The raised condition has class `c("rust_condition", "simpleCondition", "condition")`.
550///
551/// An optional `class = "name"` form prepends a custom class. An optional
552/// `data = ...` form (after `class`, before the message) attaches named fields
553/// readable as `c$<name>` in handlers — same grammar and supported value types
554/// as [`crate::error!`] (see there for details).
555///
556/// # See also
557///
558/// - [`crate::error!`] / [`crate::warning!`] / [`crate::message!`] — louder
559///   condition kinds. Pick `condition!` when "no handler = silent" is the
560///   right default (progress events, structured logging hooks).
561/// - [`std::panic!`] — escape hatch when the failure cannot be ignored.
562/// - [`crate::error_value`] — tagged-SEXP transport rationale.
563///
564/// **Name-collision note.** Because `pub mod condition` exists at the crate
565/// root, `use miniextendr_api::condition` imports the module rather than this
566/// macro, and glob imports (`use miniextendr_api::*;`) hit the same shadow.
567/// Prefer the collision-free alias [`crate::rust_condition!`], which has the
568/// identical expansion; otherwise invoke via `miniextendr_api::condition!(...)`
569/// (fully qualified) or `mx::condition!(...)` after `use miniextendr_api as mx;`.
570///
571/// # Example
572///
573/// ```ignore
574/// use miniextendr_api::condition;
575///
576/// #[miniextendr]
577/// fn signal_progress(n: i32) {
578///     condition!(class = "my_progress", "processed {n} items");
579/// }
580/// ```
581///
582/// ```r
583/// withCallingHandlers(
584///   signal_progress(42L),
585///   my_progress = function(c) cat("progress:", conditionMessage(c), "\n")
586/// )
587/// # progress: processed 42 items
588/// ```
589#[macro_export]
590macro_rules! condition {
591    (class = $class:expr, data = $data:tt, $($arg:tt)*) => {
592        ::std::panic::panic_any($crate::condition::RCondition::Condition {
593            message: ::std::format!($($arg)*),
594            class: ::std::option::Option::Some($class.to_string()),
595            data: $crate::__mx_condition_data!($data),
596        })
597    };
598    (data = $data:tt, $($arg:tt)*) => {
599        ::std::panic::panic_any($crate::condition::RCondition::Condition {
600            message: ::std::format!($($arg)*),
601            class: ::std::option::Option::None,
602            data: $crate::__mx_condition_data!($data),
603        })
604    };
605    (class = $class:expr, $($arg:tt)*) => {
606        ::std::panic::panic_any($crate::condition::RCondition::Condition {
607            message: ::std::format!($($arg)*),
608            class: ::std::option::Option::Some($class.to_string()),
609            data: ::std::option::Option::None,
610        })
611    };
612    ($($arg:tt)*) => {
613        ::std::panic::panic_any($crate::condition::RCondition::Condition {
614            message: ::std::format!($($arg)*),
615            class: ::std::option::Option::None,
616            data: ::std::option::Option::None,
617        })
618    };
619}
620
621/// Collision-free alias for [`crate::condition!`].
622///
623/// Identical expansion and grammar (`class = …`, `data = …`, and the plain
624/// `format!` forms) — it exists so that `use miniextendr_api::*;` or
625/// `use miniextendr_api::rust_condition;` gives you a usable macro name. The
626/// bare `condition!` name is shadowed at the crate root by `pub mod condition`
627/// (see the name-collision note on [`crate::condition!`]), so a glob or direct
628/// import resolves to the module, not the macro. `rust_condition!` has no such
629/// clash.
630///
631/// # Example
632///
633/// ```ignore
634/// use miniextendr_api::rust_condition;
635///
636/// #[miniextendr]
637/// fn signal_progress(n: i32) {
638///     rust_condition!(class = "my_progress", "processed {n} items");
639/// }
640/// ```
641#[macro_export]
642macro_rules! rust_condition {
643    ($($t:tt)*) => { $crate::condition!($($t)*) };
644}
645
646// endregion
647
648// region: from_tagged_sexp + repanic_if_rust_error — shim re-panic helpers
649
650impl RCondition {
651    /// Reconstruct an [`RCondition::Error`] from a tagged SEXP produced by
652    /// [`crate::error_value::make_rust_condition_value`].
653    ///
654    /// Returns `Some(RCondition)` when `sexp` has class `"rust_condition_value"` AND
655    /// the `"__rust_condition__"` attribute is `TRUE`. Returns `None` for all other
656    /// SEXPs (normal return values, `R_NilValue`, etc.).
657    ///
658    /// Reconstructs the matching variant for each kind: `"error"`/`"panic"`/
659    /// `"result_err"`/`"none_err"`/`"other_rust_error"` → [`RCondition::Error`];
660    /// `"warning"` → [`RCondition::Warning`]; `"message"` → [`RCondition::Message`];
661    /// `"condition"` → [`RCondition::Condition`]. Unknown kinds degrade to
662    /// [`RCondition::Error`] with the kind string prefixed to the message.
663    ///
664    /// # Safety
665    ///
666    /// Must be called from R's main thread.
667    pub unsafe fn from_tagged_sexp(sexp: crate::SEXP) -> Option<Self> {
668        use crate::SexpExt;
669        use crate::from_r::TryFromSexp;
670
671        // Use SexpExt::inherits_class — wraps Rf_inherits, already main-thread.
672        if !sexp.inherits_class(c"rust_condition_value") {
673            return None;
674        }
675
676        // Belt-and-suspenders PROTECT across the full inspection window. The reads
677        // below are nominally non-allocating, but R-devel's GC is aggressive enough
678        // (see MEMORY.md "Common gotchas") that a defensive guard is cheap and
679        // closes the door on subtle regressions if the read path ever changes.
680        let _guard = unsafe { crate::gc_protect::OwnedProtect::new(sexp) };
681
682        // Verify the __rust_condition__ marker attribute is TRUE (a length-1 LGLSXP
683        // with value 1). This guards against coincidental class attribute collisions.
684        let attr_sym = crate::cached_class::rust_condition_attr_symbol();
685        let marker = sexp.get_attr(attr_sym);
686        // marker should be a scalar logical TRUE: is_logical() and logical_elt(0) == 1
687        if !marker.is_logical() || marker.logical_elt(0) != 1 {
688            return None;
689        }
690
691        // It's a tagged SEXP. Read the elements.
692        // Both 3-element (legacy) and 4-element (condition) forms have:
693        //   [0] = error message (STRSXP)
694        //   [1] = kind string (STRSXP)
695        //   [2] = class name or NULL (only in 4-element form; absent in legacy)
696
697        let len = sexp.len();
698
699        // Defense-in-depth: a tagged SEXP must have at least the message and kind
700        // slots. inherits_class + __rust_condition__ marker should already imply this,
701        // but a corrupted/spoofed SEXP that satisfies both checks shouldn't OOB
702        // the vector_elt reads below.
703        if len < 2 {
704            return None;
705        }
706
707        let msg_sexp = sexp.vector_elt(0);
708        let msg: String = msg_sexp
709            .string_elt_str(0)
710            .unwrap_or("<invalid error message>")
711            .to_string();
712
713        let kind_sexp = sexp.vector_elt(1);
714        let kind: &str = kind_sexp
715            .string_elt_str(0)
716            .unwrap_or(crate::error_value::kind::PANIC);
717
718        // Class slot is element [2] in the 4-element form (NULL in legacy form)
719        let class: Option<String> = if len >= 4 {
720            let class_sexp = sexp.vector_elt(2);
721            if class_sexp.is_nil() {
722                None
723            } else {
724                class_sexp.string_elt_str(0).map(|s| s.to_string())
725            }
726        } else {
727            None
728        };
729
730        use crate::error_value::kind as kind_const;
731
732        // Slot [4] is the optional named-list condition data, present when `len >= 5`.
733        //
734        // Each field value is decoded through the single SEXP→owned-tree walker,
735        // [`RValue::try_from_sexp`], so structured fields survive the cross-package
736        // trait-ABI re-panic path (`repanic_if_rust_error`): the consumer's outer
737        // `with_r_unwind_protect` guard rebuilds the tagged SEXP from the
738        // reconstructed `RCondition`, which now carries the data — so `e$field_name`
739        // is accessible in R handlers even when the error crossed a package boundary.
740        //
741        // `RValue` is NA-aware (logical/integer/character carry `None`; double
742        // carries the `NA_REAL` bit), so NA-bearing fields now round-trip faithfully
743        // rather than being dropped. Fields whose name is missing/empty, or whose
744        // value is not R data (closures, environments, …, which `try_from_sexp`
745        // rejects) are dropped — safe degradation that preserves message/class/kind.
746        //
747        // All reads here are non-allocating copies into owned Rust values, so no new
748        // SEXPs are created and the existing `_guard` OwnedProtect suffices.
749        let data: Option<ConditionData> = if len >= 5 {
750            let data_sexp = sexp.vector_elt(4);
751            if data_sexp.is_nil() || !data_sexp.is_list() {
752                None
753            } else {
754                let data_len = data_sexp.len();
755                let names_sexp = data_sexp.get_names();
756                let mut fields: ConditionData = Vec::with_capacity(data_len);
757                for i in 0..data_len as isize {
758                    // Read the field name from the names attribute. If missing/empty, skip.
759                    let name: String = if names_sexp.is_nil() || !names_sexp.is_character() {
760                        continue;
761                    } else {
762                        match names_sexp.string_elt_str(i) {
763                            Some(s) if !s.is_empty() => s.to_string(),
764                            _ => continue,
765                        }
766                    };
767                    if let Ok(value) = crate::RValue::try_from_sexp(data_sexp.vector_elt(i)) {
768                        fields.push((name, value));
769                    }
770                }
771                if fields.is_empty() {
772                    None
773                } else {
774                    Some(fields)
775                }
776            }
777        } else {
778            None
779        };
780
781        let cond = match kind {
782            kind_const::ERROR
783            | kind_const::PANIC
784            | kind_const::RESULT_ERR
785            | kind_const::NONE_ERR
786            | kind_const::OTHER_RUST_ERROR => RCondition::Error {
787                message: msg,
788                class,
789                data,
790            },
791            kind_const::WARNING => RCondition::Warning {
792                message: msg,
793                class,
794                data,
795            },
796            kind_const::MESSAGE => RCondition::Message { message: msg, data },
797            kind_const::CONDITION => RCondition::Condition {
798                message: msg,
799                class,
800                data,
801            },
802            other => {
803                // Unknown kind — degrade to error
804                RCondition::Error {
805                    message: format!("[{other}] {msg}"),
806                    class,
807                    data,
808                }
809            }
810        };
811        Some(cond)
812    }
813}
814
815/// Inspect a SEXP returned by a trait-ABI vtable shim and, if it is a tagged
816/// error value, re-panic with the reconstructed [`RCondition`].
817///
818/// This is the "re-panic at the View boundary" step of Approach 1 from the
819/// issue-345 plan. The caller (a generated View method wrapper) does:
820///
821/// ```ignore
822/// let result = { vtable_call };
823/// ::miniextendr_api::trait_abi::repanic_if_rust_error(result);
824/// // ... convert result normally if we reach here
825/// ```
826///
827/// When `sexp` is a tagged error value:
828/// - `RCondition::Error` / `RCondition::Warning` / etc. → `panic_any!(cond)`.
829///   The outer `with_r_unwind_protect` in the consumer's C entry point will
830///   catch this and produce a tagged SEXP for the consumer's R wrapper.
831///
832/// When `sexp` is a normal value: this is a no-op.
833///
834/// # Safety
835///
836/// Must be called from R's main thread. `sexp` must be a valid (possibly
837/// tagged) SEXP.
838pub unsafe fn repanic_if_rust_error(sexp: crate::SEXP) {
839    if let Some(cond) = unsafe { RCondition::from_tagged_sexp(sexp) } {
840        std::panic::panic_any(cond);
841    }
842}
843
844// endregion
845
846// region: AsRError struct — wraps std::error::Error for Result returns
847
848/// Structured error wrapper that preserves the `std::error::Error` cause chain.
849///
850/// When displayed, formats the error message with its full source chain:
851/// ```text
852/// top-level message
853///   caused by: middle error
854///   caused by: root cause
855/// ```
856///
857/// Implements `From<E>` so it works with `?` and `.map_err(AsRError)`.
858///
859/// # Example
860///
861/// ```ignore
862/// use miniextendr_api::condition::AsRError;
863/// use std::num::ParseIntError;
864///
865/// #[miniextendr]
866/// fn parse_number(s: &str) -> Result<i32, AsRError<ParseIntError>> {
867///     s.parse::<i32>().map_err(AsRError)
868/// }
869/// ```
870pub struct AsRError<E: std::error::Error>(pub E);
871
872impl<E: std::error::Error> From<E> for AsRError<E> {
873    #[inline]
874    fn from(err: E) -> Self {
875        AsRError(err)
876    }
877}
878
879impl<E: std::error::Error> std::fmt::Display for AsRError<E> {
880    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
881        // Write the top-level message
882        write!(f, "{}", self.0)?;
883
884        // Walk the cause chain
885        let mut current: &dyn std::error::Error = &self.0;
886        while let Some(source) = current.source() {
887            write!(f, "\n  caused by: {source}")?;
888            current = source;
889        }
890
891        Ok(())
892    }
893}
894
895impl<E: std::error::Error> std::fmt::Debug for AsRError<E> {
896    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
897        write!(f, "AsRError<{}>({})", std::any::type_name::<E>(), self)
898    }
899}
900
901impl<E: std::error::Error> AsRError<E> {
902    /// Get the inner error.
903    #[inline]
904    pub fn into_inner(self) -> E {
905        self.0
906    }
907
908    /// Get the Rust type name of the wrapped error (for programmatic matching).
909    #[inline]
910    pub fn rust_type_name(&self) -> &'static str {
911        std::any::type_name::<E>()
912    }
913
914    /// Collect the full cause chain as a `Vec<String>`.
915    pub fn cause_chain(&self) -> Vec<String> {
916        let mut chain = vec![self.0.to_string()];
917        let mut current: &dyn std::error::Error = &self.0;
918        while let Some(source) = current.source() {
919            chain.push(source.to_string());
920            current = source;
921        }
922        chain
923    }
924}
925
926// endregion
927
928// region: Tests — macro grammar + payload contents (no R runtime needed)
929
930#[cfg(test)]
931mod condition_macro_tests {
932    use super::{ConditionData, RCondition};
933    use crate::RValue;
934
935    /// Catch the `panic_any(RCondition)` raised by a macro invocation and
936    /// return the payload. No R runtime needed — the macros panic before any
937    /// R API call.
938    fn catch(f: impl FnOnce() + std::panic::UnwindSafe) -> RCondition {
939        let payload = std::panic::catch_unwind(f).expect_err("macro must panic");
940        *payload
941            .downcast::<RCondition>()
942            .expect("payload must be RCondition")
943    }
944
945    fn assert_data(data: &Option<ConditionData>, expected: &[(&str, RValue)]) {
946        let data = data.as_ref().expect("data must be Some");
947        assert_eq!(data.len(), expected.len());
948        for ((name, value), (exp_name, exp_value)) in data.iter().zip(expected) {
949            assert_eq!(name, exp_name);
950            // RValue has no PartialEq (f64); compare via Debug.
951            assert_eq!(format!("{value:?}"), format!("{exp_value:?}"));
952        }
953    }
954
955    #[test]
956    fn error_message_only_backcompat() {
957        let cond = catch(|| crate::error!("plain {}", 42));
958        match cond {
959            RCondition::Error {
960                message,
961                class,
962                data,
963            } => {
964                assert_eq!(message, "plain 42");
965                assert!(class.is_none());
966                assert!(data.is_none());
967            }
968            other => panic!("wrong variant: {other:?}"),
969        }
970    }
971
972    #[test]
973    fn error_class_only_backcompat() {
974        let cond = catch(|| crate::error!(class = "my_error", "missing field: {}", "x"));
975        match cond {
976            RCondition::Error {
977                message,
978                class,
979                data,
980            } => {
981                assert_eq!(message, "missing field: x");
982                assert_eq!(class.as_deref(), Some("my_error"));
983                assert!(data.is_none());
984            }
985            other => panic!("wrong variant: {other:?}"),
986        }
987    }
988
989    #[test]
990    fn error_single_data_pair() {
991        let value = 41_i32;
992        let cond = catch(move || crate::error!(data = ("value", value), "v = {value}"));
993        match cond {
994            RCondition::Error {
995                message,
996                class,
997                data,
998            } => {
999                assert_eq!(message, "v = 41");
1000                assert!(class.is_none());
1001                assert_data(&data, &[("value", RValue::Integer(vec![Some(41)]))]);
1002            }
1003            other => panic!("wrong variant: {other:?}"),
1004        }
1005    }
1006
1007    #[test]
1008    fn error_class_and_data_list_all_value_types() {
1009        let cond = catch(|| {
1010            crate::error!(
1011                class = "validation_error",
1012                data = [
1013                    ("value", 1.5),
1014                    ("code", 7),
1015                    ("label", "lhs"),
1016                    ("fatal", false),
1017                    ("ints", vec![1, 2]),
1018                    ("reals", vec![0.5_f64]),
1019                    ("flags", vec![true]),
1020                    ("labels", vec!["a".to_string()])
1021                ],
1022                "out of range"
1023            )
1024        });
1025        match cond {
1026            RCondition::Error {
1027                message,
1028                class,
1029                data,
1030            } => {
1031                assert_eq!(message, "out of range");
1032                assert_eq!(class.as_deref(), Some("validation_error"));
1033                assert_data(
1034                    &data,
1035                    &[
1036                        ("value", RValue::Double(vec![1.5])),
1037                        ("code", RValue::Integer(vec![Some(7)])),
1038                        ("label", RValue::Character(vec![Some("lhs".into())])),
1039                        ("fatal", RValue::Logical(vec![Some(false)])),
1040                        ("ints", RValue::Integer(vec![Some(1), Some(2)])),
1041                        ("reals", RValue::Double(vec![0.5])),
1042                        ("flags", RValue::Logical(vec![Some(true)])),
1043                        ("labels", RValue::Character(vec![Some("a".into())])),
1044                    ],
1045                );
1046            }
1047            other => panic!("wrong variant: {other:?}"),
1048        }
1049    }
1050
1051    #[test]
1052    fn warning_with_class_and_data() {
1053        let cond = catch(|| crate::warning!(class = "trunc", data = ("dropped", 3), "dropped"));
1054        match cond {
1055            RCondition::Warning {
1056                message,
1057                class,
1058                data,
1059            } => {
1060                assert_eq!(message, "dropped");
1061                assert_eq!(class.as_deref(), Some("trunc"));
1062                assert_data(&data, &[("dropped", RValue::Integer(vec![Some(3)]))]);
1063            }
1064            other => panic!("wrong variant: {other:?}"),
1065        }
1066    }
1067
1068    #[test]
1069    fn message_with_data() {
1070        let cond = catch(|| crate::message!(data = ("step", 2), "step {}", 2));
1071        match cond {
1072            RCondition::Message { message, data } => {
1073                assert_eq!(message, "step 2");
1074                assert_data(&data, &[("step", RValue::Integer(vec![Some(2)]))]);
1075            }
1076            other => panic!("wrong variant: {other:?}"),
1077        }
1078    }
1079
1080    #[test]
1081    fn condition_with_class_and_data() {
1082        let cond =
1083            catch(|| crate::condition!(class = "progress", data = [("n", 10)], "processed {}", 10));
1084        match cond {
1085            RCondition::Condition {
1086                message,
1087                class,
1088                data,
1089            } => {
1090                assert_eq!(message, "processed 10");
1091                assert_eq!(class.as_deref(), Some("progress"));
1092                assert_data(&data, &[("n", RValue::Integer(vec![Some(10)]))]);
1093            }
1094            other => panic!("wrong variant: {other:?}"),
1095        }
1096    }
1097
1098    #[test]
1099    fn data_list_trailing_comma() {
1100        let cond = catch(|| crate::error!(data = [("a", 1), ("b", 2),], "msg"));
1101        match cond {
1102            RCondition::Error { data, .. } => {
1103                assert_data(
1104                    &data,
1105                    &[
1106                        ("a", RValue::Integer(vec![Some(1)])),
1107                        ("b", RValue::Integer(vec![Some(2)])),
1108                    ],
1109                );
1110            }
1111            other => panic!("wrong variant: {other:?}"),
1112        }
1113    }
1114
1115    // region: keyed builder sugar (ported from #1044/#995)
1116
1117    #[test]
1118    fn keyed_builder_arm_stringifies_idents() {
1119        let cond = catch(|| crate::error!(data = { value = 42, code = 7 }, "boom"));
1120        match cond {
1121            RCondition::Error { data, .. } => {
1122                assert_data(
1123                    &data,
1124                    &[
1125                        ("value", RValue::Integer(vec![Some(42)])),
1126                        ("code", RValue::Integer(vec![Some(7)])),
1127                    ],
1128                );
1129            }
1130            other => panic!("wrong variant: {other:?}"),
1131        }
1132    }
1133
1134    #[test]
1135    fn keyed_builder_arm_trailing_comma_and_mixed_types() {
1136        let cond = catch(|| {
1137            crate::warning!(
1138                class = "trunc",
1139                data = { dropped = 3, ratio = 0.5_f64, tag = "rows", },
1140                "dropped some"
1141            )
1142        });
1143        match cond {
1144            RCondition::Warning { data, class, .. } => {
1145                assert_eq!(class.as_deref(), Some("trunc"));
1146                assert_data(
1147                    &data,
1148                    &[
1149                        ("dropped", RValue::Integer(vec![Some(3)])),
1150                        ("ratio", RValue::Double(vec![0.5])),
1151                        ("tag", RValue::Character(vec![Some("rows".into())])),
1152                    ],
1153                );
1154            }
1155            other => panic!("wrong variant: {other:?}"),
1156        }
1157    }
1158
1159    // endregion
1160
1161    // region: NA-aware + wide-int + debug value types via the macro (#995)
1162
1163    #[test]
1164    fn option_scalar_fields_carry_na() {
1165        let cond = catch(|| {
1166            crate::error!(
1167                data = [("present", Some(9_i32)), ("missing", None::<i32>)],
1168                "opts"
1169            )
1170        });
1171        match cond {
1172            RCondition::Error { data, .. } => {
1173                assert_data(
1174                    &data,
1175                    &[
1176                        ("present", RValue::Integer(vec![Some(9)])),
1177                        ("missing", RValue::Integer(vec![None])),
1178                    ],
1179                );
1180            }
1181            other => panic!("wrong variant: {other:?}"),
1182        }
1183    }
1184
1185    #[test]
1186    fn vec_option_field_carries_embedded_na() {
1187        let cond =
1188            catch(|| crate::error!(data = ("codes", vec![Some(1_i32), None, Some(3)]), "vec"));
1189        match cond {
1190            RCondition::Error { data, .. } => {
1191                assert_data(
1192                    &data,
1193                    &[("codes", RValue::Integer(vec![Some(1), None, Some(3)]))],
1194                );
1195            }
1196            other => panic!("wrong variant: {other:?}"),
1197        }
1198    }
1199
1200    #[test]
1201    fn wide_integer_ladder_via_macro() {
1202        // Fits in i32 → integer; beyond → double.
1203        let cond = catch(|| {
1204            crate::error!(
1205                data = [("small", 42_i64), ("big", 5_000_000_000_i64)],
1206                "wide"
1207            )
1208        });
1209        match cond {
1210            RCondition::Error { data, .. } => {
1211                assert_data(
1212                    &data,
1213                    &[
1214                        ("small", RValue::Integer(vec![Some(42)])),
1215                        ("big", RValue::Double(vec![5_000_000_000.0])),
1216                    ],
1217                );
1218            }
1219            other => panic!("wrong variant: {other:?}"),
1220        }
1221    }
1222
1223    #[test]
1224    fn debug_fallback_via_macro() {
1225        let cond = catch(|| crate::error!(data = ("range", RValue::debug(0..=100)), "dbg"));
1226        match cond {
1227            RCondition::Error { data, .. } => {
1228                assert_data(
1229                    &data,
1230                    &[("range", RValue::Character(vec![Some("0..=100".into())]))],
1231                );
1232            }
1233            other => panic!("wrong variant: {other:?}"),
1234        }
1235    }
1236
1237    // endregion
1238}
1239
1240// endregion