Skip to main content

miniextendr_api/
strict.rs

1//! Strict conversion helpers for `#[miniextendr(strict)]`.
2//!
3//! These functions panic instead of silently widening when a value cannot be
4//! exactly represented as an R integer (`INTSXP`). This provides an opt-in
5//! alternative to the default `IntoR` behavior which silently falls back to
6//! `REALSXP` (f64) for out-of-range values.
7//!
8//! # Motivation
9//!
10//! R has no native 64-bit integer type. The default `i64::into_sexp()` picks
11//! `INTSXP` when the value fits and `REALSXP` otherwise — silently losing
12//! precision for values outside `[-2^53, 2^53]`. With `#[miniextendr(strict)]`,
13//! the macro generates calls to these helpers instead, which panic (→ R error)
14//! if the value doesn't fit in i32.
15//!
16//! # Paired with
17//!
18//! This is the **strict outbound** path. Its lax counterpart is the bare
19//! [`IntoR`] impls for `i64`/`u64`/`isize`/`usize` (see [`crate::into_r`]).
20//! Failure mode of staying on the lax path when you cared about exact
21//! representation: a counter / ID just above `i32::MAX` lands in R as
22//! `REALSXP`, and anything above `2^53` starts colliding silently.
23//!
24//! There is **no `TryFromSexpStrict` trait** — inbound is already
25//! strict-by-default because [`crate::from_r::TryFromSexp`] returns
26//! `Result<T, SexpError>`. The looser inbound path is
27//! [`crate::coerce::Coerce`] / [`crate::coerce::TryCoerce`].
28//!
29//! For storage-directed conversions (force `Vec<i64>` into `INTSXP` and error
30//! if any element doesn't fit) see [`crate::into_r_as::IntoRAs`] — it's a
31//! third path with value-based runtime checks.
32
33use crate::coerce::TryCoerce;
34use crate::from_r::{BatchedErrors, SexpError, TryFromSexp};
35use crate::into_r::IntoR;
36use crate::{IsNA, SEXP, SEXPTYPE, SexpExt};
37
38/// Fold a batched strict-vec [`BatchedErrors`] into the single panic every
39/// `checked_vec_*_into_sexp` raises for the return value of Rust type
40/// `container` (e.g. `"Vec<i64>"`).
41///
42/// Uses [`BatchedErrors::element_message`]'s grammar (#1192/#1097), each
43/// reason with the 1-based positions of the elements in the returned vector:
44/// `strict conversion failed for Vec<i64>: i64 value 3000000000 is outside R
45/// integer range (-2147483647..=2147483647) (element 2); use a non-strict ...`.
46/// A return value has no `e$rust_type`, so the message names the type.
47fn panic_strict_vec_batched(container: &str, errors: BatchedErrors) -> ! {
48    {
    ::core::panicking::panic_fmt(format_args!("strict conversion failed for {1}: {0}; use a non-strict function to allow lossy f64 widening",
            errors.element_message(), container));
};panic!(
49        "strict conversion failed for {container}: {}; use a non-strict function to allow \
50         lossy f64 widening",
51        errors.element_message()
52    );
53}
54
55// region: Strict OUTPUT helpers — panic when a value has no R integer
56//
57// Every helper converts through one `StrictRInt` element type, so the scalar,
58// vector, `Vec<Option<_>>` and list-of-vectors shapes share the range check and
59// the reason wording, and each names the Rust type it was given (`usize value
60// ... exceeds R integer max`, `strict conversion failed for Vec<usize>`).
61
62/// A lossy integer type for the strict outbound path: its R integer, if any,
63/// and the reason it has none.
64#[doc(hidden)]
65pub trait StrictRInt: Copy + std::fmt::Display {
66    /// The Rust type name the panic message uses.
67    const NAME: &'static str;
68
69    /// `Some` when the value is an R integer: it fits `i32` and is not
70    /// `i32::MIN`, which is `NA_integer_` in R.
71    fn to_r_int(self) -> Option<i32>;
72
73    /// Why `self` has no R integer.
74    fn range_reason(self) -> String;
75}
76
77macro_rules! impl_strict_r_int {
78    (signed: $($t:ty),*) => {$(
79        impl StrictRInt for $t {
80            const NAME: &'static str = stringify!($t);
81            #[inline]
82            fn to_r_int(self) -> Option<i32> {
83                i32::try_from(self).ok().filter(|&v| !v.is_na())
84            }
85            fn range_reason(self) -> String {
86                format!(
87                    "{} value {self} is outside R integer range ({}..={})",
88                    Self::NAME,
89                    i32::MIN + 1,
90                    i32::MAX
91                )
92            }
93        }
94    )*};
95    (unsigned: $($t:ty),*) => {$(
96        impl StrictRInt for $t {
97            const NAME: &'static str = stringify!($t);
98            #[inline]
99            fn to_r_int(self) -> Option<i32> {
100                i32::try_from(self).ok()
101            }
102            fn range_reason(self) -> String {
103                format!("{} value {self} exceeds R integer max ({})", Self::NAME, i32::MAX)
104            }
105        }
106    )*};
107}
108
109impl StrictRInt for i64 {
    const NAME: &'static str = "i64";
    #[inline]
    fn to_r_int(self) -> Option<i32> {
        i32::try_from(self).ok().filter(|&v| !v.is_na())
    }
    fn range_reason(self) -> String {
        ::alloc::__export::must_use({
                ::alloc::fmt::format(format_args!("{0} value {3} is outside R integer range ({1}..={2})",
                        Self::NAME, i32::MIN + 1, i32::MAX, self))
            })
    }
}
impl StrictRInt for isize {
    const NAME: &'static str = "isize";
    #[inline]
    fn to_r_int(self) -> Option<i32> {
        i32::try_from(self).ok().filter(|&v| !v.is_na())
    }
    fn range_reason(self) -> String {
        ::alloc::__export::must_use({
                ::alloc::fmt::format(format_args!("{0} value {3} is outside R integer range ({1}..={2})",
                        Self::NAME, i32::MIN + 1, i32::MAX, self))
            })
    }
}impl_strict_r_int!(signed: i64, isize);
110impl StrictRInt for u64 {
    const NAME: &'static str = "u64";
    #[inline]
    fn to_r_int(self) -> Option<i32> { i32::try_from(self).ok() }
    fn range_reason(self) -> String {
        ::alloc::__export::must_use({
                ::alloc::fmt::format(format_args!("{0} value {2} exceeds R integer max ({1})",
                        Self::NAME, i32::MAX, self))
            })
    }
}
impl StrictRInt for usize {
    const NAME: &'static str = "usize";
    #[inline]
    fn to_r_int(self) -> Option<i32> { i32::try_from(self).ok() }
    fn range_reason(self) -> String {
        ::alloc::__export::must_use({
                ::alloc::fmt::format(format_args!("{0} value {2} exceeds R integer max ({1})",
                        Self::NAME, i32::MAX, self))
            })
    }
}impl_strict_r_int!(unsigned: u64, usize);
111
112/// The scalar strict conversion: the R integer, or the panic.
113fn checked_scalar_into_sexp<T: StrictRInt>(val: T) -> SEXP {
114    match val.to_r_int() {
115        Some(v) => v.into_sexp(),
116        None => {
    ::core::panicking::panic_fmt(format_args!("strict conversion failed: {0}; use a non-strict function to allow lossy f64 widening",
            val.range_reason()));
}panic!(
117            "strict conversion failed: {}; use a non-strict function to allow lossy f64 widening",
118            val.range_reason()
119        ),
120    }
121}
122
123/// The `Vec<T>` / `Vec<Option<T>>` strict conversion: `None` is `NA_integer_`.
124///
125/// Walks the whole vector, batching every out-of-range element into one
126/// panic instead of aborting at the first — see `panic_strict_vec_batched`.
127fn checked_elems_into_sexp<T: StrictRInt>(
128    container: &str,
129    elems: impl ExactSizeIterator<Item = Option<T>>,
130) -> SEXP {
131    let mut coerced: Vec<Option<i32>> = Vec::with_capacity(elems.len());
132    let mut errors = BatchedErrors::default();
133    for (i, elem) in elems.enumerate() {
134        match elem.map(|x| (x, x.to_r_int())) {
135            Some((_, Some(v))) => coerced.push(Some(v)),
136            Some((x, None)) => errors.push(i, || x.range_reason()),
137            None => coerced.push(None),
138        }
139    }
140    if !errors.is_empty() {
141        panic_strict_vec_batched(container, errors);
142    }
143    coerced.into_sexp()
144}
145
146/// Strict conversion shared by borrowed slices, boxed slices and arrays.
147/// Generated wrappers supply the written return type for the diagnostic.
148#[doc(hidden)]
149pub fn checked_slice_into_sexp<T: StrictRInt>(container: &str, values: &[T]) -> SEXP {
150    checked_elems_into_sexp(container, values.iter().copied().map(Some))
151}
152
153/// The list-of-vectors strict conversion (`Vec<Vec<T>>`, `Vec<Option<Vec<T>>>`):
154/// validate every element of every row, then build the list of integer vectors
155/// (`None` rows become `NULL`).
156///
157/// Walks every row before panicking, so one panic lists every out-of-range
158/// element: its row is the `(element N)` position, and its place inside the
159/// row is the `inner position`. Nothing is allocated in R until the whole
160/// input has passed, so the panic leaves no partial list behind.
161#[doc(hidden)]
162pub fn checked_rows_into_sexp<T: StrictRInt, R: AsRef<[T]>>(
163    container: &str,
164    rows: impl ExactSizeIterator<Item = Option<R>>,
165) -> SEXP {
166    let mut coerced: Vec<Option<Vec<i32>>> = Vec::with_capacity(rows.len());
167    let mut errors = BatchedErrors::default();
168    for (i, row) in rows.enumerate() {
169        coerced.push(row.map(|row| {
170            let row = row.as_ref();
171            let mut out = Vec::with_capacity(row.len());
172            for (j, &x) in row.iter().enumerate() {
173                match x.to_r_int() {
174                    Some(v) => out.push(v),
175                    None => errors.push(i, || {
176                        ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} at inner position {1}",
                x.range_reason(), j + 1))
    })format!("{} at inner position {}", x.range_reason(), j + 1)
177                    }),
178                }
179            }
180            out
181        }));
182    }
183    if !errors.is_empty() {
184        panic_strict_vec_batched(container, errors);
185    }
186    coerced.into_sexp()
187}
188
189/// Convert `i64` to R integer, panicking if outside i32 range.
190///
191/// The valid range is `(i32::MIN, i32::MAX]` — `i32::MIN` is excluded because
192/// it is `NA_integer_` in R.
193#[inline]
194pub fn checked_into_sexp_i64(val: i64) -> SEXP {
195    checked_scalar_into_sexp(val)
196}
197
198/// Convert `u64` to R integer, panicking if > i32::MAX.
199#[inline]
200pub fn checked_into_sexp_u64(val: u64) -> SEXP {
201    checked_scalar_into_sexp(val)
202}
203
204/// Convert `isize` to R integer, panicking if outside i32 range.
205#[inline]
206pub fn checked_into_sexp_isize(val: isize) -> SEXP {
207    checked_scalar_into_sexp(val)
208}
209
210/// Convert `usize` to R integer, panicking if > i32::MAX.
211#[inline]
212pub fn checked_into_sexp_usize(val: usize) -> SEXP {
213    checked_scalar_into_sexp(val)
214}
215
216/// Convert `Vec<i64>` to R integer vector, panicking if any element is outside i32 range.
217///
218/// Walks the whole vector, batching every out-of-range element into one
219/// panic instead of aborting at the first — see `panic_strict_vec_batched`.
220pub fn checked_vec_i64_into_sexp(val: Vec<i64>) -> SEXP {
221    checked_elems_into_sexp("Vec<i64>", val.into_iter().map(Some))
222}
223
224/// Convert `Vec<u64>` to R integer vector, panicking if any element > i32::MAX.
225///
226/// Walks the whole vector, batching every out-of-range element into one
227/// panic instead of aborting at the first — see `panic_strict_vec_batched`.
228pub fn checked_vec_u64_into_sexp(val: Vec<u64>) -> SEXP {
229    checked_elems_into_sexp("Vec<u64>", val.into_iter().map(Some))
230}
231
232/// Convert `Vec<isize>` to R integer vector, panicking if any element is outside i32 range.
233pub fn checked_vec_isize_into_sexp(val: Vec<isize>) -> SEXP {
234    checked_elems_into_sexp("Vec<isize>", val.into_iter().map(Some))
235}
236
237/// Convert `Vec<usize>` to R integer vector, panicking if any element > i32::MAX.
238pub fn checked_vec_usize_into_sexp(val: Vec<usize>) -> SEXP {
239    checked_elems_into_sexp("Vec<usize>", val.into_iter().map(Some))
240}
241
242/// Convert `Vec<Option<i64>>` to R integer vector in strict mode.
243///
244/// Panics if any `Some(x)` value is outside i32 range. `None` becomes
245/// `NA_INTEGER`. Walks the whole vector, batching every out-of-range `Some`
246/// into one panic instead of aborting at the first — see
247/// `panic_strict_vec_batched`.
248pub fn checked_vec_option_i64_into_sexp(val: Vec<Option<i64>>) -> SEXP {
249    checked_elems_into_sexp("Vec<Option<i64>>", val.into_iter())
250}
251
252/// Convert `Vec<Option<u64>>` to R integer vector in strict mode.
253///
254/// Walks the whole vector, batching every out-of-range `Some` into one
255/// panic instead of aborting at the first — see `panic_strict_vec_batched`.
256pub fn checked_vec_option_u64_into_sexp(val: Vec<Option<u64>>) -> SEXP {
257    checked_elems_into_sexp("Vec<Option<u64>>", val.into_iter())
258}
259
260/// Convert `Vec<Option<isize>>` to R integer vector in strict mode.
261pub fn checked_vec_option_isize_into_sexp(val: Vec<Option<isize>>) -> SEXP {
262    checked_elems_into_sexp("Vec<Option<isize>>", val.into_iter())
263}
264
265/// Convert `Vec<Option<usize>>` to R integer vector in strict mode.
266pub fn checked_vec_option_usize_into_sexp(val: Vec<Option<usize>>) -> SEXP {
267    checked_elems_into_sexp("Vec<Option<usize>>", val.into_iter())
268}
269
270/// Convert `Option<i64>` to R integer in strict mode.
271/// Panics if `Some(x)` is outside i32 range. `None` becomes `NA_integer_`.
272#[inline]
273pub fn checked_option_i64_into_sexp(val: Option<i64>) -> SEXP {
274    match val {
275        Some(x) => checked_into_sexp_i64(x),
276        None => Option::<i32>::None.into_sexp(),
277    }
278}
279
280/// Convert `Option<u64>` to R integer in strict mode.
281/// Panics if `Some(x)` exceeds i32::MAX. `None` becomes `NA_integer_`.
282#[inline]
283pub fn checked_option_u64_into_sexp(val: Option<u64>) -> SEXP {
284    match val {
285        Some(x) => checked_into_sexp_u64(x),
286        None => Option::<i32>::None.into_sexp(),
287    }
288}
289
290/// Convert `Option<isize>` to R integer in strict mode.
291#[inline]
292pub fn checked_option_isize_into_sexp(val: Option<isize>) -> SEXP {
293    match val {
294        Some(x) => checked_into_sexp_isize(x),
295        None => Option::<i32>::None.into_sexp(),
296    }
297}
298
299/// Convert `Option<usize>` to R integer in strict mode.
300#[inline]
301pub fn checked_option_usize_into_sexp(val: Option<usize>) -> SEXP {
302    match val {
303        Some(x) => checked_into_sexp_usize(x),
304        None => Option::<i32>::None.into_sexp(),
305    }
306}
307
308/// Convert `Vec<Vec<i64>>` to an R list of integer vectors, panicking if any
309/// element of any row is outside i32 range. Every failing element is listed in
310/// one panic.
311pub fn checked_vec_vec_i64_into_sexp(val: Vec<Vec<i64>>) -> SEXP {
312    checked_rows_into_sexp("Vec<Vec<i64>>", val.into_iter().map(Some))
313}
314
315/// Convert `Vec<Vec<u64>>` to an R list of integer vectors, panicking if any
316/// element of any row exceeds `i32::MAX`.
317pub fn checked_vec_vec_u64_into_sexp(val: Vec<Vec<u64>>) -> SEXP {
318    checked_rows_into_sexp("Vec<Vec<u64>>", val.into_iter().map(Some))
319}
320
321/// Convert `Vec<Vec<isize>>` to an R list of integer vectors, panicking if any
322/// element of any row is outside i32 range.
323pub fn checked_vec_vec_isize_into_sexp(val: Vec<Vec<isize>>) -> SEXP {
324    checked_rows_into_sexp("Vec<Vec<isize>>", val.into_iter().map(Some))
325}
326
327/// Convert `Vec<Vec<usize>>` to an R list of integer vectors, panicking if any
328/// element of any row exceeds `i32::MAX`.
329pub fn checked_vec_vec_usize_into_sexp(val: Vec<Vec<usize>>) -> SEXP {
330    checked_rows_into_sexp("Vec<Vec<usize>>", val.into_iter().map(Some))
331}
332
333/// Convert `Vec<Option<Vec<i64>>>` to an R list of integer vectors (`None` →
334/// `NULL`), panicking if any element of any row is outside i32 range.
335pub fn checked_vec_option_vec_i64_into_sexp(val: Vec<Option<Vec<i64>>>) -> SEXP {
336    checked_rows_into_sexp("Vec<Option<Vec<i64>>>", val.into_iter())
337}
338
339/// Convert `Vec<Option<Vec<u64>>>` to an R list of integer vectors (`None` →
340/// `NULL`), panicking if any element of any row exceeds `i32::MAX`.
341pub fn checked_vec_option_vec_u64_into_sexp(val: Vec<Option<Vec<u64>>>) -> SEXP {
342    checked_rows_into_sexp("Vec<Option<Vec<u64>>>", val.into_iter())
343}
344
345/// Convert `Vec<Option<Vec<isize>>>` to an R list of integer vectors (`None` →
346/// `NULL`), panicking if any element of any row is outside i32 range.
347pub fn checked_vec_option_vec_isize_into_sexp(val: Vec<Option<Vec<isize>>>) -> SEXP {
348    checked_rows_into_sexp("Vec<Option<Vec<isize>>>", val.into_iter())
349}
350
351/// Convert `Vec<Option<Vec<usize>>>` to an R list of integer vectors (`None` →
352/// `NULL`), panicking if any element of any row exceeds `i32::MAX`.
353pub fn checked_vec_option_vec_usize_into_sexp(val: Vec<Option<Vec<usize>>>) -> SEXP {
354    checked_rows_into_sexp("Vec<Option<Vec<usize>>>", val.into_iter())
355}
356
357// endregion
358
359// region: Strict INPUT helpers — only accept INTSXP and REALSXP, reject RAWSXP/LGLSXP
360//
361// A rejected input is an argument error, not a panic (#1594): each helper
362// returns `Result<_, SexpError>`, and the generated wrapper turns an `Err`
363// into the same `kind = "conversion"` condition as any other conversion
364// failure (`'x' must be a single whole number: got logical`, `e$param`,
365// `e$rust_type`). The vector helpers walk the whole input and report every
366// failing element at once, with R's 1-based positions.
367
368/// The element reason for an R `NA` in a strict input that cannot hold one.
369const NA_NOT_ALLOWED: &str = "NA is not allowed";
370
371/// Convert R SEXP to `i64` in strict mode.
372///
373/// Only INTSXP and REALSXP are accepted. RAWSXP and LGLSXP are rejected.
374/// For REALSXP, uses `TryCoerce` to reject fractional, NaN, and out-of-range
375/// values; `NA` is rejected too.
376#[inline]
377pub fn checked_try_from_sexp_i64(sexp: SEXP) -> Result<i64, SexpError> {
378    checked_try_from_sexp_numeric_scalar::<i64>(sexp)
379}
380
381/// Convert R SEXP to `u64` in strict mode.
382#[inline]
383pub fn checked_try_from_sexp_u64(sexp: SEXP) -> Result<u64, SexpError> {
384    checked_try_from_sexp_numeric_scalar::<u64>(sexp)
385}
386
387/// Convert R SEXP to `isize` in strict mode.
388#[inline]
389pub fn checked_try_from_sexp_isize(sexp: SEXP) -> Result<isize, SexpError> {
390    let val = checked_try_from_sexp_i64(sexp)?;
391    narrow::<i64, isize>(val).map_err(SexpError::InvalidValue)
392}
393
394/// Convert R SEXP to `usize` in strict mode.
395#[inline]
396pub fn checked_try_from_sexp_usize(sexp: SEXP) -> Result<usize, SexpError> {
397    let val = checked_try_from_sexp_u64(sexp)?;
398    narrow::<u64, usize>(val).map_err(SexpError::InvalidValue)
399}
400
401/// Convert R SEXP to `Vec<i64>` in strict mode.
402pub fn checked_vec_try_from_sexp_i64(sexp: SEXP) -> Result<Vec<i64>, SexpError> {
403    checked_vec_try_from_sexp_numeric::<i64>(sexp)
404}
405
406/// Convert R SEXP to `Vec<u64>` in strict mode.
407pub fn checked_vec_try_from_sexp_u64(sexp: SEXP) -> Result<Vec<u64>, SexpError> {
408    checked_vec_try_from_sexp_numeric::<u64>(sexp)
409}
410
411/// Convert R SEXP to `Vec<isize>` in strict mode.
412pub fn checked_vec_try_from_sexp_isize(sexp: SEXP) -> Result<Vec<isize>, SexpError> {
413    let vals = checked_vec_try_from_sexp_i64(sexp)?;
414    batch_elements(&vals, narrow::<i64, isize>)
415}
416
417/// Convert R SEXP to `Vec<usize>` in strict mode.
418pub fn checked_vec_try_from_sexp_usize(sexp: SEXP) -> Result<Vec<usize>, SexpError> {
419    let vals = checked_vec_try_from_sexp_u64(sexp)?;
420    batch_elements(&vals, narrow::<u64, usize>)
421}
422
423/// Convert R SEXP to `Vec<Option<i64>>` in strict mode.
424///
425/// Applies the same input-SEXP-type gate as [`checked_vec_try_from_sexp_i64`]
426/// — only INTSXP and REALSXP are accepted; LGLSXP and RAWSXP are rejected.
427/// NA elements become `None`; type strictness and missingness are orthogonal.
428pub fn checked_vec_option_try_from_sexp_i64(sexp: SEXP) -> Result<Vec<Option<i64>>, SexpError> {
429    checked_vec_option_try_from_sexp_numeric::<i64>(sexp)
430}
431
432/// Convert R SEXP to `Vec<Option<u64>>` in strict mode.
433pub fn checked_vec_option_try_from_sexp_u64(sexp: SEXP) -> Result<Vec<Option<u64>>, SexpError> {
434    checked_vec_option_try_from_sexp_numeric::<u64>(sexp)
435}
436
437/// Convert R SEXP to `Vec<Option<isize>>` in strict mode.
438pub fn checked_vec_option_try_from_sexp_isize(sexp: SEXP) -> Result<Vec<Option<isize>>, SexpError> {
439    let vals = checked_vec_option_try_from_sexp_i64(sexp)?;
440    batch_elements(&vals, |opt| opt.map(narrow::<i64, isize>).transpose())
441}
442
443/// Convert R SEXP to `Vec<Option<usize>>` in strict mode.
444pub fn checked_vec_option_try_from_sexp_usize(sexp: SEXP) -> Result<Vec<Option<usize>>, SexpError> {
445    let vals = checked_vec_option_try_from_sexp_u64(sexp)?;
446    batch_elements(&vals, |opt| opt.map(narrow::<u64, usize>).transpose())
447}
448
449/// Narrow a 64-bit value to the pointer-sized type, with the reason of a
450/// failure (only reachable on a 32-bit target).
451fn narrow<S, T>(val: S) -> Result<T, String>
452where
453    S: Copy + std::fmt::Display,
454    T: TryFrom<S>,
455{
456    T::try_from(val).map_err(|_| ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{1} does not fit in {0}",
                std::any::type_name::<T>(), val))
    })format!("{val} does not fit in {}", std::any::type_name::<T>()))
