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
16use crate::coerce::TryCoerce;
17use crate::ffi::{SEXP, SEXPTYPE, SexpExt};
18use crate::from_r::TryFromSexp;
19use crate::into_r::IntoR;
20
21/// Convert `i64` to R integer, panicking if outside i32 range.
22///
23/// The valid range is `(i32::MIN, i32::MAX]` — `i32::MIN` is excluded because
24/// it is `NA_integer_` in R.
25#[inline]
26pub fn checked_into_sexp_i64(val: i64) -> SEXP {
27    if val > i32::MIN as i64 && val <= i32::MAX as i64 {
28        (val as i32).into_sexp()
29    } else {
30        panic!(
31            "strict conversion failed: i64 value {} is outside R integer range \
32             ({}..={}); use a non-strict function to allow lossy f64 widening",
33            val,
34            i32::MIN as i64 + 1,
35            i32::MAX
36        );
37    }
38}
39
40/// Convert `u64` to R integer, panicking if > i32::MAX.
41#[inline]
42pub fn checked_into_sexp_u64(val: u64) -> SEXP {
43    if val <= i32::MAX as u64 {
44        (val as i32).into_sexp()
45    } else {
46        panic!(
47            "strict conversion failed: u64 value {} exceeds R integer max ({}); \
48             use a non-strict function to allow lossy f64 widening",
49            val,
50            i32::MAX
51        );
52    }
53}
54
55/// Convert `isize` to R integer, panicking if outside i32 range.
56#[inline]
57pub fn checked_into_sexp_isize(val: isize) -> SEXP {
58    checked_into_sexp_i64(val as i64)
59}
60
61/// Convert `usize` to R integer, panicking if > i32::MAX.
62#[inline]
63pub fn checked_into_sexp_usize(val: usize) -> SEXP {
64    checked_into_sexp_u64(val as u64)
65}
66
67/// Convert `Vec<i64>` to R integer vector, panicking if any element is outside i32 range.
68pub fn checked_vec_i64_into_sexp(val: Vec<i64>) -> SEXP {
69    let coerced: Vec<i32> = val
70        .into_iter()
71        .map(|x| {
72            if x > i32::MIN as i64 && x <= i32::MAX as i64 {
73                x as i32
74            } else {
75                panic!(
76                    "strict conversion failed: i64 value {} is outside R integer range \
77                     ({}..={}); use a non-strict function to allow lossy f64 widening",
78                    x,
79                    i32::MIN as i64 + 1,
80                    i32::MAX
81                );
82            }
83        })
84        .collect();
85    coerced.into_sexp()
86}
87
88/// Convert `Vec<u64>` to R integer vector, panicking if any element > i32::MAX.
89pub fn checked_vec_u64_into_sexp(val: Vec<u64>) -> SEXP {
90    let coerced: Vec<i32> = val
91        .into_iter()
92        .map(|x| {
93            if x <= i32::MAX as u64 {
94                x as i32
95            } else {
96                panic!(
97                    "strict conversion failed: u64 value {} exceeds R integer max ({}); \
98                     use a non-strict function to allow lossy f64 widening",
99                    x,
100                    i32::MAX
101                );
102            }
103        })
104        .collect();
105    coerced.into_sexp()
106}
107
108/// Convert `Vec<isize>` to R integer vector, panicking if any element is outside i32 range.
109pub fn checked_vec_isize_into_sexp(val: Vec<isize>) -> SEXP {
110    checked_vec_i64_into_sexp(val.into_iter().map(|x| x as i64).collect())
111}
112
113/// Convert `Vec<usize>` to R integer vector, panicking if any element > i32::MAX.
114pub fn checked_vec_usize_into_sexp(val: Vec<usize>) -> SEXP {
115    checked_vec_u64_into_sexp(val.into_iter().map(|x| x as u64).collect())
116}
117
118/// Convert `Vec<Option<i64>>` to R integer vector in strict mode.
119/// Panics if any `Some(x)` value is outside i32 range. `None` becomes `NA_INTEGER`.
120pub fn checked_vec_option_i64_into_sexp(val: Vec<Option<i64>>) -> SEXP {
121    let coerced: Vec<Option<i32>> = val
122        .into_iter()
123        .map(|opt| match opt {
124            Some(x) => {
125                if x > i32::MIN as i64 && x <= i32::MAX as i64 {
126                    Some(x as i32)
127                } else {
128                    panic!(
129                        "strict conversion failed: i64 value {} is outside R integer range \
130                         ({}..={}); use a non-strict function to allow lossy f64 widening",
131                        x,
132                        i32::MIN as i64 + 1,
133                        i32::MAX
134                    );
135                }
136            }
137            None => None,
138        })
139        .collect();
140    coerced.into_sexp()
141}
142
143/// Convert `Vec<Option<u64>>` to R integer vector in strict mode.
144pub fn checked_vec_option_u64_into_sexp(val: Vec<Option<u64>>) -> SEXP {
145    let coerced: Vec<Option<i32>> = val
146        .into_iter()
147        .map(|opt| match opt {
148            Some(x) => {
149                if x <= i32::MAX as u64 {
150                    Some(x as i32)
151                } else {
152                    panic!(
153                        "strict conversion failed: u64 value {} exceeds R integer max ({}); \
154                         use a non-strict function to allow lossy f64 widening",
155                        x,
156                        i32::MAX
157                    );
158                }
159            }
160            None => None,
161        })
162        .collect();
163    coerced.into_sexp()
164}
165
166/// Convert `Vec<Option<isize>>` to R integer vector in strict mode.
167pub fn checked_vec_option_isize_into_sexp(val: Vec<Option<isize>>) -> SEXP {
168    checked_vec_option_i64_into_sexp(val.into_iter().map(|opt| opt.map(|x| x as i64)).collect())
169}
170
171/// Convert `Vec<Option<usize>>` to R integer vector in strict mode.
172pub fn checked_vec_option_usize_into_sexp(val: Vec<Option<usize>>) -> SEXP {
173    checked_vec_option_u64_into_sexp(val.into_iter().map(|opt| opt.map(|x| x as u64)).collect())
174}
175
176/// Convert `Option<i64>` to R integer in strict mode.
177/// Panics if `Some(x)` is outside i32 range. `None` becomes `NA_integer_`.
178#[inline]
179pub fn checked_option_i64_into_sexp(val: Option<i64>) -> SEXP {
180    match val {
181        Some(x) => checked_into_sexp_i64(x),
182        None => Option::<i32>::None.into_sexp(),
183    }
184}
185
186/// Convert `Option<u64>` to R integer in strict mode.
187/// Panics if `Some(x)` exceeds i32::MAX. `None` becomes `NA_integer_`.
188#[inline]
189pub fn checked_option_u64_into_sexp(val: Option<u64>) -> SEXP {
190    match val {
191        Some(x) => checked_into_sexp_u64(x),
192        None => Option::<i32>::None.into_sexp(),
193    }
194}
195
196/// Convert `Option<isize>` to R integer in strict mode.
197#[inline]
198pub fn checked_option_isize_into_sexp(val: Option<isize>) -> SEXP {
199    checked_option_i64_into_sexp(val.map(|x| x as i64))
200}
201
202/// Convert `Option<usize>` to R integer in strict mode.
203#[inline]
204pub fn checked_option_usize_into_sexp(val: Option<usize>) -> SEXP {
205    checked_option_u64_into_sexp(val.map(|x| x as u64))
206}
207
208// region: Strict INPUT helpers — only accept INTSXP and REALSXP, reject RAWSXP/LGLSXP
209
210/// Convert R SEXP to `i64` in strict mode.
211///
212/// Only INTSXP and REALSXP are accepted. RAWSXP and LGLSXP are rejected.
213/// For REALSXP, uses `TryCoerce` to reject fractional, NaN, and out-of-range values.
214#[inline]
215pub fn checked_try_from_sexp_i64(sexp: SEXP, param: &str) -> i64 {
216    checked_try_from_sexp_numeric_scalar::<i64>(sexp, param)
217}
218
219/// Convert R SEXP to `u64` in strict mode.
220#[inline]
221pub fn checked_try_from_sexp_u64(sexp: SEXP, param: &str) -> u64 {
222    checked_try_from_sexp_numeric_scalar::<u64>(sexp, param)
223}
224
225/// Convert R SEXP to `isize` in strict mode.
226#[inline]
227pub fn checked_try_from_sexp_isize(sexp: SEXP, param: &str) -> isize {
228    let val = checked_try_from_sexp_i64(sexp, param);
229    isize::try_from(val).unwrap_or_else(|_| {
230        panic!(
231            "strict conversion failed for parameter '{}': i64 value {} does not fit in isize",
232            param, val
233        )
234    })
235}
236
237/// Convert R SEXP to `usize` in strict mode.
238#[inline]
239pub fn checked_try_from_sexp_usize(sexp: SEXP, param: &str) -> usize {
240    let val = checked_try_from_sexp_u64(sexp, param);
241    usize::try_from(val).unwrap_or_else(|_| {
242        panic!(
243            "strict conversion failed for parameter '{}': u64 value {} does not fit in usize",
244            param, val
245        )
246    })
247}
248
249/// Convert R SEXP to `Vec<i64>` in strict mode.
250pub fn checked_vec_try_from_sexp_i64(sexp: SEXP, param: &str) -> Vec<i64> {
251    checked_vec_try_from_sexp_numeric::<i64>(sexp, param)
252}
253
254/// Convert R SEXP to `Vec<u64>` in strict mode.
255pub fn checked_vec_try_from_sexp_u64(sexp: SEXP, param: &str) -> Vec<u64> {
256    checked_vec_try_from_sexp_numeric::<u64>(sexp, param)
257}
258
259/// Convert R SEXP to `Vec<isize>` in strict mode.
260pub fn checked_vec_try_from_sexp_isize(sexp: SEXP, param: &str) -> Vec<isize> {
261    checked_vec_try_from_sexp_i64(sexp, param)
262        .into_iter()
263        .map(|x| {
264            isize::try_from(x).unwrap_or_else(|_| {
265            panic!(
266                "strict conversion failed for parameter '{}': i64 value {} does not fit in isize",
267                param, x
268            )
269        })
270        })
271        .collect()
272}
273
274/// Convert R SEXP to `Vec<usize>` in strict mode.
275pub fn checked_vec_try_from_sexp_usize(sexp: SEXP, param: &str) -> Vec<usize> {
276    checked_vec_try_from_sexp_u64(sexp, param)
277        .into_iter()
278        .map(|x| {
279            usize::try_from(x).unwrap_or_else(|_| {
280            panic!(
281                "strict conversion failed for parameter '{}': u64 value {} does not fit in usize",
282                param, x
283            )
284        })
285        })
286        .collect()
287}
288
289/// Generic strict scalar conversion: only INTSXP and REALSXP allowed.
290#[inline]
291fn checked_try_from_sexp_numeric_scalar<T>(sexp: SEXP, param: &str) -> T
292where
293    i32: TryCoerce<T>,
294    f64: TryCoerce<T>,
295    <i32 as TryCoerce<T>>::Error: std::fmt::Debug,
296    <f64 as TryCoerce<T>>::Error: std::fmt::Debug,
297{
298    let actual = sexp.type_of();
299    match actual {
300        SEXPTYPE::INTSXP => {
301            let value: i32 = TryFromSexp::try_from_sexp(sexp).unwrap_or_else(|e| {
302                panic!(
303                    "strict conversion failed for parameter '{}': {:?}",
304                    param, e
305                )
306            });
307            TryCoerce::<T>::try_coerce(value).unwrap_or_else(|e| {
308                panic!(
309                    "strict conversion failed for parameter '{}': {:?}",
310                    param, e
311                )
312            })
313        }
314        SEXPTYPE::REALSXP => {
315            let value: f64 = TryFromSexp::try_from_sexp(sexp).unwrap_or_else(|e| {
316                panic!(
317                    "strict conversion failed for parameter '{}': {:?}",
318                    param, e
319                )
320            });
321            TryCoerce::<T>::try_coerce(value).unwrap_or_else(|e| {
322                panic!(
323                    "strict conversion failed for parameter '{}': {:?}",
324                    param, e
325                )
326            })
327        }
328        _ => panic!(
329            "strict conversion failed for parameter '{}': expected integer or double, got {:?}",
330            param, actual
331        ),
332    }
333}
334
335/// Generic strict vector conversion: only INTSXP and REALSXP allowed.
336fn checked_vec_try_from_sexp_numeric<T>(sexp: SEXP, param: &str) -> Vec<T>
337where
338    i32: TryCoerce<T>,
339    f64: TryCoerce<T>,
340    <i32 as TryCoerce<T>>::Error: std::fmt::Debug,
341    <f64 as TryCoerce<T>>::Error: std::fmt::Debug,
342{
343    let actual = sexp.type_of();
344    match actual {
345        SEXPTYPE::INTSXP => {
346            let slice: &[i32] = unsafe { sexp.as_slice() };
347            slice
348                .iter()
349                .copied()
350                .map(|v| {
351                    TryCoerce::<T>::try_coerce(v).unwrap_or_else(|e| {
352                        panic!(
353                            "strict conversion failed for parameter '{}': {:?}",
354                            param, e
355                        )
356                    })
357                })
358                .collect()
359        }
360        SEXPTYPE::REALSXP => {
361            let slice: &[f64] = unsafe { sexp.as_slice() };
362            slice
363                .iter()
364                .copied()
365                .map(|v| {
366                    TryCoerce::<T>::try_coerce(v).unwrap_or_else(|e| {
367                        panic!(
368                            "strict conversion failed for parameter '{}': {:?}",
369                            param, e
370                        )
371                    })
372                })
373                .collect()
374        }
375        _ => panic!(
376            "strict conversion failed for parameter '{}': expected integer or double vector, got {:?}",
377            param, actual
378        ),
379    }
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385
386    #[test]
387    fn i64_in_range_succeeds() {
388        // These should not panic (we can't check SEXP in unit tests without R,
389        // but we can verify no panic occurs)
390        let _ = std::panic::catch_unwind(|| checked_into_sexp_i64(0));
391        let _ = std::panic::catch_unwind(|| checked_into_sexp_i64(42));
392        let _ = std::panic::catch_unwind(|| checked_into_sexp_i64(-1));
393        let _ = std::panic::catch_unwind(|| checked_into_sexp_i64(i32::MAX as i64));
394    }
395
396    #[test]
397    fn i64_out_of_range_panics() {
398        let result = std::panic::catch_unwind(|| checked_into_sexp_i64(i64::MAX));
399        assert!(result.is_err(), "should panic for i64::MAX");
400
401        let result = std::panic::catch_unwind(|| checked_into_sexp_i64(i32::MIN as i64));
402        assert!(result.is_err(), "should panic for i32::MIN (NA_integer_)");
403
404        let result = std::panic::catch_unwind(|| checked_into_sexp_i64(i32::MAX as i64 + 1));
405        assert!(result.is_err(), "should panic for i32::MAX + 1");
406    }
407
408    #[test]
409    fn u64_in_range_succeeds() {
410        let _ = std::panic::catch_unwind(|| checked_into_sexp_u64(0));
411        let _ = std::panic::catch_unwind(|| checked_into_sexp_u64(i32::MAX as u64));
412    }
413
414    #[test]
415    fn u64_out_of_range_panics() {
416        let result = std::panic::catch_unwind(|| checked_into_sexp_u64(i32::MAX as u64 + 1));
417        assert!(result.is_err());
418    }
419}
420// endregion