Skip to main content

miniextendr_api/
dots.rs

1//! R's `...` (variadic arguments) support.
2//!
3//! Provides [`Dots`](crate::dots::Dots), the Rust representation of R's `...` parameter. The generated
4//! R wrapper captures `...` as `list(...)` and passes it to Rust.
5//!
6//! # Usage
7//!
8//! Use `...` as the parameter type — the macro handles the rest:
9//!
10//! ```ignore
11//! #[miniextendr]
12//! pub fn greet(prefix: &str, dots: ...) {
13//!     let list = dots.as_list();
14//!     // Access by name: list.get_named::<String>("key")
15//!     // Access by position: list.get_index::<i32>(0)
16//! }
17//! ```
18//!
19//! Use `name @ ...` syntax for a custom parameter name, or combine with
20//! [`typed_list!`](crate::typed_list!) for structure validation:
21//!
22//! ```ignore
23//! #[miniextendr(dots = typed_list!(x: i32, y: f64))]
24//! pub fn validated(args: ...) {
25//!     // dots_typed.x and dots_typed.y are available
26//! }
27//! ```
28
29use crate::ffi::SEXP;
30use crate::from_r::TryFromSexp;
31use crate::list::{List, ListFromSexpError};
32use crate::typed_list::{TypedList, TypedListError, TypedListSpec, validate_list};
33
34/// Rust type representing R's `...` (variadic arguments).
35///
36/// The generated R wrapper captures `...` as `list(...)` and passes it to Rust,
37/// so `Dots` holds a list SEXP. Use [`as_list`](Dots::as_list) or
38/// [`try_list`](Dots::try_list) to access elements by name or position.
39///
40/// Declare as the last parameter: `fn foo(x: i32, _dots: &Dots)`.
41/// Use `name @ ...` syntax for a custom parameter name.
42#[derive(Debug)]
43pub struct Dots {
44    // Dots is always passed to us, they need no protection.
45    // The R wrapper passes list(...), so this is typically a VECSXP.
46    /// Raw list backing this `...` capture.
47    ///
48    /// This is usually a `VECSXP` built from `list(...)` by generated wrappers.
49    pub inner: SEXP,
50}
51
52impl Dots {
53    /// Create an empty Dots (equivalent to no `...` arguments).
54    ///
55    /// This is useful when calling R functions from Rust that expect
56    /// dots arguments but you want to pass nothing.
57    ///
58    /// # Example
59    /// ```ignore
60    /// #[miniextendr]
61    /// pub fn my_constructor(x: Doubles, dots: ...) -> Robj {
62    ///     // ...
63    /// }
64    ///
65    /// // Call from Rust with empty dots:
66    /// let result = my_constructor(data, Dots::empty());
67    /// ```
68    pub fn empty() -> Self {
69        // SAFETY: R_NilValue is always valid and represents empty dots
70        Dots { inner: SEXP::nil() }
71    }
72
73    /// Convert to a [`List`] without additional validation.
74    ///
75    /// This is a zero-cost conversion since the R wrapper already passes
76    /// `list(...)` to us. Use this when you trust the input or want
77    /// maximum performance.
78    ///
79    /// # Safety Note
80    ///
81    /// This is safe because the miniextendr macro always wraps `...` in
82    /// `list(...)` on the R side. However, if you're receiving a SEXP
83    /// from another source, use [`try_list`](Dots::try_list) instead.
84    ///
85    /// # Example
86    /// ```ignore
87    /// #[miniextendr]
88    /// pub fn process_dots(dots: ...) -> i32 {
89    ///     let list = dots.as_list();
90    ///     list.len() as i32
91    /// }
92    /// ```
93    #[inline]
94    pub fn as_list(&self) -> List {
95        // SAFETY: The R wrapper always passes list(...), which is a VECSXP.
96        // If this assumption is violated, we're in undefined behavior territory
97        // anyway, so wrapping in List is the safest reasonable choice.
98        unsafe { List::from_raw(self.inner) }
99    }
100
101    /// Try to convert to a [`List`] with full validation.
102    ///
103    /// This validates that the underlying SEXP is actually a list and
104    /// checks for duplicate names. Use this when you want strict validation
105    /// or are working with untrusted input.
106    ///
107    /// # Errors
108    ///
109    /// Returns [`ListFromSexpError`] if:
110    /// - The SEXP is not a list type (VECSXP or pairlist)
111    /// - The list contains duplicate non-NA names
112    ///
113    /// # Example
114    /// ```ignore
115    /// #[miniextendr]
116    /// pub fn safe_process_dots(dots: ...) -> Result<i32, String> {
117    ///     let list = dots.try_list().map_err(|e| e.to_string())?;
118    ///     Ok(list.len() as i32)
119    /// }
120    /// ```
121    #[inline]
122    pub fn try_list(&self) -> Result<List, ListFromSexpError> {
123        List::try_from_sexp(self.inner)
124    }
125
126    /// Get the number of elements in the dots list.
127    ///
128    /// This is equivalent to `dots.as_list().len()` but avoids
129    /// creating an intermediate List wrapper.
130    #[inline]
131    pub fn len(&self) -> isize {
132        unsafe { crate::ffi::Rf_xlength(self.inner) }
133    }
134
135    /// Returns true if no arguments were passed to `...`.
136    #[inline]
137    pub fn is_empty(&self) -> bool {
138        self.len() == 0
139    }
140
141    /// Validate the dots against a typed list specification.
142    ///
143    /// This provides structured validation with clear error messages for
144    /// functions that expect specific named arguments via `...`.
145    ///
146    /// # Example
147    ///
148    /// ```ignore
149    /// use miniextendr_api::typed_list::{TypedListSpec, TypedEntry, TypeSpec};
150    ///
151    /// #[miniextendr]
152    /// pub fn process_args(dots: ...) -> Result<i32, String> {
153    ///     let spec = TypedListSpec::new(vec![
154    ///         TypedEntry::required("alpha", TypeSpec::Numeric(Some(4))),
155    ///         TypedEntry::optional("beta", TypeSpec::List(None)),
156    ///     ]);
157    ///
158    ///     let validated = dots.typed(spec).map_err(|e| e.to_string())?;
159    ///     let alpha: Vec<f64> = validated.get("alpha").map_err(|e| e.to_string())?;
160    ///     Ok(alpha.len() as i32)
161    /// }
162    /// ```
163    ///
164    /// # Errors
165    ///
166    /// Returns [`TypedListError`] if:
167    /// - The dots are not a valid list
168    /// - A required field is missing
169    /// - A field has the wrong type or length
170    /// - Extra fields exist when `allow_extra = false`
171    #[inline]
172    pub fn typed(&self, spec: TypedListSpec) -> Result<TypedList, TypedListError> {
173        let list = self.try_list().map_err(TypedListError::NotList)?;
174        validate_list(list, &spec)
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn dots_empty_creates_nil() {
184        let dots = Dots::empty();
185        assert_eq!(dots.inner, SEXP::nil());
186    }
187}