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 hands out a `&mut` view over R's data pointer without copying, so
653/// binding the same R vector to two slice parameters aliases one buffer. That is
654/// undefined behavior whenever at least one borrow is mutable — two `&mut [T]`,
655/// or a `&mut [T]` paired with a shared `&[T]` (which borrows the same buffer).
656/// The macro-generated wrapper emits a `debug_assert!` catching this in debug
657/// builds (#1104); in release the caller is responsible for not passing the same
658/// SEXP to two such parameters when one is mutable.
659impl<T> TryFromSexp for &mut [T]
660where
661    T: crate::RNativeType + Copy,
662{
663    type Error = SexpTypeError;
664
665    #[inline]
666    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
667        let actual = sexp.type_of();
668        if actual != T::SEXP_TYPE {
669            return Err(SexpTypeError {
670                expected: T::SEXP_TYPE,
671                actual,
672            });
673        }
674        let len = sexp.len();
675        let ptr = unsafe { T::dataptr_mut(sexp) };
676        Ok(unsafe { r_slice_mut(ptr, len) })
677    }
678
679    #[inline]
680    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
681        let actual = sexp.type_of();
682        if actual != T::SEXP_TYPE {
683            return Err(SexpTypeError {
684                expected: T::SEXP_TYPE,
685                actual,
686            });
687        }
688        let len = unsafe { sexp.len_unchecked() };
689        let ptr = unsafe { T::dataptr_mut(sexp) };
690        Ok(unsafe { r_slice_mut(ptr, len) })
691    }
692}
693
694/// Blanket impl for `Option<&[T]>` where T: RNativeType
695impl<T> TryFromSexp for Option<&[T]>
696where
697    T: crate::RNativeType + Copy,
698{
699    type Error = SexpError;
700
701    #[inline]
702    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
703        if sexp.type_of() == SEXPTYPE::NILSXP {
704            return Ok(None);
705        }
706        let slice: &[T] = TryFromSexp::try_from_sexp(sexp).map_err(SexpError::from)?;
707        Ok(Some(slice))
708    }
709
710    #[inline]
711    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
712        if sexp.type_of() == SEXPTYPE::NILSXP {
713            return Ok(None);
714        }
715        let slice: &[T] =
716            unsafe { TryFromSexp::try_from_sexp_unchecked(sexp).map_err(SexpError::from)? };
717        Ok(Some(slice))
718    }
719}
720
721/// Blanket impl for `Option<&mut [T]>` where T: RNativeType
722impl<T> TryFromSexp for Option<&mut [T]>
723where
724    T: crate::RNativeType + Copy,
725{
726    type Error = SexpError;
727
728    #[inline]
729    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
730        if sexp.type_of() == SEXPTYPE::NILSXP {
731            return Ok(None);
732        }
733        let slice: &mut [T] = TryFromSexp::try_from_sexp(sexp).map_err(SexpError::from)?;
734        Ok(Some(slice))
735    }
736
737    #[inline]
738    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
739        if sexp.type_of() == SEXPTYPE::NILSXP {
740            return Ok(None);
741        }
742        let slice: &mut [T] =
743            unsafe { TryFromSexp::try_from_sexp_unchecked(sexp).map_err(SexpError::from)? };
744        Ok(Some(slice))
745    }
746}
747// endregion
748
749mod strings;
750
751// region: Result conversions (NULL -> Err(()))
752
753impl<T> TryFromSexp for Result<T, ()>
754where
755    T: TryFromSexp,
756    T::Error: Into<SexpError>,
757{
758    type Error = SexpError;
759
760    #[inline]
761    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
762        if sexp.type_of() == SEXPTYPE::NILSXP {
763            return Ok(Err(()));
764        }
765        let value = T::try_from_sexp(sexp).map_err(Into::into)?;
766        Ok(Ok(value))
767    }
768
769    #[inline]
770    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
771        if sexp.type_of() == SEXPTYPE::NILSXP {
772            return Ok(Err(()));
773        }
774        let value = unsafe { T::try_from_sexp_unchecked(sexp).map_err(Into::into)? };
775        Ok(Ok(value))
776    }
777}
778// endregion
779
780mod na_vectors;
781
782mod collections;
783
784mod tuples;
785
786// region: Fixed-size array conversions
787
788/// Blanket impl: Convert R vector to `[T; N]` where T: RNativeType.
789///
790/// Returns an error if the R vector length doesn't match N.
791/// Useful for SHA hashes ([u8; 32]), fixed-size patterns, etc.
792impl<T, const N: usize> TryFromSexp for [T; N]
793where
794    T: crate::RNativeType + Copy,
795{
796    type Error = SexpError;
797
798    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
799        let slice: &[T] = TryFromSexp::try_from_sexp(sexp)?;
800        if slice.len() != N {
801            return Err(SexpLengthError {
802                expected: N,
803                actual: slice.len(),
804            }
805            .into());
806        }
807
808        // T: Copy, length verified above. Use MaybeUninit + copy_from_slice.
809        let mut arr = std::mem::MaybeUninit::<[T; N]>::uninit();
810        unsafe {
811            // SAFETY: MaybeUninit<[T; N]> and [T; N] have the same layout.
812            // We write all N elements via copy_from_slice, so assume_init is safe.
813            let dst: &mut [T] = std::slice::from_raw_parts_mut(arr.as_mut_ptr().cast::<T>(), N);
814            dst.copy_from_slice(&slice[..N]);
815            Ok(arr.assume_init())
816        }
817    }
818
819    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
820        Self::try_from_sexp(sexp)
821    }
822}
823// endregion
824
825// region: VecDeque conversions
826
827use std::collections::VecDeque;
828
829/// Blanket impl: Convert R vector to `VecDeque<T>` where T: RNativeType.
830impl<T> TryFromSexp for VecDeque<T>
831where
832    T: crate::RNativeType + Copy,
833{
834    type Error = SexpTypeError;
835
836    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
837        let slice: &[T] = TryFromSexp::try_from_sexp(sexp)?;
838        Ok(VecDeque::from(slice.to_vec()))
839    }
840
841    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
842        let slice: &[T] = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
843        Ok(VecDeque::from(slice.to_vec()))
844    }
845}
846// endregion
847
848// region: BinaryHeap conversions
849
850use std::collections::BinaryHeap;
851
852/// Blanket impl: Convert R vector to `BinaryHeap<T>` where T: RNativeType + Ord.
853///
854/// Creates a binary heap from the R vector elements.
855impl<T> TryFromSexp for BinaryHeap<T>
856where
857    T: crate::RNativeType + Copy + Ord,
858{
859    type Error = SexpTypeError;
860
861    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
862        let slice: &[T] = TryFromSexp::try_from_sexp(sexp)?;
863        Ok(BinaryHeap::from(slice.to_vec()))
864    }
865
866    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
867        let slice: &[T] = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
868        Ok(BinaryHeap::from(slice.to_vec()))
869    }
870}
871// endregion
872
873mod cow_and_paths;
874
875// region: Option<Collection> conversions
876//
877// These convert NULL → None, and non-NULL to Some(collection).
878// This differs from Option<scalar> which converts NA → None.
879
880/// Convert R value to `Option<Vec<T>>`: NULL → None, otherwise Some(vec).
881impl<T> TryFromSexp for Option<Vec<T>>
882where
883    Vec<T>: TryFromSexp,
884    <Vec<T> as TryFromSexp>::Error: Into<SexpError>,
885{
886    type Error = SexpError;
887
888    #[inline]
889    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
890        if sexp.type_of() == SEXPTYPE::NILSXP {
891            Ok(None)
892        } else {
893            Vec::<T>::try_from_sexp(sexp).map(Some).map_err(Into::into)
894        }
895    }
896
897    #[inline]
898    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
899        if sexp.type_of() == SEXPTYPE::NILSXP {
900            Ok(None)
901        } else {
902            unsafe {
903                Vec::<T>::try_from_sexp_unchecked(sexp)
904                    .map(Some)
905                    .map_err(Into::into)
906            }
907        }
908    }
909}
910
911macro_rules! impl_option_map_try_from_sexp {
912    ($(#[$meta:meta])* $map_ty:ident) => {
913        $(#[$meta])*
914        impl<V: TryFromSexp> TryFromSexp for Option<$map_ty<String, V>>
915        where
916            V::Error: Into<SexpError>,
917        {
918            type Error = SexpError;
919
920            #[inline]
921            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
922                if sexp.type_of() == SEXPTYPE::NILSXP {
923                    Ok(None)
924                } else {
925                    $map_ty::<String, V>::try_from_sexp(sexp).map(Some)
926                }
927            }
928        }
929    };
930}
931
932impl_option_map_try_from_sexp!(
933    /// Convert R value to `Option<HashMap<String, V>>`: NULL -> None, otherwise Some(map).
934    HashMap
935);
936impl_option_map_try_from_sexp!(
937    /// Convert R value to `Option<BTreeMap<String, V>>`: NULL -> None, otherwise Some(map).
938    BTreeMap
939);
940
941macro_rules! impl_option_set_try_from_sexp {
942    ($(#[$meta:meta])* $set_ty:ident) => {
943        $(#[$meta])*
944        impl<T> TryFromSexp for Option<$set_ty<T>>
945        where
946            $set_ty<T>: TryFromSexp,
947            <$set_ty<T> as TryFromSexp>::Error: Into<SexpError>,
948        {
949            type Error = SexpError;
950
951            #[inline]
952            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
953                if sexp.type_of() == SEXPTYPE::NILSXP {
954                    Ok(None)
955                } else {
956                    $set_ty::<T>::try_from_sexp(sexp)
957                        .map(Some)
958                        .map_err(Into::into)
959                }
960            }
961        }
962    };
963}
964
965impl_option_set_try_from_sexp!(
966    /// Convert R value to `Option<HashSet<T>>`: NULL -> None, otherwise Some(set).
967    HashSet
968);
969impl_option_set_try_from_sexp!(
970    /// Convert R value to `Option<BTreeSet<T>>`: NULL -> None, otherwise Some(set).
971    BTreeSet
972);
973// endregion
974
975// region: Nested vector conversions (list of vectors)
976
977/// Convert R list (VECSXP) to `Vec<Vec<T>>`.
978///
979/// Each element of the R list must be convertible to `Vec<T>`.
980impl<T> TryFromSexp for Vec<Vec<T>>
981where
982    Vec<T>: TryFromSexp,
983    <Vec<T> as TryFromSexp>::Error: Into<SexpError>,
984{
985    type Error = SexpError;
986
987    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
988        map_vecsxp_with(sexp, |_i, elem| {
989            Vec::<T>::try_from_sexp(elem).map_err(Into::into)
990        })
991    }
992
993    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
994        unsafe {
995            map_vecsxp_with_unchecked(sexp, |_i, elem| {
996                Vec::<T>::try_from_sexp_unchecked(elem).map_err(Into::into)
997            })
998        }
999    }
1000}
1001// endregion
1002
1003// region: Coerced wrapper - bridge between TryFromSexp and TryCoerce
1004
1005use crate::coerce::Coerced;
1006
1007/// Convert R value to `Coerced<T, R>` by reading `R` and coercing to `T`.
1008///
1009/// This enables reading non-native Rust types from R with coercion:
1010///
1011/// ```ignore
1012/// // Read i64 from R integer (i32)
1013/// let val: Coerced<i64, i32> = TryFromSexp::try_from_sexp(sexp)?;
1014/// let i64_val: i64 = val.into_inner();
1015///
1016/// // Works with collections too:
1017/// let vec: Vec<Coerced<i64, i32>> = ...;
1018/// let set: HashSet<Coerced<NonZeroU32, i32>> = ...;
1019/// ```
1020impl<T, R> TryFromSexp for Coerced<T, R>
1021where
1022    R: TryFromSexp,
1023    R: TryCoerce<T>,
1024    <R as TryFromSexp>::Error: Into<SexpError>,
1025    <R as TryCoerce<T>>::Error: std::fmt::Display,
1026{
1027    type Error = SexpError;
1028
1029    #[inline]
1030    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1031        let r_val: R = R::try_from_sexp(sexp).map_err(Into::into)?;
1032        let value: T = r_val
1033            .try_coerce()
1034            .map_err(|e| SexpError::InvalidValue(format!("{e}")))?;
1035        Ok(Coerced::new(value))
1036    }
1037
1038    #[inline]
1039    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1040        let r_val: R = unsafe { R::try_from_sexp_unchecked(sexp).map_err(Into::into)? };
1041        let value: T = r_val
1042            .try_coerce()
1043            .map_err(|e| SexpError::InvalidValue(format!("{e}")))?;
1044        Ok(Coerced::new(value))
1045    }
1046}
1047// endregion
1048
1049// region: Direct Vec coercion conversions
1050//
1051// These provide direct `TryFromSexp for Vec<T>` where T is not an R native type
1052// but can be coerced from one. This mirrors the `impl_into_r_via_coerce!` pattern
1053// in into_r.rs for the reverse direction.
1054
1055/// Helper to coerce a slice element-wise into a Vec.
1056///
1057/// Walks the whole slice, accumulating every per-element coercion failure into
1058/// one batched [`SexpError::InvalidValue`] via [`BatchedErrors`] (`container`
1059/// names the target type, e.g. `"Vec<bool>"`) instead of bailing on the first
1060/// `Err`. The happy path allocates nothing for diagnostics, and a large
1061/// all-failing slice never builds more than [`BATCHED_ERROR_CAP`] messages.
1062#[inline]
1063fn coerce_slice_to_vec<R, T>(slice: &[R], container: &str) -> Result<Vec<T>, SexpError>
1064where
1065    R: Copy + TryCoerce<T>,
1066    <R as TryCoerce<T>>::Error: std::fmt::Display,
1067{
1068    let mut result = Vec::with_capacity(slice.len());
1069    let mut errors = BatchedErrors::default();
1070    for (i, v) in slice.iter().copied().enumerate() {
1071        match v.try_coerce() {
1072            Ok(x) => result.push(x),
1073            Err(e) => errors.push(|| format!("invalid value at index {i}: {e}")),
1074        }
1075    }
1076    if errors.is_empty() {
1077        Ok(result)
1078    } else {
1079        Err(errors.into_error(container))
1080    }
1081}
1082
1083/// Drive a per-element coercion, batching every failure into one diagnostic.
1084///
1085/// Backs [`from_numeric_vec_with`]'s four SEXP-type branches: successes go into
1086/// the output `Vec`, failures accumulate as `"invalid value at index <i>: <err>"`
1087/// and are combined via [`BatchedErrors`] (matching the message grammar of the
1088/// `Vec<T>` arm of `try_from_sexp_via_str_parse!`). The per-element closure wraps
1089/// coercion failures as [`SexpError::InvalidValue`]; we unwrap that inner message
1090/// so the batched entry reads `"...: value out of range"` rather than the doubled
1091/// `"...: invalid value: value out of range"` that `SexpError`'s `Display` would
1092/// produce. The happy path allocates nothing for diagnostics, and a large
1093/// all-failing vector never builds more than [`BATCHED_ERROR_CAP`] messages.
1094#[inline]
1095fn collect_coerced<U>(
1096    container: &str,
1097    len: usize,
1098    iter: impl Iterator<Item = Result<U, SexpError>>,
1099) -> Result<Vec<U>, SexpError> {
1100    let mut result = Vec::with_capacity(len);
1101    let mut errors = BatchedErrors::default();
1102    for (i, item) in iter.enumerate() {
1103        match item {
1104            Ok(v) => result.push(v),
1105            Err(SexpError::InvalidValue(msg)) => {
1106                errors.push(|| format!("invalid value at index {i}: {msg}"))
1107            }
1108            Err(other) => errors.push(|| format!("invalid value at index {i}: {other}")),
1109        }
1110    }
1111    if errors.is_empty() {
1112        Ok(result)
1113    } else {
1114        Err(errors.into_error(container))
1115    }
1116}
1117
1118/// Shared SEXP-dispatch shell for coerced numeric/logical/raw vectors.
1119///
1120/// Reads INTSXP/REALSXP/RAWSXP/LGLSXP and applies the per-element map. The only
1121/// behavioural axis (NA policy) lives entirely in the four closures the caller
1122/// passes, so the NA-unaware [`try_from_sexp_numeric_vec`] and the NA-aware
1123/// `try_from_sexp_numeric_option_vec` (in [`na_vectors`]) share one dispatch.
1124/// LGLSXP NA (`NA_LOGICAL`) and INTSXP NA (`i32::MIN`) round through as raw
1125/// sentinels here; any NA-to-`None` policy is the caller's closure to encode.
1126///
1127/// Per-element coercion failures are accumulated (not short-circuited) and
1128/// reported as one batched diagnostic; `container` names the target type for the
1129/// message (e.g. `"Vec<u32>"`, `"HashSet<i64>"`). See [`collect_coerced`].
1130#[inline]
1131pub(crate) fn from_numeric_vec_with<U, FI, FD, FR, FL>(
1132    sexp: SEXP,
1133    container: &str,
1134    map_i32: FI,
1135    map_f64: FD,
1136    map_u8: FR,
1137    map_lgl: FL,
1138) -> Result<Vec<U>, SexpError>
1139where
1140    FI: Fn(i32) -> Result<U, SexpError>,
1141    FD: Fn(f64) -> Result<U, SexpError>,
1142    FR: Fn(u8) -> Result<U, SexpError>,
1143    FL: Fn(RLogical) -> Result<U, SexpError>,
1144{
1145    let actual = sexp.type_of();
1146    match actual {
1147        SEXPTYPE::INTSXP => {
1148            let slice: &[i32] = unsafe { sexp.as_slice() };
1149            collect_coerced(container, slice.len(), slice.iter().copied().map(map_i32))
1150        }
1151        SEXPTYPE::REALSXP => {
1152            let slice: &[f64] = unsafe { sexp.as_slice() };
1153            collect_coerced(container, slice.len(), slice.iter().copied().map(map_f64))
1154        }
1155        SEXPTYPE::RAWSXP => {
1156            let slice: &[u8] = unsafe { sexp.as_slice() };
1157            collect_coerced(container, slice.len(), slice.iter().copied().map(map_u8))
1158        }
1159        SEXPTYPE::LGLSXP => {
1160            let slice: &[RLogical] = unsafe { sexp.as_slice() };
1161            collect_coerced(container, slice.len(), slice.iter().copied().map(map_lgl))
1162        }
1163        _ => Err(SexpError::InvalidValue(format!(
1164            "expected integer, numeric, logical, or raw; got {:?}",
1165            actual
1166        ))),
1167    }
1168}
1169
1170/// Shared SEXP-dispatch shell for the STRSXP (character vector) walk.
1171///
1172/// String-side counterpart of [`from_numeric_vec_with`]: checks `STRSXP`,
1173/// walks each element via `string_elt`, and applies the per-element `map`
1174/// closure to the raw CHARSXP. NA/blank-string policy (`""` vs `None` vs
1175/// error) and the target representation (`String` vs `&str` vs `Cow`) are
1176/// entirely the caller's closure to encode — this only centralizes the type
1177/// check and the walk.
1178///
1179/// Mirrors `from_numeric_vec_with`'s choice to route both the checked and
1180/// unchecked `TryFromSexp` paths through the same (checked-FFI) walk — none
1181/// of the current STRSXP vector impls define a distinct unchecked fast path
1182/// (they fall back to `TryFromSexp`'s default `try_from_sexp_unchecked`, which
1183/// just calls the checked version), so there is no unchecked-FFI twin here.
1184#[inline]
1185pub(crate) fn map_strsxp_with<U>(
1186    sexp: SEXP,
1187    mut map: impl FnMut(SEXP /* charsxp */, usize) -> Result<U, SexpError>,
1188) -> Result<Vec<U>, SexpError> {
1189    let actual = sexp.type_of();
1190    if actual != SEXPTYPE::STRSXP {
1191        return Err(SexpTypeError {
1192            expected: SEXPTYPE::STRSXP,
1193            actual,
1194        }
1195        .into());
1196    }
1197
1198    let len = sexp.len();
1199    let mut result = Vec::with_capacity(len);
1200    for i in 0..len {
1201        let charsxp = sexp.string_elt(i as crate::R_xlen_t);
1202        result.push(map(charsxp, i)?);
1203    }
1204    Ok(result)
1205}
1206
1207/// Shared scalar-STRSXP prologue: type-check + `len == 1` + `string_elt(0)`.
1208///
1209/// Returns the raw CHARSXP; NA (`SEXP::na_string()`) and blank-string
1210/// (`SEXP::blank_string()`) policy stay the caller's decision — some sites
1211/// error on NA, some map it to `""`, some to `None`, and `String`'s blank
1212/// handling has historically differed from `&str`'s (see audit D6 finding
1213/// #3), so this helper does not paper over that divergence.
1214#[inline]
1215pub(crate) fn scalar_charsxp(sexp: SEXP) -> Result<SEXP, SexpError> {
1216    let actual = sexp.type_of();
1217    if actual != SEXPTYPE::STRSXP {
1218        return Err(SexpTypeError {
1219            expected: SEXPTYPE::STRSXP,
1220            actual,
1221        }
1222        .into());
1223    }
1224
1225    let len = sexp.len();
1226    if len != 1 {
1227        return Err(SexpLengthError {
1228            expected: 1,
1229            actual: len,
1230        }
1231        .into());
1232    }
1233
1234    Ok(sexp.string_elt(0))
1235}
1236
1237/// Unchecked-FFI variant of [`scalar_charsxp`] — uses `len_unchecked` /
1238/// `string_elt_unchecked`.
1239///
1240/// # Safety
1241///
1242/// Must be called from R's main thread (same contract as
1243/// [`TryFromSexp::try_from_sexp_unchecked`]).
1244#[inline]
1245pub(crate) unsafe fn scalar_charsxp_unchecked(sexp: SEXP) -> Result<SEXP, SexpError> {
1246    let actual = sexp.type_of();
1247    if actual != SEXPTYPE::STRSXP {
1248        return Err(SexpTypeError {
1249            expected: SEXPTYPE::STRSXP,
1250            actual,
1251        }
1252        .into());
1253    }
1254
1255    let len = unsafe { sexp.len_unchecked() };
1256    if len != 1 {
1257        return Err(SexpLengthError {
1258            expected: 1,
1259            actual: len,
1260        }
1261        .into());
1262    }
1263
1264    Ok(unsafe { sexp.string_elt_unchecked(0) })
1265}
1266
1267/// Shared SEXP-dispatch shell for the VECSXP (list) walk.
1268///
1269/// List-side counterpart of [`from_numeric_vec_with`] / [`map_strsxp_with`]:
1270/// checks `VECSXP`, walks each element via `vector_elt`, and applies the
1271/// per-element `map` closure (which receives the element's index and SEXP).
1272/// Per-element policy (recursion, `NILSXP` → `None`, duplicate-pointer
1273/// aliasing checks, …) lives entirely in the closure.
1274#[inline]
1275pub(crate) fn map_vecsxp_with<U>(
1276    sexp: SEXP,
1277    mut map: impl FnMut(usize, SEXP) -> Result<U, SexpError>,
1278) -> Result<Vec<U>, SexpError> {
1279    let actual = sexp.type_of();
1280    if actual != SEXPTYPE::VECSXP {
1281        return Err(SexpTypeError {
1282            expected: SEXPTYPE::VECSXP,
1283            actual,
1284        }
1285        .into());
1286    }
1287
1288    let len = sexp.len();
1289    let mut result = Vec::with_capacity(len);
1290    for i in 0..len {
1291        let elem = sexp.vector_elt(i as crate::R_xlen_t);
1292        result.push(map(i, elem)?);
1293    }
1294    Ok(result)
1295}
1296
1297/// Unchecked-FFI variant of [`map_vecsxp_with`] — uses `len_unchecked` /
1298/// `vector_elt_unchecked` for the type-check and walk.
1299///
1300/// # Safety
1301///
1302/// Must be called from R's main thread (same contract as
1303/// [`TryFromSexp::try_from_sexp_unchecked`]).
1304#[inline]
1305pub(crate) unsafe fn map_vecsxp_with_unchecked<U>(
1306    sexp: SEXP,
1307    mut map: impl FnMut(usize, SEXP) -> Result<U, SexpError>,
1308) -> Result<Vec<U>, SexpError> {
1309    let actual = sexp.type_of();
1310    if actual != SEXPTYPE::VECSXP {
1311        return Err(SexpTypeError {
1312            expected: SEXPTYPE::VECSXP,
1313            actual,
1314        }
1315        .into());
1316    }
1317
1318    let len = unsafe { sexp.len_unchecked() };
1319    let mut result = Vec::with_capacity(len);
1320    for i in 0..len {
1321        let elem = unsafe { sexp.vector_elt_unchecked(i as crate::R_xlen_t) };
1322        result.push(map(i, elem)?);
1323    }
1324    Ok(result)
1325}
1326
1327/// Convert numeric/logical/raw vectors to `Vec<T>` with element-wise coercion.
1328///
1329/// NA-unaware: an R `NA` round-trips as the coerced sentinel rather than being
1330/// rejected. Bind `Vec<Option<T>>` (see [`na_vectors`]) when the caller can pass NA.
1331/// Per-element coercion failures batch into one diagnostic (`container` names the
1332/// target type for the message, e.g. `"Vec<u32>"`).
1333#[inline]
1334fn try_from_sexp_numeric_vec<T>(sexp: SEXP, container: &str) -> Result<Vec<T>, SexpError>
1335where
1336    i32: TryCoerce<T>,
1337    f64: TryCoerce<T>,
1338    u8: TryCoerce<T>,
1339    <i32 as TryCoerce<T>>::Error: std::fmt::Display,
1340    <f64 as TryCoerce<T>>::Error: std::fmt::Display,
1341    <u8 as TryCoerce<T>>::Error: std::fmt::Display,
1342{
1343    from_numeric_vec_with(
1344        sexp,
1345        container,
1346        coerce_value,
1347        coerce_value,
1348        coerce_value,
1349        |v: RLogical| coerce_value(v.to_i32()),
1350    )
1351}
1352
1353/// Implement `TryFromSexp for Vec<$target>` by coercing from integer/real/logical/raw.
1354macro_rules! impl_vec_try_from_sexp_numeric {
1355    ($target:ty) => {
1356        impl TryFromSexp for Vec<$target> {
1357            type Error = SexpError;
1358
1359            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1360                try_from_sexp_numeric_vec(sexp, concat!("Vec<", stringify!($target), ">"))
1361            }
1362
1363            unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1364                try_from_sexp_numeric_vec(sexp, concat!("Vec<", stringify!($target), ">"))
1365            }
1366        }
1367    };
1368}
1369
1370impl_vec_try_from_sexp_numeric!(i8);
1371impl_vec_try_from_sexp_numeric!(i16);
1372impl_vec_try_from_sexp_numeric!(i64);
1373impl_vec_try_from_sexp_numeric!(isize);
1374impl_vec_try_from_sexp_numeric!(u16);
1375impl_vec_try_from_sexp_numeric!(u32);
1376impl_vec_try_from_sexp_numeric!(u64);
1377impl_vec_try_from_sexp_numeric!(usize);
1378impl_vec_try_from_sexp_numeric!(f32);
1379
1380/// Convert R logical vector (LGLSXP) to `Vec<bool>` (errors on NA).
1381impl TryFromSexp for Vec<bool> {
1382    type Error = SexpError;
1383
1384    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1385        let actual = sexp.type_of();
1386        if actual != SEXPTYPE::LGLSXP {
1387            return Err(SexpTypeError {
1388                expected: SEXPTYPE::LGLSXP,
1389                actual,
1390            }
1391            .into());
1392        }
1393        let slice: &[RLogical] = unsafe { sexp.as_slice() };
1394        coerce_slice_to_vec(slice, "Vec<bool>")
1395    }
1396
1397    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1398        Self::try_from_sexp(sexp)
1399    }
1400}
1401// endregion
1402
1403// region: Direct HashSet / BTreeSet coercion conversions
1404
1405/// Convert numeric/logical/raw vectors to a set type with element-wise coercion.
1406///
1407/// Inherits [`try_from_sexp_numeric_vec`]'s batching; `container` names the target
1408/// set type for the message (e.g. `"HashSet<u32>"`).
1409#[inline]
1410fn try_from_sexp_numeric_set<T, S>(sexp: SEXP, container: &str) -> Result<S, SexpError>
1411where
1412    S: std::iter::FromIterator<T>,
1413    i32: TryCoerce<T>,
1414    f64: TryCoerce<T>,
1415    u8: TryCoerce<T>,
1416    <i32 as TryCoerce<T>>::Error: std::fmt::Display,
1417    <f64 as TryCoerce<T>>::Error: std::fmt::Display,
1418    <u8 as TryCoerce<T>>::Error: std::fmt::Display,
1419{
1420    let vec = try_from_sexp_numeric_vec(sexp, container)?;
1421    Ok(vec.into_iter().collect())
1422}
1423
1424macro_rules! impl_set_try_from_sexp_numeric {
1425    ($set_ty:ident, $target:ty) => {
1426        impl TryFromSexp for $set_ty<$target> {
1427            type Error = SexpError;
1428
1429            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1430                try_from_sexp_numeric_set(
1431                    sexp,
1432                    concat!(stringify!($set_ty), "<", stringify!($target), ">"),
1433                )
1434            }
1435
1436            unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1437                try_from_sexp_numeric_set(
1438                    sexp,
1439                    concat!(stringify!($set_ty), "<", stringify!($target), ">"),
1440                )
1441            }
1442        }
1443    };
1444}
1445
1446impl_set_try_from_sexp_numeric!(HashSet, i8);
1447impl_set_try_from_sexp_numeric!(HashSet, i16);
1448impl_set_try_from_sexp_numeric!(HashSet, i64);
1449impl_set_try_from_sexp_numeric!(HashSet, isize);
1450impl_set_try_from_sexp_numeric!(HashSet, u16);
1451impl_set_try_from_sexp_numeric!(HashSet, u32);
1452impl_set_try_from_sexp_numeric!(HashSet, u64);
1453impl_set_try_from_sexp_numeric!(HashSet, usize);
1454
1455impl_set_try_from_sexp_numeric!(BTreeSet, i8);
1456impl_set_try_from_sexp_numeric!(BTreeSet, i16);
1457impl_set_try_from_sexp_numeric!(BTreeSet, i64);
1458impl_set_try_from_sexp_numeric!(BTreeSet, isize);
1459impl_set_try_from_sexp_numeric!(BTreeSet, u16);
1460impl_set_try_from_sexp_numeric!(BTreeSet, u32);
1461impl_set_try_from_sexp_numeric!(BTreeSet, u64);
1462impl_set_try_from_sexp_numeric!(BTreeSet, usize);
1463
1464macro_rules! impl_set_try_from_sexp_bool {
1465    ($set_ty:ident) => {
1466        impl TryFromSexp for $set_ty<bool> {
1467            type Error = SexpError;
1468
1469            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1470                let vec: Vec<bool> = TryFromSexp::try_from_sexp(sexp)?;
1471                Ok(vec.into_iter().collect())
1472            }
1473
1474            unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1475                Self::try_from_sexp(sexp)
1476            }
1477        }
1478    };
1479}
1480
1481impl_set_try_from_sexp_bool!(HashSet);
1482impl_set_try_from_sexp_bool!(BTreeSet);
1483// endregion
1484
1485// region: ExternalPtr conversions
1486
1487use crate::externalptr::{ExternalPtr, TypeMismatchError, TypedExternal};
1488
1489/// Map a downcast [`TypeMismatchError`] to the [`SexpError`] surfaced from
1490/// `ExternalPtr<T>` argument conversion. Shared by the checked and unchecked
1491/// `TryFromSexp` paths below.
1492fn type_mismatch_to_sexp_error(e: TypeMismatchError) -> SexpError {
1493    match e {
1494        TypeMismatchError::NullPointer => {
1495            SexpError::InvalidValue("external pointer is null".to_string())
1496        }
1497        TypeMismatchError::InvalidTypeId => {
1498            SexpError::InvalidValue("external pointer has no valid type id".to_string())
1499        }
1500        TypeMismatchError::Mismatch { expected, found } => SexpError::InvalidValue(format!(
1501            "type mismatch: expected `{}`, found `{}`",
1502            expected, found
1503        )),
1504    }
1505}
1506
1507/// Error for an `ExternalPtr<T>` argument that is neither a bare `EXTPTRSXP`
1508/// nor a class handle wrapping one (audit A9 — class-wrapped handles like
1509/// `Foo$new(...)` are unwrapped automatically; this fires only when *no*
1510/// `.ptr`/slot/attribute could be recovered at all).
1511fn not_a_handle_error(actual: SEXPTYPE) -> SexpError {
1512    SexpError::InvalidValue(format!(
1513        "expected an external pointer or a miniextendr class object wrapping one, got {:?}",
1514        actual
1515    ))
1516}
1517
1518/// Convert R EXTPTRSXP to `ExternalPtr<T>`.
1519///
1520/// This enables using `ExternalPtr<T>` as parameter types in `#[miniextendr]` functions.
1521///
1522/// # Example
1523///
1524/// ```ignore
1525/// #[derive(ExternalPtr)]
1526/// struct MyData { value: i32 }
1527///
1528/// #[miniextendr]
1529/// fn process(data: ExternalPtr<MyData>) -> i32 {
1530///     data.value
1531/// }
1532/// ```
1533impl<T: TypedExternal + Send> TryFromSexp for ExternalPtr<T> {
1534    type Error = SexpError;
1535
1536    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1537        let actual = sexp.type_of();
1538        if actual != SEXPTYPE::EXTPTRSXP {
1539            // Not a bare pointer — try unwrapping a class-wrapped handle
1540            // (R6 `private$.ptr`, S4 `ptr` slot, S7 `.ptr` attribute; Env/S3
1541            // handles are already bare EXTPTRSXPs and never reach here).
1542            return match unsafe { crate::externalptr::unwrap_class_handle(sexp) } {
1543                Some(inner) => unsafe { ExternalPtr::wrap_sexp_with_error(inner) }
1544                    .map_err(type_mismatch_to_sexp_error),
1545                None => Err(not_a_handle_error(actual)),
1546            };
1547        }
1548
1549        // Use ExternalPtr's type-checked constructor
1550        unsafe { ExternalPtr::wrap_sexp_with_error(sexp) }.map_err(type_mismatch_to_sexp_error)
1551    }
1552
1553    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1554        let actual = sexp.type_of();
1555        if actual != SEXPTYPE::EXTPTRSXP {
1556            return match unsafe { crate::externalptr::unwrap_class_handle(sexp) } {
1557                Some(inner) => {
1558                    unsafe { ExternalPtr::wrap_sexp_unchecked(inner) }.ok_or_else(|| {
1559                        SexpError::InvalidValue(
1560                            "failed to convert external pointer: type mismatch or null pointer"
1561                                .to_string(),
1562                        )
1563                    })
1564                }
1565                None => Err(not_a_handle_error(actual)),
1566            };
1567        }
1568
1569        // Use ExternalPtr's type-checked constructor (unchecked variant)
1570        unsafe { ExternalPtr::wrap_sexp_unchecked(sexp) }.ok_or_else(|| {
1571            SexpError::InvalidValue(
1572                "failed to convert external pointer: type mismatch or null pointer".to_string(),
1573            )
1574        })
1575    }
1576}
1577
1578impl<T: TypedExternal + Send> TryFromSexp for Option<ExternalPtr<T>> {
1579    type Error = SexpError;
1580
1581    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1582        if sexp.type_of() == SEXPTYPE::NILSXP {
1583            return Ok(None);
1584        }
1585        let ptr: ExternalPtr<T> = TryFromSexp::try_from_sexp(sexp)?;
1586        Ok(Some(ptr))
1587    }
1588
1589    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1590        if sexp.type_of() == SEXPTYPE::NILSXP {
1591            return Ok(None);
1592        }
1593        let ptr: ExternalPtr<T> = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
1594        Ok(Some(ptr))
1595    }
1596}
1597
1598/// Convert an R list (VECSXP) of external pointers to `Vec<ExternalPtr<T>>`.
1599///
1600/// Each element must be an `EXTPTRSXP` carrying a `T`; conversion delegates to
1601/// [`ExternalPtr::<T>::try_from_sexp`] per element. This lets `#[miniextendr]`
1602/// functions accept an R `list()` of opaque handles (issue #827). The blanket
1603/// `impl_vec_try_from_sexp_list!` macro can't be used downstream for this — the
1604/// orphan rule rejects `impl TryFromSexp for Vec<ExternalPtr<T>>` in user crates
1605/// because both `Vec` and `TryFromSexp` are foreign there — so the impl lives
1606/// here, keyed on `ExternalPtr<T>` to avoid colliding with the atomic-vector impls.
1607impl<T: TypedExternal + Send> TryFromSexp for Vec<ExternalPtr<T>> {
1608    type Error = SexpError;
1609
1610    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1611        map_vecsxp_with(sexp, |_i, elem| {
1612            <ExternalPtr<T> as TryFromSexp>::try_from_sexp(elem)
1613        })
1614    }
1615
1616    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1617        unsafe {
1618            map_vecsxp_with_unchecked(sexp, |_i, elem| {
1619                <ExternalPtr<T> as TryFromSexp>::try_from_sexp_unchecked(elem)
1620            })
1621        }
1622    }
1623}
1624
1625/// Convert an R list (VECSXP) of external pointers / `NULL`s to
1626/// `Vec<Option<ExternalPtr<T>>>`. `NULL` elements map to `None`; every other
1627/// element must be an `EXTPTRSXP` carrying a `T` (issue #827).
1628impl<T: TypedExternal + Send> TryFromSexp for Vec<Option<ExternalPtr<T>>> {
1629    type Error = SexpError;
1630
1631    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1632        map_vecsxp_with(sexp, |_i, elem| {
1633            <Option<ExternalPtr<T>> as TryFromSexp>::try_from_sexp(elem)
1634        })
1635    }
1636
1637    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1638        unsafe {
1639            map_vecsxp_with_unchecked(sexp, |_i, elem| {
1640                <Option<ExternalPtr<T>> as TryFromSexp>::try_from_sexp_unchecked(elem)
1641            })
1642        }
1643    }
1644}
1645// endregion
1646
1647// region: R connections — TryFromSexp impls (issue #175, #176)
1648
1649#[cfg(feature = "connections")]
1650mod connections_from_r {
1651    use std::ffi::CStr;
1652
1653    use crate::connection::{RNullConnection, RStderr, RStdin, RStdout, Rconn};
1654    use crate::from_r::{SexpError, TryFromSexp};
1655    use crate::{Rboolean, SEXP};
1656
1657    // Read the connection description and class fields from an Rconn handle.
1658    //
1659    // # Safety
1660    // - sexp must be a valid, open R connection SEXP.
1661    // - Must be called from the R main thread.
1662    unsafe fn conn_description(sexp: SEXP) -> Option<String> {
1663        unsafe {
1664            let handle = crate::sys::R_GetConnection(sexp);
1665            let conn = handle.cast::<Rconn>().cast_const();
1666            if (*conn).description.is_null() {
1667                None
1668            } else {
1669                Some(
1670                    CStr::from_ptr((*conn).description)
1671                        .to_string_lossy()
1672                        .into_owned(),
1673                )
1674            }
1675        }
1676    }
1677
1678    unsafe fn conn_class(sexp: SEXP) -> Option<String> {
1679        unsafe {
1680            let handle = crate::sys::R_GetConnection(sexp);
1681            let conn = handle.cast::<Rconn>().cast_const();
1682            if (*conn).class.is_null() {
1683                None
1684            } else {
1685                Some(CStr::from_ptr((*conn).class).to_string_lossy().into_owned())
1686            }
1687        }
1688    }
1689
1690    unsafe fn conn_canwrite(sexp: SEXP) -> bool {
1691        unsafe {
1692            let handle = crate::sys::R_GetConnection(sexp);
1693            let conn = handle.cast::<Rconn>().cast_const();
1694            (*conn).canwrite != Rboolean::FALSE
1695        }
1696    }
1697
1698    unsafe fn conn_isopen(sexp: SEXP) -> bool {
1699        unsafe {
1700            let handle = crate::sys::R_GetConnection(sexp);
1701            let conn = handle.cast::<Rconn>().cast_const();
1702            (*conn).isopen != Rboolean::FALSE
1703        }
1704    }
1705
1706    // Strict validation: confirm description == expected_desc and class == "terminal".
1707    unsafe fn validate_terminal(sexp: SEXP, expected_desc: &str) -> Result<(), SexpError> {
1708        let desc = unsafe { conn_description(sexp) }.unwrap_or_default();
1709        if desc != expected_desc {
1710            return Err(SexpError::InvalidValue(format!(
1711                "expected terminal connection with description {:?}, got {:?}",
1712                expected_desc, desc
1713            )));
1714        }
1715        let cls = unsafe { conn_class(sexp) }.unwrap_or_default();
1716        if cls != "terminal" {
1717            return Err(SexpError::InvalidValue(format!(
1718                "expected class \"terminal\", got {:?}",
1719                cls
1720            )));
1721        }
1722        Ok(())
1723    }
1724
1725    impl TryFromSexp for RStdin {
1726        type Error = SexpError;
1727
1728        fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1729            unsafe { validate_terminal(sexp, "stdin") }?;
1730            Ok(RStdin)
1731        }
1732
1733        unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1734            Self::try_from_sexp(sexp)
1735        }
1736    }
1737
1738    impl TryFromSexp for RStdout {
1739        type Error = SexpError;
1740
1741        fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1742            unsafe { validate_terminal(sexp, "stdout") }?;
1743            Ok(RStdout)
1744        }
1745
1746        unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1747            Self::try_from_sexp(sexp)
1748        }
1749    }
1750
1751    impl TryFromSexp for RStderr {
1752        type Error = SexpError;
1753
1754        fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1755            unsafe { validate_terminal(sexp, "stderr") }?;
1756            Ok(RStderr)
1757        }
1758
1759        unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1760            Self::try_from_sexp(sexp)
1761        }
1762    }
1763
1764    /// Accepts any open, write-capable connection — not just the null device.
1765    ///
1766    /// This is intentional: validating against `description == "/dev/null"` /
1767    /// `"NUL"` is brittle across platforms, and the type's value comes from the
1768    /// RAII close-on-drop, not the specific target. Substituting a `file()`
1769    /// connection for `RNullConnection` is supported.
1770    impl TryFromSexp for RNullConnection {
1771        type Error = SexpError;
1772
1773        fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1774            if !unsafe { conn_isopen(sexp) } {
1775                return Err(SexpError::InvalidValue(
1776                    "expected an open connection".to_string(),
1777                ));
1778            }
1779            if !unsafe { conn_canwrite(sexp) } {
1780                return Err(SexpError::InvalidValue(
1781                    "expected a write-capable connection".to_string(),
1782                ));
1783            }
1784            // Preserve the SEXP so it lives as long as this Rust struct.
1785            unsafe { crate::sys::R_PreserveObject(sexp) };
1786            Ok(unsafe { RNullConnection::from_preserved_sexp(sexp) })
1787        }
1788
1789        unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1790            Self::try_from_sexp(sexp)
1791        }
1792    }
1793}
1794
1795// endregion
1796
1797// region: txtProgressBar — TryFromSexp (issue #177)
1798
1799#[cfg(feature = "connections")]
1800mod txt_progress_bar_from_r {
1801    use crate::from_r::{SexpError, TryFromSexp};
1802    use crate::sys::R_PreserveObject;
1803    use crate::txt_progress_bar::RTxtProgressBar;
1804    use crate::{SEXP, SexpExt};
1805
1806    impl TryFromSexp for RTxtProgressBar {
1807        type Error = SexpError;
1808
1809        fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1810            // Must be a list (VECSXP) with class "txtProgressBar".
1811            if !sexp.inherits_class(c"txtProgressBar") {
1812                return Err(SexpError::InvalidValue(
1813                    "expected a SEXP with class \"txtProgressBar\"".to_string(),
1814                ));
1815            }
1816            // Pin on the precious list so GC cannot collect while Rust holds it.
1817            unsafe { R_PreserveObject(sexp) };
1818            Ok(unsafe { RTxtProgressBar::from_preserved_sexp(sexp) })
1819        }
1820
1821        unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1822            Self::try_from_sexp(sexp)
1823        }
1824    }
1825}
1826
1827// endregion
1828
1829// region: Helper macros for feature-gated modules
1830
1831/// Implement `TryFromSexp for Option<T>` where T already implements TryFromSexp.
1832///
1833/// NULL → None, otherwise delegates to T::try_from_sexp and wraps in Some.
1834#[macro_export]
1835macro_rules! impl_option_try_from_sexp {
1836    ($t:ty) => {
1837        impl $crate::from_r::TryFromSexp for Option<$t> {
1838            type Error = $crate::from_r::SexpError;
1839
1840            fn try_from_sexp(sexp: $crate::SEXP) -> Result<Self, Self::Error> {
1841                use $crate::{SEXPTYPE, SexpExt};
1842                if sexp.type_of() == SEXPTYPE::NILSXP {
1843                    return Ok(None);
1844                }
1845                <$t as $crate::from_r::TryFromSexp>::try_from_sexp(sexp).map(Some)
1846            }
1847
1848            unsafe fn try_from_sexp_unchecked(sexp: $crate::SEXP) -> Result<Self, Self::Error> {
1849                use $crate::{SEXPTYPE, SexpExt};
1850                if sexp.type_of() == SEXPTYPE::NILSXP {
1851                    return Ok(None);
1852                }
1853                unsafe {
1854                    <$t as $crate::from_r::TryFromSexp>::try_from_sexp_unchecked(sexp).map(Some)
1855                }
1856            }
1857        }
1858    };
1859}
1860
1861/// Implement `TryFromSexp for Vec<T>` from R list (VECSXP).
1862///
1863/// Each element is converted via T::try_from_sexp.
1864#[macro_export]
1865macro_rules! impl_vec_try_from_sexp_list {
1866    ($t:ty) => {
1867        impl $crate::from_r::TryFromSexp for Vec<$t> {
1868            type Error = $crate::from_r::SexpError;
1869
1870            fn try_from_sexp(sexp: $crate::SEXP) -> Result<Self, Self::Error> {
1871                use $crate::from_r::SexpTypeError;
1872                use $crate::{SEXPTYPE, SexpExt};
1873
1874                let actual = sexp.type_of();
1875                if actual != SEXPTYPE::VECSXP {
1876                    return Err(SexpTypeError {
1877                        expected: SEXPTYPE::VECSXP,
1878                        actual,
1879                    }
1880                    .into());
1881                }
1882
1883                let len = sexp.len();
1884                let mut result = Vec::with_capacity(len);
1885                for i in 0..len {
1886                    let elem = sexp.vector_elt(i as $crate::R_xlen_t);
1887                    result.push(<$t as $crate::from_r::TryFromSexp>::try_from_sexp(elem)?);
1888                }
1889                Ok(result)
1890            }
1891
1892            unsafe fn try_from_sexp_unchecked(sexp: $crate::SEXP) -> Result<Self, Self::Error> {
1893                use $crate::from_r::SexpTypeError;
1894                use $crate::{SEXPTYPE, SexpExt};
1895
1896                let actual = sexp.type_of();
1897                if actual != SEXPTYPE::VECSXP {
1898                    return Err(SexpTypeError {
1899                        expected: SEXPTYPE::VECSXP,
1900                        actual,
1901                    }
1902                    .into());
1903                }
1904
1905                let len = unsafe { sexp.len_unchecked() };
1906                let mut result = Vec::with_capacity(len);
1907                for i in 0..len {
1908                    let elem = unsafe { sexp.vector_elt_unchecked(i as $crate::R_xlen_t) };
1909                    result.push(unsafe {
1910                        <$t as $crate::from_r::TryFromSexp>::try_from_sexp_unchecked(elem)?
1911                    });
1912                }
1913                Ok(result)
1914            }
1915        }
1916    };
1917}
1918
1919/// Implement `TryFromSexp for Vec<Option<T>>` from R list (VECSXP).
1920///
1921/// NULL elements become None, others are converted via T::try_from_sexp.
1922#[macro_export]
1923macro_rules! impl_vec_option_try_from_sexp_list {
1924    ($t:ty) => {
1925        impl $crate::from_r::TryFromSexp for Vec<Option<$t>> {
1926            type Error = $crate::from_r::SexpError;
1927
1928            fn try_from_sexp(sexp: $crate::SEXP) -> Result<Self, Self::Error> {
1929                use $crate::from_r::SexpTypeError;
1930                use $crate::{SEXPTYPE, SexpExt};
1931
1932                let actual = sexp.type_of();
1933                if actual != SEXPTYPE::VECSXP {
1934                    return Err(SexpTypeError {
1935                        expected: SEXPTYPE::VECSXP,
1936                        actual,
1937                    }
1938                    .into());
1939                }
1940
1941                let len = sexp.len();
1942                let mut result = Vec::with_capacity(len);
1943                for i in 0..len {
1944                    let elem = sexp.vector_elt(i as $crate::R_xlen_t);
1945                    if elem == $crate::SEXP::nil() {
1946                        result.push(None);
1947                    } else {
1948                        result.push(Some(<$t as $crate::from_r::TryFromSexp>::try_from_sexp(
1949                            elem,
1950                        )?));
1951                    }
1952                }
1953                Ok(result)
1954            }
1955
1956            unsafe fn try_from_sexp_unchecked(sexp: $crate::SEXP) -> Result<Self, Self::Error> {
1957                use $crate::from_r::SexpTypeError;
1958                use $crate::{SEXPTYPE, SexpExt};
1959
1960                let actual = sexp.type_of();
1961                if actual != SEXPTYPE::VECSXP {
1962                    return Err(SexpTypeError {
1963                        expected: SEXPTYPE::VECSXP,
1964                        actual,
1965                    }
1966                    .into());
1967                }
1968
1969                let len = unsafe { sexp.len_unchecked() };
1970                let mut result = Vec::with_capacity(len);
1971                for i in 0..len {
1972                    let elem = unsafe { sexp.vector_elt_unchecked(i as $crate::R_xlen_t) };
1973                    if elem == $crate::SEXP::nil() {
1974                        result.push(None);
1975                    } else {
1976                        result.push(Some(unsafe {
1977                            <$t as $crate::from_r::TryFromSexp>::try_from_sexp_unchecked(elem)?
1978                        }));
1979                    }
1980                }
1981                Ok(result)
1982            }
1983        }
1984    };
1985}
1986
1987/// Cap on the number of per-element failures listed in a batched vector
1988/// conversion error; the remainder is summarized as `"and N more"`.
1989///
1990/// `pub(crate)` (rather than file-private) so [`crate::into_r_as`]'s
1991/// `StorageCoerceError::Batched` (#1097) shares the exact same cap instead of
1992/// drifting from the inbound [`BatchedErrors`] path.
1993pub(crate) const BATCHED_ERROR_CAP: usize = 10;
1994
1995/// Bounded accumulator that folds indexed per-element conversion failures into
1996/// one batched [`SexpError::InvalidValue`].
1997///
1998/// Backs the `Vec<T>` / `Vec<Option<T>>` arms of
1999/// [`try_from_sexp_via_str_parse!`] (string-parse paths, #1143) **and** the
2000/// numeric-coercion vector shells [`from_numeric_vec_with`] / [`collect_coerced`]
2001/// / [`coerce_slice_to_vec`] (#1192): instead of bailing on the first NA or
2002/// coercion failure, those walk the whole vector and record each failure here.
2003///
2004/// Only the first [`BATCHED_ERROR_CAP`] messages are retained; every later
2005/// failure is counted but its message closure is never invoked. This keeps an
2006/// all-failing N-element vector from materialising N `String`s just to discard
2007/// all but 10 — the memory held is bounded regardless of input size. On
2008/// [`into_error`](Self::into_error) the retained entries are joined with `"; "`
2009/// and the remainder is summarized as `"and N more"`.
2010///
2011/// Public (but hidden) because `try_from_sexp_via_str_parse!` is
2012/// `#[macro_export]` and expands in downstream crates — not intended to be
2013/// used directly.
2014#[doc(hidden)]
2015#[derive(Default)]
2016pub struct BatchedErrors {
2017    listed: Vec<String>,
2018    total: usize,
2019}
2020
2021impl BatchedErrors {
2022    /// Record one per-element failure. `msg` is evaluated (and its `String`
2023    /// allocated) only for the first [`BATCHED_ERROR_CAP`] failures; later ones
2024    /// are counted for the `"and N more"` tail but never formatted.
2025    #[inline]
2026    pub fn push(&mut self, msg: impl FnOnce() -> String) {
2027        if self.listed.len() < BATCHED_ERROR_CAP {
2028            self.listed.push(msg());
2029        }
2030        self.total += 1;
2031    }
2032
2033    /// Whether any failure has been recorded.
2034    #[inline]
2035    pub fn is_empty(&self) -> bool {
2036        self.total == 0
2037    }
2038
2039    /// Fold the recorded failures into one [`SexpError::InvalidValue`] under
2040    /// `container` (e.g. `"Vec<u32>"`).
2041    pub fn into_error(self, container: &str) -> SexpError {
2042        debug_assert!(self.total > 0, "batching zero conversion errors");
2043        let mut msg = format!("{container} conversion failed: {}", self.listed.join("; "));
2044        if self.total > self.listed.len() {
2045            use std::fmt::Write;
2046            let _ = write!(msg, "; and {} more", self.total - self.listed.len());
2047        }
2048        SexpError::InvalidValue(msg)
2049    }
2050}
2051
2052/// Implement the four string-parse `TryFromSexp` impls (`T`, `Option<T>`,
2053/// `Vec<T>`, `Vec<Option<T>>`) for a type parsed from an R character vector.
2054///
2055/// Sibling of [`into_r_infallible!`](crate::into_r) for the reverse direction:
2056/// every "parse a scalar type out of an R string" integration (uuid, url,
2057/// regex, num-bigint) used to hand-write these four impls — some reinventing
2058/// the STRSXP validation `String`'s own `TryFromSexp` already performs.
2059/// This macro delegates to `Option<String>` / `Vec<Option<String>>`, so type,
2060/// length, and NA checks live in exactly one place.
2061///
2062/// Semantics:
2063/// - `T`: `NA_character_` / `NULL` → `SexpError::Na`; parse failure →
2064///   `InvalidValue("invalid <label>: <err>")`.
2065/// - `Option<T>`: `NA_character_` / `NULL` → `None`.
2066/// - `Vec<T>`: NA elements and parse failures are collected across the whole
2067///   vector into one batched `InvalidValue` (see [`BatchedErrors`]).
2068///   Per-element entries keep the `"NA at index <i> not allowed for Vec<T>"`
2069///   and `"invalid <label> at index <i>: <err>"` shapes; the first 10 are
2070///   listed and the remainder is summarized as `"and N more"`.
2071/// - `Vec<Option<T>>`: NA elements → `None`; parse failures batch as above.
2072///
2073/// The parse body is a closure-style `|s| expr` where `s: &str`, returning
2074/// `Result<T, E>` with `E: Display`.
2075///
2076/// ```ignore
2077/// try_from_sexp_via_str_parse!(Uuid, "UUID", |s| Uuid::parse_str(s));
2078/// ```
2079#[macro_export]
2080macro_rules! try_from_sexp_via_str_parse {
2081    ($ty:ty, $label:literal, |$s:ident| $parse:expr) => {
2082        impl $crate::from_r::TryFromSexp for $ty {
2083            type Error = $crate::from_r::SexpError;
2084
2085            fn try_from_sexp(sexp: $crate::SEXP) -> Result<Self, Self::Error> {
2086                let opt: Option<String> = $crate::from_r::TryFromSexp::try_from_sexp(sexp)?;
2087                let s = opt.ok_or($crate::from_r::SexpError::Na($crate::from_r::SexpNaError {
2088                    sexp_type: $crate::SEXPTYPE::STRSXP,
2089                }))?;
2090                let $s: &str = &s;
2091                ($parse).map_err(|e| {
2092                    $crate::from_r::SexpError::InvalidValue(format!(
2093                        concat!("invalid ", $label, ": {}"),
2094                        e
2095                    ))
2096                })
2097            }
2098        }
2099
2100        impl $crate::from_r::TryFromSexp for Option<$ty> {
2101            type Error = $crate::from_r::SexpError;
2102
2103            fn try_from_sexp(sexp: $crate::SEXP) -> Result<Self, Self::Error> {
2104                let opt: Option<String> = $crate::from_r::TryFromSexp::try_from_sexp(sexp)?;
2105                match opt {
2106                    None => Ok(None),
2107                    Some(s) => {
2108                        let $s: &str = &s;
2109                        ($parse).map(Some).map_err(|e| {
2110                            $crate::from_r::SexpError::InvalidValue(format!(
2111                                concat!("invalid ", $label, ": {}"),
2112                                e
2113                            ))
2114                        })
2115                    }
2116                }
2117            }
2118        }
2119
2120        impl $crate::from_r::TryFromSexp for Vec<$ty> {
2121            type Error = $crate::from_r::SexpError;
2122
2123            fn try_from_sexp(sexp: $crate::SEXP) -> Result<Self, Self::Error> {
2124                let values: Vec<Option<String>> = $crate::from_r::TryFromSexp::try_from_sexp(sexp)?;
2125                let mut result = Vec::with_capacity(values.len());
2126                let mut errors = $crate::from_r::BatchedErrors::default();
2127                for (i, opt) in values.into_iter().enumerate() {
2128                    match opt {
2129                        None => errors.push(|| {
2130                            format!(
2131                                concat!(
2132                                    "NA at index {} not allowed for Vec<",
2133                                    stringify!($ty),
2134                                    ">"
2135                                ),
2136                                i
2137                            )
2138                        }),
2139                        Some(s) => {
2140                            let $s: &str = &s;
2141                            match ($parse) {
2142                                Ok(v) => result.push(v),
2143                                Err(e) => errors.push(|| {
2144                                    format!(concat!("invalid ", $label, " at index {}: {}"), i, e)
2145                                }),
2146                            }
2147                        }
2148                    }
2149                }
2150                if errors.is_empty() {
2151                    Ok(result)
2152                } else {
2153                    Err(errors.into_error(concat!("Vec<", stringify!($ty), ">")))
2154                }
2155            }
2156        }
2157
2158        impl $crate::from_r::TryFromSexp for Vec<Option<$ty>> {
2159            type Error = $crate::from_r::SexpError;
2160
2161            fn try_from_sexp(sexp: $crate::SEXP) -> Result<Self, Self::Error> {
2162                let values: Vec<Option<String>> = $crate::from_r::TryFromSexp::try_from_sexp(sexp)?;
2163                let mut result = Vec::with_capacity(values.len());
2164                let mut errors = $crate::from_r::BatchedErrors::default();
2165                for (i, opt) in values.into_iter().enumerate() {
2166                    match opt {
2167                        None => result.push(None),
2168                        Some(s) => {
2169                            let $s: &str = &s;
2170                            match ($parse) {
2171                                Ok(v) => result.push(Some(v)),
2172                                Err(e) => errors.push(|| {
2173                                    format!(concat!("invalid ", $label, " at index {}: {}"), i, e)
2174                                }),
2175                            }
2176                        }
2177                    }
2178                }
2179                if errors.is_empty() {
2180                    Ok(result)
2181                } else {
2182                    Err(errors.into_error(concat!("Vec<Option<", stringify!($ty), ">>")))
2183                }
2184            }
2185        }
2186    };
2187}
2188// endregion