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//! # Thread Safety
28//!
29//! The trait provides two methods:
30//! - [`TryFromSexp::try_from_sexp`] - checked version with debug thread assertions
31//! - [`TryFromSexp::try_from_sexp_unchecked`] - unchecked version for performance-critical paths
32//!
33//! Use `try_from_sexp_unchecked` when you're certain you're on the main thread:
34//! - Inside ALTREP callbacks
35//! - Inside `#[miniextendr(unsafe(main_thread))]` functions
36//! - Inside `extern "C-unwind"` functions called directly by R
37
38use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
39
40use crate::altrep_traits::NA_REAL;
41use crate::coerce::TryCoerce;
42use crate::ffi::{RLogical, SEXP, SEXPTYPE, SexpExt};
43
44/// Check if an f64 value is R's NA_real_ (a specific NaN bit pattern).
45///
46/// This is different from `f64::is_nan()` which returns true for ALL NaN values.
47/// R's `NA_real_` is a specific NaN with a particular bit pattern, while regular
48/// NaN values (e.g., from `0.0/0.0`) should be preserved as valid values.
49#[inline]
50pub(crate) fn is_na_real(value: f64) -> bool {
51    value.to_bits() == NA_REAL.to_bits()
52}
53
54// region: CHARSXP to string conversion
55
56/// Convert CHARSXP to `&str` — zero-copy from R's string data.
57///
58/// Uses `R_CHAR` + `LENGTH` (O(1), no strlen). UTF-8 validity is guaranteed
59/// by `miniextendr_assert_utf8_locale()` at package init, so no per-string
60/// validation is needed.
61///
62/// # Safety
63///
64/// - `charsxp` must be a valid CHARSXP (not NA_STRING, not null).
65/// - The returned `&str` is only valid as long as R doesn't GC the CHARSXP.
66#[inline]
67pub(crate) unsafe fn charsxp_to_str(charsxp: SEXP) -> &'static str {
68    unsafe { charsxp_to_str_impl(charsxp.r_char(), charsxp) }
69}
70
71/// Unchecked version of [`charsxp_to_str`] (skips R thread checks on `R_CHAR`).
72#[inline]
73pub(crate) unsafe fn charsxp_to_str_unchecked(charsxp: SEXP) -> &'static str {
74    unsafe { charsxp_to_str_impl(charsxp.r_char_unchecked(), charsxp) }
75}
76
77/// Shared implementation: given a data pointer and CHARSXP, produce `&str`.
78///
79/// UTF-8 locale is asserted at init — `from_utf8_unchecked` is safe.
80#[inline]
81unsafe fn charsxp_to_str_impl(ptr: *const std::os::raw::c_char, charsxp: SEXP) -> &'static str {
82    unsafe {
83        let len: usize = charsxp.len();
84        let bytes = r_slice(ptr.cast::<u8>(), len);
85        // SAFETY: miniextendr_assert_utf8_locale() at init guarantees all
86        // CHARSXPs in this session are valid UTF-8 or ASCII.
87        debug_assert!(
88            std::str::from_utf8(bytes).is_ok(),
89            "CHARSXP contains non-UTF-8 bytes (locale assertion may have been skipped)"
90        );
91        std::str::from_utf8_unchecked(bytes)
92    }
93}
94
95/// `charsxp_to_cow` is now just an alias — all CHARSXPs are UTF-8 (asserted
96/// at init), so there's no non-UTF-8 fallback path. Returns `Cow::Borrowed`.
97#[inline]
98pub(crate) unsafe fn charsxp_to_cow(charsxp: SEXP) -> std::borrow::Cow<'static, str> {
99    std::borrow::Cow::Borrowed(unsafe { charsxp_to_str(charsxp) })
100}
101
102/// Create a slice from an R data pointer, handling the zero-length case.
103///
104/// R returns a sentinel pointer (`0x1`) instead of null for empty vectors
105/// (e.g., `LOGICAL(integer(0))` → `0x1`). Rust 1.93+ validates pointer
106/// alignment in `slice::from_raw_parts` even for `len == 0`, so passing
107/// R's sentinel directly causes a precondition-check abort.
108///
109/// This helper returns an empty slice for `len == 0` without touching the pointer.
110///
111/// # Safety
112///
113/// If `len > 0`, `ptr` must satisfy the requirements of [`std::slice::from_raw_parts`].
114#[inline(always)]
115pub(crate) unsafe fn r_slice<'a, T>(ptr: *const T, len: usize) -> &'a [T] {
116    if len == 0 {
117        &[]
118    } else {
119        unsafe { std::slice::from_raw_parts(ptr, len) }
120    }
121}
122
123/// Mutable version of [`r_slice`] for `from_raw_parts_mut`.
124///
125/// # Safety
126///
127/// If `len > 0`, `ptr` must satisfy the requirements of [`std::slice::from_raw_parts_mut`].
128#[inline(always)]
129pub(crate) unsafe fn r_slice_mut<'a, T>(ptr: *mut T, len: usize) -> &'a mut [T] {
130    if len == 0 {
131        &mut []
132    } else {
133        unsafe { std::slice::from_raw_parts_mut(ptr, len) }
134    }
135}
136
137#[derive(Debug, Clone, Copy)]
138/// Error describing an unexpected R `SEXPTYPE`.
139pub struct SexpTypeError {
140    /// Expected R type.
141    pub expected: SEXPTYPE,
142    /// Actual R type encountered.
143    pub actual: SEXPTYPE,
144}
145
146impl std::fmt::Display for SexpTypeError {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        write!(
149            f,
150            "type mismatch: expected {:?}, got {:?}",
151            self.expected, self.actual
152        )
153    }
154}
155
156impl std::error::Error for SexpTypeError {}
157
158#[derive(Debug, Clone, Copy)]
159/// Error describing an unexpected R object length.
160pub struct SexpLengthError {
161    /// Required length.
162    pub expected: usize,
163    /// Actual length encountered.
164    pub actual: usize,
165}
166
167impl std::fmt::Display for SexpLengthError {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        write!(
170            f,
171            "length mismatch: expected {}, got {}",
172            self.expected, self.actual
173        )
174    }
175}
176
177impl std::error::Error for SexpLengthError {}
178
179#[derive(Debug, Clone, Copy)]
180/// Error for NA values in conversions that require non-missing values.
181pub struct SexpNaError {
182    /// R type where an NA was found.
183    pub sexp_type: SEXPTYPE,
184}
185
186impl std::fmt::Display for SexpNaError {
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        write!(f, "unexpected NA value in {:?}", self.sexp_type)
189    }
190}
191
192impl std::error::Error for SexpNaError {}
193
194#[derive(Debug, Clone)]
195/// Unified conversion error when decoding an R `SEXP`.
196pub enum SexpError {
197    /// `SEXPTYPE` did not match the expected one.
198    Type(SexpTypeError),
199    /// Length did not match the expected one.
200    Length(SexpLengthError),
201    /// Missing value encountered where disallowed.
202    Na(SexpNaError),
203    /// Value is syntactically valid but semantically invalid (e.g. parse error).
204    InvalidValue(String),
205    /// A required field was missing from a named list.
206    MissingField(String),
207    /// A named list has duplicate non-empty names.
208    DuplicateName(String),
209    /// Failed to convert to `Either<L, R>` - both branches failed.
210    ///
211    /// Contains the error messages from attempting both conversions.
212    #[cfg(feature = "either")]
213    EitherConversion {
214        /// Error from attempting to convert to the Left type
215        left_error: String,
216        /// Error from attempting to convert to the Right type
217        right_error: String,
218    },
219}
220
221impl std::fmt::Display for SexpError {
222    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223        match self {
224            SexpError::Type(e) => write!(f, "{}", e),
225            SexpError::Length(e) => write!(f, "{}", e),
226            SexpError::Na(e) => write!(f, "{}", e),
227            SexpError::InvalidValue(msg) => write!(f, "invalid value: {}", msg),
228            SexpError::MissingField(name) => write!(f, "missing field: {}", name),
229            SexpError::DuplicateName(name) => write!(f, "duplicate name in list: {:?}", name),
230            #[cfg(feature = "either")]
231            SexpError::EitherConversion {
232                left_error,
233                right_error,
234            } => write!(
235                f,
236                "failed to convert to Either: Left failed ({}), Right failed ({})",
237                left_error, right_error
238            ),
239        }
240    }
241}
242
243impl std::error::Error for SexpError {
244    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
245        match self {
246            SexpError::Type(e) => Some(e),
247            SexpError::Length(e) => Some(e),
248            SexpError::Na(e) => Some(e),
249            SexpError::InvalidValue(_) => None,
250            SexpError::MissingField(_) => None,
251            SexpError::DuplicateName(_) => None,
252            #[cfg(feature = "either")]
253            SexpError::EitherConversion { .. } => None,
254        }
255    }
256}
257
258impl From<SexpTypeError> for SexpError {
259    fn from(e: SexpTypeError) -> Self {
260        SexpError::Type(e)
261    }
262}
263
264impl From<SexpLengthError> for SexpError {
265    fn from(e: SexpLengthError) -> Self {
266        SexpError::Length(e)
267    }
268}
269
270impl From<SexpNaError> for SexpError {
271    fn from(e: SexpNaError) -> Self {
272        SexpError::Na(e)
273    }
274}
275
276/// TryFrom-style trait for converting SEXP to Rust types.
277///
278/// # Examples
279///
280/// ```no_run
281/// use miniextendr_api::ffi::SEXP;
282/// use miniextendr_api::from_r::TryFromSexp;
283///
284/// fn example(sexp: SEXP) {
285///     let value: i32 = TryFromSexp::try_from_sexp(sexp).unwrap();
286///     let text: String = TryFromSexp::try_from_sexp(sexp).unwrap();
287/// }
288/// ```
289pub trait TryFromSexp: Sized {
290    /// The error type returned when conversion fails.
291    type Error;
292
293    /// Attempt to convert an R SEXP to this Rust type.
294    ///
295    /// In debug builds, may assert that we're on R's main thread.
296    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error>;
297
298    /// Convert from SEXP without thread safety checks.
299    ///
300    /// # Safety
301    ///
302    /// Must be called from R's main thread. In debug builds, this still
303    /// calls the checked version by default, but implementations may
304    /// skip thread assertions for performance.
305    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
306        // Default: just call the checked version
307        Self::try_from_sexp(sexp)
308    }
309}
310
311macro_rules! impl_try_from_sexp_scalar_native {
312    ($t:ty, $sexptype:ident) => {
313        impl TryFromSexp for $t {
314            type Error = SexpError;
315
316            #[inline]
317            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
318                let actual = sexp.type_of();
319                if actual != SEXPTYPE::$sexptype {
320                    return Err(SexpTypeError {
321                        expected: SEXPTYPE::$sexptype,
322                        actual,
323                    }
324                    .into());
325                }
326                let len = sexp.len();
327                if len != 1 {
328                    return Err(SexpLengthError {
329                        expected: 1,
330                        actual: len,
331                    }
332                    .into());
333                }
334                unsafe { sexp.as_slice::<$t>() }
335                    .first()
336                    .cloned()
337                    .ok_or_else(|| {
338                        SexpLengthError {
339                            expected: 1,
340                            actual: 0,
341                        }
342                        .into()
343                    })
344            }
345
346            #[inline]
347            unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
348                let actual = sexp.type_of();
349                if actual != SEXPTYPE::$sexptype {
350                    return Err(SexpTypeError {
351                        expected: SEXPTYPE::$sexptype,
352                        actual,
353                    }
354                    .into());
355                }
356                let len = unsafe { sexp.len_unchecked() };
357                if len != 1 {
358                    return Err(SexpLengthError {
359                        expected: 1,
360                        actual: len,
361                    }
362                    .into());
363                }
364                unsafe { sexp.as_slice_unchecked::<$t>() }
365                    .first()
366                    .cloned()
367                    .ok_or_else(|| {
368                        SexpLengthError {
369                            expected: 1,
370                            actual: 0,
371                        }
372                        .into()
373                    })
374            }
375        }
376    };
377}
378
379// i32 has a bespoke impl that checks for NA_integer_ (i32::MIN).
380// The shared macro is NOT used for i32 — it would silently pass NA through.
381impl TryFromSexp for i32 {
382    type Error = SexpError;
383
384    #[inline]
385    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
386        let actual = sexp.type_of();
387        if actual != SEXPTYPE::INTSXP {
388            return Err(SexpTypeError {
389                expected: SEXPTYPE::INTSXP,
390                actual,
391            }
392            .into());
393        }
394        let len = sexp.len();
395        if len != 1 {
396            return Err(SexpLengthError {
397                expected: 1,
398                actual: len,
399            }
400            .into());
401        }
402        let v = unsafe { sexp.as_slice::<i32>() }
403            .first()
404            .cloned()
405            .ok_or_else(|| {
406                SexpError::from(SexpLengthError {
407                    expected: 1,
408                    actual: 0,
409                })
410            })?;
411        if v == crate::altrep_traits::NA_INTEGER {
412            return Err(SexpNaError {
413                sexp_type: SEXPTYPE::INTSXP,
414            }
415            .into());
416        }
417        Ok(v)
418    }
419
420    #[inline]
421    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
422        let actual = sexp.type_of();
423        if actual != SEXPTYPE::INTSXP {
424            return Err(SexpTypeError {
425                expected: SEXPTYPE::INTSXP,
426                actual,
427            }
428            .into());
429        }
430        let len = unsafe { sexp.len_unchecked() };
431        if len != 1 {
432            return Err(SexpLengthError {
433                expected: 1,
434                actual: len,
435            }
436            .into());
437        }
438        let v = unsafe { sexp.as_slice_unchecked::<i32>() }
439            .first()
440            .cloned()
441            .ok_or_else(|| {
442                SexpError::from(SexpLengthError {
443                    expected: 1,
444                    actual: 0,
445                })
446            })?;
447        if v == crate::altrep_traits::NA_INTEGER {
448            return Err(SexpNaError {
449                sexp_type: SEXPTYPE::INTSXP,
450            }
451            .into());
452        }
453        Ok(v)
454    }
455}
456
457impl_try_from_sexp_scalar_native!(f64, REALSXP);
458impl_try_from_sexp_scalar_native!(u8, RAWSXP);
459impl_try_from_sexp_scalar_native!(RLogical, LGLSXP);
460impl_try_from_sexp_scalar_native!(crate::ffi::Rcomplex, CPLXSXP);
461
462/// Pass-through conversion for raw SEXP values with ALTREP auto-materialization.
463///
464/// This allows `SEXP` to be used directly in `#[miniextendr]` function signatures.
465/// When R passes an ALTREP vector (e.g., `1:10`, `seq_len(N)`),
466/// [`ensure_materialized`](crate::altrep_sexp::ensure_materialized) is called
467/// automatically to force materialization on the R main thread. After this,
468/// the SEXP's data pointer is stable and safe to access from any thread.
469///
470/// # ALTREP handling
471///
472/// | Input | Result |
473/// |---|---|
474/// | Regular SEXP | Passed through unchanged |
475/// | ALTREP SEXP | Materialized via `ensure_materialized`, then passed through |
476///
477/// To receive ALTREP without materializing, use
478/// [`AltrepSexp`](crate::altrep_sexp::AltrepSexp) as the parameter type instead.
479/// To receive the raw SEXP without any conversion (including no materialization),
480/// use `extern "C-unwind"`.
481///
482/// See `docs/ALTREP_SEXP.md` for the full guide.
483///
484/// # Safety
485///
486/// SEXP handles are only valid on R's main thread. Use with
487/// `#[miniextendr(unsafe(main_thread))]` functions.
488impl TryFromSexp for SEXP {
489    type Error = SexpError;
490
491    /// Converts a SEXP, auto-materializing ALTREP vectors.
492    ///
493    /// If the input is ALTREP, [`ensure_materialized`](crate::altrep_sexp::ensure_materialized)
494    /// is called to force materialization on the R main thread. After
495    /// materialization the data pointer is stable and the SEXP can be safely
496    /// sent to other threads.
497    #[inline]
498    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
499        Ok(unsafe { crate::altrep_sexp::ensure_materialized(sexp) })
500    }
501
502    #[inline]
503    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
504        Ok(unsafe { crate::altrep_sexp::ensure_materialized(sexp) })
505    }
506}
507
508impl TryFromSexp for Option<SEXP> {
509    type Error = SexpError;
510
511    #[inline]
512    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
513        if sexp.type_of() == SEXPTYPE::NILSXP {
514            Ok(None)
515        } else {
516            Ok(Some(unsafe {
517                crate::altrep_sexp::ensure_materialized(sexp)
518            }))
519        }
520    }
521
522    #[inline]
523    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
524        Self::try_from_sexp(sexp)
525    }
526}
527// endregion
528
529mod logical;
530
531mod coerced_scalars;
532pub(crate) use coerced_scalars::coerce_value;
533
534mod references;
535
536// region: Blanket implementations for slices with arbitrary lifetimes
537
538/// Blanket impl for `&[T]` where T: RNativeType
539///
540/// This replaces the macro-generated `&'static [T]` impls with a more composable
541/// blanket impl that works for any lifetime. This enables containers like TinyVec
542/// to use blanket impls without needing helper functions.
543impl<T> TryFromSexp for &[T]
544where
545    T: crate::ffi::RNativeType + Copy,
546{
547    type Error = SexpTypeError;
548
549    #[inline]
550    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
551        let actual = sexp.type_of();
552        if actual != T::SEXP_TYPE {
553            return Err(SexpTypeError {
554                expected: T::SEXP_TYPE,
555                actual,
556            });
557        }
558        Ok(unsafe { sexp.as_slice::<T>() })
559    }
560
561    #[inline]
562    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
563        let actual = sexp.type_of();
564        if actual != T::SEXP_TYPE {
565            return Err(SexpTypeError {
566                expected: T::SEXP_TYPE,
567                actual,
568            });
569        }
570        Ok(unsafe { sexp.as_slice_unchecked::<T>() })
571    }
572}
573
574/// Blanket impl for `&mut [T]` where T: RNativeType
575///
576/// # Safety note (aliasing)
577///
578/// This impl can produce aliased `&mut` slices if the same R vector is passed
579/// to multiple mutable slice parameters. The caller is responsible for ensuring
580/// no two `&mut` borrows alias the same SEXP.
581impl<T> TryFromSexp for &mut [T]
582where
583    T: crate::ffi::RNativeType + Copy,
584{
585    type Error = SexpTypeError;
586
587    #[inline]
588    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
589        let actual = sexp.type_of();
590        if actual != T::SEXP_TYPE {
591            return Err(SexpTypeError {
592                expected: T::SEXP_TYPE,
593                actual,
594            });
595        }
596        let len = sexp.len();
597        let ptr = unsafe { T::dataptr_mut(sexp) };
598        Ok(unsafe { r_slice_mut(ptr, len) })
599    }
600
601    #[inline]
602    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
603        let actual = sexp.type_of();
604        if actual != T::SEXP_TYPE {
605            return Err(SexpTypeError {
606                expected: T::SEXP_TYPE,
607                actual,
608            });
609        }
610        let len = unsafe { sexp.len_unchecked() };
611        let ptr = unsafe { T::dataptr_mut(sexp) };
612        Ok(unsafe { r_slice_mut(ptr, len) })
613    }
614}
615
616/// Blanket impl for `Option<&[T]>` where T: RNativeType
617impl<T> TryFromSexp for Option<&[T]>
618where
619    T: crate::ffi::RNativeType + Copy,
620{
621    type Error = SexpError;
622
623    #[inline]
624    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
625        if sexp.type_of() == SEXPTYPE::NILSXP {
626            return Ok(None);
627        }
628        let slice: &[T] = TryFromSexp::try_from_sexp(sexp).map_err(SexpError::from)?;
629        Ok(Some(slice))
630    }
631
632    #[inline]
633    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
634        if sexp.type_of() == SEXPTYPE::NILSXP {
635            return Ok(None);
636        }
637        let slice: &[T] =
638            unsafe { TryFromSexp::try_from_sexp_unchecked(sexp).map_err(SexpError::from)? };
639        Ok(Some(slice))
640    }
641}
642
643/// Blanket impl for `Option<&mut [T]>` where T: RNativeType
644impl<T> TryFromSexp for Option<&mut [T]>
645where
646    T: crate::ffi::RNativeType + Copy,
647{
648    type Error = SexpError;
649
650    #[inline]
651    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
652        if sexp.type_of() == SEXPTYPE::NILSXP {
653            return Ok(None);
654        }
655        let slice: &mut [T] = TryFromSexp::try_from_sexp(sexp).map_err(SexpError::from)?;
656        Ok(Some(slice))
657    }
658
659    #[inline]
660    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
661        if sexp.type_of() == SEXPTYPE::NILSXP {
662            return Ok(None);
663        }
664        let slice: &mut [T] =
665            unsafe { TryFromSexp::try_from_sexp_unchecked(sexp).map_err(SexpError::from)? };
666        Ok(Some(slice))
667    }
668}
669// endregion
670
671mod strings;
672
673// region: Result conversions (NULL -> Err(()))
674
675impl<T> TryFromSexp for Result<T, ()>
676where
677    T: TryFromSexp,
678    T::Error: Into<SexpError>,
679{
680    type Error = SexpError;
681
682    #[inline]
683    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
684        if sexp.type_of() == SEXPTYPE::NILSXP {
685            return Ok(Err(()));
686        }
687        let value = T::try_from_sexp(sexp).map_err(Into::into)?;
688        Ok(Ok(value))
689    }
690
691    #[inline]
692    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
693        if sexp.type_of() == SEXPTYPE::NILSXP {
694            return Ok(Err(()));
695        }
696        let value = unsafe { T::try_from_sexp_unchecked(sexp).map_err(Into::into)? };
697        Ok(Ok(value))
698    }
699}
700// endregion
701
702mod na_vectors;
703
704mod collections;
705
706// region: Fixed-size array conversions
707
708/// Blanket impl: Convert R vector to `[T; N]` where T: RNativeType.
709///
710/// Returns an error if the R vector length doesn't match N.
711/// Useful for SHA hashes ([u8; 32]), fixed-size patterns, etc.
712impl<T, const N: usize> TryFromSexp for [T; N]
713where
714    T: crate::ffi::RNativeType + Copy,
715{
716    type Error = SexpError;
717
718    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
719        let slice: &[T] = TryFromSexp::try_from_sexp(sexp)?;
720        if slice.len() != N {
721            return Err(SexpLengthError {
722                expected: N,
723                actual: slice.len(),
724            }
725            .into());
726        }
727
728        // T: Copy, length verified above. Use MaybeUninit + copy_from_slice.
729        let mut arr = std::mem::MaybeUninit::<[T; N]>::uninit();
730        unsafe {
731            // SAFETY: MaybeUninit<[T; N]> and [T; N] have the same layout.
732            // We write all N elements via copy_from_slice, so assume_init is safe.
733            let dst: &mut [T] = std::slice::from_raw_parts_mut(arr.as_mut_ptr().cast::<T>(), N);
734            dst.copy_from_slice(&slice[..N]);
735            Ok(arr.assume_init())
736        }
737    }
738
739    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
740        Self::try_from_sexp(sexp)
741    }
742}
743// endregion
744
745// region: VecDeque conversions
746
747use std::collections::VecDeque;
748
749/// Blanket impl: Convert R vector to `VecDeque<T>` where T: RNativeType.
750impl<T> TryFromSexp for VecDeque<T>
751where
752    T: crate::ffi::RNativeType + Copy,
753{
754    type Error = SexpTypeError;
755
756    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
757        let slice: &[T] = TryFromSexp::try_from_sexp(sexp)?;
758        Ok(VecDeque::from(slice.to_vec()))
759    }
760
761    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
762        let slice: &[T] = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
763        Ok(VecDeque::from(slice.to_vec()))
764    }
765}
766// endregion
767
768// region: BinaryHeap conversions
769
770use std::collections::BinaryHeap;
771
772/// Blanket impl: Convert R vector to `BinaryHeap<T>` where T: RNativeType + Ord.
773///
774/// Creates a binary heap from the R vector elements.
775impl<T> TryFromSexp for BinaryHeap<T>
776where
777    T: crate::ffi::RNativeType + Copy + Ord,
778{
779    type Error = SexpTypeError;
780
781    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
782        let slice: &[T] = TryFromSexp::try_from_sexp(sexp)?;
783        Ok(BinaryHeap::from(slice.to_vec()))
784    }
785
786    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
787        let slice: &[T] = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
788        Ok(BinaryHeap::from(slice.to_vec()))
789    }
790}
791// endregion
792
793mod cow_and_paths;
794
795// region: Option<Collection> conversions
796//
797// These convert NULL → None, and non-NULL to Some(collection).
798// This differs from Option<scalar> which converts NA → None.
799
800/// Convert R value to `Option<Vec<T>>`: NULL → None, otherwise Some(vec).
801impl<T> TryFromSexp for Option<Vec<T>>
802where
803    Vec<T>: TryFromSexp,
804    <Vec<T> as TryFromSexp>::Error: Into<SexpError>,
805{
806    type Error = SexpError;
807
808    #[inline]
809    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
810        if sexp.type_of() == SEXPTYPE::NILSXP {
811            Ok(None)
812        } else {
813            Vec::<T>::try_from_sexp(sexp).map(Some).map_err(Into::into)
814        }
815    }
816
817    #[inline]
818    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
819        if sexp.type_of() == SEXPTYPE::NILSXP {
820            Ok(None)
821        } else {
822            unsafe {
823                Vec::<T>::try_from_sexp_unchecked(sexp)
824                    .map(Some)
825                    .map_err(Into::into)
826            }
827        }
828    }
829}
830
831macro_rules! impl_option_map_try_from_sexp {
832    ($(#[$meta:meta])* $map_ty:ident) => {
833        $(#[$meta])*
834        impl<V: TryFromSexp> TryFromSexp for Option<$map_ty<String, V>>
835        where
836            V::Error: Into<SexpError>,
837        {
838            type Error = SexpError;
839
840            #[inline]
841            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
842                if sexp.type_of() == SEXPTYPE::NILSXP {
843                    Ok(None)
844                } else {
845                    $map_ty::<String, V>::try_from_sexp(sexp).map(Some)
846                }
847            }
848        }
849    };
850}
851
852impl_option_map_try_from_sexp!(
853    /// Convert R value to `Option<HashMap<String, V>>`: NULL -> None, otherwise Some(map).
854    HashMap
855);
856impl_option_map_try_from_sexp!(
857    /// Convert R value to `Option<BTreeMap<String, V>>`: NULL -> None, otherwise Some(map).
858    BTreeMap
859);
860
861macro_rules! impl_option_set_try_from_sexp {
862    ($(#[$meta:meta])* $set_ty:ident) => {
863        $(#[$meta])*
864        impl<T> TryFromSexp for Option<$set_ty<T>>
865        where
866            $set_ty<T>: TryFromSexp,
867            <$set_ty<T> as TryFromSexp>::Error: Into<SexpError>,
868        {
869            type Error = SexpError;
870
871            #[inline]
872            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
873                if sexp.type_of() == SEXPTYPE::NILSXP {
874                    Ok(None)
875                } else {
876                    $set_ty::<T>::try_from_sexp(sexp)
877                        .map(Some)
878                        .map_err(Into::into)
879                }
880            }
881        }
882    };
883}
884
885impl_option_set_try_from_sexp!(
886    /// Convert R value to `Option<HashSet<T>>`: NULL -> None, otherwise Some(set).
887    HashSet
888);
889impl_option_set_try_from_sexp!(
890    /// Convert R value to `Option<BTreeSet<T>>`: NULL -> None, otherwise Some(set).
891    BTreeSet
892);
893// endregion
894
895// region: Nested vector conversions (list of vectors)
896
897/// Convert R list (VECSXP) to `Vec<Vec<T>>`.
898///
899/// Each element of the R list must be convertible to `Vec<T>`.
900impl<T> TryFromSexp for Vec<Vec<T>>
901where
902    Vec<T>: TryFromSexp,
903    <Vec<T> as TryFromSexp>::Error: Into<SexpError>,
904{
905    type Error = SexpError;
906
907    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
908        let actual = sexp.type_of();
909        if actual != SEXPTYPE::VECSXP {
910            return Err(SexpTypeError {
911                expected: SEXPTYPE::VECSXP,
912                actual,
913            }
914            .into());
915        }
916
917        let len = sexp.len();
918        let mut result = Vec::with_capacity(len);
919
920        for i in 0..len {
921            let elem = sexp.vector_elt(i as crate::ffi::R_xlen_t);
922            let inner: Vec<T> = Vec::<T>::try_from_sexp(elem).map_err(Into::into)?;
923            result.push(inner);
924        }
925
926        Ok(result)
927    }
928
929    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
930        let actual = sexp.type_of();
931        if actual != SEXPTYPE::VECSXP {
932            return Err(SexpTypeError {
933                expected: SEXPTYPE::VECSXP,
934                actual,
935            }
936            .into());
937        }
938
939        let len = sexp.len();
940        let mut result = Vec::with_capacity(len);
941
942        for i in 0..len {
943            let elem = sexp.vector_elt(i as crate::ffi::R_xlen_t);
944            let inner: Vec<T> =
945                unsafe { Vec::<T>::try_from_sexp_unchecked(elem).map_err(Into::into)? };
946            result.push(inner);
947        }
948
949        Ok(result)
950    }
951}
952// endregion
953
954// region: Coerced wrapper - bridge between TryFromSexp and TryCoerce
955
956use crate::coerce::Coerced;
957
958/// Convert R value to `Coerced<T, R>` by reading `R` and coercing to `T`.
959///
960/// This enables reading non-native Rust types from R with coercion:
961///
962/// ```ignore
963/// // Read i64 from R integer (i32)
964/// let val: Coerced<i64, i32> = TryFromSexp::try_from_sexp(sexp)?;
965/// let i64_val: i64 = val.into_inner();
966///
967/// // Works with collections too:
968/// let vec: Vec<Coerced<i64, i32>> = ...;
969/// let set: HashSet<Coerced<NonZeroU32, i32>> = ...;
970/// ```
971impl<T, R> TryFromSexp for Coerced<T, R>
972where
973    R: TryFromSexp,
974    R: TryCoerce<T>,
975    <R as TryFromSexp>::Error: Into<SexpError>,
976    <R as TryCoerce<T>>::Error: std::fmt::Debug,
977{
978    type Error = SexpError;
979
980    #[inline]
981    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
982        let r_val: R = R::try_from_sexp(sexp).map_err(Into::into)?;
983        let value: T = r_val
984            .try_coerce()
985            .map_err(|e| SexpError::InvalidValue(format!("{e:?}")))?;
986        Ok(Coerced::new(value))
987    }
988
989    #[inline]
990    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
991        let r_val: R = unsafe { R::try_from_sexp_unchecked(sexp).map_err(Into::into)? };
992        let value: T = r_val
993            .try_coerce()
994            .map_err(|e| SexpError::InvalidValue(format!("{e:?}")))?;
995        Ok(Coerced::new(value))
996    }
997}
998// endregion
999
1000// region: Direct Vec coercion conversions
1001//
1002// These provide direct `TryFromSexp for Vec<T>` where T is not an R native type
1003// but can be coerced from one. This mirrors the `impl_into_r_via_coerce!` pattern
1004// in into_r.rs for the reverse direction.
1005
1006/// Helper to coerce a slice element-wise into a Vec.
1007#[inline]
1008fn coerce_slice_to_vec<R, T>(slice: &[R]) -> Result<Vec<T>, SexpError>
1009where
1010    R: Copy + TryCoerce<T>,
1011    <R as TryCoerce<T>>::Error: std::fmt::Debug,
1012{
1013    slice
1014        .iter()
1015        .copied()
1016        .map(|v| {
1017            v.try_coerce()
1018                .map_err(|e| SexpError::InvalidValue(format!("{e:?}")))
1019        })
1020        .collect()
1021}
1022
1023/// Convert numeric/logical/raw vectors to `Vec<T>` with element-wise coercion.
1024#[inline]
1025fn try_from_sexp_numeric_vec<T>(sexp: SEXP) -> Result<Vec<T>, SexpError>
1026where
1027    i32: TryCoerce<T>,
1028    f64: TryCoerce<T>,
1029    u8: TryCoerce<T>,
1030    <i32 as TryCoerce<T>>::Error: std::fmt::Debug,
1031    <f64 as TryCoerce<T>>::Error: std::fmt::Debug,
1032    <u8 as TryCoerce<T>>::Error: std::fmt::Debug,
1033{
1034    let actual = sexp.type_of();
1035    match actual {
1036        SEXPTYPE::INTSXP => {
1037            let slice: &[i32] = unsafe { sexp.as_slice() };
1038            coerce_slice_to_vec(slice)
1039        }
1040        SEXPTYPE::REALSXP => {
1041            let slice: &[f64] = unsafe { sexp.as_slice() };
1042            coerce_slice_to_vec(slice)
1043        }
1044        SEXPTYPE::RAWSXP => {
1045            let slice: &[u8] = unsafe { sexp.as_slice() };
1046            coerce_slice_to_vec(slice)
1047        }
1048        SEXPTYPE::LGLSXP => {
1049            let slice: &[RLogical] = unsafe { sexp.as_slice() };
1050            slice.iter().map(|v| coerce_value(v.to_i32())).collect()
1051        }
1052        _ => Err(SexpError::InvalidValue(format!(
1053            "expected integer, numeric, logical, or raw; got {:?}",
1054            actual
1055        ))),
1056    }
1057}
1058
1059/// Implement `TryFromSexp for Vec<$target>` by coercing from integer/real/logical/raw.
1060macro_rules! impl_vec_try_from_sexp_numeric {
1061    ($target:ty) => {
1062        impl TryFromSexp for Vec<$target> {
1063            type Error = SexpError;
1064
1065            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1066                try_from_sexp_numeric_vec(sexp)
1067            }
1068
1069            unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1070                try_from_sexp_numeric_vec(sexp)
1071            }
1072        }
1073    };
1074}
1075
1076impl_vec_try_from_sexp_numeric!(i8);
1077impl_vec_try_from_sexp_numeric!(i16);
1078impl_vec_try_from_sexp_numeric!(i64);
1079impl_vec_try_from_sexp_numeric!(isize);
1080impl_vec_try_from_sexp_numeric!(u16);
1081impl_vec_try_from_sexp_numeric!(u32);
1082impl_vec_try_from_sexp_numeric!(u64);
1083impl_vec_try_from_sexp_numeric!(usize);
1084impl_vec_try_from_sexp_numeric!(f32);
1085
1086/// Convert R logical vector (LGLSXP) to `Vec<bool>` (errors on NA).
1087impl TryFromSexp for Vec<bool> {
1088    type Error = SexpError;
1089
1090    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1091        let actual = sexp.type_of();
1092        if actual != SEXPTYPE::LGLSXP {
1093            return Err(SexpTypeError {
1094                expected: SEXPTYPE::LGLSXP,
1095                actual,
1096            }
1097            .into());
1098        }
1099        let slice: &[RLogical] = unsafe { sexp.as_slice() };
1100        coerce_slice_to_vec(slice)
1101    }
1102
1103    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1104        Self::try_from_sexp(sexp)
1105    }
1106}
1107
1108impl TryFromSexp for Box<[bool]> {
1109    type Error = SexpError;
1110
1111    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1112        let vec: Vec<bool> = TryFromSexp::try_from_sexp(sexp)?;
1113        Ok(vec.into_boxed_slice())
1114    }
1115}
1116// endregion
1117
1118// region: Direct HashSet / BTreeSet coercion conversions
1119
1120/// Convert numeric/logical/raw vectors to a set type with element-wise coercion.
1121#[inline]
1122fn try_from_sexp_numeric_set<T, S>(sexp: SEXP) -> Result<S, SexpError>
1123where
1124    S: std::iter::FromIterator<T>,
1125    i32: TryCoerce<T>,
1126    f64: TryCoerce<T>,
1127    u8: TryCoerce<T>,
1128    <i32 as TryCoerce<T>>::Error: std::fmt::Debug,
1129    <f64 as TryCoerce<T>>::Error: std::fmt::Debug,
1130    <u8 as TryCoerce<T>>::Error: std::fmt::Debug,
1131{
1132    let vec = try_from_sexp_numeric_vec(sexp)?;
1133    Ok(vec.into_iter().collect())
1134}
1135
1136macro_rules! impl_set_try_from_sexp_numeric {
1137    ($set_ty:ident, $target:ty) => {
1138        impl TryFromSexp for $set_ty<$target> {
1139            type Error = SexpError;
1140
1141            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1142                try_from_sexp_numeric_set(sexp)
1143            }
1144
1145            unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1146                try_from_sexp_numeric_set(sexp)
1147            }
1148        }
1149    };
1150}
1151
1152impl_set_try_from_sexp_numeric!(HashSet, i8);
1153impl_set_try_from_sexp_numeric!(HashSet, i16);
1154impl_set_try_from_sexp_numeric!(HashSet, i64);
1155impl_set_try_from_sexp_numeric!(HashSet, isize);
1156impl_set_try_from_sexp_numeric!(HashSet, u16);
1157impl_set_try_from_sexp_numeric!(HashSet, u32);
1158impl_set_try_from_sexp_numeric!(HashSet, u64);
1159impl_set_try_from_sexp_numeric!(HashSet, usize);
1160
1161impl_set_try_from_sexp_numeric!(BTreeSet, i8);
1162impl_set_try_from_sexp_numeric!(BTreeSet, i16);
1163impl_set_try_from_sexp_numeric!(BTreeSet, i64);
1164impl_set_try_from_sexp_numeric!(BTreeSet, isize);
1165impl_set_try_from_sexp_numeric!(BTreeSet, u16);
1166impl_set_try_from_sexp_numeric!(BTreeSet, u32);
1167impl_set_try_from_sexp_numeric!(BTreeSet, u64);
1168impl_set_try_from_sexp_numeric!(BTreeSet, usize);
1169
1170macro_rules! impl_set_try_from_sexp_bool {
1171    ($set_ty:ident) => {
1172        impl TryFromSexp for $set_ty<bool> {
1173            type Error = SexpError;
1174
1175            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1176                let vec: Vec<bool> = TryFromSexp::try_from_sexp(sexp)?;
1177                Ok(vec.into_iter().collect())
1178            }
1179
1180            unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1181                Self::try_from_sexp(sexp)
1182            }
1183        }
1184    };
1185}
1186
1187impl_set_try_from_sexp_bool!(HashSet);
1188impl_set_try_from_sexp_bool!(BTreeSet);
1189// endregion
1190
1191// region: ExternalPtr conversions
1192
1193use crate::externalptr::{ExternalPtr, TypeMismatchError, TypedExternal};
1194
1195/// Convert R EXTPTRSXP to `ExternalPtr<T>`.
1196///
1197/// This enables using `ExternalPtr<T>` as parameter types in `#[miniextendr]` functions.
1198///
1199/// # Example
1200///
1201/// ```ignore
1202/// #[derive(ExternalPtr)]
1203/// struct MyData { value: i32 }
1204///
1205/// #[miniextendr]
1206/// fn process(data: ExternalPtr<MyData>) -> i32 {
1207///     data.value
1208/// }
1209/// ```
1210impl<T: TypedExternal + Send> TryFromSexp for ExternalPtr<T> {
1211    type Error = SexpError;
1212
1213    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1214        let actual = sexp.type_of();
1215        if actual != SEXPTYPE::EXTPTRSXP {
1216            return Err(SexpTypeError {
1217                expected: SEXPTYPE::EXTPTRSXP,
1218                actual,
1219            }
1220            .into());
1221        }
1222
1223        // Use ExternalPtr's type-checked constructor
1224        unsafe { ExternalPtr::wrap_sexp_with_error(sexp) }.map_err(|e| match e {
1225            TypeMismatchError::NullPointer => {
1226                SexpError::InvalidValue("external pointer is null".to_string())
1227            }
1228            TypeMismatchError::InvalidTypeId => {
1229                SexpError::InvalidValue("external pointer has no valid type id".to_string())
1230            }
1231            TypeMismatchError::Mismatch { expected, found } => SexpError::InvalidValue(format!(
1232                "type mismatch: expected `{}`, found `{}`",
1233                expected, found
1234            )),
1235        })
1236    }
1237
1238    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1239        let actual = sexp.type_of();
1240        if actual != SEXPTYPE::EXTPTRSXP {
1241            return Err(SexpTypeError {
1242                expected: SEXPTYPE::EXTPTRSXP,
1243                actual,
1244            }
1245            .into());
1246        }
1247
1248        // Use ExternalPtr's type-checked constructor (unchecked variant)
1249        unsafe { ExternalPtr::wrap_sexp_unchecked(sexp) }.ok_or_else(|| {
1250            SexpError::InvalidValue(
1251                "failed to convert external pointer: type mismatch or null pointer".to_string(),
1252            )
1253        })
1254    }
1255}
1256
1257impl<T: TypedExternal + Send> TryFromSexp for Option<ExternalPtr<T>> {
1258    type Error = SexpError;
1259
1260    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1261        if sexp.type_of() == SEXPTYPE::NILSXP {
1262            return Ok(None);
1263        }
1264        let ptr: ExternalPtr<T> = TryFromSexp::try_from_sexp(sexp)?;
1265        Ok(Some(ptr))
1266    }
1267
1268    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1269        if sexp.type_of() == SEXPTYPE::NILSXP {
1270            return Ok(None);
1271        }
1272        let ptr: ExternalPtr<T> = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
1273        Ok(Some(ptr))
1274    }
1275}
1276// endregion
1277
1278// region: R connections — TryFromSexp impls (issue #175, #176)
1279
1280#[cfg(feature = "connections")]
1281mod connections_from_r {
1282    use std::ffi::CStr;
1283
1284    use crate::connection::{RNullConnection, RStderr, RStdin, RStdout, Rconn};
1285    use crate::ffi::{Rboolean, SEXP};
1286    use crate::from_r::{SexpError, TryFromSexp};
1287
1288    // Read the connection description and class fields from an Rconn handle.
1289    //
1290    // # Safety
1291    // - sexp must be a valid, open R connection SEXP.
1292    // - Must be called from the R main thread.
1293    unsafe fn conn_description(sexp: SEXP) -> Option<String> {
1294        unsafe {
1295            let handle = crate::ffi::R_GetConnection(sexp);
1296            let conn = handle.cast::<Rconn>().cast_const();
1297            if (*conn).description.is_null() {
1298                None
1299            } else {
1300                Some(
1301                    CStr::from_ptr((*conn).description)
1302                        .to_string_lossy()
1303                        .into_owned(),
1304                )
1305            }
1306        }
1307    }
1308
1309    unsafe fn conn_class(sexp: SEXP) -> Option<String> {
1310        unsafe {
1311            let handle = crate::ffi::R_GetConnection(sexp);
1312            let conn = handle.cast::<Rconn>().cast_const();
1313            if (*conn).class.is_null() {
1314                None
1315            } else {
1316                Some(CStr::from_ptr((*conn).class).to_string_lossy().into_owned())
1317            }
1318        }
1319    }
1320
1321    unsafe fn conn_canwrite(sexp: SEXP) -> bool {
1322        unsafe {
1323            let handle = crate::ffi::R_GetConnection(sexp);
1324            let conn = handle.cast::<Rconn>().cast_const();
1325            (*conn).canwrite != Rboolean::FALSE
1326        }
1327    }
1328
1329    unsafe fn conn_isopen(sexp: SEXP) -> bool {
1330        unsafe {
1331            let handle = crate::ffi::R_GetConnection(sexp);
1332            let conn = handle.cast::<Rconn>().cast_const();
1333            (*conn).isopen != Rboolean::FALSE
1334        }
1335    }
1336
1337    // Strict validation: confirm description == expected_desc and class == "terminal".
1338    unsafe fn validate_terminal(sexp: SEXP, expected_desc: &str) -> Result<(), SexpError> {
1339        let desc = unsafe { conn_description(sexp) }.unwrap_or_default();
1340        if desc != expected_desc {
1341            return Err(SexpError::InvalidValue(format!(
1342                "expected terminal connection with description {:?}, got {:?}",
1343                expected_desc, desc
1344            )));
1345        }
1346        let cls = unsafe { conn_class(sexp) }.unwrap_or_default();
1347        if cls != "terminal" {
1348            return Err(SexpError::InvalidValue(format!(
1349                "expected class \"terminal\", got {:?}",
1350                cls
1351            )));
1352        }
1353        Ok(())
1354    }
1355
1356    impl TryFromSexp for RStdin {
1357        type Error = SexpError;
1358
1359        fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1360            unsafe { validate_terminal(sexp, "stdin") }?;
1361            Ok(RStdin)
1362        }
1363
1364        unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1365            Self::try_from_sexp(sexp)
1366        }
1367    }
1368
1369    impl TryFromSexp for RStdout {
1370        type Error = SexpError;
1371
1372        fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1373            unsafe { validate_terminal(sexp, "stdout") }?;
1374            Ok(RStdout)
1375        }
1376
1377        unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1378            Self::try_from_sexp(sexp)
1379        }
1380    }
1381
1382    impl TryFromSexp for RStderr {
1383        type Error = SexpError;
1384
1385        fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1386            unsafe { validate_terminal(sexp, "stderr") }?;
1387            Ok(RStderr)
1388        }
1389
1390        unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1391            Self::try_from_sexp(sexp)
1392        }
1393    }
1394
1395    /// Accepts any open, write-capable connection — not just the null device.
1396    ///
1397    /// This is intentional: validating against `description == "/dev/null"` /
1398    /// `"NUL"` is brittle across platforms, and the type's value comes from the
1399    /// RAII close-on-drop, not the specific target. Substituting a `file()`
1400    /// connection for `RNullConnection` is supported.
1401    impl TryFromSexp for RNullConnection {
1402        type Error = SexpError;
1403
1404        fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1405            if !unsafe { conn_isopen(sexp) } {
1406                return Err(SexpError::InvalidValue(
1407                    "expected an open connection".to_string(),
1408                ));
1409            }
1410            if !unsafe { conn_canwrite(sexp) } {
1411                return Err(SexpError::InvalidValue(
1412                    "expected a write-capable connection".to_string(),
1413                ));
1414            }
1415            // Preserve the SEXP so it lives as long as this Rust struct.
1416            unsafe { crate::ffi::R_PreserveObject(sexp) };
1417            Ok(unsafe { RNullConnection::from_preserved_sexp(sexp) })
1418        }
1419
1420        unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1421            Self::try_from_sexp(sexp)
1422        }
1423    }
1424}
1425
1426// endregion
1427
1428// region: txtProgressBar — TryFromSexp (issue #177)
1429
1430#[cfg(feature = "connections")]
1431mod txt_progress_bar_from_r {
1432    use crate::ffi::{R_PreserveObject, SEXP, SexpExt};
1433    use crate::from_r::{SexpError, TryFromSexp};
1434    use crate::txt_progress_bar::RTxtProgressBar;
1435
1436    impl TryFromSexp for RTxtProgressBar {
1437        type Error = SexpError;
1438
1439        fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1440            // Must be a list (VECSXP) with class "txtProgressBar".
1441            if !sexp.inherits_class(c"txtProgressBar") {
1442                return Err(SexpError::InvalidValue(
1443                    "expected a SEXP with class \"txtProgressBar\"".to_string(),
1444                ));
1445            }
1446            // Pin on the precious list so GC cannot collect while Rust holds it.
1447            unsafe { R_PreserveObject(sexp) };
1448            Ok(unsafe { RTxtProgressBar::from_preserved_sexp(sexp) })
1449        }
1450
1451        unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1452            Self::try_from_sexp(sexp)
1453        }
1454    }
1455}
1456
1457// endregion
1458
1459// region: Helper macros for feature-gated modules
1460
1461/// Implement `TryFromSexp for Option<T>` where T already implements TryFromSexp.
1462///
1463/// NULL → None, otherwise delegates to T::try_from_sexp and wraps in Some.
1464#[macro_export]
1465macro_rules! impl_option_try_from_sexp {
1466    ($t:ty) => {
1467        impl $crate::from_r::TryFromSexp for Option<$t> {
1468            type Error = $crate::from_r::SexpError;
1469
1470            fn try_from_sexp(sexp: $crate::ffi::SEXP) -> Result<Self, Self::Error> {
1471                use $crate::ffi::{SEXPTYPE, SexpExt};
1472                if sexp.type_of() == SEXPTYPE::NILSXP {
1473                    return Ok(None);
1474                }
1475                <$t as $crate::from_r::TryFromSexp>::try_from_sexp(sexp).map(Some)
1476            }
1477
1478            unsafe fn try_from_sexp_unchecked(
1479                sexp: $crate::ffi::SEXP,
1480            ) -> Result<Self, Self::Error> {
1481                use $crate::ffi::{SEXPTYPE, SexpExt};
1482                if sexp.type_of() == SEXPTYPE::NILSXP {
1483                    return Ok(None);
1484                }
1485                unsafe {
1486                    <$t as $crate::from_r::TryFromSexp>::try_from_sexp_unchecked(sexp).map(Some)
1487                }
1488            }
1489        }
1490    };
1491}
1492
1493/// Implement `TryFromSexp for Vec<T>` from R list (VECSXP).
1494///
1495/// Each element is converted via T::try_from_sexp.
1496#[macro_export]
1497macro_rules! impl_vec_try_from_sexp_list {
1498    ($t:ty) => {
1499        impl $crate::from_r::TryFromSexp for Vec<$t> {
1500            type Error = $crate::from_r::SexpError;
1501
1502            fn try_from_sexp(sexp: $crate::ffi::SEXP) -> Result<Self, Self::Error> {
1503                use $crate::ffi::{SEXPTYPE, SexpExt};
1504                use $crate::from_r::SexpTypeError;
1505
1506                let actual = sexp.type_of();
1507                if actual != SEXPTYPE::VECSXP {
1508                    return Err(SexpTypeError {
1509                        expected: SEXPTYPE::VECSXP,
1510                        actual,
1511                    }
1512                    .into());
1513                }
1514
1515                let len = sexp.len();
1516                let mut result = Vec::with_capacity(len);
1517                for i in 0..len {
1518                    let elem = sexp.vector_elt(i as $crate::ffi::R_xlen_t);
1519                    result.push(<$t as $crate::from_r::TryFromSexp>::try_from_sexp(elem)?);
1520                }
1521                Ok(result)
1522            }
1523
1524            unsafe fn try_from_sexp_unchecked(
1525                sexp: $crate::ffi::SEXP,
1526            ) -> Result<Self, Self::Error> {
1527                use $crate::ffi::{SEXPTYPE, SexpExt};
1528                use $crate::from_r::SexpTypeError;
1529
1530                let actual = sexp.type_of();
1531                if actual != SEXPTYPE::VECSXP {
1532                    return Err(SexpTypeError {
1533                        expected: SEXPTYPE::VECSXP,
1534                        actual,
1535                    }
1536                    .into());
1537                }
1538
1539                let len = unsafe { sexp.len_unchecked() };
1540                let mut result = Vec::with_capacity(len);
1541                for i in 0..len {
1542                    let elem = unsafe { sexp.vector_elt_unchecked(i as $crate::ffi::R_xlen_t) };
1543                    result.push(unsafe {
1544                        <$t as $crate::from_r::TryFromSexp>::try_from_sexp_unchecked(elem)?
1545                    });
1546                }
1547                Ok(result)
1548            }
1549        }
1550    };
1551}
1552
1553/// Implement `TryFromSexp for Vec<Option<T>>` from R list (VECSXP).
1554///
1555/// NULL elements become None, others are converted via T::try_from_sexp.
1556#[macro_export]
1557macro_rules! impl_vec_option_try_from_sexp_list {
1558    ($t:ty) => {
1559        impl $crate::from_r::TryFromSexp for Vec<Option<$t>> {
1560            type Error = $crate::from_r::SexpError;
1561
1562            fn try_from_sexp(sexp: $crate::ffi::SEXP) -> Result<Self, Self::Error> {
1563                use $crate::ffi::{SEXPTYPE, SexpExt};
1564                use $crate::from_r::SexpTypeError;
1565
1566                let actual = sexp.type_of();
1567                if actual != SEXPTYPE::VECSXP {
1568                    return Err(SexpTypeError {
1569                        expected: SEXPTYPE::VECSXP,
1570                        actual,
1571                    }
1572                    .into());
1573                }
1574
1575                let len = sexp.len();
1576                let mut result = Vec::with_capacity(len);
1577                for i in 0..len {
1578                    let elem = sexp.vector_elt(i as $crate::ffi::R_xlen_t);
1579                    if elem == $crate::ffi::SEXP::nil() {
1580                        result.push(None);
1581                    } else {
1582                        result.push(Some(<$t as $crate::from_r::TryFromSexp>::try_from_sexp(
1583                            elem,
1584                        )?));
1585                    }
1586                }
1587                Ok(result)
1588            }
1589
1590            unsafe fn try_from_sexp_unchecked(
1591                sexp: $crate::ffi::SEXP,
1592            ) -> Result<Self, Self::Error> {
1593                use $crate::ffi::{SEXPTYPE, SexpExt};
1594                use $crate::from_r::SexpTypeError;
1595
1596                let actual = sexp.type_of();
1597                if actual != SEXPTYPE::VECSXP {
1598                    return Err(SexpTypeError {
1599                        expected: SEXPTYPE::VECSXP,
1600                        actual,
1601                    }
1602                    .into());
1603                }
1604
1605                let len = unsafe { sexp.len_unchecked() };
1606                let mut result = Vec::with_capacity(len);
1607                for i in 0..len {
1608                    let elem = unsafe { sexp.vector_elt_unchecked(i as $crate::ffi::R_xlen_t) };
1609                    if elem == $crate::ffi::SEXP::nil() {
1610                        result.push(None);
1611                    } else {
1612                        result.push(Some(unsafe {
1613                            <$t as $crate::from_r::TryFromSexp>::try_from_sexp_unchecked(elem)?
1614                        }));
1615                    }
1616                }
1617                Ok(result)
1618            }
1619        }
1620    };
1621}
1622// endregion