Skip to main content

miniextendr_api/
into_r_as.rs

1//! Storage-directed conversion to R.
2//!
3//! This module provides [`IntoRAs`], a trait for converting Rust values to R SEXPs
4//! with explicit target storage type selection.
5//!
6//! # Value-Based Semantics
7//!
8//! Conversions are **runtime-checked**: if the actual value fits the target type,
9//! it converts; if not, it errors. There is no "lossy" escape hatch - if you want
10//! lossy conversion, cast the values yourself first.
11//!
12//! # Where this fits among the three outbound paths
13//!
14//! - [`crate::into_r::IntoR`] — **lax** default: picks an R storage type for
15//!   you (e.g. `i64` → `INTSXP` if it fits, `REALSXP` otherwise). Silent.
16//! - [`crate::strict`] — **strict** opt-in via `#[miniextendr(strict)]`:
17//!   panics (→ R error) if the value can't fit `INTSXP`.
18//! - `IntoRAs` (this module) — **storage-directed**: the *caller* picks the
19//!   target R type, the conversion errors with [`StorageCoerceError`] if any
20//!   value doesn't fit.
21//!
22//! Failure mode of reaching for the default `IntoR` when you actually wanted
23//! storage control: an R-side `is.integer()` check fails because R received
24//! a `REALSXP` your `tibble` column wasn't expecting.
25//!
26//! # Example
27//!
28//! ```ignore
29//! use miniextendr_api::IntoRAs;
30//!
31//! // These succeed (values fit)
32//! let x = vec![1_i64, 2, 3];
33//! let sexp = x.into_r_as::<i32>()?;           // OK: all values in i32 range
34//!
35//! let y = vec![1.0_f64, 2.0, 3.0];
36//! let sexp = y.into_r_as::<i32>()?;           // OK: all values are integral
37//!
38//! // These fail (values don't fit)
39//! let z = vec![1_i64 << 40];
40//! let sexp = z.into_r_as::<i32>()?;           // Error: out of range
41//!
42//! let w = vec![1.5_f64];
43//! let sexp = w.into_r_as::<i32>()?;           // Error: not integral
44//!
45//! // User wants lossy? Cast first.
46//! let lossy: Vec<i32> = vec![1.5_f64, 2.7].iter().map(|&x| x as i32).collect();
47//! let sexp = lossy.into_r();                  // [1, 2] - user's responsibility
48//! ```
49
50use crate::coerce::{CoerceError, TryCoerce};
51use crate::from_r::BATCHED_ERROR_CAP;
52use crate::into_r::IntoR;
53use crate::{RLogical, SEXP};
54use std::fmt;
55
56// region: Error type
57
58/// Error type for storage-directed conversion failures.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum StorageCoerceError {
61    /// Conversion between these types is not supported.
62    Unsupported {
63        /// Source Rust type name.
64        from: &'static str,
65        /// Target storage type name.
66        to: &'static str,
67    },
68    /// Value is out of range for the target type.
69    OutOfRange {
70        /// Source Rust type name.
71        from: &'static str,
72        /// Target storage type name.
73        to: &'static str,
74        /// Failing element index for vector conversions.
75        index: Option<usize>,
76    },
77    /// Value is non-finite (NaN or Inf) but target requires finite.
78    NonFinite {
79        /// Target storage type name.
80        to: &'static str,
81        /// Failing element index for vector conversions.
82        index: Option<usize>,
83    },
84    /// Conversion would lose precision.
85    PrecisionLoss {
86        /// Target storage type name.
87        to: &'static str,
88        /// Failing element index for vector conversions.
89        index: Option<usize>,
90    },
91    /// Float value is not integral but target is integer type.
92    NotIntegral {
93        /// Target storage type name.
94        to: &'static str,
95        /// Failing element index for vector conversions.
96        index: Option<usize>,
97    },
98    /// Missing value (NA) cannot be represented in target type.
99    MissingValue {
100        /// Target storage type name.
101        to: &'static str,
102        /// Failing element index for vector conversions.
103        index: Option<usize>,
104    },
105    /// Invalid UTF-8 in string conversion.
106    InvalidUtf8 {
107        /// Failing element index for vector conversions.
108        index: Option<usize>,
109    },
110    /// Aggregated per-element failures from a vector conversion (#1097).
111    /// `listed` holds at most the first `BATCHED_ERROR_CAP` element errors
112    /// (each with `index` set); `total` counts all failures.
113    Batched {
114        /// The vector/slice type that failed to convert, e.g. `"Vec<i64>"`.
115        container: &'static str,
116        /// The first `BATCHED_ERROR_CAP` per-element failures.
117        listed: Vec<StorageCoerceError>,
118        /// Total number of failing elements (`>= listed.len()`).
119        total: usize,
120    },
121}
122
123impl fmt::Display for StorageCoerceError {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        match self {
126            StorageCoerceError::Unsupported { from, to } => {
127                write!(f, "cannot convert {} to {}", from, to)
128            }
129            StorageCoerceError::OutOfRange { from, to, index } => {
130                if let Some(i) = index {
131                    write!(f, "value at index {} out of range for {} → {}", i, from, to)
132                } else {
133                    write!(f, "value out of range for {} → {}", from, to)
134                }
135            }
136            StorageCoerceError::NonFinite { to, index } => {
137                if let Some(i) = index {
138                    write!(
139                        f,
140                        "non-finite value at index {} cannot convert to {}",
141                        i, to
142                    )
143                } else {
144                    write!(f, "non-finite value cannot convert to {}", to)
145                }
146            }
147            StorageCoerceError::PrecisionLoss { to, index } => {
148                if let Some(i) = index {
149                    write!(
150                        f,
151                        "value at index {} would lose precision converting to {}",
152                        i, to
153                    )
154                } else {
155                    write!(f, "value would lose precision converting to {}", to)
156                }
157            }
158            StorageCoerceError::NotIntegral { to, index } => {
159                if let Some(i) = index {
160                    write!(
161                        f,
162                        "non-integral value at index {} cannot convert to {}",
163                        i, to
164                    )
165                } else {
166                    write!(f, "non-integral value cannot convert to {}", to)
167                }
168            }
169            StorageCoerceError::MissingValue { to, index } => {
170                if let Some(i) = index {
171                    write!(f, "missing value at index {} cannot convert to {}", i, to)
172                } else {
173                    write!(f, "missing value cannot convert to {}", to)
174                }
175            }
176            StorageCoerceError::InvalidUtf8 { index } => {
177                if let Some(i) = index {
178                    write!(f, "invalid UTF-8 at index {}", i)
179                } else {
180                    write!(f, "invalid UTF-8")
181                }
182            }
183            StorageCoerceError::Batched {
184                container,
185                listed,
186                total,
187            } => {
188                write!(f, "{} conversion failed: ", container)?;
189                for (i, e) in listed.iter().enumerate() {
190                    if i > 0 {
191                        write!(f, "; ")?;
192                    }
193                    write!(f, "{}", e)?;
194                }
195                if *total > listed.len() {
196                    write!(f, "; and {} more", total - listed.len())?;
197                }
198                Ok(())
199            }
200        }
201    }
202}
203
204impl std::error::Error for StorageCoerceError {}
205
206impl StorageCoerceError {
207    /// Add index information to the error.
208    #[inline]
209    pub fn at_index(self, idx: usize) -> Self {
210        match self {
211            StorageCoerceError::OutOfRange { from, to, .. } => StorageCoerceError::OutOfRange {
212                from,
213                to,
214                index: Some(idx),
215            },
216            StorageCoerceError::NonFinite { to, .. } => StorageCoerceError::NonFinite {
217                to,
218                index: Some(idx),
219            },
220            StorageCoerceError::PrecisionLoss { to, .. } => StorageCoerceError::PrecisionLoss {
221                to,
222                index: Some(idx),
223            },
224            StorageCoerceError::NotIntegral { to, .. } => StorageCoerceError::NotIntegral {
225                to,
226                index: Some(idx),
227            },
228            StorageCoerceError::MissingValue { to, .. } => StorageCoerceError::MissingValue {
229                to,
230                index: Some(idx),
231            },
232            StorageCoerceError::InvalidUtf8 { .. } => {
233                StorageCoerceError::InvalidUtf8 { index: Some(idx) }
234            }
235            // Batched errors already carry per-element indices on their
236            // `listed` entries — indexing the aggregate itself is a no-op.
237            batched @ StorageCoerceError::Batched { .. } => batched,
238            other => other,
239        }
240    }
241}
242// endregion
243
244// region: Trait definition
245
246/// Storage-directed conversion to R SEXP.
247///
248/// This trait allows converting Rust values to R with an explicit target storage
249/// type. The conversion is value-based: it succeeds if all values fit the target
250/// type, and fails otherwise.
251///
252/// # Target Types
253///
254/// - `i32` → R integer (INTSXP)
255/// - `f64` → R numeric (REALSXP)
256/// - `RLogical` → R logical (LGLSXP)
257/// - `u8` → R raw (RAWSXP)
258/// - `String` → R character (STRSXP)
259///
260/// # Example
261///
262/// ```ignore
263/// use miniextendr_api::IntoRAs;
264///
265/// // Convert i64 to R integer (if values fit)
266/// let x: Vec<i64> = vec![1, 2, 3];
267/// let sexp = x.into_r_as::<i32>()?;
268///
269/// // Convert f64 to R integer (if values are integral)
270/// let y: Vec<f64> = vec![1.0, 2.0, 3.0];
271/// let sexp = y.into_r_as::<i32>()?;
272/// ```
273pub trait IntoRAs<Target> {
274    /// Convert to R SEXP with the specified target storage type.
275    fn into_r_as(self) -> Result<SEXP, StorageCoerceError>;
276}
277// endregion
278
279// region: Helper: try_coerce_scalar with error mapping
280
281/// Try to coerce a scalar value, mapping CoerceError to StorageCoerceError.
282#[inline]
283fn try_coerce_scalar<T, R>(
284    value: T,
285    from: &'static str,
286    to: &'static str,
287) -> Result<R, StorageCoerceError>
288where
289    T: TryCoerce<R>,
290    T::Error: Into<CoerceErrorKind>,
291{
292    value
293        .try_coerce()
294        .map_err(|e| map_coerce_error(e.into(), from, to))
295}
296
297/// Internal enum to unify different coerce error types.
298#[derive(Debug)]
299enum CoerceErrorKind {
300    Overflow,
301    PrecisionLoss,
302    NaN,
303    Infallible,
304}
305
306impl From<CoerceError> for CoerceErrorKind {
307    fn from(e: CoerceError) -> Self {
308        match e {
309            CoerceError::Overflow => CoerceErrorKind::Overflow,
310            CoerceError::PrecisionLoss => CoerceErrorKind::PrecisionLoss,
311            CoerceError::NaN => CoerceErrorKind::NaN,
312            CoerceError::Zero => CoerceErrorKind::Overflow, // Treat zero error as overflow
313        }
314    }
315}
316
317impl From<std::convert::Infallible> for CoerceErrorKind {
318    fn from(_: std::convert::Infallible) -> Self {
319        CoerceErrorKind::Infallible
320    }
321}
322
323fn map_coerce_error(
324    kind: CoerceErrorKind,
325    from: &'static str,
326    to: &'static str,
327) -> StorageCoerceError {
328    match kind {
329        CoerceErrorKind::Overflow => StorageCoerceError::OutOfRange {
330            from,
331            to,
332            index: None,
333        },
334        CoerceErrorKind::PrecisionLoss => StorageCoerceError::PrecisionLoss { to, index: None },
335        CoerceErrorKind::NaN => StorageCoerceError::NonFinite { to, index: None },
336        CoerceErrorKind::Infallible => unreachable!(),
337    }
338}
339// endregion
340
341// region: Scalar implementations: -> i32 (R integer)
342
343macro_rules! impl_into_r_as_scalar {
344    ($target:ty, $target_name:literal; $from:ty, $from_name:literal) => {
345        impl IntoRAs<$target> for $from {
346            #[inline]
347            fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
348                let v: $target = try_coerce_scalar(self, $from_name, $target_name)?;
349                Ok(v.into_sexp())
350            }
351        }
352    };
353}
354
355// Identity
356impl IntoRAs<i32> for i32 {
357    #[inline]
358    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
359        Ok(self.into_sexp())
360    }
361}
362
363// Widening (infallible)
364impl_into_r_as_scalar!(i32, "i32"; i8, "i8");
365impl_into_r_as_scalar!(i32, "i32"; i16, "i16");
366impl_into_r_as_scalar!(i32, "i32"; u8, "u8");
367impl_into_r_as_scalar!(i32, "i32"; u16, "u16");
368
369// Narrowing (fallible)
370impl_into_r_as_scalar!(i32, "i32"; i64, "i64");
371impl_into_r_as_scalar!(i32, "i32"; isize, "isize");
372impl_into_r_as_scalar!(i32, "i32"; u32, "u32");
373impl_into_r_as_scalar!(i32, "i32"; u64, "u64");
374impl_into_r_as_scalar!(i32, "i32"; usize, "usize");
375
376// Float to int (fallible - must be integral and in range)
377impl_into_r_as_scalar!(i32, "i32"; f32, "f32");
378impl_into_r_as_scalar!(i32, "i32"; f64, "f64");
379
380// Bool to int
381impl IntoRAs<i32> for bool {
382    #[inline]
383    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
384        Ok((self as i32).into_sexp())
385    }
386}
387// endregion
388
389// region: Scalar implementations: -> f64 (R numeric)
390
391// Identity
392impl IntoRAs<f64> for f64 {
393    #[inline]
394    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
395        if !self.is_finite() {
396            return Err(StorageCoerceError::NonFinite {
397                to: "f64",
398                index: None,
399            });
400        }
401        Ok(self.into_sexp())
402    }
403}
404
405// Widening from f32 (check finite)
406impl IntoRAs<f64> for f32 {
407    #[inline]
408    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
409        if !self.is_finite() {
410            return Err(StorageCoerceError::NonFinite {
411                to: "f64",
412                index: None,
413            });
414        }
415        Ok((self as f64).into_sexp())
416    }
417}
418
419// Widening from integers (infallible for small types)
420impl IntoRAs<f64> for i8 {
421    #[inline]
422    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
423        Ok((self as f64).into_sexp())
424    }
425}
426
427impl IntoRAs<f64> for i16 {
428    #[inline]
429    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
430        Ok((self as f64).into_sexp())
431    }
432}
433
434impl IntoRAs<f64> for i32 {
435    #[inline]
436    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
437        // i32::MIN is R's NA_integer_ sentinel. A blind `self as f64` would
438        // turn it into the finite value -2147483648.0, silently destroying the
439        // missing-value semantics. Detect it and surface MissingValue instead.
440        if self == crate::altrep_traits::NA_INTEGER {
441            return Err(StorageCoerceError::MissingValue {
442                to: "f64",
443                index: None,
444            });
445        }
446        Ok((self as f64).into_sexp())
447    }
448}
449
450impl IntoRAs<f64> for u8 {
451    #[inline]
452    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
453        Ok((self as f64).into_sexp())
454    }
455}
456
457impl IntoRAs<f64> for u16 {
458    #[inline]
459    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
460        Ok((self as f64).into_sexp())
461    }
462}
463
464impl IntoRAs<f64> for u32 {
465    #[inline]
466    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
467        Ok((self as f64).into_sexp())
468    }
469}
470
471// Large integers: check precision (> 2^53 loses precision)
472impl_into_r_as_scalar!(f64, "f64"; i64, "i64");
473impl_into_r_as_scalar!(f64, "f64"; u64, "u64");
474impl_into_r_as_scalar!(f64, "f64"; isize, "isize");
475impl_into_r_as_scalar!(f64, "f64"; usize, "usize");
476
477// Bool to f64
478impl IntoRAs<f64> for bool {
479    #[inline]
480    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
481        Ok((if self { 1.0 } else { 0.0 }).into_sexp())
482    }
483}
484// endregion
485
486// region: Scalar implementations: -> u8 (R raw)
487
488// Identity
489impl IntoRAs<u8> for u8 {
490    #[inline]
491    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
492        Ok(self.into_sexp())
493    }
494}
495
496// Narrowing (fallible)
497impl_into_r_as_scalar!(u8, "u8"; i8, "i8");
498impl_into_r_as_scalar!(u8, "u8"; i16, "i16");
499impl_into_r_as_scalar!(u8, "u8"; i32, "i32");
500impl_into_r_as_scalar!(u8, "u8"; i64, "i64");
501impl_into_r_as_scalar!(u8, "u8"; isize, "isize");
502impl_into_r_as_scalar!(u8, "u8"; u16, "u16");
503impl_into_r_as_scalar!(u8, "u8"; u32, "u32");
504impl_into_r_as_scalar!(u8, "u8"; u64, "u64");
505impl_into_r_as_scalar!(u8, "u8"; usize, "usize");
506impl_into_r_as_scalar!(u8, "u8"; f32, "f32");
507impl_into_r_as_scalar!(u8, "u8"; f64, "f64");
508// endregion
509
510// region: Scalar implementations: -> RLogical (R logical)
511
512impl IntoRAs<RLogical> for bool {
513    #[inline]
514    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
515        Ok(self.into_sexp())
516    }
517}
518
519impl IntoRAs<RLogical> for RLogical {
520    #[inline]
521    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
522        Ok(self.into_sexp())
523    }
524}
525
526// Integer to logical: only 0, 1, NA_INTEGER allowed
527impl IntoRAs<RLogical> for i32 {
528    #[inline]
529    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
530        match self {
531            0 => Ok(false.into_sexp()),
532            1 => Ok(true.into_sexp()),
533            crate::altrep_traits::NA_INTEGER => Ok(RLogical::NA.into_sexp()),
534            _ => Err(StorageCoerceError::OutOfRange {
535                from: "i32",
536                to: "RLogical",
537                index: None,
538            }),
539        }
540    }
541}
542// endregion
543
544// region: Scalar implementations: -> String (R character)
545
546impl IntoRAs<String> for String {
547    #[inline]
548    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
549        Ok(self.into_sexp())
550    }
551}
552
553impl IntoRAs<String> for &str {
554    #[inline]
555    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
556        Ok(self.into_sexp())
557    }
558}
559
560// Numeric to String: stringify (including NaN/Inf)
561impl IntoRAs<String> for f64 {
562    #[inline]
563    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
564        let s = if self.is_nan() {
565            "NaN".to_string()
566        } else if self.is_infinite() {
567            if self.is_sign_positive() {
568                "Inf".to_string()
569            } else {
570                "-Inf".to_string()
571            }
572        } else {
573            self.to_string()
574        };
575        Ok(s.into_sexp())
576    }
577}
578
579impl IntoRAs<String> for f32 {
580    #[inline]
581    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
582        <f64 as IntoRAs<String>>::into_r_as(self as f64)
583    }
584}
585
586impl IntoRAs<String> for i32 {
587    #[inline]
588    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
589        Ok(self.to_string().into_sexp())
590    }
591}
592
593impl IntoRAs<String> for i64 {
594    #[inline]
595    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
596        Ok(self.to_string().into_sexp())
597    }
598}
599
600impl IntoRAs<String> for bool {
601    #[inline]
602    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
603        Ok((if self { "TRUE" } else { "FALSE" }).into_sexp())
604    }
605}
606
607impl IntoRAs<String> for RLogical {
608    #[inline]
609    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
610        let s = match self.to_option_bool() {
611            None => "NA",
612            Some(true) => "TRUE",
613            Some(false) => "FALSE",
614        };
615        Ok(s.into_sexp())
616    }
617}
618// endregion
619
620// region: Vec implementations: -> i32 (R integer vector)
621
622macro_rules! impl_vec_into_r_as {
623    ($target:ty, $target_name:literal; $from:ty, $from_name:literal) => {
624        impl IntoRAs<$target> for Vec<$from> {
625            fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
626                let mut result = Vec::with_capacity(self.len());
627                let mut listed: Vec<StorageCoerceError> = Vec::new();
628                let mut total = 0usize;
629                for (i, val) in self.into_iter().enumerate() {
630                    match try_coerce_scalar::<$from, $target>(val, $from_name, $target_name) {
631                        Ok(v) => result.push(v),
632                        Err(e) => {
633                            if listed.len() < BATCHED_ERROR_CAP {
634                                listed.push(e.at_index(i));
635                            }
636                            total += 1;
637                        }
638                    }
639                }
640                if total > 0 {
641                    return Err(StorageCoerceError::Batched {
642                        container: concat!("Vec<", $from_name, ">"),
643                        listed,
644                        total,
645                    });
646                }
647                Ok(result.into_sexp())
648            }
649        }
650
651        impl IntoRAs<$target> for &[$from] {
652            fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
653                let mut result = Vec::with_capacity(self.len());
654                let mut listed: Vec<StorageCoerceError> = Vec::new();
655                let mut total = 0usize;
656                for (i, &val) in self.iter().enumerate() {
657                    match try_coerce_scalar::<$from, $target>(val, $from_name, $target_name) {
658                        Ok(v) => result.push(v),
659                        Err(e) => {
660                            if listed.len() < BATCHED_ERROR_CAP {
661                                listed.push(e.at_index(i));
662                            }
663                            total += 1;
664                        }
665                    }
666                }
667                if total > 0 {
668                    return Err(StorageCoerceError::Batched {
669                        container: concat!("&[", $from_name, "]"),
670                        listed,
671                        total,
672                    });
673                }
674                Ok(result.into_sexp())
675            }
676        }
677    };
678}
679
680// Identity (direct copy)
681impl IntoRAs<i32> for Vec<i32> {
682    #[inline]
683    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
684        Ok(self.into_sexp())
685    }
686}
687
688impl IntoRAs<i32> for &[i32] {
689    #[inline]
690    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
691        Ok(self.into_sexp())
692    }
693}
694
695impl_vec_into_r_as!(i32, "i32"; i8, "i8");
696impl_vec_into_r_as!(i32, "i32"; i16, "i16");
697impl_vec_into_r_as!(i32, "i32"; u8, "u8");
698impl_vec_into_r_as!(i32, "i32"; u16, "u16");
699impl_vec_into_r_as!(i32, "i32"; i64, "i64");
700impl_vec_into_r_as!(i32, "i32"; isize, "isize");
701impl_vec_into_r_as!(i32, "i32"; u32, "u32");
702impl_vec_into_r_as!(i32, "i32"; u64, "u64");
703impl_vec_into_r_as!(i32, "i32"; usize, "usize");
704impl_vec_into_r_as!(i32, "i32"; f32, "f32");
705impl_vec_into_r_as!(i32, "i32"; f64, "f64");
706
707// Vec<bool> -> i32
708impl IntoRAs<i32> for Vec<bool> {
709    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
710        let result: Vec<i32> = self.into_iter().map(|b| b as i32).collect();
711        Ok(result.into_sexp())
712    }
713}
714
715impl IntoRAs<i32> for &[bool] {
716    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
717        let result: Vec<i32> = self.iter().map(|&b| b as i32).collect();
718        Ok(result.into_sexp())
719    }
720}
721// endregion
722
723// region: Vec implementations: -> f64 (R numeric vector)
724
725// Identity - but check for finite values
726impl IntoRAs<f64> for Vec<f64> {
727    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
728        let mut listed: Vec<StorageCoerceError> = Vec::new();
729        let mut total = 0usize;
730        for (i, &val) in self.iter().enumerate() {
731            if !val.is_finite() {
732                if listed.len() < BATCHED_ERROR_CAP {
733                    listed.push(StorageCoerceError::NonFinite {
734                        to: "f64",
735                        index: Some(i),
736                    });
737                }
738                total += 1;
739            }
740        }
741        if total > 0 {
742            return Err(StorageCoerceError::Batched {
743                container: "Vec<f64>",
744                listed,
745                total,
746            });
747        }
748        Ok(self.into_sexp())
749    }
750}
751
752impl IntoRAs<f64> for &[f64] {
753    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
754        let mut listed: Vec<StorageCoerceError> = Vec::new();
755        let mut total = 0usize;
756        for (i, &val) in self.iter().enumerate() {
757            if !val.is_finite() {
758                if listed.len() < BATCHED_ERROR_CAP {
759                    listed.push(StorageCoerceError::NonFinite {
760                        to: "f64",
761                        index: Some(i),
762                    });
763                }
764                total += 1;
765            }
766        }
767        if total > 0 {
768            return Err(StorageCoerceError::Batched {
769                container: "&[f64]",
770                listed,
771                total,
772            });
773        }
774        Ok(self.into_sexp())
775    }
776}
777
778// f32 - check finite then widen
779impl IntoRAs<f64> for Vec<f32> {
780    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
781        let mut result = Vec::with_capacity(self.len());
782        let mut listed: Vec<StorageCoerceError> = Vec::new();
783        let mut total = 0usize;
784        for (i, val) in self.into_iter().enumerate() {
785            if !val.is_finite() {
786                if listed.len() < BATCHED_ERROR_CAP {
787                    listed.push(StorageCoerceError::NonFinite {
788                        to: "f64",
789                        index: Some(i),
790                    });
791                }
792                total += 1;
793            } else {
794                result.push(val as f64);
795            }
796        }
797        if total > 0 {
798            return Err(StorageCoerceError::Batched {
799                container: "Vec<f32>",
800                listed,
801                total,
802            });
803        }
804        Ok(result.into_sexp())
805    }
806}
807
808impl IntoRAs<f64> for &[f32] {
809    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
810        let mut result = Vec::with_capacity(self.len());
811        let mut listed: Vec<StorageCoerceError> = Vec::new();
812        let mut total = 0usize;
813        for (i, &val) in self.iter().enumerate() {
814            if !val.is_finite() {
815                if listed.len() < BATCHED_ERROR_CAP {
816                    listed.push(StorageCoerceError::NonFinite {
817                        to: "f64",
818                        index: Some(i),
819                    });
820                }
821                total += 1;
822            } else {
823                result.push(val as f64);
824            }
825        }
826        if total > 0 {
827            return Err(StorageCoerceError::Batched {
828                container: "&[f32]",
829                listed,
830                total,
831            });
832        }
833        Ok(result.into_sexp())
834    }
835}
836
837// Small integers - infallible widening
838macro_rules! impl_vec_into_r_as_f64_infallible {
839    ($from:ty) => {
840        impl IntoRAs<f64> for Vec<$from> {
841            fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
842                let result: Vec<f64> = self.into_iter().map(|v| v as f64).collect();
843                Ok(result.into_sexp())
844            }
845        }
846
847        impl IntoRAs<f64> for &[$from] {
848            fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
849                let result: Vec<f64> = self.iter().map(|&v| v as f64).collect();
850                Ok(result.into_sexp())
851            }
852        }
853    };
854}
855
856impl_vec_into_r_as_f64_infallible!(i8);
857impl_vec_into_r_as_f64_infallible!(i16);
858impl_vec_into_r_as_f64_infallible!(u8);
859impl_vec_into_r_as_f64_infallible!(u16);
860impl_vec_into_r_as_f64_infallible!(u32);
861
862// i32 -> f64: widening is value-preserving EXCEPT for i32::MIN, which is R's
863// NA_integer_ sentinel. Casting it would yield the finite -2147483648.0 and
864// silently drop the missing-value semantics, so detect it and error instead.
865impl IntoRAs<f64> for Vec<i32> {
866    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
867        let mut result = Vec::with_capacity(self.len());
868        let mut listed: Vec<StorageCoerceError> = Vec::new();
869        let mut total = 0usize;
870        for (i, val) in self.into_iter().enumerate() {
871            if val == crate::altrep_traits::NA_INTEGER {
872                if listed.len() < BATCHED_ERROR_CAP {
873                    listed.push(StorageCoerceError::MissingValue {
874                        to: "f64",
875                        index: Some(i),
876                    });
877                }
878                total += 1;
879            } else {
880                result.push(val as f64);
881            }
882        }
883        if total > 0 {
884            return Err(StorageCoerceError::Batched {
885                container: "Vec<i32>",
886                listed,
887                total,
888            });
889        }
890        Ok(result.into_sexp())
891    }
892}
893
894impl IntoRAs<f64> for &[i32] {
895    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
896        let mut result = Vec::with_capacity(self.len());
897        let mut listed: Vec<StorageCoerceError> = Vec::new();
898        let mut total = 0usize;
899        for (i, &val) in self.iter().enumerate() {
900            if val == crate::altrep_traits::NA_INTEGER {
901                if listed.len() < BATCHED_ERROR_CAP {
902                    listed.push(StorageCoerceError::MissingValue {
903                        to: "f64",
904                        index: Some(i),
905                    });
906                }
907                total += 1;
908            } else {
909                result.push(val as f64);
910            }
911        }
912        if total > 0 {
913            return Err(StorageCoerceError::Batched {
914                container: "&[i32]",
915                listed,
916                total,
917            });
918        }
919        Ok(result.into_sexp())
920    }
921}
922
923// Large integers - check precision
924impl_vec_into_r_as!(f64, "f64"; i64, "i64");
925impl_vec_into_r_as!(f64, "f64"; u64, "u64");
926impl_vec_into_r_as!(f64, "f64"; isize, "isize");
927impl_vec_into_r_as!(f64, "f64"; usize, "usize");
928
929// Vec<bool> -> f64
930impl IntoRAs<f64> for Vec<bool> {
931    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
932        let result: Vec<f64> = self
933            .into_iter()
934            .map(|b| if b { 1.0 } else { 0.0 })
935            .collect();
936        Ok(result.into_sexp())
937    }
938}
939
940impl IntoRAs<f64> for &[bool] {
941    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
942        let result: Vec<f64> = self.iter().map(|&b| if b { 1.0 } else { 0.0 }).collect();
943        Ok(result.into_sexp())
944    }
945}
946// endregion
947
948// region: Vec implementations: -> u8 (R raw vector)
949
950// Identity
951impl IntoRAs<u8> for Vec<u8> {
952    #[inline]
953    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
954        Ok(self.into_sexp())
955    }
956}
957
958impl IntoRAs<u8> for &[u8] {
959    #[inline]
960    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
961        Ok(self.into_sexp())
962    }
963}
964
965impl_vec_into_r_as!(u8, "u8"; i8, "i8");
966impl_vec_into_r_as!(u8, "u8"; i16, "i16");
967impl_vec_into_r_as!(u8, "u8"; i32, "i32");
968impl_vec_into_r_as!(u8, "u8"; i64, "i64");
969impl_vec_into_r_as!(u8, "u8"; isize, "isize");
970impl_vec_into_r_as!(u8, "u8"; u16, "u16");
971impl_vec_into_r_as!(u8, "u8"; u32, "u32");
972impl_vec_into_r_as!(u8, "u8"; u64, "u64");
973impl_vec_into_r_as!(u8, "u8"; usize, "usize");
974impl_vec_into_r_as!(u8, "u8"; f32, "f32");
975impl_vec_into_r_as!(u8, "u8"; f64, "f64");
976// endregion
977
978// region: Vec implementations: -> RLogical (R logical vector)
979
980impl IntoRAs<RLogical> for Vec<bool> {
981    #[inline]
982    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
983        Ok(self.into_sexp())
984    }
985}
986
987impl IntoRAs<RLogical> for &[bool] {
988    #[inline]
989    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
990        Ok(self.into_sexp())
991    }
992}
993// endregion
994
995// region: Vec implementations: -> String (R character vector)
996
997impl IntoRAs<String> for Vec<String> {
998    #[inline]
999    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
1000        Ok(self.into_sexp())
1001    }
1002}
1003
1004impl IntoRAs<String> for &[String] {
1005    #[inline]
1006    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
1007        Ok(self.into_sexp())
1008    }
1009}
1010
1011impl IntoRAs<String> for Vec<&str> {
1012    #[inline]
1013    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
1014        Ok(self.into_sexp())
1015    }
1016}
1017
1018impl IntoRAs<String> for &[&str] {
1019    #[inline]
1020    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
1021        Ok(self.into_sexp())
1022    }
1023}
1024
1025// Numeric vectors to String (stringify)
1026impl IntoRAs<String> for Vec<f64> {
1027    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
1028        let strings: Vec<String> = self
1029            .into_iter()
1030            .map(|v| {
1031                if v.is_nan() {
1032                    "NaN".to_string()
1033                } else if v.is_infinite() {
1034                    if v.is_sign_positive() {
1035                        "Inf".to_string()
1036                    } else {
1037                        "-Inf".to_string()
1038                    }
1039                } else {
1040                    v.to_string()
1041                }
1042            })
1043            .collect();
1044        Ok(strings.into_sexp())
1045    }
1046}
1047
1048impl IntoRAs<String> for Vec<i32> {
1049    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
1050        let strings: Vec<String> = self.into_iter().map(|v| v.to_string()).collect();
1051        Ok(strings.into_sexp())
1052    }
1053}
1054
1055impl IntoRAs<String> for Vec<i64> {
1056    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
1057        let strings: Vec<String> = self.into_iter().map(|v| v.to_string()).collect();
1058        Ok(strings.into_sexp())
1059    }
1060}
1061
1062impl IntoRAs<String> for Vec<bool> {
1063    fn into_r_as(self) -> Result<SEXP, StorageCoerceError> {
1064        let strings: Vec<String> = self
1065            .into_iter()
1066            .map(|b| if b { "TRUE" } else { "FALSE" }.to_string())
1067            .collect();
1068        Ok(strings.into_sexp())
1069    }
1070}
1071// endregion
1072
1073// region: Tests
1074
1075#[cfg(test)]
1076mod tests {
1077    use super::*;
1078
1079    // Note: These are compile-time tests to ensure the trait is implemented correctly.
1080    // Runtime tests require R to be initialized and should go in the integration tests.
1081
1082    fn _assert_into_r_as<T, Target>()
1083    where
1084        T: IntoRAs<Target>,
1085    {
1086    }
1087
1088    #[test]
1089    fn test_trait_bounds() {
1090        // Scalars -> i32
1091        _assert_into_r_as::<i32, i32>();
1092        _assert_into_r_as::<i64, i32>();
1093        _assert_into_r_as::<f64, i32>();
1094        _assert_into_r_as::<bool, i32>();
1095
1096        // Scalars -> f64
1097        _assert_into_r_as::<f64, f64>();
1098        _assert_into_r_as::<i32, f64>();
1099        _assert_into_r_as::<i64, f64>();
1100        _assert_into_r_as::<bool, f64>();
1101
1102        // Scalars -> u8
1103        _assert_into_r_as::<u8, u8>();
1104        _assert_into_r_as::<i32, u8>();
1105
1106        // Scalars -> RLogical
1107        _assert_into_r_as::<bool, RLogical>();
1108        _assert_into_r_as::<i32, RLogical>();
1109
1110        // Scalars -> String
1111        _assert_into_r_as::<String, String>();
1112        _assert_into_r_as::<&str, String>();
1113        _assert_into_r_as::<f64, String>();
1114        _assert_into_r_as::<i32, String>();
1115        _assert_into_r_as::<bool, String>();
1116
1117        // Vecs -> i32
1118        _assert_into_r_as::<Vec<i32>, i32>();
1119        _assert_into_r_as::<Vec<i64>, i32>();
1120        _assert_into_r_as::<Vec<f64>, i32>();
1121
1122        // Vecs -> f64
1123        _assert_into_r_as::<Vec<f64>, f64>();
1124        _assert_into_r_as::<Vec<i32>, f64>();
1125        _assert_into_r_as::<Vec<i64>, f64>();
1126
1127        // Vecs -> u8
1128        _assert_into_r_as::<Vec<u8>, u8>();
1129        _assert_into_r_as::<Vec<i32>, u8>();
1130
1131        // Vecs -> String
1132        _assert_into_r_as::<Vec<String>, String>();
1133        _assert_into_r_as::<Vec<f64>, String>();
1134    }
1135
1136    // Batched-error tests (#1097). These only exercise the failure path
1137    // (`into_sexp()` is never reached), so they don't need `with_r_thread` —
1138    // unlike the success-path integration tests in `conversion_coverage.rs`.
1139
1140    /// Failures at indices 3, 17, 42 must all be listed in one `Batched`
1141    /// error's `Display` string, not just the first one.
1142    #[test]
1143    fn batched_vec_i64_to_i32_lists_all_failing_indices() {
1144        let mut v = vec![0i64; 43];
1145        for &i in &[3usize, 17, 42] {
1146            v[i] = i64::MAX;
1147        }
1148        let result = IntoRAs::<i32>::into_r_as(v);
1149        match result {
1150            Err(StorageCoerceError::Batched {
1151                container,
1152                listed,
1153                total,
1154            }) => {
1155                assert_eq!(container, "Vec<i64>");
1156                assert_eq!(total, 3);
1157                assert_eq!(listed.len(), 3);
1158                let display = StorageCoerceError::Batched {
1159                    container,
1160                    listed,
1161                    total,
1162                }
1163                .to_string();
1164                assert!(display.contains("index 3"), "{display}");
1165                assert!(display.contains("index 17"), "{display}");
1166                assert!(display.contains("index 42"), "{display}");
1167            }
1168            other => panic!("expected Batched, got {other:?}"),
1169        }
1170    }
1171
1172    /// A 15-failure input lists only the first `BATCHED_ERROR_CAP` (10) and
1173    /// summarizes the rest as `"and 5 more"`.
1174    #[test]
1175    fn batched_vec_i64_to_i32_caps_listed_and_summarizes_rest() {
1176        let v = vec![i64::MAX; 15];
1177        let result = IntoRAs::<i32>::into_r_as(v);
1178        match result {
1179            Err(e @ StorageCoerceError::Batched { .. }) => {
1180                if let StorageCoerceError::Batched { listed, total, .. } = &e {
1181                    assert_eq!(*total, 15);
1182                    assert_eq!(listed.len(), BATCHED_ERROR_CAP);
1183                }
1184                let display = e.to_string();
1185                assert!(display.contains("and 5 more"), "{display}");
1186            }
1187            other => panic!("expected Batched, got {other:?}"),
1188        }
1189    }
1190
1191    /// The `&[T]` slice arms carry their own container label (`"&[i64]"`,
1192    /// not `"Vec<i64>"`) and batch failures the same way as the `Vec` arms.
1193    #[test]
1194    fn batched_slice_i64_to_i32_lists_all_failing_indices() {
1195        let mut v = vec![0i64; 6];
1196        v[1] = i64::MAX;
1197        v[4] = i64::MIN;
1198        let result = IntoRAs::<i32>::into_r_as(v.as_slice());
1199        match result {
1200            Err(e @ StorageCoerceError::Batched { .. }) => {
1201                if let StorageCoerceError::Batched {
1202                    container,
1203                    listed,
1204                    total,
1205                } = &e
1206                {
1207                    assert_eq!(*container, "&[i64]");
1208                    assert_eq!(*total, 2);
1209                    assert_eq!(listed.len(), 2);
1210                }
1211                let display = e.to_string();
1212                assert!(display.contains("&[i64] conversion failed"), "{display}");
1213                assert!(display.contains("index 1"), "{display}");
1214                assert!(display.contains("index 4"), "{display}");
1215            }
1216            other => panic!("expected Batched, got {other:?}"),
1217        }
1218    }
1219
1220    /// Scalar conversion failures must keep the plain (non-`Batched`) shape
1221    /// and Display grammar — only vector conversions batch.
1222    #[test]
1223    fn scalar_failure_display_unchanged() {
1224        let result = IntoRAs::<i32>::into_r_as(i64::MAX);
1225        match result {
1226            Err(e @ StorageCoerceError::OutOfRange { .. }) => {
1227                assert_eq!(e.to_string(), "value out of range for i64 → i32");
1228            }
1229            other => panic!("expected scalar OutOfRange, got {other:?}"),
1230        }
1231    }
1232}
1233// endregion