Skip to main content

miniextendr_api/from_r/
tuples.rs

1//! Tuple conversions: read an R list (VECSXP) positionally into `(A, B, ...)`.
2//!
3//! Inbound counterpart of the `IntoR` tuple family (`crate::into_r`, tuple to
4//! unnamed list region) — same arities (1 through 8), same VECSXP shape.
5//!
6//! Semantics:
7//! - The input must be a list (VECSXP) of exactly N elements; names are
8//!   ignored (conversion is positional).
9//! - Element `i` converts via `<Ti as TryFromSexp>::try_from_sexp`.
10//! - All failing elements are collected into one batched diagnostic
11//!   (1-based positions, matching R indexing) instead of bailing on the
12//!   first failure.
13
14use crate::from_r::{SexpError, SexpLengthError, SexpTypeError, TryFromSexp};
15use crate::{SEXP, SEXPTYPE, SexpExt};
16
17fn check_list_shape(sexp: SEXP, expected_len: usize, len: usize) -> Result<(), SexpError> {
18    let actual = sexp.type_of();
19    if actual != SEXPTYPE::VECSXP {
20        return Err(SexpTypeError {
21            expected: SEXPTYPE::VECSXP,
22            actual,
23        }
24        .into());
25    }
26    if len != expected_len {
27        return Err(SexpLengthError {
28            expected: expected_len,
29            actual: len,
30        }
31        .into());
32    }
33    Ok(())
34}
35
36fn batch_tuple_errors(errors: Vec<String>) -> SexpError {
37    SexpError::InvalidValue(format!("tuple conversion failed: {}", errors.join("; ")))
38}
39
40/// Implement `TryFromSexp` for tuples of various sizes (1-8).
41/// Reads an unnamed R list (VECSXP) positionally; mirrors `impl_tuple_into_r!`.
42macro_rules! impl_tuple_try_from_sexp {
43    (($($T:ident),+), ($($idx:tt),+), $n:expr) => {
44        impl<$($T: TryFromSexp),+> TryFromSexp for ($($T,)+)
45        where
46            $(<$T as TryFromSexp>::Error: Into<SexpError>,)+
47        {
48            type Error = SexpError;
49
50            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
51                check_list_shape(sexp, $n, sexp.len())?;
52
53                let mut errors: Vec<String> = Vec::new();
54                let partial = (
55                    $(
56                        match <$T as TryFromSexp>::try_from_sexp(
57                            sexp.vector_elt($idx as isize),
58                        ) {
59                            Ok(v) => Some(v),
60                            Err(e) => {
61                                errors.push(format!(
62                                    "element {}: {}",
63                                    $idx + 1,
64                                    Into::<SexpError>::into(e)
65                                ));
66                                None
67                            }
68                        },
69                    )+
70                );
71                if !errors.is_empty() {
72                    return Err(batch_tuple_errors(errors));
73                }
74                // Every slot is Some: the errors vec was empty.
75                Ok(($(partial.$idx.unwrap(),)+))
76            }
77
78            unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
79                check_list_shape(sexp, $n, unsafe { sexp.len_unchecked() })?;
80
81                let mut errors: Vec<String> = Vec::new();
82                let partial = (
83                    $(
84                        match unsafe {
85                            <$T as TryFromSexp>::try_from_sexp_unchecked(
86                                sexp.vector_elt_unchecked($idx as isize),
87                            )
88                        } {
89                            Ok(v) => Some(v),
90                            Err(e) => {
91                                errors.push(format!(
92                                    "element {}: {}",
93                                    $idx + 1,
94                                    Into::<SexpError>::into(e)
95                                ));
96                                None
97                            }
98                        },
99                    )+
100                );
101                if !errors.is_empty() {
102                    return Err(batch_tuple_errors(errors));
103                }
104                Ok(($(partial.$idx.unwrap(),)+))
105            }
106        }
107    };
108}
109
110// Implement for tuples of sizes 1-8, mirroring the IntoR tuple family.
111impl_tuple_try_from_sexp!((A), (0), 1);
112impl_tuple_try_from_sexp!((A, B), (0, 1), 2);
113impl_tuple_try_from_sexp!((A, B, C), (0, 1, 2), 3);
114impl_tuple_try_from_sexp!((A, B, C, D), (0, 1, 2, 3), 4);
115impl_tuple_try_from_sexp!((A, B, C, D, E), (0, 1, 2, 3, 4), 5);
116impl_tuple_try_from_sexp!((A, B, C, D, E, F), (0, 1, 2, 3, 4, 5), 6);
117impl_tuple_try_from_sexp!((A, B, C, D, E, F, G), (0, 1, 2, 3, 4, 5, 6), 7);
118impl_tuple_try_from_sexp!((A, B, C, D, E, F, G, H), (0, 1, 2, 3, 4, 5, 6, 7), 8);