miniextendr_api/into_r/result.rs
1//! `Result<T, E>` conversions to R.
2//!
3//! Used by `#[miniextendr(unwrap_in_r)]` to pass Results to R as values
4//! (`Err` becomes `list(error = ...)`), instead of unwrapping in Rust.
5//! Also provides `NullOnErr` for `Result<T, ()>` → NULL-on-error semantics.
6//!
7//! # Tradeoff
8//!
9//! The two impls in this module have different gating conditions:
10//!
11//! - `IntoR for Result<T, E: Display>` (the `list(error = ...)` shape) fires
12//! **only** under `#[miniextendr(unwrap_in_r)]` (Result-as-value). Without
13//! that attribute, a `Result<T, E>` return acts as an **error boundary**:
14//! `Err(e)` panics with `Debug`-formatted `e`, the framework's
15//! tagged-condition transport carries it to R, and the wrapper raises a
16//! classed `rust_*` condition. Failure mode of opting into `unwrap_in_r`
17//! without telling R callers: they suddenly need to check `$error` on every
18//! return value instead of using `tryCatch`.
19//! - `IntoR for Result<T, NullOnErr>` fires whenever the error type is `()`,
20//! **independent of `unwrap_in_r`**: the macro rewrites `Result<T, ()>` to
21//! `Result<T, NullOnErr>` before the `unwrap_in_r` check, so `Err(())`
22//! returns `NULL` to R in both modes. A unit error is a deliberate
23//! "no value" sentinel, not a Rust failure — it never raises an R error.
24
25use std::collections::HashMap;
26
27use crate::into_r::IntoR;
28
29/// Convert `Result<T, E>` to R (value-style, for `#[miniextendr(unwrap_in_r)]`).
30///
31/// # Behavior
32///
33/// - `Ok(value)` → returns the converted value directly
34/// - `Err(msg)` → returns `list(error = "<msg>")` (value-style error)
35///
36/// # When This Is Used
37///
38/// This impl is **only used** when `#[miniextendr(unwrap_in_r)]` is specified.
39/// Without that attribute, `#[miniextendr]` functions returning `Result<T, E>`
40/// will unwrap in Rust and raise an R error on `Err` (error boundary semantics).
41///
42/// # Error Handling Summary
43///
44/// | Mode | On `Err(e)` | Bound Required |
45/// |------|-------------|----------------|
46/// | Default | R error via panic | `E: Debug` |
47/// | `unwrap_in_r` | `list(error = ...)` | `E: Display` |
48///
49/// **Default** (without `unwrap_in_r`): `Result<T, E>` acts as an error boundary:
50/// - `Ok(v)` → `v` converted to R
51/// - `Err(e)` → R error with Debug-formatted message (requires `E: Debug`)
52///
53/// **With `unwrap_in_r`**: `Result<T, E>` is passed through to R:
54/// - `Ok(v)` → `v` converted to R
55/// - `Err(e)` → `list(error = e.to_string())` (requires `E: Display`)
56///
57/// **Exception**: `Result<T, ()>` never reaches this impl in either mode —
58/// the macro rewrites it to `Result<T, NullOnErr>` before the `unwrap_in_r`
59/// check, and `Err(())` returns `NULL`. See [`NullOnErr`].
60///
61/// # Example
62///
63/// ```ignore
64/// // Default: error boundary - Err becomes R stop()
65/// #[miniextendr]
66/// fn divide(x: f64, y: f64) -> Result<f64, String> {
67/// if y == 0.0 { Err("division by zero".into()) }
68/// else { Ok(x / y) }
69/// }
70/// // In R: tryCatch(divide(1, 0), error = ...) catches the error
71///
72/// // Value-style: Err becomes list(error = ...)
73/// #[miniextendr(unwrap_in_r)]
74/// fn divide_safe(x: f64, y: f64) -> Result<f64, String> {
75/// if y == 0.0 { Err("division by zero".into()) }
76/// else { Ok(x / y) }
77/// }
78/// // In R: result <- divide_safe(1, 0)
79/// // if (!is.null(result$error)) { handle error }
80/// ```
81impl<T, E> IntoR for Result<T, E>
82where
83 T: IntoR,
84 E: std::fmt::Display,
85{
86 type Error = std::convert::Infallible;
87 fn try_into_sexp(self) -> Result<crate::SEXP, Self::Error> {
88 Ok(self.into_sexp())
89 }
90 unsafe fn try_into_sexp_unchecked(self) -> Result<crate::SEXP, Self::Error> {
91 self.try_into_sexp()
92 }
93 fn into_sexp(self) -> crate::SEXP {
94 match self {
95 Ok(value) => value.into_sexp(),
96 Err(msg) => {
97 // Create list(error = msg) for R-side error handling
98 let mut map = HashMap::with_capacity(1);
99 map.insert("error".to_string(), msg.to_string());
100 map.into_sexp()
101 }
102 }
103 }
104}
105
106/// Marker type for `Result<T, ()>` that converts `Err(())` to NULL.
107///
108/// This type is used internally by the `#[miniextendr]` macro when handling
109/// `Result<T, ()>` return types. When the error type is `()`, there's no
110/// error message to report, so we return NULL instead of raising an error.
111///
112/// # Usage
113///
114/// You typically don't use this directly. When you write:
115///
116/// ```ignore
117/// #[miniextendr]
118/// fn maybe_value(x: i32) -> Result<i32, ()> {
119/// if x > 0 { Ok(x) } else { Err(()) }
120/// }
121/// ```
122///
123/// The macro generates code that converts `Err(())` to `Err(NullOnErr)` and
124/// returns `NULL` in R. This rewrite fires whenever the error type is `()`,
125/// **whether or not** `#[miniextendr(unwrap_in_r)]` is set — the unit-error
126/// check runs before the `unwrap_in_r` check.
127///
128/// # Note
129///
130/// `NullOnErr` intentionally does NOT implement `Display` to avoid conflicting
131/// with the generic `IntoR for Result<T, E: Display>` impl. It has its own
132/// specialized `IntoR` impl that returns NULL on error.
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub struct NullOnErr;
135
136/// Convert `Result<T, NullOnErr>` to R, returning NULL on error.
137///
138/// This is a special case for `Result<T, ()>` types where the error
139/// carries no information. Instead of raising an R error, we return NULL.
140/// Fires whenever the error type is `()`, independent of
141/// `#[miniextendr(unwrap_in_r)]`.
142impl<T: IntoR> IntoR for Result<T, NullOnErr> {
143 type Error = std::convert::Infallible;
144 fn try_into_sexp(self) -> Result<crate::SEXP, Self::Error> {
145 Ok(self.into_sexp())
146 }
147 unsafe fn try_into_sexp_unchecked(self) -> Result<crate::SEXP, Self::Error> {
148 self.try_into_sexp()
149 }
150 fn into_sexp(self) -> crate::SEXP {
151 match self {
152 Ok(value) => value.into_sexp(),
153 Err(NullOnErr) => crate::SEXP::nil(),
154 }
155 }
156}