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.
3233use crate::coerce::TryCoerce;
34use crate::from_r::{BatchedErrors, SexpError, TryFromSexp};
35use crate::into_r::IntoR;
36use crate::{IsNA, SEXP, SEXPTYPE, SexpExt};
3738/// 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}
5455// 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>`).
6162/// 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.
67const NAME: &'static str;
6869/// `Some` when the value is an R integer: it fits `i32` and is not
70 /// `i32::MIN`, which is `NA_integer_` in R.
71fn to_r_int(self) -> Option<i32>;
7273/// Why `self` has no R integer.
74fn range_reason(self) -> String;
75}
7677macro_rules!impl_strict_r_int {
78 (signed: $($t:ty),*) => {$(
79impl StrictRInt for $t {
80const NAME: &'static str = stringify!($t);
81#[inline]
82fn to_r_int(self) -> Option<i32> {
83 i32::try_from(self).ok().filter(|&v| !v.is_na())
84 }
85fn range_reason(self) -> String {
86format!(
87"{} value {self} is outside R integer range ({}..={})",
88Self::NAME,
89 i32::MIN + 1,
90 i32::MAX
91 )
92 }
93 }
94 )*};
95 (unsigned: $($t:ty),*) => {$(
96impl StrictRInt for $t {
97const NAME: &'static str = stringify!($t);
98#[inline]
99fn to_r_int(self) -> Option<i32> {
100 i32::try_from(self).ok()
101 }
102fn range_reason(self) -> String {
103format!("{} value {self} exceeds R integer max ({})", Self::NAME, i32::MAX)
104 }
105 }
106 )*};
107}
108109impl 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);
111112/// The scalar strict conversion: the R integer, or the panic.
113fn checked_scalar_into_sexp<T: StrictRInt>(val: T) -> SEXP {
114match val.to_r_int() {
115Some(v) => v.into_sexp(),
116None => {
::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}
122123/// 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 {
131let mut coerced: Vec<Option<i32>> = Vec::with_capacity(elems.len());
132let mut errors = BatchedErrors::default();
133for (i, elem) in elems.enumerate() {
134match elem.map(|x| (x, x.to_r_int())) {
135Some((_, Some(v))) => coerced.push(Some(v)),
136Some((x, None)) => errors.push(i, || x.range_reason()),
137None => coerced.push(None),
138 }
139 }
140if !errors.is_empty() {
141panic_strict_vec_batched(container, errors);
142 }
143coerced.into_sexp()
144}
145146/// 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 {
150checked_elems_into_sexp(container, values.iter().copied().map(Some))
151}
152153/// 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 {
166let mut coerced: Vec<Option<Vec<i32>>> = Vec::with_capacity(rows.len());
167let mut errors = BatchedErrors::default();
168for (i, row) in rows.enumerate() {
169 coerced.push(row.map(|row| {
170let row = row.as_ref();
171let mut out = Vec::with_capacity(row.len());
172for (j, &x) in row.iter().enumerate() {
173match x.to_r_int() {
174Some(v) => out.push(v),
175None => 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 }
183if !errors.is_empty() {
184panic_strict_vec_batched(container, errors);
185 }
186coerced.into_sexp()
187}
188189/// 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 {
195checked_scalar_into_sexp(val)
196}
197198/// Convert `u64` to R integer, panicking if > i32::MAX.
199#[inline]
200pub fn checked_into_sexp_u64(val: u64) -> SEXP {
201checked_scalar_into_sexp(val)
202}
203204/// Convert `isize` to R integer, panicking if outside i32 range.
205#[inline]
206pub fn checked_into_sexp_isize(val: isize) -> SEXP {
207checked_scalar_into_sexp(val)
208}
209210/// Convert `usize` to R integer, panicking if > i32::MAX.
211#[inline]
212pub fn checked_into_sexp_usize(val: usize) -> SEXP {
213checked_scalar_into_sexp(val)
214}
215216/// 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 {
221checked_elems_into_sexp("Vec<i64>", val.into_iter().map(Some))
222}
223224/// 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 {
229checked_elems_into_sexp("Vec<u64>", val.into_iter().map(Some))
230}
231232/// 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 {
234checked_elems_into_sexp("Vec<isize>", val.into_iter().map(Some))
235}
236237/// Convert `Vec<usize>` to R integer vector, panicking if any element > i32::MAX.
238pub fn checked_vec_usize_into_sexp(val: Vec<usize>) -> SEXP {
239checked_elems_into_sexp("Vec<usize>", val.into_iter().map(Some))
240}
241242/// 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 {
249checked_elems_into_sexp("Vec<Option<i64>>", val.into_iter())
250}
251252/// 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 {
257checked_elems_into_sexp("Vec<Option<u64>>", val.into_iter())
258}
259260/// Convert `Vec<Option<isize>>` to R integer vector in strict mode.
261pub fn checked_vec_option_isize_into_sexp(val: Vec<Option<isize>>) -> SEXP {
262checked_elems_into_sexp("Vec<Option<isize>>", val.into_iter())
263}
264265/// Convert `Vec<Option<usize>>` to R integer vector in strict mode.
266pub fn checked_vec_option_usize_into_sexp(val: Vec<Option<usize>>) -> SEXP {
267checked_elems_into_sexp("Vec<Option<usize>>", val.into_iter())
268}
269270/// 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 {
274match val {
275Some(x) => checked_into_sexp_i64(x),
276None => Option::<i32>::None.into_sexp(),
277 }
278}
279280/// 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 {
284match val {
285Some(x) => checked_into_sexp_u64(x),
286None => Option::<i32>::None.into_sexp(),
287 }
288}
289290/// Convert `Option<isize>` to R integer in strict mode.
291#[inline]
292pub fn checked_option_isize_into_sexp(val: Option<isize>) -> SEXP {
293match val {
294Some(x) => checked_into_sexp_isize(x),
295None => Option::<i32>::None.into_sexp(),
296 }
297}
298299/// Convert `Option<usize>` to R integer in strict mode.
300#[inline]
301pub fn checked_option_usize_into_sexp(val: Option<usize>) -> SEXP {
302match val {
303Some(x) => checked_into_sexp_usize(x),
304None => Option::<i32>::None.into_sexp(),
305 }
306}
307308/// 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 {
312checked_rows_into_sexp("Vec<Vec<i64>>", val.into_iter().map(Some))
313}
314315/// 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 {
318checked_rows_into_sexp("Vec<Vec<u64>>", val.into_iter().map(Some))
319}
320321/// 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 {
324checked_rows_into_sexp("Vec<Vec<isize>>", val.into_iter().map(Some))
325}
326327/// 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 {
330checked_rows_into_sexp("Vec<Vec<usize>>", val.into_iter().map(Some))
331}
332333/// 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 {
336checked_rows_into_sexp("Vec<Option<Vec<i64>>>", val.into_iter())
337}
338339/// 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 {
342checked_rows_into_sexp("Vec<Option<Vec<u64>>>", val.into_iter())
343}
344345/// 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 {
348checked_rows_into_sexp("Vec<Option<Vec<isize>>>", val.into_iter())
349}
350351/// 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 {
354checked_rows_into_sexp("Vec<Option<Vec<usize>>>", val.into_iter())
355}
356357// endregion
358359// 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.
367368/// The element reason for an R `NA` in a strict input that cannot hold one.
369const NA_NOT_ALLOWED: &str = "NA is not allowed";
370371/// 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> {
378checked_try_from_sexp_numeric_scalar::<i64>(sexp)
379}
380381/// Convert R SEXP to `u64` in strict mode.
382#[inline]
383pub fn checked_try_from_sexp_u64(sexp: SEXP) -> Result<u64, SexpError> {
384checked_try_from_sexp_numeric_scalar::<u64>(sexp)
385}
386387/// Convert R SEXP to `isize` in strict mode.
388#[inline]
389pub fn checked_try_from_sexp_isize(sexp: SEXP) -> Result<isize, SexpError> {
390let val = checked_try_from_sexp_i64(sexp)?;
391narrow::<i64, isize>(val).map_err(SexpError::InvalidValue)
392}
393394/// Convert R SEXP to `usize` in strict mode.
395#[inline]
396pub fn checked_try_from_sexp_usize(sexp: SEXP) -> Result<usize, SexpError> {
397let val = checked_try_from_sexp_u64(sexp)?;
398narrow::<u64, usize>(val).map_err(SexpError::InvalidValue)
399}
400401/// Convert R SEXP to `Vec<i64>` in strict mode.
402pub fn checked_vec_try_from_sexp_i64(sexp: SEXP) -> Result<Vec<i64>, SexpError> {
403checked_vec_try_from_sexp_numeric::<i64>(sexp)
404}
405406/// Convert R SEXP to `Vec<u64>` in strict mode.
407pub fn checked_vec_try_from_sexp_u64(sexp: SEXP) -> Result<Vec<u64>, SexpError> {
408checked_vec_try_from_sexp_numeric::<u64>(sexp)
409}
410411/// Convert R SEXP to `Vec<isize>` in strict mode.
412pub fn checked_vec_try_from_sexp_isize(sexp: SEXP) -> Result<Vec<isize>, SexpError> {
413let vals = checked_vec_try_from_sexp_i64(sexp)?;
414batch_elements(&vals, narrow::<i64, isize>)
415}
416417/// Convert R SEXP to `Vec<usize>` in strict mode.
418pub fn checked_vec_try_from_sexp_usize(sexp: SEXP) -> Result<Vec<usize>, SexpError> {
419let vals = checked_vec_try_from_sexp_u64(sexp)?;
420batch_elements(&vals, narrow::<u64, usize>)
421}
422423/// 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> {
429checked_vec_option_try_from_sexp_numeric::<i64>(sexp)
430}
431432/// 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> {
434checked_vec_option_try_from_sexp_numeric::<u64>(sexp)
435}
436437/// 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> {
439let vals = checked_vec_option_try_from_sexp_i64(sexp)?;
440batch_elements(&vals, |opt| opt.map(narrow::<i64, isize>).transpose())
441}
442443/// 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> {
445let vals = checked_vec_option_try_from_sexp_u64(sexp)?;
446batch_elements(&vals, |opt| opt.map(narrow::<u64, usize>).transpose())
447}
448449/// 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
453S: 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}
458459/// The strict input-type gate: only INTSXP and REALSXP are accepted.
460fn strict_type_error(actual: SEXPTYPE) -> SexpError {
461crate::from_r::SexpTypeError {
462 expected: SEXPTYPE::INTSXP,
463actual,
464 }
465 .into()
466}
467468/// 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> {
475let mut out = Vec::with_capacity(slice.len());
476let mut errors = BatchedErrors::default();
477for (i, &v) in slice.iter().enumerate() {
478match convert(v) {
479Ok(x) => out.push(x),
480Err(reason) => errors.push(i, || reason),
481 }
482 }
483if errors.is_empty() {
484Ok(out)
485 } else {
486Err(errors.into_element_error())
487 }
488}
489490/// One INTSXP element: `NA_integer_` is `None`; any other value coerces.
491fn strict_int_elt<T>(v: i32) -> Result<Option<T>, String>
492where
493i32: TryCoerce<T>,
494 <i32as TryCoerce<T>>::Error: std::fmt::Display,
495{
496if v.is_na() {
497return Ok(None);
498 }
499 TryCoerce::<T>::try_coerce(v)
500 .map(Some)
501 .map_err(|e| e.to_string())
502}
503504/// 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
508f64: TryCoerce<T>,
509 <f64as TryCoerce<T>>::Error: std::fmt::Display,
510{
511if v.is_na() {
512return Ok(None);
513 }
514 TryCoerce::<T>::try_coerce(v)
515 .map(Some)
516 .map_err(|e| e.to_string())
517}
518519/// 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
524i32: TryCoerce<T>,
525f64: TryCoerce<T>,
526 <i32as TryCoerce<T>>::Error: std::fmt::Display,
527 <f64as TryCoerce<T>>::Error: std::fmt::Display,
528{
529let actual = sexp.type_of();
530let value = match actual {
531 SEXPTYPE::INTSXP => {
532let value: i32 = TryFromSexp::try_from_sexp(sexp)?;
533strict_int_elt::<T>(value)
534 }
535 SEXPTYPE::REALSXP => {
536let value: f64 = TryFromSexp::try_from_sexp(sexp)?;
537strict_real_elt::<T>(value)
538 }
539_ => return Err(strict_type_error(actual)),
540 };
541match value {
542Ok(Some(v)) => Ok(v),
543Ok(None) => Err(crate::from_r::SexpNaError { sexp_type: actual }.into()),
544Err(reason) => Err(SexpError::InvalidValue(reason)),
545 }
546}
547548/// 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
552i32: TryCoerce<T>,
553f64: TryCoerce<T>,
554 <i32as TryCoerce<T>>::Error: std::fmt::Display,
555 <f64as TryCoerce<T>>::Error: std::fmt::Display,
556{
557let required = |v: Option<T>| v.ok_or_else(|| NA_NOT_ALLOWED.to_string());
558match sexp.type_of() {
559 SEXPTYPE::INTSXP => {
560let slice: &[i32] = unsafe { sexp.as_slice() };
561batch_elements(slice, |v| strict_int_elt::<T>(v).and_then(required))
562 }
563 SEXPTYPE::REALSXP => {
564let slice: &[f64] = unsafe { sexp.as_slice() };
565batch_elements(slice, |v| strict_real_elt::<T>(v).and_then(required))
566 }
567 actual => Err(strict_type_error(actual)),
568 }
569}
570571/// 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
578i32: TryCoerce<T>,
579f64: TryCoerce<T>,
580 <i32as TryCoerce<T>>::Error: std::fmt::Display,
581 <f64as TryCoerce<T>>::Error: std::fmt::Display,
582{
583match sexp.type_of() {
584 SEXPTYPE::INTSXP => {
585let slice: &[i32] = unsafe { sexp.as_slice() };
586batch_elements(slice, strict_int_elt::<T>)
587 }
588 SEXPTYPE::REALSXP => {
589let slice: &[f64] = unsafe { sexp.as_slice() };
590batch_elements(slice, strict_real_elt::<T>)
591 }
592 actual => Err(strict_type_error(actual)),
593 }
594}
595596#[cfg(test)]
597mod tests {
598use super::*;
599600#[test]
601fn i64_in_range_has_an_r_integer() {
602for value in [0, 42, -1, i64::from(i32::MAX)] {
603assert_eq!(value.to_r_int(), i32::try_from(value).ok());
604 }
605 }
606607#[test]
608fn i64_out_of_range_panics() {
609let result = std::panic::catch_unwind(|| checked_into_sexp_i64(i64::MAX));
610assert!(result.is_err(), "should panic for i64::MAX");
611612let result = std::panic::catch_unwind(|| checked_into_sexp_i64(i32::MIN as i64));
613assert!(result.is_err(), "should panic for i32::MIN (NA_integer_)");
614615let result = std::panic::catch_unwind(|| checked_into_sexp_i64(i32::MAX as i64 + 1));
616assert!(result.is_err(), "should panic for i32::MAX + 1");
617 }
618619#[test]
620fn u64_in_range_has_an_r_integer() {
621assert_eq!(0_u64.to_r_int(), Some(0));
622assert_eq!(u64::try_from(i32::MAX).unwrap().to_r_int(), Some(i32::MAX));
623 }
624625#[test]
626fn borrowed_and_boxed_rows_report_every_invalid_position() {
627let bad = [i64::MAX, 1, i64::MIN];
628let message = panic_message(
629 std::panic::catch_unwind(|| checked_slice_into_sexp("&[i64]", &bad)).unwrap_err(),
630 );
631assert!(message.contains("&[i64]"), "{message}");
632assert!(message.contains("element 1"), "{message}");
633assert!(message.contains("element 3"), "{message}");
634for 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 [
649Some(Box::<[i64]>::from(bad)),
650None,
651Some(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 ] {
668assert!(message.contains("inner position 1"), "{message}");
669assert!(message.contains("inner position 3"), "{message}");
670assert!(message.contains("elements 1, 3"), "{message}");
671 }
672 }
673674#[test]
675fn u64_out_of_range_panics() {
676let result = std::panic::catch_unwind(|| checked_into_sexp_u64(i32::MAX as u64 + 1));
677assert!(result.is_err());
678 }
679680/// Downcast a `panic!` payload (produced via format args, so always a
681 /// `String`) into an owned `String` for message assertions.
682fn panic_message(payload: Box<dyn std::any::Any + Send>) -> String {
683*payload
684 .downcast::<String>()
685 .expect("panic payload should be a String")
686 }
687688#[test]
689fn vec_i64_batches_multiple_out_of_range_indices() {
690let result =
691 std::panic::catch_unwind(|| checked_vec_i64_into_sexp(vec![1, i64::MAX, 2, i64::MIN]));
692let msg = panic_message(result.expect_err("should panic for out-of-range elements"));
693assert!(
694 msg.starts_with("strict conversion failed for Vec<i64>: i64 value"),
695"{msg}"
696);
697assert!(msg.contains("(element 2)"), "{msg}");
698assert!(msg.contains("(element 4)"), "{msg}");
699assert!(!msg.contains("index"), "{msg}");
700assert!(
701 !msg.contains("and "),
702"should not summarize under the cap: {msg}"
703);
704assert!(
705 msg.ends_with("use a non-strict function to allow lossy f64 widening"),
706"{msg}"
707);
708 }
709710#[test]
711fn vec_i64_batches_caps_and_summarizes_remainder() {
712let vals: Vec<i64> = std::iter::repeat_n(i64::MAX, 15).collect();
713let result = std::panic::catch_unwind(|| checked_vec_i64_into_sexp(vals));
714let msg = panic_message(result.expect_err("should panic for out-of-range elements"));
715assert!(msg.contains("and 5 more"), "{msg}");
716 }
717718#[test]
719fn vec_u64_batches_multiple_out_of_range_indices() {
720let bad = i32::MAX as u64 + 1;
721let result = std::panic::catch_unwind(|| checked_vec_u64_into_sexp(vec![0, bad, 1, bad]));
722let msg = panic_message(result.expect_err("should panic for out-of-range elements"));
723assert!(
724 msg.starts_with("strict conversion failed for Vec<u64>: u64 value"),
725"{msg}"
726);
727assert!(msg.contains("(elements 2, 4)"), "{msg}");
728assert!(!msg.contains("index"), "{msg}");
729 }
730731#[test]
732fn vec_option_i64_batches_multiple_out_of_range_indices() {
733let result = std::panic::catch_unwind(|| {
734 checked_vec_option_i64_into_sexp(vec![Some(1), Some(i64::MAX), None, Some(i64::MIN)])
735 });
736let msg = panic_message(result.expect_err("should panic for out-of-range elements"));
737assert!(
738 msg.starts_with("strict conversion failed for Vec<Option<i64>>: i64 value"),
739"{msg}"
740);
741assert!(msg.contains("(element 2)"), "{msg}");
742assert!(msg.contains("(element 4)"), "{msg}");
743assert!(!msg.contains("index"), "{msg}");
744 }
745746#[test]
747fn vec_option_u64_batches_multiple_out_of_range_indices() {
748let bad = i32::MAX as u64 + 1;
749let result = std::panic::catch_unwind(|| {
750 checked_vec_option_u64_into_sexp(vec![Some(0), Some(bad), None, Some(bad)])
751 });
752let msg = panic_message(result.expect_err("should panic for out-of-range elements"));
753assert!(
754 msg.starts_with("strict conversion failed for Vec<Option<u64>>: u64 value"),
755"{msg}"
756);
757assert!(msg.contains("(elements 2, 4)"), "{msg}");
758assert!(!msg.contains("index"), "{msg}");
759 }
760761#[test]
762fn pointer_sized_helpers_name_their_own_type() {
763let big = 3_000_000_000usize;
764let 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 );
768assert!(
769 msg.starts_with("strict conversion failed for Vec<usize>: usize value 3000000000"),
770"{msg}"
771);
772let 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 );
776assert!(
777 msg.starts_with("strict conversion failed: usize value 3000000000 exceeds"),
778"{msg}"
779);
780let 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 );
786assert!(
787 msg.starts_with("strict conversion failed for Vec<Option<isize>>: isize value"),
788"{msg}"
789);
790assert!(msg.contains("(element 2)"), "{msg}");
791 }
792793#[test]
794fn vec_vec_usize_batches_every_row_before_panicking() {
795let big = 3_000_000_000usize;
796let result = std::panic::catch_unwind(|| {
797 checked_vec_vec_usize_into_sexp(vec![vec![1, 2], vec![big, 3], vec![], vec![4, big]])
798 });
799let msg = panic_message(result.expect_err("should panic for out-of-range elements"));
800assert_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 }
808809#[test]
810fn vec_vec_i64_names_its_type_and_groups_equal_reasons() {
811let result = std::panic::catch_unwind(|| {
812 checked_vec_vec_i64_into_sexp(vec![vec![i64::MAX], vec![0], vec![i64::MAX]])
813 });
814let msg = panic_message(result.expect_err("should panic for out-of-range elements"));
815assert!(
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.
820assert!(msg.contains("at inner position 1 (elements 1, 3)"), "{msg}");
821 }
822823#[test]
824fn vec_vec_isize_rejects_na_integer_sentinel() {
825let result = std::panic::catch_unwind(|| {
826 checked_vec_vec_isize_into_sexp(vec![vec![i32::MIN as isize]])
827 });
828let msg = panic_message(result.expect_err("i32::MIN is NA_integer_"));
829assert!(
830 msg.starts_with("strict conversion failed for Vec<Vec<isize>>: isize value"),
831"{msg}"
832);
833 }
834835#[test]
836fn vec_option_vec_u64_skips_none_rows() {
837let bad = i32::MAX as u64 + 1;
838let result = std::panic::catch_unwind(|| {
839 checked_vec_option_vec_u64_into_sexp(vec![None, Some(vec![0, bad])])
840 });
841let msg = panic_message(result.expect_err("should panic for out-of-range elements"));
842assert!(
843 msg.starts_with("strict conversion failed for Vec<Option<Vec<u64>>>: u64 value"),
844"{msg}"
845);
846assert!(msg.contains("at inner position 2 (element 2)"), "{msg}");
847 }
848849#[test]
850fn vec_vec_caps_listed_failures() {
851let rows: Vec<Vec<u64>> = std::iter::repeat_n(vec![u64::MAX], 15).collect();
852let result = std::panic::catch_unwind(|| checked_vec_vec_u64_into_sexp(rows));
853let msg = panic_message(result.expect_err("should panic for out-of-range elements"));
854assert!(msg.contains("and 5 more"), "{msg}");
855 }
856}
857// endregion