Skip to main content

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}