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