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.
325/// Invoke via `miniextendr_api::error!(...)` (fully qualified) or via
326/// `mx::error!(...)` after `use miniextendr_api as mx;`.
327///
328/// # Examples
329///
330/// ```ignore
331/// use miniextendr_api as mx;
332///
333/// #[miniextendr]
334/// fn fail() {
335///     mx::error!("something went wrong: {}", 42);
336/// }
337///
338/// // With a custom class for tryCatch:
339/// #[miniextendr]
340/// fn typed_fail(name: &str) {
341///     mx::error!(class = "my_error", "missing field: {name}");
342/// }
343/// ```
344///
345/// ```r
346/// tryCatch(fail(), rust_error = function(e) conditionMessage(e))
347/// # [1] "something went wrong: 42"
348///
349/// tryCatch(typed_fail("x"), my_error = function(e) "caught!")
350/// # [1] "caught!"
351/// ```
352#[macro_export]
353macro_rules! error {
354    (class = $class:expr, data = $data:tt, $($arg:tt)*) => {
355        ::std::panic::panic_any($crate::condition::RCondition::Error {
356            message: ::std::format!($($arg)*),
357            class: ::std::option::Option::Some($class.to_string()),
358            data: $crate::__mx_condition_data!($data),
359        })
360    };
361    (data = $data:tt, $($arg:tt)*) => {
362        ::std::panic::panic_any($crate::condition::RCondition::Error {
363            message: ::std::format!($($arg)*),
364            class: ::std::option::Option::None,
365            data: $crate::__mx_condition_data!($data),
366        })
367    };
368    (class = $class:expr, $($arg:tt)*) => {
369        ::std::panic::panic_any($crate::condition::RCondition::Error {
370            message: ::std::format!($($arg)*),
371            class: ::std::option::Option::Some($class.to_string()),
372            data: ::std::option::Option::None,
373        })
374    };
375    ($($arg:tt)*) => {
376        ::std::panic::panic_any($crate::condition::RCondition::Error {
377            message: ::std::format!($($arg)*),
378            class: ::std::option::Option::None,
379            data: ::std::option::Option::None,
380        })
381    };
382}
383
384/// Raise an R warning from Rust with `rust_warning` class layering.
385///
386/// Rides the tagged-condition transport that every `#[miniextendr]` function uses.
387/// Unlike `panic!`, execution continues after `warning!` is caught by a handler.
388/// The raised condition has class `c("rust_warning", "simpleWarning", "warning", "condition")`.
389///
390/// An optional `class = "name"` form prepends a custom class. An optional
391/// `data = ...` form (after `class`, before the message) attaches named fields
392/// readable as `w$<name>` in handlers — same grammar and supported value types
393/// as [`crate::error!`] (see there for details):
394///
395/// ```ignore
396/// warning!(class = "truncation", data = ("dropped", n), "dropped {n} rows");
397/// ```
398///
399/// # See also
400///
401/// - [`crate::error!`] — fatal sibling; aborts the call instead of continuing.
402/// - [`crate::message!`] / [`crate::condition!`] — softer signal kinds (muffled
403///   by `suppressMessages` / silent without handler, respectively).
404/// - [`std::panic!`] — escape hatch when "continue after this" is not a sensible
405///   semantic.
406/// - [`crate::error_value`] — tagged-SEXP transport rationale.
407///
408/// No name-collision caveat: there is no `pub mod warning`, so
409/// `use miniextendr_api::warning;` then `warning!(...)` works directly.
410///
411/// # Example
412///
413/// ```ignore
414/// use miniextendr_api::warning;
415///
416/// #[miniextendr]
417/// fn maybe_warn(x: i32) -> i32 {
418///     if x > 100 {
419///         warning!("x is large: {x}");
420///     }
421///     x * 2
422/// }
423/// ```
424///
425/// ```r
426/// withCallingHandlers(
427///   maybe_warn(200L),
428///   warning = function(w) { cat("saw:", conditionMessage(w)); invokeRestart("muffleWarning") }
429/// )
430/// # saw: x is large: 200
431/// # [1] 400
432/// ```
433#[macro_export]
434macro_rules! warning {
435    (class = $class:expr, data = $data:tt, $($arg:tt)*) => {
436        ::std::panic::panic_any($crate::condition::RCondition::Warning {
437            message: ::std::format!($($arg)*),
438            class: ::std::option::Option::Some($class.to_string()),
439            data: $crate::__mx_condition_data!($data),
440        })
441    };
442    (data = $data:tt, $($arg:tt)*) => {
443        ::std::panic::panic_any($crate::condition::RCondition::Warning {
444            message: ::std::format!($($arg)*),
445            class: ::std::option::Option::None,
446            data: $crate::__mx_condition_data!($data),
447        })
448    };
449    (class = $class:expr, $($arg:tt)*) => {
450        ::std::panic::panic_any($crate::condition::RCondition::Warning {
451            message: ::std::format!($($arg)*),
452            class: ::std::option::Option::Some($class.to_string()),
453            data: ::std::option::Option::None,
454        })
455    };
456    ($($arg:tt)*) => {
457        ::std::panic::panic_any($crate::condition::RCondition::Warning {
458            message: ::std::format!($($arg)*),
459            class: ::std::option::Option::None,
460            data: ::std::option::Option::None,
461        })
462    };
463}
464
465/// Emit an R message from Rust with `rust_message` class layering.
466///
467/// Rides the tagged-condition transport that every `#[miniextendr]` function uses.
468/// The raised condition has class `c("rust_message", "simpleMessage", "message", "condition")`.
469/// Muffled by `suppressMessages()` automatically (standard R restart mechanism).
470///
471/// An optional `data = ...` form (before the message) attaches named fields
472/// readable as `m$<name>` in `withCallingHandlers` — same grammar and
473/// supported value types as [`crate::error!`] (see there for details). There
474/// is no `class =` form for `message!`.
475///
476/// # See also
477///
478/// - [`crate::warning!`] / [`crate::condition!`] — louder/quieter sibling kinds.
479/// - [`crate::error!`] — for fatal failures.
480/// - [`std::panic!`] — escape hatch.
481/// - [`crate::error_value`] — tagged-SEXP transport rationale.
482///
483/// No name-collision caveat: there is no `pub mod message`, so
484/// `use miniextendr_api::message;` then `message!(...)` works directly.
485///
486/// # Example
487///
488/// ```ignore
489/// use miniextendr_api::message;
490///
491/// #[miniextendr]
492/// fn log_step(step: i32) {
493///     message!("step {} complete", step);
494/// }
495/// ```
496///
497/// ```r
498/// log_step(3L)
499/// # step 3 complete
500///
501/// suppressMessages(log_step(3L))  # no output
502/// ```
503#[macro_export]
504macro_rules! message {
505    (data = $data:tt, $($arg:tt)*) => {
506        ::std::panic::panic_any($crate::condition::RCondition::Message {
507            message: ::std::format!($($arg)*),
508            data: $crate::__mx_condition_data!($data),
509        })
510    };
511    ($($arg:tt)*) => {
512        ::std::panic::panic_any($crate::condition::RCondition::Message {
513            message: ::std::format!($($arg)*),
514            data: ::std::option::Option::None,
515        })
516    };
517}
518
519/// Signal a generic R condition from Rust with `rust_condition` class layering.
520///
521/// Rides the tagged-condition transport that every `#[miniextendr]` function uses.
522/// Unlike `error!`, a bare condition is a silent no-op if there is no handler.
523/// The raised condition has class `c("rust_condition", "simpleCondition", "condition")`.
524///
525/// An optional `class = "name"` form prepends a custom class. An optional
526/// `data = ...` form (after `class`, before the message) attaches named fields
527/// readable as `c$<name>` in handlers — same grammar and supported value types
528/// as [`crate::error!`] (see there for details).
529///
530/// # See also
531///
532/// - [`crate::error!`] / [`crate::warning!`] / [`crate::message!`] — louder
533///   condition kinds. Pick `condition!` when "no handler = silent" is the
534///   right default (progress events, structured logging hooks).
535/// - [`std::panic!`] — escape hatch when the failure cannot be ignored.
536/// - [`crate::error_value`] — tagged-SEXP transport rationale.
537///
538/// **Name-collision note.** Because `pub mod condition` exists at the crate
539/// root, `use miniextendr_api::condition` imports the module rather than this
540/// macro. Invoke via `miniextendr_api::condition!(...)` (fully qualified) or
541/// via `mx::condition!(...)` after `use miniextendr_api as mx;`.
542///
543/// # Example
544///
545/// ```ignore
546/// use miniextendr_api::condition;
547///
548/// #[miniextendr]
549/// fn signal_progress(n: i32) {
550///     condition!(class = "my_progress", "processed {n} items");
551/// }
552/// ```
553///
554/// ```r
555/// withCallingHandlers(
556///   signal_progress(42L),
557///   my_progress = function(c) cat("progress:", conditionMessage(c), "\n")
558/// )
559/// # progress: processed 42 items
560/// ```
561#[macro_export]
562macro_rules! condition {
563    (class = $class:expr, data = $data:tt, $($arg:tt)*) => {
564        ::std::panic::panic_any($crate::condition::RCondition::Condition {
565            message: ::std::format!($($arg)*),
566            class: ::std::option::Option::Some($class.to_string()),
567            data: $crate::__mx_condition_data!($data),
568        })
569    };
570    (data = $data:tt, $($arg:tt)*) => {
571        ::std::panic::panic_any($crate::condition::RCondition::Condition {
572            message: ::std::format!($($arg)*),
573            class: ::std::option::Option::None,
574            data: $crate::__mx_condition_data!($data),
575        })
576    };
577    (class = $class:expr, $($arg:tt)*) => {
578        ::std::panic::panic_any($crate::condition::RCondition::Condition {
579            message: ::std::format!($($arg)*),
580            class: ::std::option::Option::Some($class.to_string()),
581            data: ::std::option::Option::None,
582        })
583    };
584    ($($arg:tt)*) => {
585        ::std::panic::panic_any($crate::condition::RCondition::Condition {
586            message: ::std::format!($($arg)*),
587            class: ::std::option::Option::None,
588            data: ::std::option::Option::None,
589        })
590    };
591}
592
593// endregion
594
595// region: from_tagged_sexp + repanic_if_rust_error — shim re-panic helpers
596
597impl RCondition {
598    /// Reconstruct an [`RCondition::Error`] from a tagged SEXP produced by
599    /// [`crate::error_value::make_rust_condition_value`].
600    ///
601    /// Returns `Some(RCondition)` when `sexp` has class `"rust_condition_value"` AND
602    /// the `"__rust_condition__"` attribute is `TRUE`. Returns `None` for all other
603    /// SEXPs (normal return values, `R_NilValue`, etc.).
604    ///
605    /// Reconstructs the matching variant for each kind: `"error"`/`"panic"`/
606    /// `"result_err"`/`"none_err"`/`"other_rust_error"` → [`RCondition::Error`];
607    /// `"warning"` → [`RCondition::Warning`]; `"message"` → [`RCondition::Message`];
608    /// `"condition"` → [`RCondition::Condition`]. Unknown kinds degrade to
609    /// [`RCondition::Error`] with the kind string prefixed to the message.
610    ///
611    /// # Safety
612    ///
613    /// Must be called from R's main thread.
614    pub unsafe fn from_tagged_sexp(sexp: crate::SEXP) -> Option<Self> {
615        use crate::SexpExt;
616        use crate::from_r::TryFromSexp;
617
618        // Use SexpExt::inherits_class — wraps Rf_inherits, already main-thread.
619        if !sexp.inherits_class(c"rust_condition_value") {
620            return None;
621        }
622
623        // Belt-and-suspenders PROTECT across the full inspection window. The reads
624        // below are nominally non-allocating, but R-devel's GC is aggressive enough
625        // (see MEMORY.md "Common gotchas") that a defensive guard is cheap and
626        // closes the door on subtle regressions if the read path ever changes.
627        let _guard = unsafe { crate::gc_protect::OwnedProtect::new(sexp) };
628
629        // Verify the __rust_condition__ marker attribute is TRUE (a length-1 LGLSXP
630        // with value 1). This guards against coincidental class attribute collisions.
631        let attr_sym = crate::cached_class::rust_condition_attr_symbol();
632        let marker = sexp.get_attr(attr_sym);
633        // marker should be a scalar logical TRUE: is_logical() and logical_elt(0) == 1
634        if !marker.is_logical() || marker.logical_elt(0) != 1 {
635            return None;
636        }
637
638        // It's a tagged SEXP. Read the elements.
639        // Both 3-element (legacy) and 4-element (condition) forms have:
640        //   [0] = error message (STRSXP)
641        //   [1] = kind string (STRSXP)
642        //   [2] = class name or NULL (only in 4-element form; absent in legacy)
643
644        let len = sexp.len();
645
646        // Defense-in-depth: a tagged SEXP must have at least the message and kind
647        // slots. inherits_class + __rust_condition__ marker should already imply this,
648        // but a corrupted/spoofed SEXP that satisfies both checks shouldn't OOB
649        // the vector_elt reads below.
650        if len < 2 {
651            return None;
652        }
653
654        let msg_sexp = sexp.vector_elt(0);
655        let msg: String = msg_sexp
656            .string_elt_str(0)
657            .unwrap_or("<invalid error message>")
658            .to_string();
659
660        let kind_sexp = sexp.vector_elt(1);
661        let kind: &str = kind_sexp
662            .string_elt_str(0)
663            .unwrap_or(crate::error_value::kind::PANIC);
664
665        // Class slot is element [2] in the 4-element form (NULL in legacy form)
666        let class: Option<String> = if len >= 4 {
667            let class_sexp = sexp.vector_elt(2);
668            if class_sexp.is_nil() {
669                None
670            } else {
671                class_sexp.string_elt_str(0).map(|s| s.to_string())
672            }
673        } else {
674            None
675        };
676
677        use crate::error_value::kind as kind_const;
678
679        // Slot [4] is the optional named-list condition data, present when `len >= 5`.
680        //
681        // Each field value is decoded through the single SEXP→owned-tree walker,
682        // [`RValue::try_from_sexp`], so structured fields survive the cross-package
683        // trait-ABI re-panic path (`repanic_if_rust_error`): the consumer's outer
684        // `with_r_unwind_protect` guard rebuilds the tagged SEXP from the
685        // reconstructed `RCondition`, which now carries the data — so `e$field_name`
686        // is accessible in R handlers even when the error crossed a package boundary.
687        //
688        // `RValue` is NA-aware (logical/integer/character carry `None`; double
689        // carries the `NA_REAL` bit), so NA-bearing fields now round-trip faithfully
690        // rather than being dropped. Fields whose name is missing/empty, or whose
691        // value is not R data (closures, environments, …, which `try_from_sexp`
692        // rejects) are dropped — safe degradation that preserves message/class/kind.
693        //
694        // All reads here are non-allocating copies into owned Rust values, so no new
695        // SEXPs are created and the existing `_guard` OwnedProtect suffices.
696        let data: Option<ConditionData> = if len >= 5 {
697            let data_sexp = sexp.vector_elt(4);
698            if data_sexp.is_nil() || !data_sexp.is_list() {
699                None
700            } else {
701                let data_len = data_sexp.len();
702                let names_sexp = data_sexp.get_names();
703                let mut fields: ConditionData = Vec::with_capacity(data_len);
704                for i in 0..data_len as isize {
705                    // Read the field name from the names attribute. If missing/empty, skip.
706                    let name: String = if names_sexp.is_nil() || !names_sexp.is_character() {
707                        continue;
708                    } else {
709                        match names_sexp.string_elt_str(i) {
710                            Some(s) if !s.is_empty() => s.to_string(),
711                            _ => continue,
712                        }
713                    };
714                    if let Ok(value) = crate::RValue::try_from_sexp(data_sexp.vector_elt(i)) {
715                        fields.push((name, value));
716                    }
717                }
718                if fields.is_empty() {
719                    None
720                } else {
721                    Some(fields)
722                }
723            }
724        } else {
725            None
726        };
727
728        let cond = match kind {
729            kind_const::ERROR
730            | kind_const::PANIC
731            | kind_const::RESULT_ERR
732            | kind_const::NONE_ERR
733            | kind_const::OTHER_RUST_ERROR => RCondition::Error {
734                message: msg,
735                class,
736                data,
737            },
738            kind_const::WARNING => RCondition::Warning {
739                message: msg,
740                class,
741                data,
742            },
743            kind_const::MESSAGE => RCondition::Message { message: msg, data },
744            kind_const::CONDITION => RCondition::Condition {
745                message: msg,
746                class,
747                data,
748            },
749            other => {
750                // Unknown kind — degrade to error
751                RCondition::Error {
752                    message: format!("[{other}] {msg}"),
753                    class,
754                    data,
755                }
756            }
757        };
758        Some(cond)
759    }
760}
761
762/// Inspect a SEXP returned by a trait-ABI vtable shim and, if it is a tagged
763/// error value, re-panic with the reconstructed [`RCondition`].
764///
765/// This is the "re-panic at the View boundary" step of Approach 1 from the
766/// issue-345 plan. The caller (a generated View method wrapper) does:
767///
768/// ```ignore
769/// let result = { vtable_call };
770/// ::miniextendr_api::trait_abi::repanic_if_rust_error(result);
771/// // ... convert result normally if we reach here
772/// ```
773///
774/// When `sexp` is a tagged error value:
775/// - `RCondition::Error` / `RCondition::Warning` / etc. → `panic_any!(cond)`.
776///   The outer `with_r_unwind_protect` in the consumer's C entry point will
777///   catch this and produce a tagged SEXP for the consumer's R wrapper.
778///
779/// When `sexp` is a normal value: this is a no-op.
780///
781/// # Safety
782///
783/// Must be called from R's main thread. `sexp` must be a valid (possibly
784/// tagged) SEXP.
785pub unsafe fn repanic_if_rust_error(sexp: crate::SEXP) {
786    if let Some(cond) = unsafe { RCondition::from_tagged_sexp(sexp) } {
787        std::panic::panic_any(cond);
788    }
789}
790
791// endregion
792
793// region: AsRError struct — wraps std::error::Error for Result returns
794
795/// Structured error wrapper that preserves the `std::error::Error` cause chain.
796///
797/// When displayed, formats the error message with its full source chain:
798/// ```text
799/// top-level message
800///   caused by: middle error
801///   caused by: root cause
802/// ```
803///
804/// Implements `From<E>` so it works with `?` and `.map_err(AsRError)`.
805///
806/// # Example
807///
808/// ```ignore
809/// use miniextendr_api::condition::AsRError;
810/// use std::num::ParseIntError;
811///
812/// #[miniextendr]
813/// fn parse_number(s: &str) -> Result<i32, AsRError<ParseIntError>> {
814///     s.parse::<i32>().map_err(AsRError)
815/// }
816/// ```
817pub struct AsRError<E: std::error::Error>(pub E);
818
819impl<E: std::error::Error> From<E> for AsRError<E> {
820    #[inline]
821    fn from(err: E) -> Self {
822        AsRError(err)
823    }
824}
825
826impl<E: std::error::Error> std::fmt::Display for AsRError<E> {
827    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
828        // Write the top-level message
829        write!(f, "{}", self.0)?;
830
831        // Walk the cause chain
832        let mut current: &dyn std::error::Error = &self.0;
833        while let Some(source) = current.source() {
834            write!(f, "\n  caused by: {source}")?;
835            current = source;
836        }
837
838        Ok(())
839    }
840}
841
842impl<E: std::error::Error> std::fmt::Debug for AsRError<E> {
843    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
844        write!(f, "AsRError<{}>({})", std::any::type_name::<E>(), self)
845    }
846}
847
848impl<E: std::error::Error> AsRError<E> {
849    /// Get the inner error.
850    #[inline]
851    pub fn into_inner(self) -> E {
852        self.0
853    }
854
855    /// Get the Rust type name of the wrapped error (for programmatic matching).
856    #[inline]
857    pub fn rust_type_name(&self) -> &'static str {
858        std::any::type_name::<E>()
859    }
860
861    /// Collect the full cause chain as a `Vec<String>`.
862    pub fn cause_chain(&self) -> Vec<String> {
863        let mut chain = vec![self.0.to_string()];
864        let mut current: &dyn std::error::Error = &self.0;
865        while let Some(source) = current.source() {
866            chain.push(source.to_string());
867            current = source;
868        }
869        chain
870    }
871}
872
873// endregion
874
875// region: Tests — macro grammar + payload contents (no R runtime needed)
876
877#[cfg(test)]
878mod condition_macro_tests {
879    use super::{ConditionData, RCondition};
880    use crate::RValue;
881
882    /// Catch the `panic_any(RCondition)` raised by a macro invocation and
883    /// return the payload. No R runtime needed — the macros panic before any
884    /// R API call.
885    fn catch(f: impl FnOnce() + std::panic::UnwindSafe) -> RCondition {
886        let payload = std::panic::catch_unwind(f).expect_err("macro must panic");
887        *payload
888            .downcast::<RCondition>()
889            .expect("payload must be RCondition")
890    }
891
892    fn assert_data(data: &Option<ConditionData>, expected: &[(&str, RValue)]) {
893        let data = data.as_ref().expect("data must be Some");
894        assert_eq!(data.len(), expected.len());
895        for ((name, value), (exp_name, exp_value)) in data.iter().zip(expected) {
896            assert_eq!(name, exp_name);
897            // RValue has no PartialEq (f64); compare via Debug.
898            assert_eq!(format!("{value:?}"), format!("{exp_value:?}"));
899        }
900    }
901
902    #[test]
903    fn error_message_only_backcompat() {
904        let cond = catch(|| crate::error!("plain {}", 42));
905        match cond {
906            RCondition::Error {
907                message,
908                class,
909                data,
910            } => {
911                assert_eq!(message, "plain 42");
912                assert!(class.is_none());
913                assert!(data.is_none());
914            }
915            other => panic!("wrong variant: {other:?}"),
916        }
917    }
918
919    #[test]
920    fn error_class_only_backcompat() {
921        let cond = catch(|| crate::error!(class = "my_error", "missing field: {}", "x"));
922        match cond {
923            RCondition::Error {
924                message,
925                class,
926                data,
927            } => {
928                assert_eq!(message, "missing field: x");
929                assert_eq!(class.as_deref(), Some("my_error"));
930                assert!(data.is_none());
931            }
932            other => panic!("wrong variant: {other:?}"),
933        }
934    }
935
936    #[test]
937    fn error_single_data_pair() {
938        let value = 41_i32;
939        let cond = catch(move || crate::error!(data = ("value", value), "v = {value}"));
940        match cond {
941            RCondition::Error {
942                message,
943                class,
944                data,
945            } => {
946                assert_eq!(message, "v = 41");
947                assert!(class.is_none());
948                assert_data(&data, &[("value", RValue::Integer(vec![Some(41)]))]);
949            }
950            other => panic!("wrong variant: {other:?}"),
951        }
952    }
953
954    #[test]
955    fn error_class_and_data_list_all_value_types() {
956        let cond = catch(|| {
957            crate::error!(
958                class = "validation_error",
959                data = [
960                    ("value", 1.5),
961                    ("code", 7),
962                    ("label", "lhs"),
963                    ("fatal", false),
964                    ("ints", vec![1, 2]),
965                    ("reals", vec![0.5_f64]),
966                    ("flags", vec![true]),
967                    ("labels", vec!["a".to_string()])
968                ],
969                "out of range"
970            )
971        });
972        match cond {
973            RCondition::Error {
974                message,
975                class,
976                data,
977            } => {
978                assert_eq!(message, "out of range");
979                assert_eq!(class.as_deref(), Some("validation_error"));
980                assert_data(
981                    &data,
982                    &[
983                        ("value", RValue::Double(vec![1.5])),
984                        ("code", RValue::Integer(vec![Some(7)])),
985                        ("label", RValue::Character(vec![Some("lhs".into())])),
986                        ("fatal", RValue::Logical(vec![Some(false)])),
987                        ("ints", RValue::Integer(vec![Some(1), Some(2)])),
988                        ("reals", RValue::Double(vec![0.5])),
989                        ("flags", RValue::Logical(vec![Some(true)])),
990                        ("labels", RValue::Character(vec![Some("a".into())])),
991                    ],
992                );
993            }
994            other => panic!("wrong variant: {other:?}"),
995        }
996    }
997
998    #[test]
999    fn warning_with_class_and_data() {
1000        let cond = catch(|| crate::warning!(class = "trunc", data = ("dropped", 3), "dropped"));
1001        match cond {
1002            RCondition::Warning {
1003                message,
1004                class,
1005                data,
1006            } => {
1007                assert_eq!(message, "dropped");
1008                assert_eq!(class.as_deref(), Some("trunc"));
1009                assert_data(&data, &[("dropped", RValue::Integer(vec![Some(3)]))]);
1010            }
1011            other => panic!("wrong variant: {other:?}"),
1012        }
1013    }
1014
1015    #[test]
1016    fn message_with_data() {
1017        let cond = catch(|| crate::message!(data = ("step", 2), "step {}", 2));
1018        match cond {
1019            RCondition::Message { message, data } => {
1020                assert_eq!(message, "step 2");
1021                assert_data(&data, &[("step", RValue::Integer(vec![Some(2)]))]);
1022            }
1023            other => panic!("wrong variant: {other:?}"),
1024        }
1025    }
1026
1027    #[test]
1028    fn condition_with_class_and_data() {
1029        let cond =
1030            catch(|| crate::condition!(class = "progress", data = [("n", 10)], "processed {}", 10));
1031        match cond {
1032            RCondition::Condition {
1033                message,
1034                class,
1035                data,
1036            } => {
1037                assert_eq!(message, "processed 10");
1038                assert_eq!(class.as_deref(), Some("progress"));
1039                assert_data(&data, &[("n", RValue::Integer(vec![Some(10)]))]);
1040            }
1041            other => panic!("wrong variant: {other:?}"),
1042        }
1043    }
1044
1045    #[test]
1046    fn data_list_trailing_comma() {
1047        let cond = catch(|| crate::error!(data = [("a", 1), ("b", 2),], "msg"));
1048        match cond {
1049            RCondition::Error { data, .. } => {
1050                assert_data(
1051                    &data,
1052                    &[
1053                        ("a", RValue::Integer(vec![Some(1)])),
1054                        ("b", RValue::Integer(vec![Some(2)])),
1055                    ],
1056                );
1057            }
1058            other => panic!("wrong variant: {other:?}"),
1059        }
1060    }
1061
1062    // region: keyed builder sugar (ported from #1044/#995)
1063
1064    #[test]
1065    fn keyed_builder_arm_stringifies_idents() {
1066        let cond = catch(|| crate::error!(data = { value = 42, code = 7 }, "boom"));
1067        match cond {
1068            RCondition::Error { data, .. } => {
1069                assert_data(
1070                    &data,
1071                    &[
1072                        ("value", RValue::Integer(vec![Some(42)])),
1073                        ("code", RValue::Integer(vec![Some(7)])),
1074                    ],
1075                );
1076            }
1077            other => panic!("wrong variant: {other:?}"),
1078        }
1079    }
1080
1081    #[test]
1082    fn keyed_builder_arm_trailing_comma_and_mixed_types() {
1083        let cond = catch(|| {
1084            crate::warning!(
1085                class = "trunc",
1086                data = { dropped = 3, ratio = 0.5_f64, tag = "rows", },
1087                "dropped some"
1088            )
1089        });
1090        match cond {
1091            RCondition::Warning { data, class, .. } => {
1092                assert_eq!(class.as_deref(), Some("trunc"));
1093                assert_data(
1094                    &data,
1095                    &[
1096                        ("dropped", RValue::Integer(vec![Some(3)])),
1097                        ("ratio", RValue::Double(vec![0.5])),
1098                        ("tag", RValue::Character(vec![Some("rows".into())])),
1099                    ],
1100                );
1101            }
1102            other => panic!("wrong variant: {other:?}"),
1103        }
1104    }
1105
1106    // endregion
1107
1108    // region: NA-aware + wide-int + debug value types via the macro (#995)
1109
1110    #[test]
1111    fn option_scalar_fields_carry_na() {
1112        let cond = catch(|| {
1113            crate::error!(
1114                data = [("present", Some(9_i32)), ("missing", None::<i32>)],
1115                "opts"
1116            )
1117        });
1118        match cond {
1119            RCondition::Error { data, .. } => {
1120                assert_data(
1121                    &data,
1122                    &[
1123                        ("present", RValue::Integer(vec![Some(9)])),
1124                        ("missing", RValue::Integer(vec![None])),
1125                    ],
1126                );
1127            }
1128            other => panic!("wrong variant: {other:?}"),
1129        }
1130    }
1131
1132    #[test]
1133    fn vec_option_field_carries_embedded_na() {
1134        let cond =
1135            catch(|| crate::error!(data = ("codes", vec![Some(1_i32), None, Some(3)]), "vec"));
1136        match cond {
1137            RCondition::Error { data, .. } => {
1138                assert_data(
1139                    &data,
1140                    &[("codes", RValue::Integer(vec![Some(1), None, Some(3)]))],
1141                );
1142            }
1143            other => panic!("wrong variant: {other:?}"),
1144        }
1145    }
1146
1147    #[test]
1148    fn wide_integer_ladder_via_macro() {
1149        // Fits in i32 → integer; beyond → double.
1150        let cond = catch(|| {
1151            crate::error!(
1152                data = [("small", 42_i64), ("big", 5_000_000_000_i64)],
1153                "wide"
1154            )
1155        });
1156        match cond {
1157            RCondition::Error { data, .. } => {
1158                assert_data(
1159                    &data,
1160                    &[
1161                        ("small", RValue::Integer(vec![Some(42)])),
1162                        ("big", RValue::Double(vec![5_000_000_000.0])),
1163                    ],
1164                );
1165            }
1166            other => panic!("wrong variant: {other:?}"),
1167        }
1168    }
1169
1170    #[test]
1171    fn debug_fallback_via_macro() {
1172        let cond = catch(|| crate::error!(data = ("range", RValue::debug(0..=100)), "dbg"));
1173        match cond {
1174            RCondition::Error { data, .. } => {
1175                assert_data(
1176                    &data,
1177                    &[("range", RValue::Character(vec![Some("0..=100".into())]))],
1178                );
1179            }
1180            other => panic!("wrong variant: {other:?}"),
1181        }
1182    }
1183
1184    // endregion
1185}
1186
1187// endregion