Skip to main content

miniextendr_api/
from_r.rs

1#![allow(rustdoc::private_intra_doc_links)]
2//! Conversions from R SEXP to Rust types.
3//!
4//! This module provides [`TryFromSexp`] implementations for converting R values to Rust types:
5//!
6//! | R Type | Rust Type | Access Method |
7//! |--------|-----------|---------------|
8//! | INTSXP | `i32`, `&[i32]` | `INTEGER()` / `DATAPTR_RO` |
9//! | REALSXP | `f64`, `&[f64]` | `REAL()` / `DATAPTR_RO` |
10//! | LGLSXP | `RLogical`, `&[RLogical]` | `LOGICAL()` / `DATAPTR_RO` |
11//! | RAWSXP | `u8`, `&[u8]` | `RAW()` / `DATAPTR_RO` |
12//! | CPLXSXP | `Rcomplex` | `COMPLEX()` / `DATAPTR_RO` |
13//! | STRSXP | `&str`, `String` | `STRING_ELT()` + `R_CHAR()` (UTF-8 locale asserted at init) |
14//!
15//! # Submodules
16//!
17//! | Module | Contents |
18//! |--------|----------|
19//! | [`logical`] | `Rboolean`.string_elt(`bool`, `Option<bool>` |
20//! | [`coerced_scalars`] | Multi-source numeric scalars (`i8`..`usize`) + large integers (`i64`, `u64`) |
21//! | [`references`] | Borrowed views: `&T`, `&mut T`, `&[T]`, `Vec<&T>` |
22//! | [`strings`] | `&str`, `String`, `char` from STRSXP |
23//! | [`na_vectors`] | `Vec<Option<T>>`, `Box<[Option<T>]>` with NA awareness |
24//! | [`collections`] | `HashMap`, `BTreeMap`, `HashSet`, `BTreeSet` |
25//! | [`cow_and_paths`] | `Cow<[T]>`, `PathBuf`, `OsString`, string sets |
26//!
27//! # Choosing the right inbound conversion
28//!
29//! [`TryFromSexp`] is the strict inbound path: it returns `Result<T, SexpError>`
30//! and rejects mismatched [`SEXPTYPE`]s outright (no silent coercion). When you
31//! need to *accept* arguments coming from multiple R native types, reach for
32//! the [`crate::coerce::Coerce`] / [`crate::coerce::TryCoerce`] traits instead
33//! — those are the looser inbound path and the entry point for the multi-source
34//! scalars handled in [`coerced_scalars`].
35//!
36//! The strict-vs-lax pairing for *outbound* conversion lives on
37//! [`crate::into_r::IntoR`] (lax, default) vs [`crate::strict`] (`#[miniextendr(strict)]`).
38//! There is intentionally no `TryFromSexpStrict` trait — inbound is already
39//! strict-by-default because it returns `Result`.
40//!
41//! # Thread Safety
42//!
43//! The trait provides two methods:
44//! - [`TryFromSexp::try_from_sexp`] - checked version with debug thread assertions
45//! - [`TryFromSexp::try_from_sexp_unchecked`] - unchecked version for performance-critical paths
46//!
47//! Use `try_from_sexp_unchecked` when you're certain you're on the main thread:
48//! - Inside ALTREP callbacks
49//! - Inside standalone `#[miniextendr]` functions (they run on the main thread)
50//! - Inside `extern "C-unwind"` functions called directly by R
51
52use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
53
54use crate::altrep_traits::NA_REAL;
55use crate::coerce::TryCoerce;
56use crate::{RLogical, SEXP, SEXPTYPE, SexpExt};
57
58/// Check if an f64 value is R's NA_real_ (a specific NaN bit pattern).
59///
60/// This is different from `f64::is_nan()` which returns true for ALL NaN values.
61/// R's `NA_real_` is a specific NaN with a particular bit pattern, while regular
62/// NaN values (e.g., from `0.0/0.0`) should be preserved as valid values.
63#[inline]
64pub(crate) fn is_na_real(value: f64) -> bool {
65    value.to_bits() == NA_REAL.to_bits()
66}
67
68// region: CHARSXP to string conversion
69
70/// Convert CHARSXP to `&str` — zero-copy from R's string data.
71///
72/// Uses `R_CHAR` + `LENGTH` (O(1), no strlen). UTF-8 validity is guaranteed
73/// by `miniextendr_assert_utf8_locale()` at package init, so no per-string
74/// validation is needed.
75///
76/// # Safety
77///
78/// - `charsxp` must be a valid CHARSXP (not NA_STRING, not null).
79/// - The returned `&str` is only valid as long as R doesn't GC the CHARSXP.
80#[inline]
81pub(crate) unsafe fn charsxp_to_str(charsxp: SEXP) -> &'static str {
82    unsafe { charsxp_to_str_impl(charsxp.r_char(), charsxp) }
83}
84
85/// Unchecked version of [`charsxp_to_str`] (skips R thread checks on `R_CHAR`).
86#[inline]
87pub(crate) unsafe fn charsxp_to_str_unchecked(charsxp: SEXP) -> &'static str {
88    unsafe { charsxp_to_str_impl(charsxp.r_char_unchecked(), charsxp) }
89}
90
91/// Shared implementation: given a data pointer and CHARSXP, produce `&str`.
92///
93/// UTF-8 locale is asserted at init — `from_utf8_unchecked` is safe.
94#[inline]
95unsafe fn charsxp_to_str_impl(ptr: *const std::os::raw::c_char, charsxp: SEXP) -> &'static str {
96    unsafe {
97        let len: usize = charsxp.len();
98        let bytes = r_slice(ptr.cast::<u8>(), len);
99        // SAFETY: miniextendr_assert_utf8_locale() at init guarantees all
100        // CHARSXPs in this session are valid UTF-8 or ASCII.
101        debug_assert!(
102            std::str::from_utf8(bytes).is_ok(),
103            "CHARSXP contains non-UTF-8 bytes (locale assertion may have been skipped)"
104        );
105        std::str::from_utf8_unchecked(bytes)
106    }
107}
108
109/// `charsxp_to_cow` is now just an alias — all CHARSXPs are UTF-8 (asserted
110/// at init), so there's no non-UTF-8 fallback path. Returns `Cow::Borrowed`.
111#[inline]
112pub(crate) unsafe fn charsxp_to_cow(charsxp: SEXP) -> std::borrow::Cow<'static, str> {
113    std::borrow::Cow::Borrowed(unsafe { charsxp_to_str(charsxp) })
114}
115
116/// Convert CHARSXP to an owned, lossy `String`.
117///
118/// NA/null-defensive: returns `None` for `NA_character_`, `R_NilValue`, or a
119/// null SEXP. Non-UTF-8 bytes are replaced (`CStr::to_string_lossy`) rather
120/// than rejected. Unlike [`charsxp_to_str`] (the UTF-8-asserted hot path for
121/// package-internal CHARSXPs), this is for defensive reads of *arbitrary* R
122/// objects — S4 class attributes, `geterrmessage()` output, vctrs field
123/// names, `tzone` attributes — where the CHARSXP's origin and encoding
124/// aren't guaranteed.
125///
126/// # Safety
127///
128/// `charsxp` must be a valid SEXP. It may be `R_NilValue` or a null SEXP
129/// (both map to `None`); if it is neither of those and not `NA_character_`,
130/// it must actually be a CHARSXP.
131#[inline]
132pub(crate) unsafe fn charsxp_to_string_lossy(charsxp: SEXP) -> Option<String> {
133    if charsxp.is_null_or_nil() || charsxp.is_na_string() {
134        return None;
135    }
136    let ptr = charsxp.r_char();
137    if ptr.is_null() {
138        return None;
139    }
140    Some(
141        unsafe { std::ffi::CStr::from_ptr(ptr) }
142            .to_string_lossy()
143            .into_owned(),
144    )
145}
146
147/// Create a slice from an R data pointer, handling the zero-length case.
148///
149/// R returns a sentinel pointer (`0x1`) instead of null for empty vectors
150/// (e.g., `LOGICAL(integer(0))` → `0x1`). Rust 1.93+ validates pointer
151/// alignment in `slice::from_raw_parts` even for `len == 0`, so passing
152/// R's sentinel directly causes a precondition-check abort.
153///
154/// This helper returns an empty slice for `len == 0` without touching the pointer.
155///
156/// # Safety
157///
158/// If `len > 0`, `ptr` must satisfy the requirements of [`std::slice::from_raw_parts`].
159#[inline(always)]
160pub(crate) unsafe fn r_slice<'a, T>(ptr: *const T, len: usize) -> &'a [T] {
161    if len == 0 {
162        &[]
163    } else {
164        unsafe { std::slice::from_raw_parts(ptr, len) }
165    }
166}
167
168/// Mutable version of [`r_slice`] for `from_raw_parts_mut`.
169///
170/// # Safety
171///
172/// If `len > 0`, `ptr` must satisfy the requirements of [`std::slice::from_raw_parts_mut`].
173#[inline(always)]
174pub(crate) unsafe fn r_slice_mut<'a, T>(ptr: *mut T, len: usize) -> &'a mut [T] {
175    if len == 0 {
176        &mut []
177    } else {
178        unsafe { std::slice::from_raw_parts_mut(ptr, len) }
179    }
180}
181
182#[derive(Debug, Clone, Copy)]
183/// Error describing an unexpected R `SEXPTYPE`.
184pub struct SexpTypeError {
185    /// Expected R type.
186    pub expected: SEXPTYPE,
187    /// Actual R type encountered.
188    pub actual: SEXPTYPE,
189}
190
191impl std::fmt::Display for SexpTypeError {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        write!(
194            f,
195            "type mismatch: expected {:?}, got {:?}",
196            self.expected, self.actual
197        )
198    }
199}
200
201impl std::error::Error for SexpTypeError {}
202
203#[derive(Debug, Clone, Copy)]
204/// Error describing an unexpected R object length.
205pub struct SexpLengthError {
206    /// Required length.
207    pub expected: usize,
208    /// Actual length encountered.
209    pub actual: usize,
210}
211
212impl std::fmt::Display for SexpLengthError {
213    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214        write!(
215            f,
216            "length mismatch: expected {}, got {}",
217            self.expected, self.actual
218        )
219    }
220}
221
222impl std::error::Error for SexpLengthError {}
223
224#[derive(Debug, Clone, Copy)]
225/// Error for NA values in conversions that require non-missing values.
226pub struct SexpNaError {
227    /// R type where an NA was found.
228    pub sexp_type: SEXPTYPE,
229}
230
231impl std::fmt::Display for SexpNaError {
232    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233        write!(f, "unexpected NA value in {:?}", self.sexp_type)
234    }
235}
236
237impl std::error::Error for SexpNaError {}
238
239#[derive(Debug, Clone)]
240/// Unified conversion error when decoding an R `SEXP`.
241pub enum SexpError {
242    /// `SEXPTYPE` did not match the expected one.
243    Type(SexpTypeError),
244    /// Length did not match the expected one.
245    Length(SexpLengthError),
246    /// Missing value encountered where disallowed.
247    Na(SexpNaError),
248    /// Value is syntactically valid but semantically invalid (e.g. parse error).
249    InvalidValue(String),
250    /// A required field was missing from a named list.
251    MissingField(String),
252    /// A named list has duplicate non-empty names.
253    DuplicateName(String),
254    /// Failed to convert to `Either<L, R>` - both branches failed.
255    ///
256    /// Contains the error messages from attempting both conversions.
257    #[cfg(feature = "either")]
258    EitherConversion {
259        /// Error from attempting to convert to the Left type
260        left_error: String,
261        /// Error from attempting to convert to the Right type
262        right_error: String,
263    },
264}
265
266impl std::fmt::Display for SexpError {
267    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268        match self {
269            SexpError::Type(e) => write!(f, "{}", e),
270            SexpError::Length(e) => write!(f, "{}", e),
271            SexpError::Na(e) => write!(f, "{}", e),
272            SexpError::InvalidValue(msg) => write!(f, "invalid value: {}", msg),
273            SexpError::MissingField(name) => write!(f, "missing field: {}", name),
274            SexpError::DuplicateName(name) => write!(f, "duplicate name in list: {:?}", name),
275            #[cfg(feature = "either")]
276            SexpError::EitherConversion {
277                left_error,
278                right_error,
279            } => write!(
280                f,
281                "failed to convert to Either: Left failed ({}), Right failed ({})",
282                left_error, right_error
283            ),
284        }
285    }
286}
287
288impl std::error::Error for SexpError {
289    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
290        match self {
291            SexpError::Type(e) => Some(e),
292            SexpError::Length(e) => Some(e),
293            SexpError::Na(e) => Some(e),
294            SexpError::InvalidValue(_) => None,
295            SexpError::MissingField(_) => None,
296            SexpError::DuplicateName(_) => None,
297            #[cfg(feature = "either")]
298            SexpError::EitherConversion { .. } => None,
299        }
300    }
301}
302
303impl From<SexpTypeError> for SexpError {
304    fn from(e: SexpTypeError) -> Self {
305        SexpError::Type(e)
306    }
307}
308
309impl From<SexpLengthError> for SexpError {
310    fn from(e: SexpLengthError) -> Self {
311        SexpError::Length(e)
312    }
313}
314
315impl From<SexpNaError> for SexpError {
316    fn from(e: SexpNaError) -> Self {
317        SexpError::Na(e)
318    }
319}
320
321/// TryFrom-style trait for converting SEXP to Rust types.
322///
323/// Inbound counterpart of [`crate::into_r::IntoR`]. Strict by construction
324/// (returns `Result`) — for a looser, multi-source coercion path use
325/// [`crate::coerce::Coerce`] / [`crate::coerce::TryCoerce`].
326///
327/// # Examples
328///
329/// ```no_run
330/// use miniextendr_api::SEXP;
331/// use miniextendr_api::from_r::TryFromSexp;
332///
333/// fn example(sexp: SEXP) {
334///     let value: i32 = TryFromSexp::try_from_sexp(sexp).unwrap();
335///     let text: String = TryFromSexp::try_from_sexp(sexp).unwrap();
336/// }
337/// ```
338pub trait TryFromSexp: Sized {
339    /// The error type returned when conversion fails.
340    type Error;
341
342    /// Attempt to convert an R SEXP to this Rust type.
343    ///
344    /// In debug builds, may assert that we're on R's main thread.
345    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error>;
346
347    /// Convert from SEXP without thread safety checks.
348    ///
349    /// # Safety
350    ///
351    /// Must be called from R's main thread. In debug builds, this still
352    /// calls the checked version by default, but implementations may
353    /// skip thread assertions for performance.
354    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
355        // Default: just call the checked version
356        Self::try_from_sexp(sexp)
357    }
358}
359
360// region: Box<[T]> delegates to Vec<T>
361//
362// A boxed slice converts exactly like the owned vector — read the vector, then
363// `into_boxed_slice()` (an O(1), allocation-free shrink). This single blanket
364// replaces every hand-rolled / macro-generated `Box<[X]>` impl: any element
365// type whose `Vec<X>` is convertible gets `Box<[X]>` for free, inheriting the
366// vector impl's error type and NA semantics by construction.
367
368impl<T> TryFromSexp for Box<[T]>
369where
370    Vec<T>: TryFromSexp,
371{
372    type Error = <Vec<T> as TryFromSexp>::Error;
373
374    #[inline]
375    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
376        <Vec<T> as TryFromSexp>::try_from_sexp(sexp).map(|v| v.into_boxed_slice())
377    }
378
379    #[inline]
380    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
381        unsafe { <Vec<T> as TryFromSexp>::try_from_sexp_unchecked(sexp) }
382            .map(|v| v.into_boxed_slice())
383    }
384}
385// endregion
386
387macro_rules! impl_try_from_sexp_scalar_native {
388    ($t:ty, $sexptype:ident) => {
389        impl TryFromSexp for $t {
390            type Error = SexpError;
391
392            #[inline]
393            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
394                let actual = sexp.type_of();
395                if actual != SEXPTYPE::$sexptype {
396                    return Err(SexpTypeError {
397                        expected: SEXPTYPE::$sexptype,
398                        actual,
399                    }
400                    .into());
401                }
402                let len = sexp.len();
403                if len != 1 {
404                    return Err(SexpLengthError {
405                        expected: 1,
406                        actual: len,
407                    }
408                    .into());
409                }
410                unsafe { sexp.as_slice::<$t>() }
411                    .first()
412                    .cloned()
413                    .ok_or_else(|| {
414                        SexpLengthError {
415                            expected: 1,
416                            actual: 0,
417                        }
418                        .into()
419                    })
420            }
421
422            #[inline]
423            unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
424                let actual = sexp.type_of();
425                if actual != SEXPTYPE::$sexptype {
426                    return Err(SexpTypeError {
427                        expected: SEXPTYPE::$sexptype,
428                        actual,
429                    }
430                    .into());
431                }
432                let len = unsafe { sexp.len_unchecked() };
433                if len != 1 {
434                    return Err(SexpLengthError {
435                        expected: 1,
436                        actual: len,
437                    }
438                    .into());
439                }
440                unsafe { sexp.as_slice_unchecked::<$t>() }
441                    .first()
442                    .cloned()
443                    .ok_or_else(|| {
444                        SexpLengthError {
445                            expected: 1,
446                            actual: 0,
447                        }
448                        .into()
449                    })
450            }
451        }
452    };
453}
454
455// i32 has a bespoke impl that checks for NA_integer_ (i32::MIN).
456// The shared macro is NOT used for i32 — it would silently pass NA through.
457impl TryFromSexp for i32 {
458    type Error = SexpError;
459
460    #[inline]
461    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
462        let actual = sexp.type_of();
463        if actual != SEXPTYPE::INTSXP {
464            return Err(SexpTypeError {
465                expected: SEXPTYPE::INTSXP,
466                actual,
467            }
468            .into());
469        }
470        let len = sexp.len();
471        if len != 1 {
472            return Err(SexpLengthError {
473                expected: 1,
474                actual: len,
475            }
476            .into());
477        }
478        let v = unsafe { sexp.as_slice::<i32>() }
479            .first()
480            .cloned()
481            .ok_or_else(|| {
482                SexpError::from(SexpLengthError {
483                    expected: 1,
484                    actual: 0,
485                })
486            })?;
487        if v == crate::altrep_traits::NA_INTEGER {
488            return Err(SexpNaError {
489                sexp_type: SEXPTYPE::INTSXP,
490            }
491            .into());
492        }
493        Ok(v)
494    }
495
496    #[inline]
497    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
498        let actual = sexp.type_of();
499        if actual != SEXPTYPE::INTSXP {
500            return Err(SexpTypeError {
501                expected: SEXPTYPE::INTSXP,
502                actual,
503            }
504            .into());
505        }
506        let len = unsafe { sexp.len_unchecked() };
507        if len != 1 {
508            return Err(SexpLengthError {
509                expected: 1,
510                actual: len,
511            }
512            .into());
513        }
514        let v = unsafe { sexp.as_slice_unchecked::<i32>() }
515            .first()
516            .cloned()
517            .ok_or_else(|| {
518                SexpError::from(SexpLengthError {
519                    expected: 1,
520                    actual: 0,
521                })
522            })?;
523        if v == crate::altrep_traits::NA_INTEGER {
524            return Err(SexpNaError {
525                sexp_type: SEXPTYPE::INTSXP,
526            }
527            .into());
528        }
529        Ok(v)
530    }
531}
532
533impl_try_from_sexp_scalar_native!(f64, REALSXP);
534impl_try_from_sexp_scalar_native!(u8, RAWSXP);
535impl_try_from_sexp_scalar_native!(RLogical, LGLSXP);
536impl_try_from_sexp_scalar_native!(crate::Rcomplex, CPLXSXP);
537
538/// Pass-through conversion for raw SEXP values with ALTREP auto-materialization.
539///
540/// This allows `SEXP` to be used directly in `#[miniextendr]` function signatures.
541/// When R passes an ALTREP vector (e.g., `1:10`, `seq_len(N)`),
542/// [`ensure_materialized`](crate::altrep_sexp::ensure_materialized) is called
543/// automatically to force materialization on the R main thread. After this,
544/// the SEXP's data pointer is stable and safe to access from any thread.
545///
546/// # ALTREP handling
547///
548/// | Input | Result |
549/// |---|---|
550/// | Regular SEXP | Passed through unchanged |
551/// | ALTREP SEXP | Materialized via `ensure_materialized`, then passed through |
552///
553/// To receive ALTREP without materializing, use
554/// [`AltrepSexp`](crate::altrep_sexp::AltrepSexp) as the parameter type instead.
555/// To receive the raw SEXP without any conversion (including no materialization),
556/// use `extern "C-unwind"`.
557///
558/// # Safety
559///
560/// SEXP handles are only valid on R's main thread. Standalone `#[miniextendr]`
561/// functions taking a `SEXP` parameter run on the main thread automatically.
562impl TryFromSexp for SEXP {
563    type Error = SexpError;
564
565    /// Converts a SEXP, auto-materializing ALTREP vectors.
566    ///
567    /// If the input is ALTREP, [`ensure_materialized`](crate::altrep_sexp::ensure_materialized)
568    /// is called to force materialization on the R main thread. After
569    /// materialization the data pointer is stable and the SEXP can be safely
570    /// sent to other threads.
571    #[inline]
572    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
573        Ok(unsafe { crate::altrep_sexp::ensure_materialized(sexp) })
574    }
575
576    #[inline]
577    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
578        Ok(unsafe { crate::altrep_sexp::ensure_materialized(sexp) })
579    }
580}
581
582impl TryFromSexp for Option<SEXP> {
583    type Error = SexpError;
584
585    #[inline]
586    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
587        if sexp.type_of() == SEXPTYPE::NILSXP {
588            Ok(None)
589        } else {
590            Ok(Some(unsafe {
591                crate::altrep_sexp::ensure_materialized(sexp)
592            }))
593        }
594    }
595
596    #[inline]
597    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
598        Self::try_from_sexp(sexp)
599    }
600}
601// endregion
602
603mod logical;
604
605mod coerced_scalars;
606pub(crate) use coerced_scalars::coerce_value;
607
608mod references;
609
610// region: Blanket implementations for slices with arbitrary lifetimes
611
612/// Blanket impl for `&[T]` where T: RNativeType
613///
614/// This replaces the macro-generated `&'static [T]` impls with a more composable
615/// blanket impl that works for any lifetime. This enables containers like TinyVec
616/// to use blanket impls without needing helper functions.
617impl<T> TryFromSexp for &[T]
618where
619    T: crate::RNativeType + Copy,
620{
621    type Error = SexpTypeError;
622
623    #[inline]
624    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
625        let actual = sexp.type_of();
626        if actual != T::SEXP_TYPE {
627            return Err(SexpTypeError {
628                expected: T::SEXP_TYPE,
629                actual,
630            });
631        }
632        Ok(unsafe { sexp.as_slice::<T>() })
633    }
634
635    #[inline]
636    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
637        let actual = sexp.type_of();
638        if actual != T::SEXP_TYPE {
639            return Err(SexpTypeError {
640                expected: T::SEXP_TYPE,
641                actual,
642            });
643        }
644        Ok(unsafe { sexp.as_slice_unchecked::<T>() })
645    }
646}
647
648/// Blanket impl for `&mut [T]` where T: RNativeType
649///
650/// # Safety note (aliasing)
651///
652/// This impl can produce aliased `&mut` slices if the same R vector is passed
653/// to multiple mutable slice parameters. The caller is responsible for ensuring
654/// no two `&mut` borrows alias the same SEXP.
655impl<T> TryFromSexp for &mut [T]
656where
657    T: crate::RNativeType + Copy,
658{
659    type Error = SexpTypeError;
660
661    #[inline]
662    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
663        let actual = sexp.type_of();
664        if actual != T::SEXP_TYPE {
665            return Err(SexpTypeError {
666                expected: T::SEXP_TYPE,
667                actual,
668            });
669        }
670        let len = sexp.len();
671        let ptr = unsafe { T::dataptr_mut(sexp) };
672        Ok(unsafe { r_slice_mut(ptr, len) })
673    }
674
675    #[inline]
676    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
677        let actual = sexp.type_of();
678        if actual != T::SEXP_TYPE {
679            return Err(SexpTypeError {
680                expected: T::SEXP_TYPE,
681                actual,
682            });
683        }
684        let len = unsafe { sexp.len_unchecked() };
685        let ptr = unsafe { T::dataptr_mut(sexp) };
686        Ok(unsafe { r_slice_mut(ptr, len) })
687    }
688}
689
690/// Blanket impl for `Option<&[T]>` where T: RNativeType
691impl<T> TryFromSexp for Option<&[T]>
692where
693    T: crate::RNativeType + Copy,
694{
695    type Error = SexpError;
696
697    #[inline]
698    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
699        if sexp.type_of() == SEXPTYPE::NILSXP {
700            return Ok(None);
701        }
702        let slice: &[T] = TryFromSexp::try_from_sexp(sexp).map_err(SexpError::from)?;
703        Ok(Some(slice))
704    }
705
706    #[inline]
707    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
708        if sexp.type_of() == SEXPTYPE::NILSXP {
709            return Ok(None);
710        }
711        let slice: &[T] =
712            unsafe { TryFromSexp::try_from_sexp_unchecked(sexp).map_err(SexpError::from)? };
713        Ok(Some(slice))
714    }
715}
716
717/// Blanket impl for `Option<&mut [T]>` where T: RNativeType
718impl<T> TryFromSexp for Option<&mut [T]>
719where
720    T: crate::RNativeType + Copy,
721{
722    type Error = SexpError;
723
724    #[inline]
725    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
726        if sexp.type_of() == SEXPTYPE::NILSXP {
727            return Ok(None);
728        }
729        let slice: &mut [T] = TryFromSexp::try_from_sexp(sexp).map_err(SexpError::from)?;
730        Ok(Some(slice))
731    }
732
733    #[inline]
734    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
735        if sexp.type_of() == SEXPTYPE::NILSXP {
736            return Ok(None);
737        }
738        let slice: &mut [T] =
739            unsafe { TryFromSexp::try_from_sexp_unchecked(sexp).map_err(SexpError::from)? };
740        Ok(Some(slice))
741    }
742}
743// endregion
744
745mod strings;
746
747// region: Result conversions (NULL -> Err(()))
748
749impl<T> TryFromSexp for Result<T, ()>
750where
751    T: TryFromSexp,
752    T::Error: Into<SexpError>,
753{
754    type Error = SexpError;
755
756    #[inline]
757    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
758        if sexp.type_of() == SEXPTYPE::NILSXP {
759            return Ok(Err(()));
760        }
761        let value = T::try_from_sexp(sexp).map_err(Into::into)?;
762        Ok(Ok(value))
763    }
764
765    #[inline]
766    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
767        if sexp.type_of() == SEXPTYPE::NILSXP {
768            return Ok(Err(()));
769        }
770        let value = unsafe { T::try_from_sexp_unchecked(sexp).map_err(Into::into)? };
771        Ok(Ok(value))
772    }
773}
774// endregion
775
776mod na_vectors;
777
778mod collections;
779
780mod tuples;
781
782// region: Fixed-size array conversions
783
784/// Blanket impl: Convert R vector to `[T; N]` where T: RNativeType.
785///
786/// Returns an error if the R vector length doesn't match N.
787/// Useful for SHA hashes ([u8; 32]), fixed-size patterns, etc.
788impl<T, const N: usize> TryFromSexp for [T; N]
789where
790    T: crate::RNativeType + Copy,
791{
792    type Error = SexpError;
793
794    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
795        let slice: &[T] = TryFromSexp::try_from_sexp(sexp)?;
796        if slice.len() != N {
797            return Err(SexpLengthError {
798                expected: N,
799                actual: slice.len(),
800            }
801            .into());
802        }
803
804        // T: Copy, length verified above. Use MaybeUninit + copy_from_slice.
805        let mut arr = std::mem::MaybeUninit::<[T; N]>::uninit();
806        unsafe {
807            // SAFETY: MaybeUninit<[T; N]> and [T; N] have the same layout.
808            // We write all N elements via copy_from_slice, so assume_init is safe.
809            let dst: &mut [T] = std::slice::from_raw_parts_mut(arr.as_mut_ptr().cast::<T>(), N);
810            dst.copy_from_slice(&slice[..N]);
811            Ok(arr.assume_init())
812        }
813    }
814
815    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
816        Self::try_from_sexp(sexp)
817    }
818}
819// endregion
820
821// region: VecDeque conversions
822
823use std::collections::VecDeque;
824
825/// Blanket impl: Convert R vector to `VecDeque<T>` where T: RNativeType.
826impl<T> TryFromSexp for VecDeque<T>
827where
828    T: crate::RNativeType + Copy,
829{
830    type Error = SexpTypeError;
831
832    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
833        let slice: &[T] = TryFromSexp::try_from_sexp(sexp)?;
834        Ok(VecDeque::from(slice.to_vec()))
835    }
836
837    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
838        let slice: &[T] = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
839        Ok(VecDeque::from(slice.to_vec()))
840    }
841}
842// endregion
843
844// region: BinaryHeap conversions
845
846use std::collections::BinaryHeap;
847
848/// Blanket impl: Convert R vector to `BinaryHeap<T>` where T: RNativeType + Ord.
849///
850/// Creates a binary heap from the R vector elements.
851impl<T> TryFromSexp for BinaryHeap<T>
852where
853    T: crate::RNativeType + Copy + Ord,
854{
855    type Error = SexpTypeError;
856
857    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
858        let slice: &[T] = TryFromSexp::try_from_sexp(sexp)?;
859        Ok(BinaryHeap::from(slice.to_vec()))
860    }
861
862    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
863        let slice: &[T] = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
864        Ok(BinaryHeap::from(slice.to_vec()))
865    }
866}
867// endregion
868
869mod cow_and_paths;
870
871// region: Option<Collection> conversions
872//
873// These convert NULL → None, and non-NULL to Some(collection).
874// This differs from Option<scalar> which converts NA → None.
875
876/// Convert R value to `Option<Vec<T>>`: NULL → None, otherwise Some(vec).
877impl<T> TryFromSexp for Option<Vec<T>>
878where
879    Vec<T>: TryFromSexp,
880    <Vec<T> as TryFromSexp>::Error: Into<SexpError>,
881{
882    type Error = SexpError;
883
884    #[inline]
885    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
886        if sexp.type_of() == SEXPTYPE::NILSXP {
887            Ok(None)
888        } else {
889            Vec::<T>::try_from_sexp(sexp).map(Some).map_err(Into::into)
890        }
891    }
892
893    #[inline]
894    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
895        if sexp.type_of() == SEXPTYPE::NILSXP {
896            Ok(None)
897        } else {
898            unsafe {
899                Vec::<T>::try_from_sexp_unchecked(sexp)
900                    .map(Some)
901                    .map_err(Into::into)
902            }
903        }
904    }
905}
906
907macro_rules! impl_option_map_try_from_sexp {
908    ($(#[$meta:meta])* $map_ty:ident) => {
909        $(#[$meta])*
910        impl<V: TryFromSexp> TryFromSexp for Option<$map_ty<String, V>>
911        where
912            V::Error: Into<SexpError>,
913        {
914            type Error = SexpError;
915
916            #[inline]
917            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
918                if sexp.type_of() == SEXPTYPE::NILSXP {
919                    Ok(None)
920                } else {
921                    $map_ty::<String, V>::try_from_sexp(sexp).map(Some)
922                }
923            }
924        }
925    };
926}
927
928impl_option_map_try_from_sexp!(
929    /// Convert R value to `Option<HashMap<String, V>>`: NULL -> None, otherwise Some(map).
930    HashMap
931);
932impl_option_map_try_from_sexp!(
933    /// Convert R value to `Option<BTreeMap<String, V>>`: NULL -> None, otherwise Some(map).
934    BTreeMap
935);
936
937macro_rules! impl_option_set_try_from_sexp {
938    ($(#[$meta:meta])* $set_ty:ident) => {
939        $(#[$meta])*
940        impl<T> TryFromSexp for Option<$set_ty<T>>
941        where
942            $set_ty<T>: TryFromSexp,
943            <$set_ty<T> as TryFromSexp>::Error: Into<SexpError>,
944        {
945            type Error = SexpError;
946
947            #[inline]
948            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
949                if sexp.type_of() == SEXPTYPE::NILSXP {
950                    Ok(None)
951                } else {
952                    $set_ty::<T>::try_from_sexp(sexp)
953                        .map(Some)
954                        .map_err(Into::into)
955                }
956            }
957        }
958    };
959}
960
961impl_option_set_try_from_sexp!(
962    /// Convert R value to `Option<HashSet<T>>`: NULL -> None, otherwise Some(set).
963    HashSet
964);
965impl_option_set_try_from_sexp!(
966    /// Convert R value to `Option<BTreeSet<T>>`: NULL -> None, otherwise Some(set).
967    BTreeSet
968);
969// endregion
970
971// region: Nested vector conversions (list of vectors)
972
973/// Convert R list (VECSXP) to `Vec<Vec<T>>`.
974///
975/// Each element of the R list must be convertible to `Vec<T>`.
976impl<T> TryFromSexp for Vec<Vec<T>>
977where
978    Vec<T>: TryFromSexp,
979    <Vec<T> as TryFromSexp>::Error: Into<SexpError>,
980{
981    type Error = SexpError;
982
983    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
984        map_vecsxp_with(sexp, |_i, elem| {
985            Vec::<T>::try_from_sexp(elem).map_err(Into::into)
986        })
987    }
988
989    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
990        unsafe {
991            map_vecsxp_with_unchecked(sexp, |_i, elem| {
992                Vec::<T>::try_from_sexp_unchecked(elem).map_err(Into::into)
993            })
994        }
995    }
996}
997// endregion
998
999// region: Coerced wrapper - bridge between TryFromSexp and TryCoerce
1000
1001use crate::coerce::Coerced;
1002
1003/// Convert R value to `Coerced<T, R>` by reading `R` and coercing to `T`.
1004///
1005/// This enables reading non-native Rust types from R with coercion:
1006///
1007/// ```ignore
1008/// // Read i64 from R integer (i32)
1009/// let val: Coerced<i64, i32> = TryFromSexp::try_from_sexp(sexp)?;
1010/// let i64_val: i64 = val.into_inner();
1011///
1012/// // Works with collections too:
1013/// let vec: Vec<Coerced<i64, i32>> = ...;
1014/// let set: HashSet<Coerced<NonZeroU32, i32>> = ...;
1015/// ```
1016impl<T, R> TryFromSexp for Coerced<T, R>
1017where
1018    R: TryFromSexp,
1019    R: TryCoerce<T>,
1020    <R as TryFromSexp>::Error: Into<SexpError>,
1021    <R as TryCoerce<T>>::Error: std::fmt::Debug,
1022{
1023    type Error = SexpError;
1024
1025    #[inline]
1026    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1027        let r_val: R = R::try_from_sexp(sexp).map_err(Into::into)?;
1028        let value: T = r_val
1029            .try_coerce()
1030            .map_err(|e| SexpError::InvalidValue(format!("{e:?}")))?;
1031        Ok(Coerced::new(value))
1032    }
1033
1034    #[inline]
1035    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1036        let r_val: R = unsafe { R::try_from_sexp_unchecked(sexp).map_err(Into::into)? };
1037        let value: T = r_val
1038            .try_coerce()
1039            .map_err(|e| SexpError::InvalidValue(format!("{e:?}")))?;
1040        Ok(Coerced::new(value))
1041    }
1042}
1043// endregion
1044
1045// region: Direct Vec coercion conversions
1046//
1047// These provide direct `TryFromSexp for Vec<T>` where T is not an R native type
1048// but can be coerced from one. This mirrors the `impl_into_r_via_coerce!` pattern
1049// in into_r.rs for the reverse direction.
1050
1051/// Helper to coerce a slice element-wise into a Vec.
1052#[inline]
1053fn coerce_slice_to_vec<R, T>(slice: &[R]) -> Result<Vec<T>, SexpError>
1054where
1055    R: Copy + TryCoerce<T>,
1056    <R as TryCoerce<T>>::Error: std::fmt::Debug,
1057{
1058    slice
1059        .iter()
1060        .copied()
1061        .map(|v| {
1062            v.try_coerce()
1063                .map_err(|e| SexpError::InvalidValue(format!("{e:?}")))
1064        })
1065        .collect()
1066}
1067
1068/// Shared SEXP-dispatch shell for coerced numeric/logical/raw vectors.
1069///
1070/// Reads INTSXP/REALSXP/RAWSXP/LGLSXP and applies the per-element map. The only
1071/// behavioural axis (NA policy) lives entirely in the four closures the caller
1072/// passes, so the NA-unaware [`try_from_sexp_numeric_vec`] and the NA-aware
1073/// `try_from_sexp_numeric_option_vec` (in [`na_vectors`]) share one dispatch.
1074/// LGLSXP NA (`NA_LOGICAL`) and INTSXP NA (`i32::MIN`) round through as raw
1075/// sentinels here; any NA-to-`None` policy is the caller's closure to encode.
1076#[inline]
1077pub(crate) fn from_numeric_vec_with<U, FI, FD, FR, FL>(
1078    sexp: SEXP,
1079    map_i32: FI,
1080    map_f64: FD,
1081    map_u8: FR,
1082    map_lgl: FL,
1083) -> Result<Vec<U>, SexpError>
1084where
1085    FI: Fn(i32) -> Result<U, SexpError>,
1086    FD: Fn(f64) -> Result<U, SexpError>,
1087    FR: Fn(u8) -> Result<U, SexpError>,
1088    FL: Fn(RLogical) -> Result<U, SexpError>,
1089{
1090    let actual = sexp.type_of();
1091    match actual {
1092        SEXPTYPE::INTSXP => {
1093            let slice: &[i32] = unsafe { sexp.as_slice() };
1094            slice.iter().copied().map(map_i32).collect()
1095        }
1096        SEXPTYPE::REALSXP => {
1097            let slice: &[f64] = unsafe { sexp.as_slice() };
1098            slice.iter().copied().map(map_f64).collect()
1099        }
1100        SEXPTYPE::RAWSXP => {
1101            let slice: &[u8] = unsafe { sexp.as_slice() };
1102            slice.iter().copied().map(map_u8).collect()
1103        }
1104        SEXPTYPE::LGLSXP => {
1105            let slice: &[RLogical] = unsafe { sexp.as_slice() };
1106            slice.iter().copied().map(map_lgl).collect()
1107        }
1108        _ => Err(SexpError::InvalidValue(format!(
1109            "expected integer, numeric, logical, or raw; got {:?}",
1110            actual
1111        ))),
1112    }
1113}
1114
1115/// Shared SEXP-dispatch shell for the STRSXP (character vector) walk.
1116///
1117/// String-side counterpart of [`from_numeric_vec_with`]: checks `STRSXP`,
1118/// walks each element via `string_elt`, and applies the per-element `map`
1119/// closure to the raw CHARSXP. NA/blank-string policy (`""` vs `None` vs
1120/// error) and the target representation (`String` vs `&str` vs `Cow`) are
1121/// entirely the caller's closure to encode — this only centralizes the type
1122/// check and the walk.
1123///
1124/// Mirrors `from_numeric_vec_with`'s choice to route both the checked and
1125/// unchecked `TryFromSexp` paths through the same (checked-FFI) walk — none
1126/// of the current STRSXP vector impls define a distinct unchecked fast path
1127/// (they fall back to `TryFromSexp`'s default `try_from_sexp_unchecked`, which
1128/// just calls the checked version), so there is no unchecked-FFI twin here.
1129#[inline]
1130pub(crate) fn map_strsxp_with<U>(
1131    sexp: SEXP,
1132    mut map: impl FnMut(SEXP /* charsxp */, usize) -> Result<U, SexpError>,
1133) -> Result<Vec<U>, SexpError> {
1134    let actual = sexp.type_of();
1135    if actual != SEXPTYPE::STRSXP {
1136        return Err(SexpTypeError {
1137            expected: SEXPTYPE::STRSXP,
1138            actual,
1139        }
1140        .into());
1141    }
1142
1143    let len = sexp.len();
1144    let mut result = Vec::with_capacity(len);
1145    for i in 0..len {
1146        let charsxp = sexp.string_elt(i as crate::R_xlen_t);
1147        result.push(map(charsxp, i)?);
1148    }
1149    Ok(result)
1150}
1151
1152/// Shared scalar-STRSXP prologue: type-check + `len == 1` + `string_elt(0)`.
1153///
1154/// Returns the raw CHARSXP; NA (`SEXP::na_string()`) and blank-string
1155/// (`SEXP::blank_string()`) policy stay the caller's decision — some sites
1156/// error on NA, some map it to `""`, some to `None`, and `String`'s blank
1157/// handling has historically differed from `&str`'s (see audit D6 finding
1158/// #3), so this helper does not paper over that divergence.
1159#[inline]
1160pub(crate) fn scalar_charsxp(sexp: SEXP) -> Result<SEXP, SexpError> {
1161    let actual = sexp.type_of();
1162    if actual != SEXPTYPE::STRSXP {
1163        return Err(SexpTypeError {
1164            expected: SEXPTYPE::STRSXP,
1165            actual,
1166        }
1167        .into());
1168    }
1169
1170    let len = sexp.len();
1171    if len != 1 {
1172        return Err(SexpLengthError {
1173            expected: 1,
1174            actual: len,
1175        }
1176        .into());
1177    }
1178
1179    Ok(sexp.string_elt(0))
1180}
1181
1182/// Unchecked-FFI variant of [`scalar_charsxp`] — uses `len_unchecked` /
1183/// `string_elt_unchecked`.
1184///
1185/// # Safety
1186///
1187/// Must be called from R's main thread (same contract as
1188/// [`TryFromSexp::try_from_sexp_unchecked`]).
1189#[inline]
1190pub(crate) unsafe fn scalar_charsxp_unchecked(sexp: SEXP) -> Result<SEXP, SexpError> {
1191    let actual = sexp.type_of();
1192    if actual != SEXPTYPE::STRSXP {
1193        return Err(SexpTypeError {
1194            expected: SEXPTYPE::STRSXP,
1195            actual,
1196        }
1197        .into());
1198    }
1199
1200    let len = unsafe { sexp.len_unchecked() };
1201    if len != 1 {
1202        return Err(SexpLengthError {
1203            expected: 1,
1204            actual: len,
1205        }
1206        .into());
1207    }
1208
1209    Ok(unsafe { sexp.string_elt_unchecked(0) })
1210}
1211
1212/// Shared SEXP-dispatch shell for the VECSXP (list) walk.
1213///
1214/// List-side counterpart of [`from_numeric_vec_with`] / [`map_strsxp_with`]:
1215/// checks `VECSXP`, walks each element via `vector_elt`, and applies the
1216/// per-element `map` closure (which receives the element's index and SEXP).
1217/// Per-element policy (recursion, `NILSXP` → `None`, duplicate-pointer
1218/// aliasing checks, …) lives entirely in the closure.
1219#[inline]
1220pub(crate) fn map_vecsxp_with<U>(
1221    sexp: SEXP,
1222    mut map: impl FnMut(usize, SEXP) -> Result<U, SexpError>,
1223) -> Result<Vec<U>, SexpError> {
1224    let actual = sexp.type_of();
1225    if actual != SEXPTYPE::VECSXP {
1226        return Err(SexpTypeError {
1227            expected: SEXPTYPE::VECSXP,
1228            actual,
1229        }
1230        .into());
1231    }
1232
1233    let len = sexp.len();
1234    let mut result = Vec::with_capacity(len);
1235    for i in 0..len {
1236        let elem = sexp.vector_elt(i as crate::R_xlen_t);
1237        result.push(map(i, elem)?);
1238    }
1239    Ok(result)
1240}
1241
1242/// Unchecked-FFI variant of [`map_vecsxp_with`] — uses `len_unchecked` /
1243/// `vector_elt_unchecked` for the type-check and walk.
1244///
1245/// # Safety
1246///
1247/// Must be called from R's main thread (same contract as
1248/// [`TryFromSexp::try_from_sexp_unchecked`]).
1249#[inline]
1250pub(crate) unsafe fn map_vecsxp_with_unchecked<U>(
1251    sexp: SEXP,
1252    mut map: impl FnMut(usize, SEXP) -> Result<U, SexpError>,
1253) -> Result<Vec<U>, SexpError> {
1254    let actual = sexp.type_of();
1255    if actual != SEXPTYPE::VECSXP {
1256        return Err(SexpTypeError {
1257            expected: SEXPTYPE::VECSXP,
1258            actual,
1259        }
1260        .into());
1261    }
1262
1263    let len = unsafe { sexp.len_unchecked() };
1264    let mut result = Vec::with_capacity(len);
1265    for i in 0..len {
1266        let elem = unsafe { sexp.vector_elt_unchecked(i as crate::R_xlen_t) };
1267        result.push(map(i, elem)?);
1268    }
1269    Ok(result)
1270}
1271
1272/// Convert numeric/logical/raw vectors to `Vec<T>` with element-wise coercion.
1273///
1274/// NA-unaware: an R `NA` round-trips as the coerced sentinel rather than being
1275/// rejected. Bind `Vec<Option<T>>` (see [`na_vectors`]) when the caller can pass NA.
1276#[inline]
1277fn try_from_sexp_numeric_vec<T>(sexp: SEXP) -> Result<Vec<T>, SexpError>
1278where
1279    i32: TryCoerce<T>,
1280    f64: TryCoerce<T>,
1281    u8: TryCoerce<T>,
1282    <i32 as TryCoerce<T>>::Error: std::fmt::Debug,
1283    <f64 as TryCoerce<T>>::Error: std::fmt::Debug,
1284    <u8 as TryCoerce<T>>::Error: std::fmt::Debug,
1285{
1286    from_numeric_vec_with(
1287        sexp,
1288        coerce_value,
1289        coerce_value,
1290        coerce_value,
1291        |v: RLogical| coerce_value(v.to_i32()),
1292    )
1293}
1294
1295/// Implement `TryFromSexp for Vec<$target>` by coercing from integer/real/logical/raw.
1296macro_rules! impl_vec_try_from_sexp_numeric {
1297    ($target:ty) => {
1298        impl TryFromSexp for Vec<$target> {
1299            type Error = SexpError;
1300
1301            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1302                try_from_sexp_numeric_vec(sexp)
1303            }
1304
1305            unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1306                try_from_sexp_numeric_vec(sexp)
1307            }
1308        }
1309    };
1310}
1311
1312impl_vec_try_from_sexp_numeric!(i8);
1313impl_vec_try_from_sexp_numeric!(i16);
1314impl_vec_try_from_sexp_numeric!(i64);
1315impl_vec_try_from_sexp_numeric!(isize);
1316impl_vec_try_from_sexp_numeric!(u16);
1317impl_vec_try_from_sexp_numeric!(u32);
1318impl_vec_try_from_sexp_numeric!(u64);
1319impl_vec_try_from_sexp_numeric!(usize);
1320impl_vec_try_from_sexp_numeric!(f32);
1321
1322/// Convert R logical vector (LGLSXP) to `Vec<bool>` (errors on NA).
1323impl TryFromSexp for Vec<bool> {
1324    type Error = SexpError;
1325
1326    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1327        let actual = sexp.type_of();
1328        if actual != SEXPTYPE::LGLSXP {
1329            return Err(SexpTypeError {
1330                expected: SEXPTYPE::LGLSXP,
1331                actual,
1332            }
1333            .into());
1334        }
1335        let slice: &[RLogical] = unsafe { sexp.as_slice() };
1336        coerce_slice_to_vec(slice)
1337    }
1338
1339    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1340        Self::try_from_sexp(sexp)
1341    }
1342}
1343// endregion
1344
1345// region: Direct HashSet / BTreeSet coercion conversions
1346
1347/// Convert numeric/logical/raw vectors to a set type with element-wise coercion.
1348#[inline]
1349fn try_from_sexp_numeric_set<T, S>(sexp: SEXP) -> Result<S, SexpError>
1350where
1351    S: std::iter::FromIterator<T>,
1352    i32: TryCoerce<T>,
1353    f64: TryCoerce<T>,
1354    u8: TryCoerce<T>,
1355    <i32 as TryCoerce<T>>::Error: std::fmt::Debug,
1356    <f64 as TryCoerce<T>>::Error: std::fmt::Debug,
1357    <u8 as TryCoerce<T>>::Error: std::fmt::Debug,
1358{
1359    let vec = try_from_sexp_numeric_vec(sexp)?;
1360    Ok(vec.into_iter().collect())
1361}
1362
1363macro_rules! impl_set_try_from_sexp_numeric {
1364    ($set_ty:ident, $target:ty) => {
1365        impl TryFromSexp for $set_ty<$target> {
1366            type Error = SexpError;
1367
1368            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1369                try_from_sexp_numeric_set(sexp)
1370            }
1371
1372            unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1373                try_from_sexp_numeric_set(sexp)
1374            }
1375        }
1376    };
1377}
1378
1379impl_set_try_from_sexp_numeric!(HashSet, i8);
1380impl_set_try_from_sexp_numeric!(HashSet, i16);
1381impl_set_try_from_sexp_numeric!(HashSet, i64);
1382impl_set_try_from_sexp_numeric!(HashSet, isize);
1383impl_set_try_from_sexp_numeric!(HashSet, u16);
1384impl_set_try_from_sexp_numeric!(HashSet, u32);
1385impl_set_try_from_sexp_numeric!(HashSet, u64);
1386impl_set_try_from_sexp_numeric!(HashSet, usize);
1387
1388impl_set_try_from_sexp_numeric!(BTreeSet, i8);
1389impl_set_try_from_sexp_numeric!(BTreeSet, i16);
1390impl_set_try_from_sexp_numeric!(BTreeSet, i64);
1391impl_set_try_from_sexp_numeric!(BTreeSet, isize);
1392impl_set_try_from_sexp_numeric!(BTreeSet, u16);
1393impl_set_try_from_sexp_numeric!(BTreeSet, u32);
1394impl_set_try_from_sexp_numeric!(BTreeSet, u64);
1395impl_set_try_from_sexp_numeric!(BTreeSet, usize);
1396
1397macro_rules! impl_set_try_from_sexp_bool {
1398    ($set_ty:ident) => {
1399        impl TryFromSexp for $set_ty<bool> {
1400            type Error = SexpError;
1401
1402            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1403                let vec: Vec<bool> = TryFromSexp::try_from_sexp(sexp)?;
1404                Ok(vec.into_iter().collect())
1405            }
1406
1407            unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1408                Self::try_from_sexp(sexp)
1409            }
1410        }
1411    };
1412}
1413
1414impl_set_try_from_sexp_bool!(HashSet);
1415impl_set_try_from_sexp_bool!(BTreeSet);
1416// endregion
1417
1418// region: ExternalPtr conversions
1419
1420use crate::externalptr::{ExternalPtr, TypeMismatchError, TypedExternal};
1421
1422/// Map a downcast [`TypeMismatchError`] to the [`SexpError`] surfaced from
1423/// `ExternalPtr<T>` argument conversion. Shared by the checked and unchecked
1424/// `TryFromSexp` paths below.
1425fn type_mismatch_to_sexp_error(e: TypeMismatchError) -> SexpError {
1426    match e {
1427        TypeMismatchError::NullPointer => {
1428            SexpError::InvalidValue("external pointer is null".to_string())
1429        }
1430        TypeMismatchError::InvalidTypeId => {
1431            SexpError::InvalidValue("external pointer has no valid type id".to_string())
1432        }
1433        TypeMismatchError::Mismatch { expected, found } => SexpError::InvalidValue(format!(
1434            "type mismatch: expected `{}`, found `{}`",
1435            expected, found
1436        )),
1437    }
1438}
1439
1440/// Error for an `ExternalPtr<T>` argument that is neither a bare `EXTPTRSXP`
1441/// nor a class handle wrapping one (audit A9 — class-wrapped handles like
1442/// `Foo$new(...)` are unwrapped automatically; this fires only when *no*
1443/// `.ptr`/slot/attribute could be recovered at all).
1444fn not_a_handle_error(actual: SEXPTYPE) -> SexpError {
1445    SexpError::InvalidValue(format!(
1446        "expected an external pointer or a miniextendr class object wrapping one, got {:?}",
1447        actual
1448    ))
1449}
1450
1451/// Convert R EXTPTRSXP to `ExternalPtr<T>`.
1452///
1453/// This enables using `ExternalPtr<T>` as parameter types in `#[miniextendr]` functions.
1454///
1455/// # Example
1456///
1457/// ```ignore
1458/// #[derive(ExternalPtr)]
1459/// struct MyData { value: i32 }
1460///
1461/// #[miniextendr]
1462/// fn process(data: ExternalPtr<MyData>) -> i32 {
1463///     data.value
1464/// }
1465/// ```
1466impl<T: TypedExternal + Send> TryFromSexp for ExternalPtr<T> {
1467    type Error = SexpError;
1468
1469    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1470        let actual = sexp.type_of();
1471        if actual != SEXPTYPE::EXTPTRSXP {
1472            // Not a bare pointer — try unwrapping a class-wrapped handle
1473            // (R6 `private$.ptr`, S4 `ptr` slot, S7 `.ptr` attribute; Env/S3
1474            // handles are already bare EXTPTRSXPs and never reach here).
1475            return match unsafe { crate::externalptr::unwrap_class_handle(sexp) } {
1476                Some(inner) => unsafe { ExternalPtr::wrap_sexp_with_error(inner) }
1477                    .map_err(type_mismatch_to_sexp_error),
1478                None => Err(not_a_handle_error(actual)),
1479            };
1480        }
1481
1482        // Use ExternalPtr's type-checked constructor
1483        unsafe { ExternalPtr::wrap_sexp_with_error(sexp) }.map_err(type_mismatch_to_sexp_error)
1484    }
1485
1486    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1487        let actual = sexp.type_of();
1488        if actual != SEXPTYPE::EXTPTRSXP {
1489            return match unsafe { crate::externalptr::unwrap_class_handle(sexp) } {
1490                Some(inner) => {
1491                    unsafe { ExternalPtr::wrap_sexp_unchecked(inner) }.ok_or_else(|| {
1492                        SexpError::InvalidValue(
1493                            "failed to convert external pointer: type mismatch or null pointer"
1494                                .to_string(),
1495                        )
1496                    })
1497                }
1498                None => Err(not_a_handle_error(actual)),
1499            };
1500        }
1501
1502        // Use ExternalPtr's type-checked constructor (unchecked variant)
1503        unsafe { ExternalPtr::wrap_sexp_unchecked(sexp) }.ok_or_else(|| {
1504            SexpError::InvalidValue(
1505                "failed to convert external pointer: type mismatch or null pointer".to_string(),
1506            )
1507        })
1508    }
1509}
1510
1511impl<T: TypedExternal + Send> TryFromSexp for Option<ExternalPtr<T>> {
1512    type Error = SexpError;
1513
1514    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1515        if sexp.type_of() == SEXPTYPE::NILSXP {
1516            return Ok(None);
1517        }
1518        let ptr: ExternalPtr<T> = TryFromSexp::try_from_sexp(sexp)?;
1519        Ok(Some(ptr))
1520    }
1521
1522    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1523        if sexp.type_of() == SEXPTYPE::NILSXP {
1524            return Ok(None);
1525        }
1526        let ptr: ExternalPtr<T> = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
1527        Ok(Some(ptr))
1528    }
1529}
1530
1531/// Convert an R list (VECSXP) of external pointers to `Vec<ExternalPtr<T>>`.
1532///
1533/// Each element must be an `EXTPTRSXP` carrying a `T`; conversion delegates to
1534/// [`ExternalPtr::<T>::try_from_sexp`] per element. This lets `#[miniextendr]`
1535/// functions accept an R `list()` of opaque handles (issue #827). The blanket
1536/// `impl_vec_try_from_sexp_list!` macro can't be used downstream for this — the
1537/// orphan rule rejects `impl TryFromSexp for Vec<ExternalPtr<T>>` in user crates
1538/// because both `Vec` and `TryFromSexp` are foreign there — so the impl lives
1539/// here, keyed on `ExternalPtr<T>` to avoid colliding with the atomic-vector impls.
1540impl<T: TypedExternal + Send> TryFromSexp for Vec<ExternalPtr<T>> {
1541    type Error = SexpError;
1542
1543    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1544        map_vecsxp_with(sexp, |_i, elem| {
1545            <ExternalPtr<T> as TryFromSexp>::try_from_sexp(elem)
1546        })
1547    }
1548
1549    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1550        unsafe {
1551            map_vecsxp_with_unchecked(sexp, |_i, elem| {
1552                <ExternalPtr<T> as TryFromSexp>::try_from_sexp_unchecked(elem)
1553            })
1554        }
1555    }
1556}
1557
1558/// Convert an R list (VECSXP) of external pointers / `NULL`s to
1559/// `Vec<Option<ExternalPtr<T>>>`. `NULL` elements map to `None`; every other
1560/// element must be an `EXTPTRSXP` carrying a `T` (issue #827).
1561impl<T: TypedExternal + Send> TryFromSexp for Vec<Option<ExternalPtr<T>>> {
1562    type Error = SexpError;
1563
1564    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1565        map_vecsxp_with(sexp, |_i, elem| {
1566            <Option<ExternalPtr<T>> as TryFromSexp>::try_from_sexp(elem)
1567        })
1568    }
1569
1570    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1571        unsafe {
1572            map_vecsxp_with_unchecked(sexp, |_i, elem| {
1573                <Option<ExternalPtr<T>> as TryFromSexp>::try_from_sexp_unchecked(elem)
1574            })
1575        }
1576    }
1577}
1578// endregion
1579
1580// region: R connections — TryFromSexp impls (issue #175, #176)
1581
1582#[cfg(feature = "connections")]
1583mod connections_from_r {
1584    use std::ffi::CStr;
1585
1586    use crate::connection::{RNullConnection, RStderr, RStdin, RStdout, Rconn};
1587    use crate::from_r::{SexpError, TryFromSexp};
1588    use crate::{Rboolean, SEXP};
1589
1590    // Read the connection description and class fields from an Rconn handle.
1591    //
1592    // # Safety
1593    // - sexp must be a valid, open R connection SEXP.
1594    // - Must be called from the R main thread.
1595    unsafe fn conn_description(sexp: SEXP) -> Option<String> {
1596        unsafe {
1597            let handle = crate::sys::R_GetConnection(sexp);
1598            let conn = handle.cast::<Rconn>().cast_const();
1599            if (*conn).description.is_null() {
1600                None
1601            } else {
1602                Some(
1603                    CStr::from_ptr((*conn).description)
1604                        .to_string_lossy()
1605                        .into_owned(),
1606                )
1607            }
1608        }
1609    }
1610
1611    unsafe fn conn_class(sexp: SEXP) -> Option<String> {
1612        unsafe {
1613            let handle = crate::sys::R_GetConnection(sexp);
1614            let conn = handle.cast::<Rconn>().cast_const();
1615            if (*conn).class.is_null() {
1616                None
1617            } else {
1618                Some(CStr::from_ptr((*conn).class).to_string_lossy().into_owned())
1619            }
1620        }
1621    }
1622
1623    unsafe fn conn_canwrite(sexp: SEXP) -> bool {
1624        unsafe {
1625            let handle = crate::sys::R_GetConnection(sexp);
1626            let conn = handle.cast::<Rconn>().cast_const();
1627            (*conn).canwrite != Rboolean::FALSE
1628        }
1629    }
1630
1631    unsafe fn conn_isopen(sexp: SEXP) -> bool {
1632        unsafe {
1633            let handle = crate::sys::R_GetConnection(sexp);
1634            let conn = handle.cast::<Rconn>().cast_const();
1635            (*conn).isopen != Rboolean::FALSE
1636        }
1637    }
1638
1639    // Strict validation: confirm description == expected_desc and class == "terminal".
1640    unsafe fn validate_terminal(sexp: SEXP, expected_desc: &str) -> Result<(), SexpError> {
1641        let desc = unsafe { conn_description(sexp) }.unwrap_or_default();
1642        if desc != expected_desc {
1643            return Err(SexpError::InvalidValue(format!(
1644                "expected terminal connection with description {:?}, got {:?}",
1645                expected_desc, desc
1646            )));
1647        }
1648        let cls = unsafe { conn_class(sexp) }.unwrap_or_default();
1649        if cls != "terminal" {
1650            return Err(SexpError::InvalidValue(format!(
1651                "expected class \"terminal\", got {:?}",
1652                cls
1653            )));
1654        }
1655        Ok(())
1656    }
1657
1658    impl TryFromSexp for RStdin {
1659        type Error = SexpError;
1660
1661        fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1662            unsafe { validate_terminal(sexp, "stdin") }?;
1663            Ok(RStdin)
1664        }
1665
1666        unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1667            Self::try_from_sexp(sexp)
1668        }
1669    }
1670
1671    impl TryFromSexp for RStdout {
1672        type Error = SexpError;
1673
1674        fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1675            unsafe { validate_terminal(sexp, "stdout") }?;
1676            Ok(RStdout)
1677        }
1678
1679        unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1680            Self::try_from_sexp(sexp)
1681        }
1682    }
1683
1684    impl TryFromSexp for RStderr {
1685        type Error = SexpError;
1686
1687        fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1688            unsafe { validate_terminal(sexp, "stderr") }?;
1689            Ok(RStderr)
1690        }
1691
1692        unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1693            Self::try_from_sexp(sexp)
1694        }
1695    }
1696
1697    /// Accepts any open, write-capable connection — not just the null device.
1698    ///
1699    /// This is intentional: validating against `description == "/dev/null"` /
1700    /// `"NUL"` is brittle across platforms, and the type's value comes from the
1701    /// RAII close-on-drop, not the specific target. Substituting a `file()`
1702    /// connection for `RNullConnection` is supported.
1703    impl TryFromSexp for RNullConnection {
1704        type Error = SexpError;
1705
1706        fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1707            if !unsafe { conn_isopen(sexp) } {
1708                return Err(SexpError::InvalidValue(
1709                    "expected an open connection".to_string(),
1710                ));
1711            }
1712            if !unsafe { conn_canwrite(sexp) } {
1713                return Err(SexpError::InvalidValue(
1714                    "expected a write-capable connection".to_string(),
1715                ));
1716            }
1717            // Preserve the SEXP so it lives as long as this Rust struct.
1718            unsafe { crate::sys::R_PreserveObject(sexp) };
1719            Ok(unsafe { RNullConnection::from_preserved_sexp(sexp) })
1720        }
1721
1722        unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1723            Self::try_from_sexp(sexp)
1724        }
1725    }
1726}
1727
1728// endregion
1729
1730// region: txtProgressBar — TryFromSexp (issue #177)
1731
1732#[cfg(feature = "connections")]
1733mod txt_progress_bar_from_r {
1734    use crate::from_r::{SexpError, TryFromSexp};
1735    use crate::sys::R_PreserveObject;
1736    use crate::txt_progress_bar::RTxtProgressBar;
1737    use crate::{SEXP, SexpExt};
1738
1739    impl TryFromSexp for RTxtProgressBar {
1740        type Error = SexpError;
1741
1742        fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1743            // Must be a list (VECSXP) with class "txtProgressBar".
1744            if !sexp.inherits_class(c"txtProgressBar") {
1745                return Err(SexpError::InvalidValue(
1746                    "expected a SEXP with class \"txtProgressBar\"".to_string(),
1747                ));
1748            }
1749            // Pin on the precious list so GC cannot collect while Rust holds it.
1750            unsafe { R_PreserveObject(sexp) };
1751            Ok(unsafe { RTxtProgressBar::from_preserved_sexp(sexp) })
1752        }
1753
1754        unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1755            Self::try_from_sexp(sexp)
1756        }
1757    }
1758}
1759
1760// endregion
1761
1762// region: Helper macros for feature-gated modules
1763
1764/// Implement `TryFromSexp for Option<T>` where T already implements TryFromSexp.
1765///
1766/// NULL → None, otherwise delegates to T::try_from_sexp and wraps in Some.
1767#[macro_export]
1768macro_rules! impl_option_try_from_sexp {
1769    ($t:ty) => {
1770        impl $crate::from_r::TryFromSexp for Option<$t> {
1771            type Error = $crate::from_r::SexpError;
1772
1773            fn try_from_sexp(sexp: $crate::SEXP) -> Result<Self, Self::Error> {
1774                use $crate::{SEXPTYPE, SexpExt};
1775                if sexp.type_of() == SEXPTYPE::NILSXP {
1776                    return Ok(None);
1777                }
1778                <$t as $crate::from_r::TryFromSexp>::try_from_sexp(sexp).map(Some)
1779            }
1780
1781            unsafe fn try_from_sexp_unchecked(sexp: $crate::SEXP) -> Result<Self, Self::Error> {
1782                use $crate::{SEXPTYPE, SexpExt};
1783                if sexp.type_of() == SEXPTYPE::NILSXP {
1784                    return Ok(None);
1785                }
1786                unsafe {
1787                    <$t as $crate::from_r::TryFromSexp>::try_from_sexp_unchecked(sexp).map(Some)
1788                }
1789            }
1790        }
1791    };
1792}
1793
1794/// Implement `TryFromSexp for Vec<T>` from R list (VECSXP).
1795///
1796/// Each element is converted via T::try_from_sexp.
1797#[macro_export]
1798macro_rules! impl_vec_try_from_sexp_list {
1799    ($t:ty) => {
1800        impl $crate::from_r::TryFromSexp for Vec<$t> {
1801            type Error = $crate::from_r::SexpError;
1802
1803            fn try_from_sexp(sexp: $crate::SEXP) -> Result<Self, Self::Error> {
1804                use $crate::from_r::SexpTypeError;
1805                use $crate::{SEXPTYPE, SexpExt};
1806
1807                let actual = sexp.type_of();
1808                if actual != SEXPTYPE::VECSXP {
1809                    return Err(SexpTypeError {
1810                        expected: SEXPTYPE::VECSXP,
1811                        actual,
1812                    }
1813                    .into());
1814                }
1815
1816                let len = sexp.len();
1817                let mut result = Vec::with_capacity(len);
1818                for i in 0..len {
1819                    let elem = sexp.vector_elt(i as $crate::R_xlen_t);
1820                    result.push(<$t as $crate::from_r::TryFromSexp>::try_from_sexp(elem)?);
1821                }
1822                Ok(result)
1823            }
1824
1825            unsafe fn try_from_sexp_unchecked(sexp: $crate::SEXP) -> Result<Self, Self::Error> {
1826                use $crate::from_r::SexpTypeError;
1827                use $crate::{SEXPTYPE, SexpExt};
1828
1829                let actual = sexp.type_of();
1830                if actual != SEXPTYPE::VECSXP {
1831                    return Err(SexpTypeError {
1832                        expected: SEXPTYPE::VECSXP,
1833                        actual,
1834                    }
1835                    .into());
1836                }
1837
1838                let len = unsafe { sexp.len_unchecked() };
1839                let mut result = Vec::with_capacity(len);
1840                for i in 0..len {
1841                    let elem = unsafe { sexp.vector_elt_unchecked(i as $crate::R_xlen_t) };
1842                    result.push(unsafe {
1843                        <$t as $crate::from_r::TryFromSexp>::try_from_sexp_unchecked(elem)?
1844                    });
1845                }
1846                Ok(result)
1847            }
1848        }
1849    };
1850}
1851
1852/// Implement `TryFromSexp for Vec<Option<T>>` from R list (VECSXP).
1853///
1854/// NULL elements become None, others are converted via T::try_from_sexp.
1855#[macro_export]
1856macro_rules! impl_vec_option_try_from_sexp_list {
1857    ($t:ty) => {
1858        impl $crate::from_r::TryFromSexp for Vec<Option<$t>> {
1859            type Error = $crate::from_r::SexpError;
1860
1861            fn try_from_sexp(sexp: $crate::SEXP) -> Result<Self, Self::Error> {
1862                use $crate::from_r::SexpTypeError;
1863                use $crate::{SEXPTYPE, SexpExt};
1864
1865                let actual = sexp.type_of();
1866                if actual != SEXPTYPE::VECSXP {
1867                    return Err(SexpTypeError {
1868                        expected: SEXPTYPE::VECSXP,
1869                        actual,
1870                    }
1871                    .into());
1872                }
1873
1874                let len = sexp.len();
1875                let mut result = Vec::with_capacity(len);
1876                for i in 0..len {
1877                    let elem = sexp.vector_elt(i as $crate::R_xlen_t);
1878                    if elem == $crate::SEXP::nil() {
1879                        result.push(None);
1880                    } else {
1881                        result.push(Some(<$t as $crate::from_r::TryFromSexp>::try_from_sexp(
1882                            elem,
1883                        )?));
1884                    }
1885                }
1886                Ok(result)
1887            }
1888
1889            unsafe fn try_from_sexp_unchecked(sexp: $crate::SEXP) -> Result<Self, Self::Error> {
1890                use $crate::from_r::SexpTypeError;
1891                use $crate::{SEXPTYPE, SexpExt};
1892
1893                let actual = sexp.type_of();
1894                if actual != SEXPTYPE::VECSXP {
1895                    return Err(SexpTypeError {
1896                        expected: SEXPTYPE::VECSXP,
1897                        actual,
1898                    }
1899                    .into());
1900                }
1901
1902                let len = unsafe { sexp.len_unchecked() };
1903                let mut result = Vec::with_capacity(len);
1904                for i in 0..len {
1905                    let elem = unsafe { sexp.vector_elt_unchecked(i as $crate::R_xlen_t) };
1906                    if elem == $crate::SEXP::nil() {
1907                        result.push(None);
1908                    } else {
1909                        result.push(Some(unsafe {
1910                            <$t as $crate::from_r::TryFromSexp>::try_from_sexp_unchecked(elem)?
1911                        }));
1912                    }
1913                }
1914                Ok(result)
1915            }
1916        }
1917    };
1918}
1919
1920/// Cap on the number of per-element failures listed in a batched vector
1921/// conversion error; the remainder is summarized as `"and N more"`.
1922const BATCHED_ERROR_CAP: usize = 10;
1923
1924/// Combine indexed per-element conversion failures into one batched
1925/// [`SexpError::InvalidValue`].
1926///
1927/// Backs the `Vec<T>` / `Vec<Option<T>>` arms of
1928/// [`try_from_sexp_via_str_parse!`]: instead of bailing on the first NA or
1929/// parse failure, those arms walk the whole vector, accumulate the
1930/// already-formatted per-element messages, and hand them here. Entries are
1931/// joined with `"; "`; at most the first 10 are listed and the remainder is
1932/// summarized as `"and N more"`.
1933///
1934/// Public (but hidden) because `try_from_sexp_via_str_parse!` is
1935/// `#[macro_export]` and expands in downstream crates — not intended to be
1936/// called directly.
1937#[doc(hidden)]
1938pub fn batch_conversion_errors(container: &str, errors: Vec<String>) -> SexpError {
1939    debug_assert!(!errors.is_empty(), "batching zero conversion errors");
1940    let total = errors.len();
1941    let mut msg = format!(
1942        "{container} conversion failed: {}",
1943        errors[..total.min(BATCHED_ERROR_CAP)].join("; ")
1944    );
1945    if total > BATCHED_ERROR_CAP {
1946        use std::fmt::Write;
1947        let _ = write!(msg, "; and {} more", total - BATCHED_ERROR_CAP);
1948    }
1949    SexpError::InvalidValue(msg)
1950}
1951
1952/// Implement the four string-parse `TryFromSexp` impls (`T`, `Option<T>`,
1953/// `Vec<T>`, `Vec<Option<T>>`) for a type parsed from an R character vector.
1954///
1955/// Sibling of [`into_r_infallible!`](crate::into_r) for the reverse direction:
1956/// every "parse a scalar type out of an R string" integration (uuid, url,
1957/// regex, num-bigint) used to hand-write these four impls — some reinventing
1958/// the STRSXP validation `String`'s own `TryFromSexp` already performs.
1959/// This macro delegates to `Option<String>` / `Vec<Option<String>>`, so type,
1960/// length, and NA checks live in exactly one place.
1961///
1962/// Semantics:
1963/// - `T`: `NA_character_` / `NULL` → `SexpError::Na`; parse failure →
1964///   `InvalidValue("invalid <label>: <err>")`.
1965/// - `Option<T>`: `NA_character_` / `NULL` → `None`.
1966/// - `Vec<T>`: NA elements and parse failures are collected across the whole
1967///   vector into one batched `InvalidValue` (see [`batch_conversion_errors`]).
1968///   Per-element entries keep the `"NA at index <i> not allowed for Vec<T>"`
1969///   and `"invalid <label> at index <i>: <err>"` shapes; the first 10 are
1970///   listed and the remainder is summarized as `"and N more"`.
1971/// - `Vec<Option<T>>`: NA elements → `None`; parse failures batch as above.
1972///
1973/// The parse body is a closure-style `|s| expr` where `s: &str`, returning
1974/// `Result<T, E>` with `E: Display`.
1975///
1976/// ```ignore
1977/// try_from_sexp_via_str_parse!(Uuid, "UUID", |s| Uuid::parse_str(s));
1978/// ```
1979#[macro_export]
1980macro_rules! try_from_sexp_via_str_parse {
1981    ($ty:ty, $label:literal, |$s:ident| $parse:expr) => {
1982        impl $crate::from_r::TryFromSexp for $ty {
1983            type Error = $crate::from_r::SexpError;
1984
1985            fn try_from_sexp(sexp: $crate::SEXP) -> Result<Self, Self::Error> {
1986                let opt: Option<String> = $crate::from_r::TryFromSexp::try_from_sexp(sexp)?;
1987                let s = opt.ok_or($crate::from_r::SexpError::Na($crate::from_r::SexpNaError {
1988                    sexp_type: $crate::SEXPTYPE::STRSXP,
1989                }))?;
1990                let $s: &str = &s;
1991                ($parse).map_err(|e| {
1992                    $crate::from_r::SexpError::InvalidValue(format!(
1993                        concat!("invalid ", $label, ": {}"),
1994                        e
1995                    ))
1996                })
1997            }
1998        }
1999
2000        impl $crate::from_r::TryFromSexp for Option<$ty> {
2001            type Error = $crate::from_r::SexpError;
2002
2003            fn try_from_sexp(sexp: $crate::SEXP) -> Result<Self, Self::Error> {
2004                let opt: Option<String> = $crate::from_r::TryFromSexp::try_from_sexp(sexp)?;
2005                match opt {
2006                    None => Ok(None),
2007                    Some(s) => {
2008                        let $s: &str = &s;
2009                        ($parse).map(Some).map_err(|e| {
2010                            $crate::from_r::SexpError::InvalidValue(format!(
2011                                concat!("invalid ", $label, ": {}"),
2012                                e
2013                            ))
2014                        })
2015                    }
2016                }
2017            }
2018        }
2019
2020        impl $crate::from_r::TryFromSexp for Vec<$ty> {
2021            type Error = $crate::from_r::SexpError;
2022
2023            fn try_from_sexp(sexp: $crate::SEXP) -> Result<Self, Self::Error> {
2024                let values: Vec<Option<String>> = $crate::from_r::TryFromSexp::try_from_sexp(sexp)?;
2025                let mut result = Vec::with_capacity(values.len());
2026                let mut errors: Vec<String> = Vec::new();
2027                for (i, opt) in values.into_iter().enumerate() {
2028                    match opt {
2029                        None => errors.push(format!(
2030                            concat!("NA at index {} not allowed for Vec<", stringify!($ty), ">"),
2031                            i
2032                        )),
2033                        Some(s) => {
2034                            let $s: &str = &s;
2035                            match ($parse) {
2036                                Ok(v) => result.push(v),
2037                                Err(e) => errors.push(format!(
2038                                    concat!("invalid ", $label, " at index {}: {}"),
2039                                    i, e
2040                                )),
2041                            }
2042                        }
2043                    }
2044                }
2045                if errors.is_empty() {
2046                    Ok(result)
2047                } else {
2048                    Err($crate::from_r::batch_conversion_errors(
2049                        concat!("Vec<", stringify!($ty), ">"),
2050                        errors,
2051                    ))
2052                }
2053            }
2054        }
2055
2056        impl $crate::from_r::TryFromSexp for Vec<Option<$ty>> {
2057            type Error = $crate::from_r::SexpError;
2058
2059            fn try_from_sexp(sexp: $crate::SEXP) -> Result<Self, Self::Error> {
2060                let values: Vec<Option<String>> = $crate::from_r::TryFromSexp::try_from_sexp(sexp)?;
2061                let mut result = Vec::with_capacity(values.len());
2062                let mut errors: Vec<String> = Vec::new();
2063                for (i, opt) in values.into_iter().enumerate() {
2064                    match opt {
2065                        None => result.push(None),
2066                        Some(s) => {
2067                            let $s: &str = &s;
2068                            match ($parse) {
2069                                Ok(v) => result.push(Some(v)),
2070                                Err(e) => errors.push(format!(
2071                                    concat!("invalid ", $label, " at index {}: {}"),
2072                                    i, e
2073                                )),
2074                            }
2075                        }
2076                    }
2077                }
2078                if errors.is_empty() {
2079                    Ok(result)
2080                } else {
2081                    Err($crate::from_r::batch_conversion_errors(
2082                        concat!("Vec<Option<", stringify!($ty), ">>"),
2083                        errors,
2084                    ))
2085                }
2086            }
2087        }
2088    };
2089}
2090// endregion