Skip to main content

miniextendr_api/
rvalue.rs

1//! `RValue` — an owned, `Send`, R-native value tree.
2//!
3//! The `serde_json::Value` analogue for R's base data types: an owned (no live
4//! `SEXP`), inspectable (`Debug + Clone`), `Send` (crosses the worker boundary)
5//! representation of "an arbitrary R native value". Use it wherever you need to
6//! carry a dynamic/heterogeneous R value through Rust — e.g. condition `data =`
7//! payloads, which travel through `panic_any` and may be raised on the worker
8//! thread where a live `SEXP` would be illegal.
9//!
10//! The variants enumerate R's (finite, closed) data type system — vectors only,
11//! since R has no true scalars. NA is modelled with `Option` where R carries it
12//! out of band (logical/integer/character); `Double` carries NA in the `NA_REAL`
13//! bit pattern inside `Vec<f64>` (no `Option`), matching R's in-band convention.
14//!
15//! Out of scope (existing homes): closures/environments/symbols/language objects
16//! (not data), S4 objects, `EXTPTRSXP` (use [`crate::externalptr::ExternalPtr`]),
17//! ALTREP internals. Attribute-carrying values (factor, `Date`, `POSIXct`) are
18//! modelled as their plain `Integer`/`Double` storage in v1 — richer handling is
19//! deferred until a consumer needs it.
20
21use crate::SEXP;
22use crate::SexpExt;
23use crate::from_r::{SexpError, SexpLengthError, SexpNaError, SexpTypeError, TryFromSexp};
24use crate::into_r::IntoR;
25use crate::sexp_types::{Rcomplex, SEXPTYPE};
26
27/// An owned, `Send`, R-native value tree. See the [module docs](self).
28#[derive(Debug, Clone)]
29pub enum RValue {
30    /// `NULL` (`NILSXP`).
31    Null,
32    /// Logical vector (`LGLSXP`). `None` is `NA`.
33    Logical(Vec<Option<bool>>),
34    /// Integer vector (`INTSXP`). `None` is `NA`.
35    Integer(Vec<Option<i32>>),
36    /// Double vector (`REALSXP`). NA is carried in the `NA_REAL` bit pattern.
37    Double(Vec<f64>),
38    /// Complex vector (`CPLXSXP`).
39    Complex(Vec<Rcomplex>),
40    /// Character vector (`STRSXP`). `None` is `NA`.
41    Character(Vec<Option<String>>),
42    /// Raw byte vector (`RAWSXP`). No NA.
43    Raw(Vec<u8>),
44    /// Generic list (`VECSXP`). Recursive; a `None` name is an unnamed slot.
45    List(Vec<(Option<String>, RValue)>),
46}
47
48// region: IntoR
49
50impl IntoR for RValue {
51    type Error = std::convert::Infallible;
52
53    fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
54        Ok(self.into_sexp())
55    }
56
57    unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
58        self.try_into_sexp()
59    }
60
61    fn into_sexp(self) -> SEXP {
62        match self {
63            RValue::Null => SEXP::nil(),
64            RValue::Logical(v) => v.into_sexp(),
65            RValue::Integer(v) => v.into_sexp(),
66            RValue::Double(v) => v.into_sexp(),
67            RValue::Complex(v) => v.into_sexp(),
68            RValue::Character(v) => v.into_sexp(),
69            RValue::Raw(v) => v.into_sexp(),
70            RValue::List(pairs) => list_into_sexp(pairs),
71        }
72    }
73}
74
75/// Build a `VECSXP` from list pairs, recursing into each child.
76///
77/// Each child `SEXP` is `protect_raw`-rooted before the next child allocates —
78/// the same GC discipline as `AsNamedList::into_sexp` (#1030/#1045), required
79/// under `gctorture`. The scope drops once the list is built.
80fn list_into_sexp(pairs: Vec<(Option<String>, RValue)>) -> SEXP {
81    use crate::list::List;
82    // SAFETY: `IntoR::into_sexp` for `#[miniextendr]` return values runs on the R
83    // main thread.
84    unsafe {
85        let scope = crate::gc_protect::ProtectScope::new();
86        if pairs.iter().any(|(name, _)| name.is_some()) {
87            // Named (R fills unnamed slots with ""). `from_raw_pairs` sets names.
88            let named: Vec<(String, SEXP)> = pairs
89                .into_iter()
90                .map(|(name, val)| (name.unwrap_or_default(), scope.protect_raw(val.into_sexp())))
91                .collect();
92            List::from_raw_pairs(named).into_sexp()
93        } else {
94            let values: Vec<SEXP> = pairs
95                .into_iter()
96                .map(|(_, val)| scope.protect_raw(val.into_sexp()))
97                .collect();
98            List::from_raw_values(values).into_sexp()
99        }
100    }
101}
102
103// endregion
104
105// region: TryFromSexp
106
107impl TryFromSexp for RValue {
108    type Error = SexpError;
109
110    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
111        use crate::altrep_traits::{NA_INTEGER, NA_LOGICAL};
112
113        let n = sexp.len();
114        match sexp.type_of() {
115            SEXPTYPE::NILSXP => Ok(RValue::Null),
116            SEXPTYPE::LGLSXP => Ok(RValue::Logical(
117                (0..n as isize)
118                    .map(|i| {
119                        let x = sexp.logical_elt(i);
120                        (x != NA_LOGICAL).then_some(x != 0)
121                    })
122                    .collect(),
123            )),
124            SEXPTYPE::INTSXP => Ok(RValue::Integer(
125                (0..n as isize)
126                    .map(|i| {
127                        let x = sexp.integer_elt(i);
128                        (x != NA_INTEGER).then_some(x)
129                    })
130                    .collect(),
131            )),
132            // NA stays in the bit pattern — no Option needed (decision 2).
133            SEXPTYPE::REALSXP => Ok(RValue::Double(
134                (0..n as isize).map(|i| sexp.real_elt(i)).collect(),
135            )),
136            SEXPTYPE::CPLXSXP => Ok(RValue::Complex(
137                (0..n as isize).map(|i| sexp.complex_elt(i)).collect(),
138            )),
139            SEXPTYPE::STRSXP => Ok(RValue::Character(
140                (0..n as isize)
141                    .map(|i| {
142                        if sexp.string_elt(i).is_na_string() {
143                            None
144                        } else {
145                            sexp.string_elt_str(i).map(str::to_string)
146                        }
147                    })
148                    .collect(),
149            )),
150            // SAFETY: type checked to be RAWSXP; `as_slice` handles the empty (0x1) case.
151            SEXPTYPE::RAWSXP => Ok(RValue::Raw(unsafe { sexp.as_slice::<u8>() }.to_vec())),
152            SEXPTYPE::VECSXP => {
153                let names = sexp.get_names();
154                let mut pairs = Vec::with_capacity(n);
155                for i in 0..n as isize {
156                    // `||` short-circuits, so `string_elt` is only reached for a
157                    // character names vector. NA / empty names are unnamed slots.
158                    let name = if names.is_nil()
159                        || !names.is_character()
160                        || names.string_elt(i).is_na_string()
161                    {
162                        None
163                    } else {
164                        names
165                            .string_elt_str(i)
166                            .filter(|s| !s.is_empty())
167                            .map(str::to_string)
168                    };
169                    pairs.push((name, RValue::try_from_sexp(sexp.vector_elt(i))?));
170                }
171                Ok(RValue::List(pairs))
172            }
173            other => Err(SexpError::InvalidValue(format!(
174                "RValue cannot represent SEXP of type {other:?} (only R data vectors are modelled)"
175            ))),
176        }
177    }
178}
179
180// endregion
181
182// region: ergonomic From impls (scalars wrap to length-1 vectors, per R semantics)
183
184impl From<i32> for RValue {
185    fn from(v: i32) -> Self {
186        RValue::Integer(vec![Some(v)])
187    }
188}
189impl From<f64> for RValue {
190    fn from(v: f64) -> Self {
191        RValue::Double(vec![v])
192    }
193}
194impl From<bool> for RValue {
195    fn from(v: bool) -> Self {
196        RValue::Logical(vec![Some(v)])
197    }
198}
199impl From<String> for RValue {
200    fn from(v: String) -> Self {
201        RValue::Character(vec![Some(v)])
202    }
203}
204impl From<&str> for RValue {
205    fn from(v: &str) -> Self {
206        RValue::Character(vec![Some(v.to_string())])
207    }
208}
209impl From<Vec<i32>> for RValue {
210    fn from(v: Vec<i32>) -> Self {
211        RValue::Integer(v.into_iter().map(Some).collect())
212    }
213}
214impl From<Vec<f64>> for RValue {
215    fn from(v: Vec<f64>) -> Self {
216        RValue::Double(v)
217    }
218}
219impl From<Vec<bool>> for RValue {
220    fn from(v: Vec<bool>) -> Self {
221        RValue::Logical(v.into_iter().map(Some).collect())
222    }
223}
224impl From<Vec<String>> for RValue {
225    fn from(v: Vec<String>) -> Self {
226        RValue::Character(v.into_iter().map(Some).collect())
227    }
228}
229impl From<Vec<&str>> for RValue {
230    fn from(v: Vec<&str>) -> Self {
231        RValue::Character(v.into_iter().map(|s| Some(s.to_string())).collect())
232    }
233}
234
235// endregion
236
237// region: NA-aware Option / Vec<Option> + wide-integer ladder (ported from #1044/#995)
238
239// `Option<T>` → length-1 vector, `None` → `NA`. Integer/logical/character carry
240// NA out of band via the `None` slot; `Double` carries it in the `NA_REAL` bit
241// pattern (no `Option<f64>` variant — see the module docs).
242
243impl From<Option<i32>> for RValue {
244    fn from(v: Option<i32>) -> Self {
245        RValue::Integer(vec![v])
246    }
247}
248impl From<Option<f64>> for RValue {
249    fn from(v: Option<f64>) -> Self {
250        RValue::Double(vec![v.unwrap_or(crate::altrep_traits::NA_REAL)])
251    }
252}
253impl From<Option<bool>> for RValue {
254    fn from(v: Option<bool>) -> Self {
255        RValue::Logical(vec![v])
256    }
257}
258impl From<Option<String>> for RValue {
259    fn from(v: Option<String>) -> Self {
260        RValue::Character(vec![v])
261    }
262}
263impl From<Option<&str>> for RValue {
264    fn from(v: Option<&str>) -> Self {
265        RValue::Character(vec![v.map(str::to_string)])
266    }
267}
268impl From<Vec<Option<i32>>> for RValue {
269    fn from(v: Vec<Option<i32>>) -> Self {
270        RValue::Integer(v)
271    }
272}
273impl From<Vec<Option<f64>>> for RValue {
274    fn from(v: Vec<Option<f64>>) -> Self {
275        RValue::Double(
276            v.into_iter()
277                .map(|x| x.unwrap_or(crate::altrep_traits::NA_REAL))
278                .collect(),
279        )
280    }
281}
282impl From<Vec<Option<bool>>> for RValue {
283    fn from(v: Vec<Option<bool>>) -> Self {
284        RValue::Logical(v)
285    }
286}
287impl From<Vec<Option<String>>> for RValue {
288    fn from(v: Vec<Option<String>>) -> Self {
289        RValue::Character(v)
290    }
291}
292impl From<Vec<Option<&str>>> for RValue {
293    fn from(v: Vec<Option<&str>>) -> Self {
294        RValue::Character(v.into_iter().map(|s| s.map(str::to_string)).collect())
295    }
296}
297
298/// Wide-integer ladder: an `i64` (or `u32`) that fits in `i32` **and** is not
299/// `i32::MIN` (R's `NA_integer_`) becomes an `Integer`; anything else becomes a
300/// `Double`. Mirrors R's own integer→double promotion for out-of-range values.
301impl From<i64> for RValue {
302    fn from(v: i64) -> Self {
303        match i32::try_from(v) {
304            Ok(n) if n != i32::MIN => RValue::Integer(vec![Some(n)]),
305            // Out of `i32` range (or exactly `NA_integer_`) — promote to double.
306            // `i64 → f64` is lossy past 2^53, matching R's own integer overflow.
307            _ => RValue::Double(vec![v as f64]),
308        }
309    }
310}
311impl From<u32> for RValue {
312    /// Lossless widening to `i64`, then the shared ladder. A `u32` is never
313    /// `i32::MIN` as an `i32`, so only the `> i32::MAX` case falls to `Double`.
314    fn from(v: u32) -> Self {
315        RValue::from(i64::from(v))
316    }
317}
318
319// endregion
320
321// region: inspection accessors + owned extraction (#1050 decision 4, on demand)
322
323impl RValue {
324    /// The `SEXPTYPE` this value materialises to via [`IntoR`].
325    pub fn sexptype(&self) -> SEXPTYPE {
326        match self {
327            RValue::Null => SEXPTYPE::NILSXP,
328            RValue::Logical(_) => SEXPTYPE::LGLSXP,
329            RValue::Integer(_) => SEXPTYPE::INTSXP,
330            RValue::Double(_) => SEXPTYPE::REALSXP,
331            RValue::Complex(_) => SEXPTYPE::CPLXSXP,
332            RValue::Character(_) => SEXPTYPE::STRSXP,
333            RValue::Raw(_) => SEXPTYPE::RAWSXP,
334            RValue::List(_) => SEXPTYPE::VECSXP,
335        }
336    }
337
338    /// `true` if this is [`RValue::Null`].
339    pub fn is_null(&self) -> bool {
340        matches!(self, RValue::Null)
341    }
342
343    /// Wrap any `T: Debug` as a length-1 `Character` carrying its `{:?}`
344    /// rendering — the escape hatch for a value with no R-native mapping
345    /// (e.g. a Rust range). The rendering happens eagerly here, so nothing
346    /// borrows across the `Send` boundary a condition payload crosses.
347    ///
348    /// ```ignore
349    /// # use miniextendr_api::RValue;
350    /// assert_eq!(RValue::debug(0..=100).as_str(), Some("0..=100"));
351    /// ```
352    pub fn debug<T: std::fmt::Debug>(value: T) -> Self {
353        RValue::Character(vec![Some(format!("{value:?}"))])
354    }
355
356    /// The single non-NA `i32` of a length-1 `Integer`, else `None`.
357    ///
358    /// Returns `None` for the wrong variant, a length other than 1, or NA —
359    /// the borrow-friendly counterpart to `i32::try_from` when you don't need
360    /// to know *why* it failed.
361    pub fn as_i32(&self) -> Option<i32> {
362        match self {
363            RValue::Integer(v) if v.len() == 1 => v[0],
364            _ => None,
365        }
366    }
367
368    /// The single non-NA `f64` of a length-1 `Double`, else `None`.
369    ///
370    /// R's NA (the `NA_REAL` bit pattern) yields `None`, matching [`as_i32`](Self::as_i32).
371    pub fn as_f64(&self) -> Option<f64> {
372        match self {
373            RValue::Double(v) if v.len() == 1 => {
374                let x = v[0];
375                (x.to_bits() != crate::altrep_traits::NA_REAL.to_bits()).then_some(x)
376            }
377            _ => None,
378        }
379    }
380
381    /// The single non-NA `bool` of a length-1 `Logical`, else `None`.
382    pub fn as_bool(&self) -> Option<bool> {
383        match self {
384            RValue::Logical(v) if v.len() == 1 => v[0],
385            _ => None,
386        }
387    }
388
389    /// The single non-NA string of a length-1 `Character`, else `None`.
390    pub fn as_str(&self) -> Option<&str> {
391        match self {
392            RValue::Character(v) if v.len() == 1 => v[0].as_deref(),
393            _ => None,
394        }
395    }
396}
397
398/// `Type` error for a variant mismatch: the value's `SEXPTYPE` vs the expected one.
399fn wrong_type(expected: SEXPTYPE, actual: &RValue) -> SexpError {
400    SexpError::Type(SexpTypeError {
401        expected,
402        actual: actual.sexptype(),
403    })
404}
405
406/// `TryFrom<RValue>` for a scalar pulled from a length-1 Option-backed variant.
407///
408/// Fails with `Length` for a non-scalar, `Na` for NA, `Type` for the wrong
409/// variant — the failure modes a numeric `CoerceError` can't express, which is
410/// why this is `TryFrom`/`SexpError` rather than a `coerce.rs` impl.
411macro_rules! try_from_scalar_opt {
412    ($t:ty, $variant:ident, $ty:expr) => {
413        impl TryFrom<RValue> for $t {
414            type Error = SexpError;
415            fn try_from(value: RValue) -> Result<Self, SexpError> {
416                match value {
417                    RValue::$variant(xs) => {
418                        if xs.len() != 1 {
419                            return Err(SexpError::Length(SexpLengthError {
420                                expected: 1,
421                                actual: xs.len(),
422                            }));
423                        }
424                        xs.into_iter()
425                            .next()
426                            .unwrap()
427                            .ok_or(SexpError::Na(SexpNaError { sexp_type: $ty }))
428                    }
429                    other => Err(wrong_type($ty, &other)),
430                }
431            }
432        }
433    };
434}
435try_from_scalar_opt!(i32, Integer, SEXPTYPE::INTSXP);
436try_from_scalar_opt!(bool, Logical, SEXPTYPE::LGLSXP);
437try_from_scalar_opt!(String, Character, SEXPTYPE::STRSXP);
438
439// `Double` carries NA in the bit pattern, so it can't reuse the Option macro.
440impl TryFrom<RValue> for f64 {
441    type Error = SexpError;
442    fn try_from(value: RValue) -> Result<Self, SexpError> {
443        match value {
444            RValue::Double(xs) => {
445                if xs.len() != 1 {
446                    return Err(SexpError::Length(SexpLengthError {
447                        expected: 1,
448                        actual: xs.len(),
449                    }));
450                }
451                let x = xs[0];
452                if x.to_bits() == crate::altrep_traits::NA_REAL.to_bits() {
453                    Err(SexpError::Na(SexpNaError {
454                        sexp_type: SEXPTYPE::REALSXP,
455                    }))
456                } else {
457                    Ok(x)
458                }
459            }
460            other => Err(wrong_type(SEXPTYPE::REALSXP, &other)),
461        }
462    }
463}
464
465/// `TryFrom<RValue>` for the whole vector of a single variant (NA preserved).
466macro_rules! try_from_vec {
467    ($t:ty, $variant:ident, $ty:expr) => {
468        impl TryFrom<RValue> for $t {
469            type Error = SexpError;
470            fn try_from(value: RValue) -> Result<Self, SexpError> {
471                match value {
472                    RValue::$variant(xs) => Ok(xs),
473                    other => Err(wrong_type($ty, &other)),
474                }
475            }
476        }
477    };
478}
479try_from_vec!(Vec<Option<bool>>, Logical, SEXPTYPE::LGLSXP);
480try_from_vec!(Vec<Option<i32>>, Integer, SEXPTYPE::INTSXP);
481try_from_vec!(Vec<f64>, Double, SEXPTYPE::REALSXP);
482try_from_vec!(Vec<Rcomplex>, Complex, SEXPTYPE::CPLXSXP);
483try_from_vec!(Vec<Option<String>>, Character, SEXPTYPE::STRSXP);
484try_from_vec!(Vec<u8>, Raw, SEXPTYPE::RAWSXP);
485try_from_vec!(Vec<(Option<String>, RValue)>, List, SEXPTYPE::VECSXP);
486
487// endregion
488
489#[cfg(test)]
490mod tests {
491    use super::*;
492
493    // Accessors and TryFrom operate on owned `RValue` — no SEXP, no R runtime.
494
495    #[test]
496    fn as_scalar_accessors() {
497        assert_eq!(RValue::from(7i32).as_i32(), Some(7));
498        assert_eq!(RValue::from(1.5).as_f64(), Some(1.5));
499        assert_eq!(RValue::from(true).as_bool(), Some(true));
500        assert_eq!(RValue::from("hi").as_str(), Some("hi"));
501
502        // wrong variant
503        assert_eq!(RValue::from("hi").as_i32(), None);
504        // wrong length
505        assert_eq!(RValue::Integer(vec![Some(1), Some(2)]).as_i32(), None);
506        // NA
507        assert_eq!(RValue::Integer(vec![None]).as_i32(), None);
508        assert_eq!(
509            RValue::Double(vec![crate::altrep_traits::NA_REAL]).as_f64(),
510            None
511        );
512        assert!(RValue::Null.is_null());
513    }
514
515    #[test]
516    fn try_from_scalar_ok_and_errors() {
517        assert_eq!(i32::try_from(RValue::from(7i32)).unwrap(), 7);
518        assert_eq!(f64::try_from(RValue::from(2.5)).unwrap(), 2.5);
519        assert!(!bool::try_from(RValue::from(false)).unwrap());
520        assert_eq!(String::try_from(RValue::from("x")).unwrap(), "x");
521
522        // wrong variant → Type
523        assert!(matches!(
524            i32::try_from(RValue::from("x")),
525            Err(SexpError::Type(_))
526        ));
527        // wrong length → Length
528        assert!(matches!(
529            i32::try_from(RValue::Integer(vec![Some(1), Some(2)])),
530            Err(SexpError::Length(_))
531        ));
532        // NA → Na
533        assert!(matches!(
534            i32::try_from(RValue::Integer(vec![None])),
535            Err(SexpError::Na(_))
536        ));
537        assert!(matches!(
538            f64::try_from(RValue::Double(vec![crate::altrep_traits::NA_REAL])),
539            Err(SexpError::Na(_))
540        ));
541    }
542
543    #[test]
544    fn from_option_scalars() {
545        assert!(matches!(RValue::from(Some(7_i32)), RValue::Integer(v) if v == vec![Some(7)]));
546        assert!(matches!(RValue::from(None::<i32>), RValue::Integer(v) if v == vec![None]));
547        assert!(matches!(RValue::from(Some(true)), RValue::Logical(v) if v == vec![Some(true)]));
548        assert!(matches!(RValue::from(None::<bool>), RValue::Logical(v) if v == vec![None]));
549        assert!(
550            matches!(RValue::from(Some("x")), RValue::Character(v) if v == vec![Some("x".to_string())])
551        );
552        assert!(matches!(RValue::from(None::<&str>), RValue::Character(v) if v == vec![None]));
553        assert!(
554            matches!(RValue::from(Some("y".to_string())), RValue::Character(v) if v == vec![Some("y".to_string())])
555        );
556
557        // f64 has no Option variant: Some → value, None → NA_REAL bit pattern.
558        assert_eq!(RValue::from(Some(1.5_f64)).as_f64(), Some(1.5));
559        assert_eq!(RValue::from(None::<f64>).as_f64(), None);
560    }
561
562    #[test]
563    fn from_vec_option() {
564        assert!(
565            matches!(RValue::from(vec![Some(1_i32), None, Some(3)]), RValue::Integer(v) if v == vec![Some(1), None, Some(3)])
566        );
567        assert!(
568            matches!(RValue::from(vec![Some(true), None]), RValue::Logical(v) if v == vec![Some(true), None])
569        );
570        assert!(
571            matches!(RValue::from(vec![Some("a".to_string()), None]), RValue::Character(v) if v == vec![Some("a".to_string()), None])
572        );
573        assert!(
574            matches!(RValue::from(vec![Some("b"), None]), RValue::Character(v) if v == vec![Some("b".to_string()), None])
575        );
576
577        // Vec<Option<f64>> → Double with NA_REAL in the None slot.
578        let RValue::Double(v) = RValue::from(vec![Some(0.5_f64), None]) else {
579            panic!("expected Double");
580        };
581        assert_eq!(v[0], 0.5);
582        assert_eq!(v[1].to_bits(), crate::altrep_traits::NA_REAL.to_bits());
583    }
584
585    #[test]
586    fn from_wide_integer_ladder() {
587        // Fits in i32 (and not i32::MIN) → Integer.
588        assert!(matches!(RValue::from(42_i64), RValue::Integer(v) if v == vec![Some(42)]));
589        assert!(
590            matches!(RValue::from(i32::MAX as i64), RValue::Integer(v) if v == vec![Some(i32::MAX)])
591        );
592        assert!(matches!(RValue::from(7_u32), RValue::Integer(v) if v == vec![Some(7)]));
593        // i32::MIN is NA_integer_ → promote to Double rather than emit NA.
594        assert!(matches!(RValue::from(i32::MIN as i64), RValue::Double(_)));
595        // Just past i32::MAX → Double.
596        assert!(
597            matches!(RValue::from(i32::MAX as i64 + 1), RValue::Double(v) if v == vec![(i32::MAX as i64 + 1) as f64])
598        );
599        // u32::MAX exceeds i32::MAX → Double.
600        assert!(
601            matches!(RValue::from(u32::MAX), RValue::Double(v) if v == vec![f64::from(u32::MAX)])
602        );
603    }
604
605    #[test]
606    fn debug_stringifies() {
607        assert_eq!(RValue::debug(0..=100).as_str(), Some("0..=100"));
608        assert_eq!(RValue::debug(vec![1, 2]).as_str(), Some("[1, 2]"));
609    }
610
611    #[test]
612    fn try_from_whole_vector() {
613        let v: Vec<Option<i32>> = RValue::Integer(vec![Some(1), None]).try_into().unwrap();
614        assert_eq!(v, vec![Some(1), None]);
615
616        let raw: Vec<u8> = RValue::Raw(vec![1, 2, 3]).try_into().unwrap();
617        assert_eq!(raw, vec![1, 2, 3]);
618
619        // wrong variant → Type
620        let bad: Result<Vec<u8>, _> = RValue::from(1i32).try_into();
621        assert!(matches!(bad, Err(SexpError::Type(_))));
622    }
623}