Skip to main content

miniextendr_api/from_r/
coerced_scalars.rs

1//! Coerced scalar conversions (multi-source numeric) and large integer scalars.
2//!
3//! **The `SEXPTYPE` literals here are the source of truth (#882).** They are
4//! *runtime* match arms on `sexp.type_of()`, deliberately accepting several
5//! source types and coercing into one target Rust type — there is no single `T`
6//! whose `T::SEXP_TYPE` they could be folded into (the whole point is the 1:N
7//! input fan-in). Leave them.
8//!
9//! These types accept multiple R source types (INTSXP, REALSXP, RAWSXP, LGLSXP)
10//! and coerce to the target Rust type via [`TryCoerce`].
11//!
12//! Covers: `i8`, `i16`, `u16`, `u32`, `f32` (sub-native scalars) and
13//! `i64`, `u64`, `isize`, `usize` (large integers via f64 intermediary).
14//!
15//! # Tradeoff
16//!
17//! This is the **looser** inbound path. The strict alternative is the bare
18//! [`TryFromSexp`] impl on the matching R native
19//! type (`i32`, `f64`, `&[i32]`, …) — those reject any mismatched
20//! [`SEXPTYPE`] outright instead of coercing. Failure mode of preferring the
21//! coerced path when you wanted strict: an R caller silently passes `1.7`
22//! (REALSXP) into a Rust `i32` argument and gets a truncated `1`.
23//!
24//! Outbound counterparts for the large-integer types in this module live in
25//! `crate::into_r::large_integers` (lax, default) and [`crate::strict`]
26//! (`#[miniextendr(strict)]` opt-in).
27
28use crate::coerce::TryCoerce;
29use crate::from_r::{SexpError, SexpNaError, TryFromSexp};
30use crate::{IsNA, RLogical, SEXP, SEXPTYPE, SexpExt};
31
32/// The type error of a coerced numeric scalar given something that is not
33/// integer, double, logical or raw: worded in R terms by the argument error
34/// (`got character`), like the vector shells' (`from_numeric_vec_with`).
35fn numeric_type_error(actual: SEXPTYPE) -> SexpError {
36    crate::from_r::SexpTypeError {
37        expected: SEXPTYPE::REALSXP,
38        actual,
39    }
40    .into()
41}
42
43/// NA-rejecting error for a logical (`LGLSXP`) scalar that holds `NA`.
44///
45/// The bare integer/float scalar paths must reject `NA_logical_` rather than
46/// silently coercing R's `NA_LOGICAL` sentinel (`i32::MIN`) into a finite
47/// number. Mirrors the guard the bare `i32` (`INTSXP`) impl already applies.
48#[inline]
49fn lglsxp_na_error() -> SexpError {
50    SexpNaError {
51        sexp_type: SEXPTYPE::LGLSXP,
52    }
53    .into()
54}
55
56#[inline]
57pub(crate) fn coerce_value<R, T>(value: R) -> Result<T, SexpError>
58where
59    R: TryCoerce<T>,
60    <R as TryCoerce<T>>::Error: std::fmt::Display,
61{
62    value
63        .try_coerce()
64        .map_err(|e| SexpError::InvalidValue(::alloc::__export::must_use({ ::alloc::fmt::format(format_args!("{0}", e)) })format!("{e}")))
65}
66
67/// [`coerce_value`] for a double, with R's NA kept apart from NaN.
68///
69/// An integer target refuses `NA_real_` as a NaN; R tells the two apart
70/// (`is.nan(NA_real_)` is `FALSE`), so the refusal says `NA is not allowed`,
71/// as it does for `NA_integer_` and a logical `NA`. A float target keeps
72/// `NA_real_` (as a NaN), so nothing changes for it.
73#[inline]
74pub(crate) fn coerce_real_value<T>(value: f64) -> Result<T, SexpError>
75where
76    f64: TryCoerce<T>,
77    <f64 as TryCoerce<T>>::Error: std::fmt::Display,
78{
79    coerce_value(value).map_err(|e| {
80        if value.is_na() {
81            SexpNaError {
82                sexp_type: SEXPTYPE::REALSXP,
83            }
84            .into()
85        } else {
86            e
87        }
88    })
89}
90
91#[inline]
92pub(super) fn try_from_sexp_numeric_scalar<T>(sexp: SEXP) -> Result<T, SexpError>
93where
94    i32: TryCoerce<T>,
95    f64: TryCoerce<T>,
96    u8: TryCoerce<T>,
97    <i32 as TryCoerce<T>>::Error: std::fmt::Display,
98    <f64 as TryCoerce<T>>::Error: std::fmt::Display,
99    <u8 as TryCoerce<T>>::Error: std::fmt::Display,
100{
101    let actual = sexp.type_of();
102    match actual {
103        SEXPTYPE::INTSXP => {
104            let value: i32 = TryFromSexp::try_from_sexp(sexp)?;
105            coerce_value(value)
106        }
107        SEXPTYPE::REALSXP => {
108            let value: f64 = TryFromSexp::try_from_sexp(sexp)?;
109            coerce_real_value(value)
110        }
111        SEXPTYPE::RAWSXP => {
112            let value: u8 = TryFromSexp::try_from_sexp(sexp)?;
113            coerce_value(value)
114        }
115        SEXPTYPE::LGLSXP => {
116            let value: RLogical = TryFromSexp::try_from_sexp(sexp)?;
117            if value.is_na() {
118                return Err(lglsxp_na_error());
119            }
120            coerce_value(value.to_i32())
121        }
122        _ => Err(numeric_type_error(actual)),
123    }
124}
125
126#[inline]
127unsafe fn try_from_sexp_numeric_scalar_unchecked<T>(sexp: SEXP) -> Result<T, SexpError>
128where
129    i32: TryCoerce<T>,
130    f64: TryCoerce<T>,
131    u8: TryCoerce<T>,
132    <i32 as TryCoerce<T>>::Error: std::fmt::Display,
133    <f64 as TryCoerce<T>>::Error: std::fmt::Display,
134    <u8 as TryCoerce<T>>::Error: std::fmt::Display,
135{
136    let actual = sexp.type_of();
137    match actual {
138        SEXPTYPE::INTSXP => {
139            let value: i32 = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
140            coerce_value(value)
141        }
142        SEXPTYPE::REALSXP => {
143            let value: f64 = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
144            coerce_real_value(value)
145        }
146        SEXPTYPE::RAWSXP => {
147            let value: u8 = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
148            coerce_value(value)
149        }
150        SEXPTYPE::LGLSXP => {
151            let value: RLogical = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
152            if value.is_na() {
153                return Err(lglsxp_na_error());
154            }
155            coerce_value(value.to_i32())
156        }
157        _ => Err(numeric_type_error(actual)),
158    }
159}
160
161#[inline]
162fn try_from_sexp_numeric_option<T>(sexp: SEXP) -> Result<Option<T>, SexpError>
163where
164    i32: TryCoerce<T>,
165    f64: TryCoerce<T>,
166    u8: TryCoerce<T>,
167    <i32 as TryCoerce<T>>::Error: std::fmt::Display,
168    <f64 as TryCoerce<T>>::Error: std::fmt::Display,
169    <u8 as TryCoerce<T>>::Error: std::fmt::Display,
170{
171    if sexp.type_of() == SEXPTYPE::NILSXP {
172        return Ok(None);
173    }
174
175    let actual = sexp.type_of();
176    match actual {
177        SEXPTYPE::INTSXP => {
178            // Read raw i32 without the NA guard — we handle NA here by returning None.
179            let len = sexp.len();
180            if len != 1 {
181                return Err(crate::from_r::SexpLengthError {
182                    expected: 1,
183                    actual: len,
184                }
185                .into());
186            }
187            let value = unsafe { sexp.as_slice::<i32>() }
188                .first()
189                .cloned()
190                .ok_or_else(|| {
191                    SexpError::from(crate::from_r::SexpLengthError {
192                        expected: 1,
193                        actual: 0,
194                    })
195                })?;
196            if value.is_na() {
197                Ok(None)
198            } else {
199                coerce_value(value).map(Some)
200            }
201        }
202        SEXPTYPE::REALSXP => {
203            let value: f64 = TryFromSexp::try_from_sexp(sexp)?;
204            if value.is_na() {
205                Ok(None)
206            } else {
207                coerce_value(value).map(Some)
208            }
209        }
210        SEXPTYPE::RAWSXP => {
211            let value: u8 = TryFromSexp::try_from_sexp(sexp)?;
212            coerce_value(value).map(Some)
213        }
214        SEXPTYPE::LGLSXP => {
215            let value: RLogical = TryFromSexp::try_from_sexp(sexp)?;
216            if value.is_na() {
217                Ok(None)
218            } else {
219                coerce_value(value.to_i32()).map(Some)
220            }
221        }
222        _ => Err(numeric_type_error(actual)),
223    }
224}
225
226#[inline]
227unsafe fn try_from_sexp_numeric_option_unchecked<T>(sexp: SEXP) -> Result<Option<T>, SexpError>
228where
229    i32: TryCoerce<T>,
230    f64: TryCoerce<T>,
231    u8: TryCoerce<T>,
232    <i32 as TryCoerce<T>>::Error: std::fmt::Display,
233    <f64 as TryCoerce<T>>::Error: std::fmt::Display,
234    <u8 as TryCoerce<T>>::Error: std::fmt::Display,
235{
236    if sexp.type_of() == SEXPTYPE::NILSXP {
237        return Ok(None);
238    }
239
240    let actual = sexp.type_of();
241    match actual {
242        SEXPTYPE::INTSXP => {
243            // Read raw i32 without the NA guard — we handle NA here by returning None.
244            let len = unsafe { sexp.len_unchecked() };
245            if len != 1 {
246                return Err(crate::from_r::SexpLengthError {
247                    expected: 1,
248                    actual: len,
249                }
250                .into());
251            }
252            let value = unsafe { sexp.as_slice_unchecked::<i32>() }
253                .first()
254                .cloned()
255                .ok_or_else(|| {
256                    SexpError::from(crate::from_r::SexpLengthError {
257                        expected: 1,
258                        actual: 0,
259                    })
260                })?;
261            if value.is_na() {
262                Ok(None)
263            } else {
264                coerce_value(value).map(Some)
265            }
266        }
267        SEXPTYPE::REALSXP => {
268            let value: f64 = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
269            if value.is_na() {
270                Ok(None)
271            } else {
272                coerce_value(value).map(Some)
273            }
274        }
275        SEXPTYPE::RAWSXP => {
276            let value: u8 = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
277            coerce_value(value).map(Some)
278        }
279        SEXPTYPE::LGLSXP => {
280            let value: RLogical = unsafe { TryFromSexp::try_from_sexp_unchecked(sexp)? };
281            if value.is_na() {
282                Ok(None)
283            } else {
284                coerce_value(value.to_i32()).map(Some)
285            }
286        }
287        _ => Err(numeric_type_error(actual)),
288    }
289}
290
291impl TryFromSexp for i8 {
292    type Error = SexpError;
293
294    #[inline]
295    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
296        try_from_sexp_numeric_scalar(sexp)
297    }
298
299    #[inline]
300    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
301        unsafe { try_from_sexp_numeric_scalar_unchecked(sexp) }
302    }
303}
304
305impl TryFromSexp for i16 {
306    type Error = SexpError;
307
308    #[inline]
309    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
310        try_from_sexp_numeric_scalar(sexp)
311    }
312
313    #[inline]
314    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
315        unsafe { try_from_sexp_numeric_scalar_unchecked(sexp) }
316    }
317}
318
319impl TryFromSexp for u16 {
320    type Error = SexpError;
321
322    #[inline]
323    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
324        try_from_sexp_numeric_scalar(sexp)
325    }
326
327    #[inline]
328    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
329        unsafe { try_from_sexp_numeric_scalar_unchecked(sexp) }
330    }
331}
332
333impl TryFromSexp for u32 {
334    type Error = SexpError;
335
336    #[inline]
337    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
338        try_from_sexp_numeric_scalar(sexp)
339    }
340
341    #[inline]
342    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
343        unsafe { try_from_sexp_numeric_scalar_unchecked(sexp) }
344    }
345}
346
347impl TryFromSexp for f32 {
348    type Error = SexpError;
349
350    #[inline]
351    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
352        try_from_sexp_numeric_scalar(sexp)
353    }
354
355    #[inline]
356    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
357        unsafe { try_from_sexp_numeric_scalar_unchecked(sexp) }
358    }
359}
360
361impl TryFromSexp for Option<i8> {
362    type Error = SexpError;
363
364    #[inline]
365    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
366        try_from_sexp_numeric_option(sexp)
367    }
368
369    #[inline]
370    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
371        unsafe { try_from_sexp_numeric_option_unchecked(sexp) }
372    }
373}
374
375impl TryFromSexp for Option<i16> {
376    type Error = SexpError;
377
378    #[inline]
379    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
380        try_from_sexp_numeric_option(sexp)
381    }
382
383    #[inline]
384    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
385        unsafe { try_from_sexp_numeric_option_unchecked(sexp) }
386    }
387}
388
389impl TryFromSexp for Option<u16> {
390    type Error = SexpError;
391
392    #[inline]
393    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
394        try_from_sexp_numeric_option(sexp)
395    }
396
397    #[inline]
398    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
399        unsafe { try_from_sexp_numeric_option_unchecked(sexp) }
400    }
401}
402
403impl TryFromSexp for Option<u32> {
404    type Error = SexpError;
405
406    #[inline]
407    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
408        try_from_sexp_numeric_option(sexp)
409    }
410
411    #[inline]
412    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
413        unsafe { try_from_sexp_numeric_option_unchecked(sexp) }
414    }
415}
416
417impl TryFromSexp for Option<f32> {
418    type Error = SexpError;
419
420    #[inline]
421    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
422        try_from_sexp_numeric_option(sexp)
423    }
424
425    #[inline]
426    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
427        unsafe { try_from_sexp_numeric_option_unchecked(sexp) }
428    }
429}
430// endregion
431
432// region: Large integer scalar conversions (via f64)
433//
434// R doesn't have native 64-bit integers, so these read from REALSXP (f64)
435// and convert with range/precision checking.
436//
437// **Round-trip precision:**
438// - Values in [-2^53, 2^53] round-trip exactly: i64 → R → i64
439// - Values outside this range may not round-trip due to f64 precision loss
440//
441// **Conversion behavior:**
442// - Reads from REALSXP (f64) or INTSXP (i32)
443// - Validates the value is a whole number (no fractional part)
444// - Validates the value fits in the target type's range
445// - Returns error for NA values (use Option<i64> for nullable)
446
447/// Convert R numeric scalar to `i64`.
448///
449/// # Behavior
450///
451/// - Reads from REALSXP (f64) or INTSXP (i32)
452/// - Validates value is a whole number (no fractional part)
453/// - Validates value fits in i64 range
454/// - Returns `Err` for NA values
455///
456/// # Example
457///
458/// ```ignore
459/// // From R integer
460/// let val: i64 = TryFromSexp::try_from_sexp(int_sexp)?;
461///
462/// // From R numeric (must be whole number)
463/// let val: i64 = TryFromSexp::try_from_sexp(real_sexp)?;
464///
465/// // Error: 3.14 is not a whole number
466/// let val: Result<i64, _> = TryFromSexp::try_from_sexp(pi_sexp);
467/// ```
468impl TryFromSexp for i64 {
469    type Error = SexpError;
470
471    #[inline]
472    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
473        try_from_sexp_numeric_scalar(sexp)
474    }
475
476    #[inline]
477    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
478        unsafe { try_from_sexp_numeric_scalar_unchecked(sexp) }
479    }
480}
481
482/// Convert R numeric scalar to `u64`.
483///
484/// Same behavior as [`i64`](impl TryFromSexp for i64), but also validates
485/// the value is non-negative.
486impl TryFromSexp for u64 {
487    type Error = SexpError;
488
489    #[inline]
490    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
491        try_from_sexp_numeric_scalar(sexp)
492    }
493
494    #[inline]
495    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
496        unsafe { try_from_sexp_numeric_scalar_unchecked(sexp) }
497    }
498}
499
500/// Convert R numeric scalar to `Option<i64>`, with NA → `None`.
501impl TryFromSexp for Option<i64> {
502    type Error = SexpError;
503
504    #[inline]
505    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
506        try_from_sexp_numeric_option(sexp)
507    }
508
509    #[inline]
510    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
511        unsafe { try_from_sexp_numeric_option_unchecked(sexp) }
512    }
513}
514
515impl TryFromSexp for Option<u64> {
516    type Error = SexpError;
517
518    #[inline]
519    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
520        try_from_sexp_numeric_option(sexp)
521    }
522
523    #[inline]
524    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
525        unsafe { try_from_sexp_numeric_option_unchecked(sexp) }
526    }
527}
528
529impl TryFromSexp for usize {
530    type Error = SexpError;
531
532    #[inline]
533    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
534        try_from_sexp_numeric_scalar(sexp)
535    }
536
537    #[inline]
538    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
539        unsafe { try_from_sexp_numeric_scalar_unchecked(sexp) }
540    }
541}
542
543impl TryFromSexp for Option<usize> {
544    type Error = SexpError;
545
546    #[inline]
547    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
548        try_from_sexp_numeric_option(sexp)
549    }
550
551    #[inline]
552    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
553        unsafe { try_from_sexp_numeric_option_unchecked(sexp) }
554    }
555}
556
557impl TryFromSexp for isize {
558    type Error = SexpError;
559
560    #[inline]
561    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
562        try_from_sexp_numeric_scalar(sexp)
563    }
564
565    #[inline]
566    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
567        unsafe { try_from_sexp_numeric_scalar_unchecked(sexp) }
568    }
569}
570
571impl TryFromSexp for Option<isize> {
572    type Error = SexpError;
573
574    #[inline]
575    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
576        try_from_sexp_numeric_option(sexp)
577    }
578
579    #[inline]
580    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
581        unsafe { try_from_sexp_numeric_option_unchecked(sexp) }
582    }
583}
584// endregion