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::TryFromSexp;
35use crate::into_r::IntoR;
36use crate::{SEXP, SEXPTYPE, SexpExt};
37
38/// Convert `i64` to R integer, panicking if outside i32 range.
39///
40/// The valid range is `(i32::MIN, i32::MAX]` — `i32::MIN` is excluded because
41/// it is `NA_integer_` in R.
42#[inline]
43pub fn checked_into_sexp_i64(val: i64) -> SEXP {
44    if val > i32::MIN as i64 && val <= i32::MAX as i64 {
45        (val as i32).into_sexp()
46    } else {
47        panic!(
48            "strict conversion failed: i64 value {} is outside R integer range \
49             ({}..={}); use a non-strict function to allow lossy f64 widening",
50            val,
51            i32::MIN as i64 + 1,
52            i32::MAX
53        );
54    }
55}
56
57/// Convert `u64` to R integer, panicking if > i32::MAX.
58#[inline]
59pub fn checked_into_sexp_u64(val: u64) -> SEXP {
60    if val <= i32::MAX as u64 {
61        (val as i32).into_sexp()
62    } else {
63        panic!(
64            "strict conversion failed: u64 value {} exceeds R integer max ({}); \
65             use a non-strict function to allow lossy f64 widening",
66            val,
67            i32::MAX
68        );
69    }
70}
71
72/// Convert `isize` to R integer, panicking if outside i32 range.
73#[inline]
74pub fn checked_into_sexp_isize(val: isize) -> SEXP {
75    checked_into_sexp_i64(val as i64)
76}
77
78/// Convert `usize` to R integer, panicking if > i32::MAX.
79#[inline]
80pub fn checked_into_sexp_usize(val: usize) -> SEXP {
81    checked_into_sexp_u64(val as u64)
82}
83
84/// Convert `Vec<i64>` to R integer vector, panicking if any element is outside i32 range.
85pub fn checked_vec_i64_into_sexp(val: Vec<i64>) -> SEXP {
86    let coerced: Vec<i32> = val
87        .into_iter()
88        .map(|x| {
89            if x > i32::MIN as i64 && x <= i32::MAX as i64 {
90                x as i32
91            } else {
92                panic!(
93                    "strict conversion failed: i64 value {} is outside R integer range \
94                     ({}..={}); use a non-strict function to allow lossy f64 widening",
95                    x,
96                    i32::MIN as i64 + 1,
97                    i32::MAX
98                );
99            }
100        })
101        .collect();
102    coerced.into_sexp()
103}
104
105/// Convert `Vec<u64>` to R integer vector, panicking if any element > i32::MAX.
106pub fn checked_vec_u64_into_sexp(val: Vec<u64>) -> SEXP {
107    let coerced: Vec<i32> = val
108        .into_iter()
109        .map(|x| {
110            if x <= i32::MAX as u64 {
111                x as i32
112            } else {
113                panic!(
114                    "strict conversion failed: u64 value {} exceeds R integer max ({}); \
115                     use a non-strict function to allow lossy f64 widening",
116                    x,
117                    i32::MAX
118                );
119            }
120        })
121        .collect();
122    coerced.into_sexp()
123}
124
125/// Convert `Vec<isize>` to R integer vector, panicking if any element is outside i32 range.
126pub fn checked_vec_isize_into_sexp(val: Vec<isize>) -> SEXP {
127    checked_vec_i64_into_sexp(val.into_iter().map(|x| x as i64).collect())
128}
129
130/// Convert `Vec<usize>` to R integer vector, panicking if any element > i32::MAX.
131pub fn checked_vec_usize_into_sexp(val: Vec<usize>) -> SEXP {
132    checked_vec_u64_into_sexp(val.into_iter().map(|x| x as u64).collect())
133}
134
135/// Convert `Vec<Option<i64>>` to R integer vector in strict mode.
136/// Panics if any `Some(x)` value is outside i32 range. `None` becomes `NA_INTEGER`.
137pub fn checked_vec_option_i64_into_sexp(val: Vec<Option<i64>>) -> SEXP {
138    let coerced: Vec<Option<i32>> = val
139        .into_iter()
140        .map(|opt| match opt {
141            Some(x) => {
142                if x > i32::MIN as i64 && x <= i32::MAX as i64 {
143                    Some(x as i32)
144                } else {
145                    panic!(
146                        "strict conversion failed: i64 value {} is outside R integer range \
147                         ({}..={}); use a non-strict function to allow lossy f64 widening",
148                        x,
149                        i32::MIN as i64 + 1,
150                        i32::MAX
151                    );
152                }
153            }
154            None => None,
155        })
156        .collect();
157    coerced.into_sexp()
158}
159
160/// Convert `Vec<Option<u64>>` to R integer vector in strict mode.
161pub fn checked_vec_option_u64_into_sexp(val: Vec<Option<u64>>) -> SEXP {
162    let coerced: Vec<Option<i32>> = val
163        .into_iter()
164        .map(|opt| match opt {
165            Some(x) => {
166                if x <= i32::MAX as u64 {
167                    Some(x as i32)
168                } else {
169                    panic!(
170                        "strict conversion failed: u64 value {} exceeds R integer max ({}); \
171                         use a non-strict function to allow lossy f64 widening",
172                        x,
173                        i32::MAX
174                    );
175                }
176            }
177            None => None,
178        })
179        .collect();
180    coerced.into_sexp()
181}
182
183/// Convert `Vec<Option<isize>>` to R integer vector in strict mode.
184pub fn checked_vec_option_isize_into_sexp(val: Vec<Option<isize>>) -> SEXP {
185    checked_vec_option_i64_into_sexp(val.into_iter().map(|opt| opt.map(|x| x as i64)).collect())
186}
187
188/// Convert `Vec<Option<usize>>` to R integer vector in strict mode.
189pub fn checked_vec_option_usize_into_sexp(val: Vec<Option<usize>>) -> SEXP {
190    checked_vec_option_u64_into_sexp(val.into_iter().map(|opt| opt.map(|x| x as u64)).collect())
191}
192
193/// Convert `Option<i64>` to R integer in strict mode.
194/// Panics if `Some(x)` is outside i32 range. `None` becomes `NA_integer_`.
195#[inline]
196pub fn checked_option_i64_into_sexp(val: Option<i64>) -> SEXP {
197    match val {
198        Some(x) => checked_into_sexp_i64(x),
199        None => Option::<i32>::None.into_sexp(),
200    }
201}
202
203/// Convert `Option<u64>` to R integer in strict mode.
204/// Panics if `Some(x)` exceeds i32::MAX. `None` becomes `NA_integer_`.
205#[inline]
206pub fn checked_option_u64_into_sexp(val: Option<u64>) -> SEXP {
207    match val {
208        Some(x) => checked_into_sexp_u64(x),
209        None => Option::<i32>::None.into_sexp(),
210    }
211}
212
213/// Convert `Option<isize>` to R integer in strict mode.
214#[inline]
215pub fn checked_option_isize_into_sexp(val: Option<isize>) -> SEXP {
216    checked_option_i64_into_sexp(val.map(|x| x as i64))
217}
218
219/// Convert `Option<usize>` to R integer in strict mode.
220#[inline]
221pub fn checked_option_usize_into_sexp(val: Option<usize>) -> SEXP {
222    checked_option_u64_into_sexp(val.map(|x| x as u64))
223}
224
225// region: Strict INPUT helpers — only accept INTSXP and REALSXP, reject RAWSXP/LGLSXP
226
227/// Convert R SEXP to `i64` in strict mode.
228///
229/// Only INTSXP and REALSXP are accepted. RAWSXP and LGLSXP are rejected.
230/// For REALSXP, uses `TryCoerce` to reject fractional, NaN, and out-of-range values.
231#[inline]
232pub fn checked_try_from_sexp_i64(sexp: SEXP, param: &str) -> i64 {
233    checked_try_from_sexp_numeric_scalar::<i64>(sexp, param)
234}
235
236/// Convert R SEXP to `u64` in strict mode.
237#[inline]
238pub fn checked_try_from_sexp_u64(sexp: SEXP, param: &str) -> u64 {
239    checked_try_from_sexp_numeric_scalar::<u64>(sexp, param)
240}
241
242/// Convert R SEXP to `isize` in strict mode.
243#[inline]
244pub fn checked_try_from_sexp_isize(sexp: SEXP, param: &str) -> isize {
245    let val = checked_try_from_sexp_i64(sexp, param);
246    isize::try_from(val).unwrap_or_else(|_| {
247        panic!(
248            "strict conversion failed for parameter '{}': i64 value {} does not fit in isize",
249            param, val
250        )
251    })
252}
253
254/// Convert R SEXP to `usize` in strict mode.
255#[inline]
256pub fn checked_try_from_sexp_usize(sexp: SEXP, param: &str) -> usize {
257    let val = checked_try_from_sexp_u64(sexp, param);
258    usize::try_from(val).unwrap_or_else(|_| {
259        panic!(
260            "strict conversion failed for parameter '{}': u64 value {} does not fit in usize",
261            param, val
262        )
263    })
264}
265
266/// Convert R SEXP to `Vec<i64>` in strict mode.
267pub fn checked_vec_try_from_sexp_i64(sexp: SEXP, param: &str) -> Vec<i64> {
268    checked_vec_try_from_sexp_numeric::<i64>(sexp, param)
269}
270
271/// Convert R SEXP to `Vec<u64>` in strict mode.
272pub fn checked_vec_try_from_sexp_u64(sexp: SEXP, param: &str) -> Vec<u64> {
273    checked_vec_try_from_sexp_numeric::<u64>(sexp, param)
274}
275
276/// Convert R SEXP to `Vec<isize>` in strict mode.
277pub fn checked_vec_try_from_sexp_isize(sexp: SEXP, param: &str) -> Vec<isize> {
278    checked_vec_try_from_sexp_i64(sexp, param)
279        .into_iter()
280        .map(|x| {
281            isize::try_from(x).unwrap_or_else(|_| {
282            panic!(
283                "strict conversion failed for parameter '{}': i64 value {} does not fit in isize",
284                param, x
285            )
286        })
287        })
288        .collect()
289}
290
291/// Convert R SEXP to `Vec<usize>` in strict mode.
292pub fn checked_vec_try_from_sexp_usize(sexp: SEXP, param: &str) -> Vec<usize> {
293    checked_vec_try_from_sexp_u64(sexp, param)
294        .into_iter()
295        .map(|x| {
296            usize::try_from(x).unwrap_or_else(|_| {
297            panic!(
298                "strict conversion failed for parameter '{}': u64 value {} does not fit in usize",
299                param, x
300            )
301        })
302        })
303        .collect()
304}
305
306/// Convert R SEXP to `Vec<Option<i64>>` in strict mode.
307///
308/// Applies the same input-SEXP-type gate as [`checked_vec_try_from_sexp_i64`]
309/// — only INTSXP and REALSXP are accepted; LGLSXP and RAWSXP are rejected.
310/// NA elements become `None`; type strictness and missingness are orthogonal.
311pub fn checked_vec_option_try_from_sexp_i64(sexp: SEXP, param: &str) -> Vec<Option<i64>> {
312    checked_vec_option_try_from_sexp_numeric::<i64>(sexp, param)
313}
314
315/// Convert R SEXP to `Vec<Option<u64>>` in strict mode.
316pub fn checked_vec_option_try_from_sexp_u64(sexp: SEXP, param: &str) -> Vec<Option<u64>> {
317    checked_vec_option_try_from_sexp_numeric::<u64>(sexp, param)
318}
319
320/// Convert R SEXP to `Vec<Option<isize>>` in strict mode.
321pub fn checked_vec_option_try_from_sexp_isize(sexp: SEXP, param: &str) -> Vec<Option<isize>> {
322    checked_vec_option_try_from_sexp_i64(sexp, param)
323        .into_iter()
324        .map(|opt| {
325            opt.map(|x| {
326                isize::try_from(x).unwrap_or_else(|_| {
327                    panic!(
328                        "strict conversion failed for parameter '{}': i64 value {} does not fit in isize",
329                        param, x
330                    )
331                })
332            })
333        })
334        .collect()
335}
336
337/// Convert R SEXP to `Vec<Option<usize>>` in strict mode.
338pub fn checked_vec_option_try_from_sexp_usize(sexp: SEXP, param: &str) -> Vec<Option<usize>> {
339    checked_vec_option_try_from_sexp_u64(sexp, param)
340        .into_iter()
341        .map(|opt| {
342            opt.map(|x| {
343                usize::try_from(x).unwrap_or_else(|_| {
344                    panic!(
345                        "strict conversion failed for parameter '{}': u64 value {} does not fit in usize",
346                        param, x
347                    )
348                })
349            })
350        })
351        .collect()
352}
353
354/// Generic strict scalar conversion: only INTSXP and REALSXP allowed.
355#[inline]
356fn checked_try_from_sexp_numeric_scalar<T>(sexp: SEXP, param: &str) -> T
357where
358    i32: TryCoerce<T>,
359    f64: TryCoerce<T>,
360    <i32 as TryCoerce<T>>::Error: std::fmt::Debug,
361    <f64 as TryCoerce<T>>::Error: std::fmt::Debug,
362{
363    let actual = sexp.type_of();
364    match actual {
365        SEXPTYPE::INTSXP => {
366            let value: i32 = TryFromSexp::try_from_sexp(sexp).unwrap_or_else(|e| {
367                panic!(
368                    "strict conversion failed for parameter '{}': {:?}",
369                    param, e
370                )
371            });
372            TryCoerce::<T>::try_coerce(value).unwrap_or_else(|e| {
373                panic!(
374                    "strict conversion failed for parameter '{}': {:?}",
375                    param, e
376                )
377            })
378        }
379        SEXPTYPE::REALSXP => {
380            let value: f64 = TryFromSexp::try_from_sexp(sexp).unwrap_or_else(|e| {
381                panic!(
382                    "strict conversion failed for parameter '{}': {:?}",
383                    param, e
384                )
385            });
386            TryCoerce::<T>::try_coerce(value).unwrap_or_else(|e| {
387                panic!(
388                    "strict conversion failed for parameter '{}': {:?}",
389                    param, e
390                )
391            })
392        }
393        _ => panic!(
394            "strict conversion failed for parameter '{}': expected integer or double, got {:?}",
395            param, actual
396        ),
397    }
398}
399
400/// Generic strict vector conversion: only INTSXP and REALSXP allowed.
401fn checked_vec_try_from_sexp_numeric<T>(sexp: SEXP, param: &str) -> Vec<T>
402where
403    i32: TryCoerce<T>,
404    f64: TryCoerce<T>,
405    <i32 as TryCoerce<T>>::Error: std::fmt::Debug,
406    <f64 as TryCoerce<T>>::Error: std::fmt::Debug,
407{
408    let actual = sexp.type_of();
409    match actual {
410        SEXPTYPE::INTSXP => {
411            let slice: &[i32] = unsafe { sexp.as_slice() };
412            slice
413                .iter()
414                .copied()
415                .map(|v| {
416                    TryCoerce::<T>::try_coerce(v).unwrap_or_else(|e| {
417                        panic!(
418                            "strict conversion failed for parameter '{}': {:?}",
419                            param, e
420                        )
421                    })
422                })
423                .collect()
424        }
425        SEXPTYPE::REALSXP => {
426            let slice: &[f64] = unsafe { sexp.as_slice() };
427            slice
428                .iter()
429                .copied()
430                .map(|v| {
431                    TryCoerce::<T>::try_coerce(v).unwrap_or_else(|e| {
432                        panic!(
433                            "strict conversion failed for parameter '{}': {:?}",
434                            param, e
435                        )
436                    })
437                })
438                .collect()
439        }
440        _ => panic!(
441            "strict conversion failed for parameter '{}': expected integer or double vector, got {:?}",
442            param, actual
443        ),
444    }
445}
446
447/// Generic strict `Vec<Option<T>>` conversion: only INTSXP and REALSXP allowed.
448///
449/// Mirrors [`checked_vec_try_from_sexp_numeric`] but maps R's NA sentinel
450/// (`NA_INTEGER` for INTSXP, `NA_REAL` for REALSXP) to `None` instead of
451/// erroring — missingness is orthogonal to the input-type gate.
452fn checked_vec_option_try_from_sexp_numeric<T>(sexp: SEXP, param: &str) -> Vec<Option<T>>
453where
454    i32: TryCoerce<T>,
455    f64: TryCoerce<T>,
456    <i32 as TryCoerce<T>>::Error: std::fmt::Debug,
457    <f64 as TryCoerce<T>>::Error: std::fmt::Debug,
458{
459    let actual = sexp.type_of();
460    match actual {
461        SEXPTYPE::INTSXP => {
462            let slice: &[i32] = unsafe { sexp.as_slice() };
463            slice
464                .iter()
465                .copied()
466                .map(|v| {
467                    if v == crate::altrep_traits::NA_INTEGER {
468                        None
469                    } else {
470                        Some(TryCoerce::<T>::try_coerce(v).unwrap_or_else(|e| {
471                            panic!(
472                                "strict conversion failed for parameter '{}': {:?}",
473                                param, e
474                            )
475                        }))
476                    }
477                })
478                .collect()
479        }
480        SEXPTYPE::REALSXP => {
481            let slice: &[f64] = unsafe { sexp.as_slice() };
482            slice
483                .iter()
484                .copied()
485                .map(|v| {
486                    if crate::from_r::is_na_real(v) {
487                        None
488                    } else {
489                        Some(TryCoerce::<T>::try_coerce(v).unwrap_or_else(|e| {
490                            panic!(
491                                "strict conversion failed for parameter '{}': {:?}",
492                                param, e
493                            )
494                        }))
495                    }
496                })
497                .collect()
498        }
499        _ => panic!(
500            "strict conversion failed for parameter '{}': expected integer or double vector, got {:?}",
501            param, actual
502        ),
503    }
504}
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509
510    #[test]
511    fn i64_in_range_succeeds() {
512        // These should not panic (we can't check SEXP in unit tests without R,
513        // but we can verify no panic occurs)
514        let _ = std::panic::catch_unwind(|| checked_into_sexp_i64(0));
515        let _ = std::panic::catch_unwind(|| checked_into_sexp_i64(42));
516        let _ = std::panic::catch_unwind(|| checked_into_sexp_i64(-1));
517        let _ = std::panic::catch_unwind(|| checked_into_sexp_i64(i32::MAX as i64));
518    }
519
520    #[test]
521    fn i64_out_of_range_panics() {
522        let result = std::panic::catch_unwind(|| checked_into_sexp_i64(i64::MAX));
523        assert!(result.is_err(), "should panic for i64::MAX");
524
525        let result = std::panic::catch_unwind(|| checked_into_sexp_i64(i32::MIN as i64));
526        assert!(result.is_err(), "should panic for i32::MIN (NA_integer_)");
527
528        let result = std::panic::catch_unwind(|| checked_into_sexp_i64(i32::MAX as i64 + 1));
529        assert!(result.is_err(), "should panic for i32::MAX + 1");
530    }
531
532    #[test]
533    fn u64_in_range_succeeds() {
534        let _ = std::panic::catch_unwind(|| checked_into_sexp_u64(0));
535        let _ = std::panic::catch_unwind(|| checked_into_sexp_u64(i32::MAX as u64));
536    }
537
538    #[test]
539    fn u64_out_of_range_panics() {
540        let result = std::panic::catch_unwind(|| checked_into_sexp_u64(i32::MAX as u64 + 1));
541        assert!(result.is_err());
542    }
543}
544// endregion