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