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