Skip to main content

extendr_api/robj/
into_robj.rs

1use super::*;
2use crate::scalar::Scalar;
3use crate::single_threaded;
4use extendr_ffi::{
5    cetype_t, R_BlankString, R_NaInt, R_NaReal, R_NaString, R_NilValue, Rcomplex, Rf_mkCharLenCE,
6    COMPLEX, INTEGER, LOGICAL, RAW, REAL, SET_STRING_ELT, SEXPTYPE,
7};
8mod repeat_into_robj;
9
10/// Returns an `CHARSXP` based on the provided `&str`.
11///
12/// Note that R does string interning, thus repeated application of this
13/// function on the same string, will incur little computational cost.
14///
15/// Note, that you must protect the return value somehow.
16pub(crate) fn str_to_character(s: &str) -> SEXP {
17    unsafe {
18        if s.is_na() {
19            R_NaString
20        } else if s.is_empty() {
21            R_BlankString
22        } else {
23            single_threaded(|| {
24                // this function embeds a terminating \nul
25                Rf_mkCharLenCE(s.as_ptr().cast(), s.len() as i32, cetype_t::CE_UTF8)
26            })
27        }
28    }
29}
30
31/// Convert a null to an Robj.
32impl From<()> for Robj {
33    fn from(_: ()) -> Self {
34        // Note: we do not need to protect this.
35        unsafe { Robj::from_sexp(R_NilValue) }
36    }
37}
38
39/// Convert a [`Result`] to an [`Robj`].
40///
41/// To use the `?`-operator, an extendr-function must return either [`extendr_api::error::Result`] or [`std::result::Result`].
42/// Use of `panic!` in extendr is discouraged due to memory leakage.
43///
44/// Alternative behaviors enabled by feature toggles:
45/// extendr-api supports different conversions from [`Result<T,E>`] into `Robj`.
46/// Below, `x_ok` represents an R variable on R side which was returned from rust via `T::into_robj()` or similar.
47/// Likewise, `x_err` was returned to R side from rust via `E::into_robj()` or similar.
48/// extendr-api
49/// * `result_list`: `Ok(T)` is encoded as `list(ok = x_ok, err = NULL)` and `Err` as `list(ok = NULL, err = e_err)`.
50/// * `result_condition'`: `Ok(T)` is encoded as `x_ok` and `Err(E)` as `condition(msg="extendr_error", value = x_err, class=c("extendr_error", "error", "condition"))`
51/// * More than one enabled feature: Only one feature gate will take effect, the current order of precedence is [`result_list`, `result_condition`, ... ].
52/// * Neither of the above (default): `Ok(T)` is encoded as `x_ok` and `Err(E)` will trigger `throw_r_error()` with the error message.
53/// ```
54/// use extendr_api::prelude::*;
55/// fn my_func() -> Result<f64> {
56///     Ok(1.0)
57/// }
58///
59/// test! {
60///     assert_eq!(r!(my_func()), r!(1.0));
61/// }
62/// ```
63///
64/// [`extendr_api::error::Result`]: crate::error::Result
65#[cfg(not(any(feature = "result_list", feature = "result_condition")))]
66impl<T, E> From<std::result::Result<T, E>> for Robj
67where
68    T: Into<Robj>,
69    E: std::fmt::Debug + std::fmt::Display,
70{
71    fn from(res: std::result::Result<T, E>) -> Self {
72        match res {
73            Ok(val) => val.into(),
74            Err(err) => panic!("{}", err),
75        }
76    }
77}
78
79/// Convert a [`Result`] to an [`Robj`]. Return either `Ok` value or `Err` value wrapped in an
80/// error condition. This allows using `?` operator in functions
81/// and returning [`Result<T>`] without panicking on `Err`. `T` must implement [`IntoRobj`].
82///
83/// Returns `Ok` value as is. Returns `Err` wrapped in an R error condition. The `Err` is placed in
84/// $value field of the condition, and its message is set to 'extendr_err'
85#[cfg(all(feature = "result_condition", not(feature = "result_list")))]
86impl<T, E> From<std::result::Result<T, E>> for Robj
87where
88    T: Into<Robj>,
89    E: Into<Robj>,
90{
91    fn from(res: std::result::Result<T, E>) -> Self {
92        use crate as extendr_api;
93        match res {
94            Ok(x) => x.into(),
95            Err(x) => {
96                let mut err = list!(message = "extendr_err", value = x.into());
97                err.set_class(["extendr_error", "error", "condition"])
98                    .expect("internal error: failed to set class");
99                err.into()
100            }
101        }
102    }
103}
104
105/// Convert a `Result` to an R `List` with an `ok` and `err` elements.
106/// This allows using `?` operator in functions
107/// and returning [`std::result::Result`] or [`extendr_api::error::Result`]
108/// without panicking on `Err`.
109///
110/// [`extendr_api::error::Result`]: crate::error::Result
111#[cfg(feature = "result_list")]
112impl<T, E> From<std::result::Result<T, E>> for Robj
113where
114    T: Into<Robj>,
115    E: Into<Robj>,
116{
117    fn from(res: std::result::Result<T, E>) -> Self {
118        use crate as extendr_api;
119        let mut result = match res {
120            Ok(x) => list!(ok = x.into(), err = NULL),
121            Err(x) => {
122                let err_robj = x.into();
123                if err_robj.is_null() {
124                    panic!("Internal error: result_list not allowed to return NULL as err-value")
125                }
126                list!(ok = NULL, err = err_robj)
127            }
128        };
129        result
130            .set_class(&["extendr_result"])
131            .expect("Internal error: failed to set class");
132        result.into()
133    }
134}
135
136// string conversions from Error trait to Robj and String
137impl From<Error> for Robj {
138    fn from(res: Error) -> Self {
139        res.to_string().into()
140    }
141}
142impl From<Error> for String {
143    fn from(res: Error) -> Self {
144        res.to_string()
145    }
146}
147
148/// Convert an Robj reference into a borrowed Robj.
149impl From<&Robj> for Robj {
150    // Note: we should probably have a much better reference
151    // mechanism as double-free or underprotection is a distinct possibility.
152    fn from(val: &Robj) -> Self {
153        unsafe { Robj::from_sexp(val.get()) }
154    }
155}
156
157/// This is an extension trait to provide a convenience method `into_robj()`.
158///
159/// Defer to `From<T> for Robj`-impls if you have custom types.
160///
161pub trait IntoRobj {
162    fn into_robj(self) -> Robj;
163}
164
165impl<T> IntoRobj for T
166where
167    Robj: From<T>,
168{
169    fn into_robj(self) -> Robj {
170        self.into()
171    }
172}
173
174/// `ToVectorValue` is a trait that allows many different types
175/// to be converted to vectors. It is used as a type parameter
176/// to `collect_robj()`.
177pub trait ToVectorValue {
178    fn sexptype() -> SEXPTYPE {
179        SEXPTYPE::NILSXP
180    }
181
182    fn to_real(&self) -> f64
183    where
184        Self: Sized,
185    {
186        0.
187    }
188
189    fn to_complex(&self) -> Rcomplex
190    where
191        Self: Sized,
192    {
193        Rcomplex { r: 0., i: 0. }
194    }
195
196    fn to_integer(&self) -> i32
197    where
198        Self: Sized,
199    {
200        i32::MIN
201    }
202
203    fn to_logical(&self) -> i32
204    where
205        Self: Sized,
206    {
207        i32::MIN
208    }
209
210    fn to_raw(&self) -> u8
211    where
212        Self: Sized,
213    {
214        0
215    }
216
217    fn to_sexp(&self) -> SEXP
218    where
219        Self: Sized,
220    {
221        unsafe { R_NilValue }
222    }
223}
224
225macro_rules! impl_real_tvv {
226    ($t: ty) => {
227        impl ToVectorValue for $t {
228            fn sexptype() -> SEXPTYPE {
229                SEXPTYPE::REALSXP
230            }
231
232            fn to_real(&self) -> f64 {
233                *self as f64
234            }
235        }
236
237        impl ToVectorValue for &$t {
238            fn sexptype() -> SEXPTYPE {
239                SEXPTYPE::REALSXP
240            }
241
242            fn to_real(&self) -> f64 {
243                **self as f64
244            }
245        }
246
247        impl ToVectorValue for Option<$t> {
248            fn sexptype() -> SEXPTYPE {
249                SEXPTYPE::REALSXP
250            }
251
252            fn to_real(&self) -> f64 {
253                if self.is_some() {
254                    self.unwrap() as f64
255                } else {
256                    unsafe { R_NaReal }
257                }
258            }
259        }
260    };
261}
262
263impl_real_tvv!(f64);
264impl_real_tvv!(f32);
265
266// Since these types might exceeds the max or min of R's 32bit integer, we need
267// to return as REALSXP
268impl_real_tvv!(i64);
269impl_real_tvv!(u32);
270impl_real_tvv!(u64);
271impl_real_tvv!(usize);
272
273macro_rules! impl_complex_tvv {
274    ($t: ty) => {
275        impl ToVectorValue for $t {
276            fn sexptype() -> SEXPTYPE {
277                SEXPTYPE::CPLXSXP
278            }
279
280            fn to_complex(&self) -> Rcomplex {
281                unsafe { std::mem::transmute(*self) }
282            }
283        }
284
285        impl ToVectorValue for &$t {
286            fn sexptype() -> SEXPTYPE {
287                SEXPTYPE::CPLXSXP
288            }
289
290            fn to_complex(&self) -> Rcomplex {
291                unsafe { std::mem::transmute(**self) }
292            }
293        }
294    };
295}
296
297impl_complex_tvv!(c64);
298impl_complex_tvv!(Rcplx);
299impl_complex_tvv!((f64, f64));
300
301macro_rules! impl_integer_tvv {
302    ($t: ty) => {
303        impl ToVectorValue for $t {
304            fn sexptype() -> SEXPTYPE {
305                SEXPTYPE::INTSXP
306            }
307
308            fn to_integer(&self) -> i32 {
309                *self as i32
310            }
311        }
312
313        impl ToVectorValue for &$t {
314            fn sexptype() -> SEXPTYPE {
315                SEXPTYPE::INTSXP
316            }
317
318            fn to_integer(&self) -> i32 {
319                **self as i32
320            }
321        }
322
323        impl ToVectorValue for Option<$t> {
324            fn sexptype() -> SEXPTYPE {
325                SEXPTYPE::INTSXP
326            }
327
328            fn to_integer(&self) -> i32 {
329                if self.is_some() {
330                    self.unwrap() as i32
331                } else {
332                    unsafe { R_NaInt }
333                }
334            }
335        }
336    };
337}
338
339impl_integer_tvv!(i8);
340impl_integer_tvv!(i16);
341impl_integer_tvv!(i32);
342impl_integer_tvv!(u16);
343
344impl ToVectorValue for u8 {
345    fn sexptype() -> SEXPTYPE {
346        SEXPTYPE::RAWSXP
347    }
348
349    fn to_raw(&self) -> u8 {
350        *self
351    }
352}
353
354impl ToVectorValue for &u8 {
355    fn sexptype() -> SEXPTYPE {
356        SEXPTYPE::RAWSXP
357    }
358
359    fn to_raw(&self) -> u8 {
360        **self
361    }
362}
363
364macro_rules! impl_str_tvv {
365    ($t: ty) => {
366        impl ToVectorValue for $t {
367            fn sexptype() -> SEXPTYPE {
368                SEXPTYPE::STRSXP
369            }
370
371            fn to_sexp(&self) -> SEXP
372            where
373                Self: Sized,
374            {
375                str_to_character(self.as_ref())
376            }
377        }
378
379        impl ToVectorValue for &$t {
380            fn sexptype() -> SEXPTYPE {
381                SEXPTYPE::STRSXP
382            }
383
384            fn to_sexp(&self) -> SEXP
385            where
386                Self: Sized,
387            {
388                str_to_character(self.as_ref())
389            }
390        }
391
392        impl ToVectorValue for Option<$t> {
393            fn sexptype() -> SEXPTYPE {
394                SEXPTYPE::STRSXP
395            }
396
397            fn to_sexp(&self) -> SEXP
398            where
399                Self: Sized,
400            {
401                if let Some(s) = self {
402                    str_to_character(s.as_ref())
403                } else {
404                    unsafe { R_NaString }
405                }
406            }
407        }
408    };
409}
410
411impl_str_tvv! {&str}
412impl_str_tvv! {String}
413
414impl ToVectorValue for bool {
415    fn sexptype() -> SEXPTYPE {
416        SEXPTYPE::LGLSXP
417    }
418
419    fn to_logical(&self) -> i32
420    where
421        Self: Sized,
422    {
423        *self as i32
424    }
425}
426
427impl ToVectorValue for &bool {
428    fn sexptype() -> SEXPTYPE {
429        SEXPTYPE::LGLSXP
430    }
431
432    fn to_logical(&self) -> i32
433    where
434        Self: Sized,
435    {
436        **self as i32
437    }
438}
439
440impl ToVectorValue for Rbool {
441    fn sexptype() -> SEXPTYPE {
442        SEXPTYPE::LGLSXP
443    }
444
445    fn to_logical(&self) -> i32
446    where
447        Self: Sized,
448    {
449        self.inner()
450    }
451}
452
453impl ToVectorValue for &Rbool {
454    fn sexptype() -> SEXPTYPE {
455        SEXPTYPE::LGLSXP
456    }
457
458    fn to_logical(&self) -> i32
459    where
460        Self: Sized,
461    {
462        self.inner()
463    }
464}
465
466impl ToVectorValue for Option<bool> {
467    fn sexptype() -> SEXPTYPE {
468        SEXPTYPE::LGLSXP
469    }
470
471    fn to_logical(&self) -> i32 {
472        if self.is_some() {
473            self.unwrap() as i32
474        } else {
475            unsafe { R_NaInt }
476        }
477    }
478}
479
480// Not thread safe.
481fn fixed_size_collect<I>(iter: I, len: usize) -> Robj
482where
483    I: Iterator,
484    I: Sized,
485    I::Item: ToVectorValue,
486{
487    single_threaded(|| unsafe {
488        // Length of the vector is known in advance.
489        let sexptype = I::Item::sexptype();
490        if sexptype != SEXPTYPE::NILSXP {
491            let res = Robj::alloc_vector(sexptype, len);
492            let sexp = res.get();
493            match sexptype {
494                SEXPTYPE::REALSXP => {
495                    let ptr = REAL(sexp);
496                    for (i, v) in iter.enumerate() {
497                        *ptr.add(i) = v.to_real();
498                    }
499                }
500                SEXPTYPE::CPLXSXP => {
501                    let ptr = COMPLEX(sexp);
502                    for (i, v) in iter.enumerate() {
503                        *ptr.add(i) = v.to_complex();
504                    }
505                }
506                SEXPTYPE::INTSXP => {
507                    let ptr = INTEGER(sexp);
508                    for (i, v) in iter.enumerate() {
509                        *ptr.add(i) = v.to_integer();
510                    }
511                }
512                SEXPTYPE::LGLSXP => {
513                    let ptr = LOGICAL(sexp);
514                    for (i, v) in iter.enumerate() {
515                        *ptr.add(i) = v.to_logical();
516                    }
517                }
518                SEXPTYPE::STRSXP => {
519                    for (i, v) in iter.enumerate() {
520                        SET_STRING_ELT(sexp, i as isize, v.to_sexp());
521                    }
522                }
523                SEXPTYPE::RAWSXP => {
524                    let ptr = RAW(sexp);
525                    for (i, v) in iter.enumerate() {
526                        *ptr.add(i) = v.to_raw();
527                    }
528                }
529                _ => {
530                    panic!("unexpected SEXPTYPE in collect_robj");
531                }
532            }
533            res
534        } else {
535            Robj::from(())
536        }
537    })
538}
539
540/// Extensions to iterators for R objects including [RobjItertools::collect_robj()].
541pub trait RobjItertools: Iterator {
542    /// Convert a wide range of iterators to Robj.
543    /// ```
544    /// use extendr_api::prelude::*;
545    ///
546    /// test! {
547    /// // Integer iterators.
548    /// let robj = (0..3).collect_robj();
549    /// assert_eq!(robj.as_integer_vector().unwrap(), vec![0, 1, 2]);
550    ///
551    /// // Logical iterators.
552    /// let robj = (0..3).map(|x| x % 2 == 0).collect_robj();
553    /// assert_eq!(robj.as_logical_vector().unwrap(), vec![TRUE, FALSE, TRUE]);
554    ///
555    /// // Numeric iterators.
556    /// let robj = (0..3).map(|x| x as f64).collect_robj();
557    /// assert_eq!(robj.as_real_vector().unwrap(), vec![0., 1., 2.]);
558    ///
559    /// // String iterators.
560    /// let robj = (0..3).map(|x| format!("{}", x)).collect_robj();
561    /// assert_eq!(robj.as_str_vector(), Some(vec!["0", "1", "2"]));
562    /// }
563    /// ```
564    fn collect_robj(self) -> Robj
565    where
566        Self: Iterator,
567        Self: Sized,
568        Self::Item: ToVectorValue,
569    {
570        if let (len, Some(max)) = self.size_hint() {
571            if len == max {
572                return fixed_size_collect(self, len);
573            }
574        }
575        // If the size is indeterminate, create a vector and call recursively.
576        let vec: Vec<_> = self.collect();
577        assert!(vec.iter().size_hint() == (vec.len(), Some(vec.len())));
578        vec.into_iter().collect_robj()
579    }
580
581    /// Collects an iterable into an [`RArray`].
582    /// The iterable must yield items column by column (aka Fortan order)
583    ///
584    /// # Arguments
585    ///
586    /// * `dims` - an array containing the length of each dimension
587    fn collect_rarray<const LEN: usize>(
588        self,
589        dims: [usize; LEN],
590    ) -> Result<RArray<Self::Item, [usize; LEN]>>
591    where
592        Self: Iterator,
593        Self: Sized,
594        Self::Item: ToVectorValue,
595        Robj: for<'a> AsTypedSlice<'a, Self::Item>,
596    {
597        let mut vector = self.collect_robj();
598        let prod = dims.iter().product::<usize>();
599        if prod != vector.len() {
600            return Err(Error::Other(format!(
601                "The vector length ({}) does not match the length implied by the dimensions ({})",
602                vector.len(),
603                prod
604            )));
605        }
606        vector.set_attrib(wrapper::symbol::dim_symbol(), dims.iter().collect_robj())?;
607        let _data = vector.as_typed_slice().ok_or(Error::Other(
608            "Unknown error in converting to slice".to_string(),
609        ))?;
610        Ok(RArray::from_parts(vector, dims))
611    }
612}
613
614// Thanks to *pretzelhammer* on stackoverflow for this.
615impl<T> RobjItertools for T where T: Iterator {}
616
617// Scalars which are ToVectorValue
618impl<T> From<T> for Robj
619where
620    T: ToVectorValue,
621{
622    fn from(scalar: T) -> Self {
623        Some(scalar).into_iter().collect_robj()
624    }
625}
626
627macro_rules! impl_from_as_iterator {
628    ($t: ty) => {
629        impl<T> From<$t> for Robj
630        where
631            $t: RobjItertools,
632            <$t as Iterator>::Item: ToVectorValue,
633            T: ToVectorValue,
634        {
635            fn from(val: $t) -> Self {
636                val.collect_robj()
637            }
638        }
639    };
640}
641
642// impl<T> From<Range<T>> for Robj
643// where
644//     Range<T> : RobjItertools,
645//     <Range<T> as Iterator>::Item: ToVectorValue,
646//     T : ToVectorValue
647// {
648//     fn from(val: Range<T>) -> Self {
649//         val.collect_robj()
650//     }
651// } //
652
653impl<T, const N: usize> From<[T; N]> for Robj
654where
655    T: ToVectorValue,
656{
657    fn from(val: [T; N]) -> Self {
658        fixed_size_collect(val.into_iter(), N)
659    }
660}
661
662impl<'a, T, const N: usize> From<&'a [T; N]> for Robj
663where
664    Self: 'a,
665    &'a T: ToVectorValue + 'a,
666{
667    fn from(val: &'a [T; N]) -> Self {
668        fixed_size_collect(val.iter(), N)
669    }
670}
671
672impl<'a, T, const N: usize> From<&'a mut [T; N]> for Robj
673where
674    Self: 'a,
675    &'a mut T: ToVectorValue + 'a,
676{
677    fn from(val: &'a mut [T; N]) -> Self {
678        fixed_size_collect(val.iter_mut(), N)
679    }
680}
681
682impl<T: ToVectorValue + Clone> From<&Vec<T>> for Robj {
683    fn from(value: &Vec<T>) -> Self {
684        let len = value.len();
685        fixed_size_collect(value.iter().cloned(), len)
686    }
687}
688
689impl<T: ToVectorValue> From<Vec<T>> for Robj {
690    fn from(value: Vec<T>) -> Self {
691        let len = value.len();
692        fixed_size_collect(value.into_iter(), len)
693    }
694}
695
696impl<'a, T> From<&'a [T]> for Robj
697where
698    Self: 'a,
699    T: 'a,
700    &'a T: ToVectorValue,
701{
702    fn from(val: &'a [T]) -> Self {
703        val.iter().collect_robj()
704    }
705}
706
707impl_from_as_iterator! {Range<T>}
708impl_from_as_iterator! {RangeInclusive<T>}
709
710impl From<Vec<Robj>> for Robj {
711    /// Convert a vector of Robj into a list.
712    fn from(val: Vec<Robj>) -> Self {
713        List::from_values(val.iter()).into()
714    }
715}
716
717impl From<Vec<Rstr>> for Robj {
718    /// Convert a vector of Rstr into strings.
719    fn from(val: Vec<Rstr>) -> Self {
720        Strings::from_values(val).into()
721    }
722}
723
724#[cfg(test)]
725mod test {
726    use super::*;
727    use crate as extendr_api;
728
729    #[test]
730    fn test_vec_rint_to_robj() {
731        test! {
732            let int_vec = vec![3,4,0,-2];
733            let int_vec_robj: Robj = int_vec.clone().into();
734            // unsafe { extendr_ffi::Rf_PrintValue(int_vec_robj.get())}
735            assert_eq!(int_vec_robj.as_integer_slice().unwrap(), &int_vec);
736
737            let rint_vec = vec![Rint::new(3), Rint::new(4), Rint::new(0), Rint::new(-2)];
738            let rint_vec_robj: Robj = rint_vec.into();
739            // unsafe { extendr_ffi::Rf_PrintValue(rint_vec_robj.get())}
740            assert_eq!(rint_vec_robj.as_integer_slice().unwrap(), &int_vec);
741        }
742    }
743
744    #[test]
745    fn test_collect_rarray_matrix() {
746        test! {
747            // Check that collect_rarray works the same as R's matrix() function
748            let rmat = (1i32..=16).collect_rarray([4, 4]);
749            assert!(rmat.is_ok());
750            assert_eq!(Robj::from(rmat), R!("matrix(1:16, nrow=4)").unwrap());
751        }
752    }
753
754    #[test]
755    fn test_collect_rarray_tensor() {
756        test! {
757            // Check that collect_rarray works the same as R's array() function
758            let rmat = (1i32..=16).collect_rarray([2, 4, 2]);
759            assert!(rmat.is_ok());
760            assert_eq!(Robj::from(rmat), R!("array(1:16, dim=c(2, 4, 2))").unwrap());
761        }
762    }
763
764    #[test]
765    fn test_collect_rarray_matrix_failure() {
766        test! {
767            // Check that collect_rarray fails when given an invalid shape
768            let rmat = (1i32..=16).collect_rarray([3, 3]);
769            assert!(rmat.is_err());
770            let msg = rmat.unwrap_err().to_string();
771            assert!(msg.contains('9'));
772            assert!(msg.contains("dimension"));
773        }
774    }
775
776    #[test]
777    fn test_collect_tensor_failure() {
778        test! {
779            // Check that collect_rarray fails when given an invalid shape
780            let rmat = (1i32..=16).collect_rarray([3, 3, 3]);
781            assert!(rmat.is_err());
782            let msg = rmat.unwrap_err().to_string();
783            assert!(msg.contains("27"));
784            assert!(msg.contains("dimension"));
785        }
786    }
787
788    #[test]
789    #[cfg(all(feature = "result_condition", not(feature = "result_list")))]
790    fn test_result_condition() {
791        use crate::prelude::*;
792        fn my_err_f() -> std::result::Result<f64, f64> {
793            Err(42.0) // return err float
794        }
795
796        test! {
797                  assert_eq!(
798                    r!(my_err_f()),
799                    R!(
800        "structure(list(message = 'extendr_err',
801        value = 42.0), class = c('extendr_error', 'error', 'condition'))"
802                    ).unwrap()
803                );
804            }
805    }
806
807    #[test]
808    #[cfg(feature = "result_list")]
809    fn test_result_list() {
810        use crate::prelude::*;
811        fn my_err_f() -> std::result::Result<f64, String> {
812            Err("We have water in the engine room!".to_string())
813        }
814
815        fn my_ok_f() -> std::result::Result<f64, String> {
816            Ok(123.123)
817        }
818
819        test! {
820            assert_eq!(
821                r!(my_err_f()),
822                R!("x=list(ok=NULL, err='We have water in the engine room!')
823                    class(x)='extendr_result'
824                    x"
825                ).unwrap()
826            );
827            assert_eq!(
828                r!(my_ok_f()),
829                R!("x = list(ok=123.123, err=NULL)
830                    class(x)='extendr_result'
831                    x"
832                ).unwrap()
833            );
834        }
835    }
836}