457}
458
459/// The strict input-type gate: only INTSXP and REALSXP are accepted.
460fn strict_type_error(actual: SEXPTYPE) -> SexpError {
461    crate::from_r::SexpTypeError {
462        expected: SEXPTYPE::INTSXP,
463        actual,
464    }
465    .into()
466}
467
468/// Convert every element of `slice`, batching every failure (its reason and
469/// 1-based position) into one [`SexpError::InvalidValue`] instead of stopping
470/// at the first.
471fn batch_elements<S: Copy, U>(
472    slice: &[S],
473    convert: impl Fn(S) -> Result<U, String>,
474) -> Result<Vec<U>, SexpError> {
475    let mut out = Vec::with_capacity(slice.len());
476    let mut errors = BatchedErrors::default();
477    for (i, &v) in slice.iter().enumerate() {
478        match convert(v) {
479            Ok(x) => out.push(x),
480            Err(reason) => errors.push(i, || reason),
481        }
482    }
483    if errors.is_empty() {
484        Ok(out)
485    } else {
486        Err(errors.into_element_error())
487    }
488}
489
490/// One INTSXP element: `NA_integer_` is `None`; any other value coerces.
491fn strict_int_elt<T>(v: i32) -> Result<Option<T>, String>
492where
493    i32: TryCoerce<T>,
494    <i32 as TryCoerce<T>>::Error: std::fmt::Display,
495{
496    if v.is_na() {
497        return Ok(None);
498    }
499    TryCoerce::<T>::try_coerce(v)
500        .map(Some)
501        .map_err(|e| e.to_string())
502}
503
504/// One REALSXP element: `NA_real_` is `None`; any other value coerces, so a
505/// fractional, NaN or out-of-range double fails.
506fn strict_real_elt<T>(v: f64) -> Result<Option<T>, String>
507where
508    f64: TryCoerce<T>,
509    <f64 as TryCoerce<T>>::Error: std::fmt::Display,
510{
511    if v.is_na() {
512        return Ok(None);
513    }
514    TryCoerce::<T>::try_coerce(v)
515        .map(Some)
516        .map_err(|e| e.to_string())
517}
518
519/// Generic strict scalar conversion: only INTSXP and REALSXP allowed, of
520/// length 1 and not `NA`.
521#[inline]
522fn checked_try_from_sexp_numeric_scalar<T>(sexp: SEXP) -> Result<T, SexpError>
523where
524    i32: TryCoerce<T>,
525    f64: TryCoerce<T>,
526    <i32 as TryCoerce<T>>::Error: std::fmt::Display,
527    <f64 as TryCoerce<T>>::Error: std::fmt::Display,
528{
529    let actual = sexp.type_of();
530    let value = match actual {
531        SEXPTYPE::INTSXP => {
532            let value: i32 = TryFromSexp::try_from_sexp(sexp)?;
533            strict_int_elt::<T>(value)
534        }
535        SEXPTYPE::REALSXP => {
536            let value: f64 = TryFromSexp::try_from_sexp(sexp)?;
537            strict_real_elt::<T>(value)
538        }
539        _ => return Err(strict_type_error(actual)),
540    };
541    match value {
542        Ok(Some(v)) => Ok(v),
543        Ok(None) => Err(crate::from_r::SexpNaError { sexp_type: actual }.into()),
544        Err(reason) => Err(SexpError::InvalidValue(reason)),
545    }
546}
547
548/// Generic strict vector conversion: only INTSXP and REALSXP allowed; an `NA`
549/// element is an error, like any other element that does not convert.
550fn checked_vec_try_from_sexp_numeric<T>(sexp: SEXP) -> Result<Vec<T>, SexpError>
551where
552    i32: TryCoerce<T>,
553    f64: TryCoerce<T>,
554    <i32 as TryCoerce<T>>::Error: std::fmt::Display,
555    <f64 as TryCoerce<T>>::Error: std::fmt::Display,
556{
557    let required = |v: Option<T>| v.ok_or_else(|| NA_NOT_ALLOWED.to_string());
558    match sexp.type_of() {
559        SEXPTYPE::INTSXP => {
560            let slice: &[i32] = unsafe { sexp.as_slice() };
561            batch_elements(slice, |v| strict_int_elt::<T>(v).and_then(required))
562        }
563        SEXPTYPE::REALSXP => {
564            let slice: &[f64] = unsafe { sexp.as_slice() };
565            batch_elements(slice, |v| strict_real_elt::<T>(v).and_then(required))
566        }
567        actual => Err(strict_type_error(actual)),
568    }
569}
570
571/// Generic strict `Vec<Option<T>>` conversion: only INTSXP and REALSXP allowed.
572///
573/// Mirrors [`checked_vec_try_from_sexp_numeric`] but maps R's NA sentinel
574/// (`NA_INTEGER` for INTSXP, `NA_REAL` for REALSXP) to `None` instead of
575/// erroring — missingness is orthogonal to the input-type gate.
576fn checked_vec_option_try_from_sexp_numeric<T>(sexp: SEXP) -> Result<Vec<Option<T>>, SexpError>
577where
578    i32: TryCoerce<T>,
579    f64: TryCoerce<T>,
580    <i32 as TryCoerce<T>>::Error: std::fmt::Display,
581    <f64 as TryCoerce<T>>::Error: std::fmt::Display,
582{
583    match sexp.type_of() {
584        SEXPTYPE::INTSXP => {
585            let slice: &[i32] = unsafe { sexp.as_slice() };
586            batch_elements(slice, strict_int_elt::<T>)
587        }
588        SEXPTYPE::REALSXP => {
589            let slice: &[f64] = unsafe { sexp.as_slice() };
590            batch_elements(slice, strict_real_elt::<T>)
591        }
592        actual => Err(strict_type_error(actual)),
593    }
594}
595
596#[cfg(test)]
597mod tests {
598    use super::*;
599
600    #[test]
601    fn i64_in_range_has_an_r_integer() {
602        for value in [0, 42, -1, i64::from(i32::MAX)] {
603            assert_eq!(value.to_r_int(), i32::try_from(value).ok());
604        }
605    }
606
607    #[test]
608    fn i64_out_of_range_panics() {
609        let result = std::panic::catch_unwind(|| checked_into_sexp_i64(i64::MAX));
610        assert!(result.is_err(), "should panic for i64::MAX");
611
612        let result = std::panic::catch_unwind(|| checked_into_sexp_i64(i32::MIN as i64));
613        assert!(result.is_err(), "should panic for i32::MIN (NA_integer_)");
614
615        let result = std::panic::catch_unwind(|| checked_into_sexp_i64(i32::MAX as i64 + 1));
616        assert!(result.is_err(), "should panic for i32::MAX + 1");
617    }
618
619    #[test]
620    fn u64_in_range_has_an_r_integer() {
621        assert_eq!(0_u64.to_r_int(), Some(0));
622        assert_eq!(u64::try_from(i32::MAX).unwrap().to_r_int(), Some(i32::MAX));
623    }
624
625    #[test]
626    fn borrowed_and_boxed_rows_report_every_invalid_position() {
627        let bad = [i64::MAX, 1, i64::MIN];
628        let message = panic_message(
629            std::panic::catch_unwind(|| checked_slice_into_sexp("&[i64]", &bad)).unwrap_err(),
630        );
631        assert!(message.contains("&[i64]"), "{message}");
632        assert!(message.contains("element 1"), "{message}");
633        assert!(message.contains("element 3"), "{message}");
634        for message in [
635            panic_message(
636                std::panic::catch_unwind(|| {
637                    checked_rows_into_sexp::<i64, _>(
638                        "Vec<Option<&[i64]>>",
639                        [Some(bad.as_slice()), None, Some(bad.as_slice())].into_iter(),
640                    )
641                })
642                .unwrap_err(),
643            ),
644            panic_message(
645                std::panic::catch_unwind(|| {
646                    checked_rows_into_sexp::<i64, _>(
647                        "Vec<Box<[i64]>>",
648                        [
649                            Some(Box::<[i64]>::from(bad)),
650                            None,
651                            Some(Box::<[i64]>::from(bad)),
652                        ]
653                        .into_iter(),
654                    )
655                })
656                .unwrap_err(),
657            ),
658            panic_message(
659                std::panic::catch_unwind(|| {
660                    checked_rows_into_sexp::<i64, _>(
661                        "Vec<[i64; 3]>",
662                        [Some(bad), None, Some(bad)].into_iter(),
663                    )
664                })
665                .unwrap_err(),
666            ),
667        ] {
668            assert!(message.contains("inner position 1"), "{message}");
669            assert!(message.contains("inner position 3"), "{message}");
670            assert!(message.contains("elements 1, 3"), "{message}");
671        }
672    }
673
674    #[test]
675    fn u64_out_of_range_panics() {
676        let result = std::panic::catch_unwind(|| checked_into_sexp_u64(i32::MAX as u64 + 1));
677        assert!(result.is_err());
678    }
679
680    /// Downcast a `panic!` payload (produced via format args, so always a
681    /// `String`) into an owned `String` for message assertions.
682    fn panic_message(payload: Box<dyn std::any::Any + Send>) -> String {
683        *payload
684            .downcast::<String>()
685            .expect("panic payload should be a String")
686    }
687
688    #[test]
689    fn vec_i64_batches_multiple_out_of_range_indices() {
690        let result =
691            std::panic::catch_unwind(|| checked_vec_i64_into_sexp(vec![1, i64::MAX, 2, i64::MIN]));
692        let msg = panic_message(result.expect_err("should panic for out-of-range elements"));
693        assert!(
694            msg.starts_with("strict conversion failed for Vec<i64>: i64 value"),
695            "{msg}"
696        );
697        assert!(msg.contains("(element 2)"), "{msg}");
698        assert!(msg.contains("(element 4)"), "{msg}");
699        assert!(!msg.contains("index"), "{msg}");
700        assert!(
701            !msg.contains("and "),
702            "should not summarize under the cap: {msg}"
703        );
704        assert!(
705            msg.ends_with("use a non-strict function to allow lossy f64 widening"),
706            "{msg}"
707        );
708    }
709
710    #[test]
711    fn vec_i64_batches_caps_and_summarizes_remainder() {
712        let vals: Vec<i64> = std::iter::repeat_n(i64::MAX, 15).collect();
713        let result = std::panic::catch_unwind(|| checked_vec_i64_into_sexp(vals));
714        let msg = panic_message(result.expect_err("should panic for out-of-range elements"));
715        assert!(msg.contains("and 5 more"), "{msg}");
716    }
717
718    #[test]
719    fn vec_u64_batches_multiple_out_of_range_indices() {
720        let bad = i32::MAX as u64 + 1;
721        let result = std::panic::catch_unwind(|| checked_vec_u64_into_sexp(vec![0, bad, 1, bad]));
722        let msg = panic_message(result.expect_err("should panic for out-of-range elements"));
723        assert!(
724            msg.starts_with("strict conversion failed for Vec<u64>: u64 value"),
725            "{msg}"
726        );
727        assert!(msg.contains("(elements 2, 4)"), "{msg}");
728        assert!(!msg.contains("index"), "{msg}");
729    }
730
731    #[test]
732    fn vec_option_i64_batches_multiple_out_of_range_indices() {
733        let result = std::panic::catch_unwind(|| {
734            checked_vec_option_i64_into_sexp(vec![Some(1), Some(i64::MAX), None, Some(i64::MIN)])
735        });
736        let msg = panic_message(result.expect_err("should panic for out-of-range elements"));
737        assert!(
738            msg.starts_with("strict conversion failed for Vec<Option<i64>>: i64 value"),
739            "{msg}"
740        );
741        assert!(msg.contains("(element 2)"), "{msg}");
742        assert!(msg.contains("(element 4)"), "{msg}");
743        assert!(!msg.contains("index"), "{msg}");
744    }
745
746    #[test]
747    fn vec_option_u64_batches_multiple_out_of_range_indices() {
748        let bad = i32::MAX as u64 + 1;
749        let result = std::panic::catch_unwind(|| {
750            checked_vec_option_u64_into_sexp(vec![Some(0), Some(bad), None, Some(bad)])
751        });
752        let msg = panic_message(result.expect_err("should panic for out-of-range elements"));
753        assert!(
754            msg.starts_with("strict conversion failed for Vec<Option<u64>>: u64 value"),
755            "{msg}"
756        );
757        assert!(msg.contains("(elements 2, 4)"), "{msg}");
758        assert!(!msg.contains("index"), "{msg}");
759    }
760
761    #[test]
762    fn pointer_sized_helpers_name_their_own_type() {
763        let big = 3_000_000_000usize;
764        let msg = panic_message(
765            std::panic::catch_unwind(|| checked_vec_usize_into_sexp(vec![1, big]))
766                .expect_err("should panic for out-of-range elements"),
767        );
768        assert!(
769            msg.starts_with("strict conversion failed for Vec<usize>: usize value 3000000000"),
770            "{msg}"
771        );
772        let msg = panic_message(
773            std::panic::catch_unwind(|| checked_into_sexp_usize(big))
774                .expect_err("should panic for an out-of-range value"),
775        );
776        assert!(
777            msg.starts_with("strict conversion failed: usize value 3000000000 exceeds"),
778            "{msg}"
779        );
780        let msg = panic_message(
781            std::panic::catch_unwind(|| {
782                checked_vec_option_isize_into_sexp(vec![None, Some(isize::MIN)])
783            })
784            .expect_err("should panic for out-of-range elements"),
785        );
786        assert!(
787            msg.starts_with("strict conversion failed for Vec<Option<isize>>: isize value"),
788            "{msg}"
789        );
790        assert!(msg.contains("(element 2)"), "{msg}");
791    }
792
793    #[test]
794    fn vec_vec_usize_batches_every_row_before_panicking() {
795        let big = 3_000_000_000usize;
796        let result = std::panic::catch_unwind(|| {
797            checked_vec_vec_usize_into_sexp(vec![vec![1, 2], vec![big, 3], vec![], vec![4, big]])
798        });
799        let msg = panic_message(result.expect_err("should panic for out-of-range elements"));
800        assert_eq!(
801            msg,
802            "strict conversion failed for Vec<Vec<usize>>: usize value 3000000000 exceeds R \
803             integer max (2147483647) at inner position 1 (element 2); usize value 3000000000 \
804             exceeds R integer max (2147483647) at inner position 2 (element 4); use a \
805             non-strict function to allow lossy f64 widening"
806        );
807    }
808
809    #[test]
810    fn vec_vec_i64_names_its_type_and_groups_equal_reasons() {
811        let result = std::panic::catch_unwind(|| {
812            checked_vec_vec_i64_into_sexp(vec![vec![i64::MAX], vec![0], vec![i64::MAX]])
813        });
814        let msg = panic_message(result.expect_err("should panic for out-of-range elements"));
815        assert!(
816            msg.starts_with("strict conversion failed for Vec<Vec<i64>>: i64 value"),
817            "{msg}"
818        );
819        // The same value at the same inner position shares one position list.
820        assert!(msg.contains("at inner position 1 (elements 1, 3)"), "{msg}");
821    }
822
823    #[test]
824    fn vec_vec_isize_rejects_na_integer_sentinel() {
825        let result = std::panic::catch_unwind(|| {
826            checked_vec_vec_isize_into_sexp(vec![vec![i32::MIN as isize]])
827        });
828        let msg = panic_message(result.expect_err("i32::MIN is NA_integer_"));
829        assert!(
830            msg.starts_with("strict conversion failed for Vec<Vec<isize>>: isize value"),
831            "{msg}"
832        );
833    }
834
835    #[test]
836    fn vec_option_vec_u64_skips_none_rows() {
837        let bad = i32::MAX as u64 + 1;
838        let result = std::panic::catch_unwind(|| {
839            checked_vec_option_vec_u64_into_sexp(vec![None, Some(vec![0, bad])])
840        });
841        let msg = panic_message(result.expect_err("should panic for out-of-range elements"));
842        assert!(
843            msg.starts_with("strict conversion failed for Vec<Option<Vec<u64>>>: u64 value"),
844            "{msg}"
845        );
846        assert!(msg.contains("at inner position 2 (element 2)"), "{msg}");
847    }
848
849    #[test]
850    fn vec_vec_caps_listed_failures() {
851        let rows: Vec<Vec<u64>> = std::iter::repeat_n(vec![u64::MAX], 15).collect();
852        let result = std::panic::catch_unwind(|| checked_vec_vec_u64_into_sexp(rows));
853        let msg = panic_message(result.expect_err("should panic for out-of-range elements"));
854        assert!(msg.contains("and 5 more"), "{msg}");
855    }
856}
857// endregion