Skip to main content

miniextendr_api/
dots.rs

1//! R's `...` (variadic arguments) support.
2//!
3//! Provides [`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::from_r::{SexpTypeError, TryFromSexp};
30use crate::list::List;
31use crate::typed_list::{TypedList, TypedListError, TypedListSpec, validate_list};
32use crate::{SEXP, SexpExt};
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 it as a `&Dots` parameter at any position, one per function:
41/// `fn foo(x: i32, rest: &Dots, flag: bool)` becomes the R formals
42/// `x, ..., flag`, and parameters after it are matched by name only in R.
43/// Rust `...` (or `name: ...`) is the same as a trailing `&Dots` parameter.
44#[derive(#[automatically_derived]
impl ::core::fmt::Debug for Dots {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field1_finish(f, "Dots", "inner",
            &&self.inner)
    }
}Debug)]
45pub struct Dots {
46    // Dots is always passed to us, they need no protection.
47    // The R wrapper passes list(...), so this is typically a VECSXP.
48    /// Raw list backing this `...` capture.
49    ///
50    /// This is usually a `VECSXP` built from `list(...)` by generated wrappers.
51    pub inner: SEXP,
52}
53
54impl Dots {
55    /// Create an empty Dots (equivalent to no `...` arguments).
56    ///
57    /// This is useful when calling R functions from Rust that expect
58    /// dots arguments but you want to pass nothing.
59    ///
60    /// # Example
61    /// ```ignore
62    /// #[miniextendr]
63    /// pub fn my_constructor(x: Doubles, dots: ...) -> Robj {
64    ///     // ...
65    /// }
66    ///
67    /// // Call from Rust with empty dots:
68    /// let result = my_constructor(data, Dots::empty());
69    /// ```
70    pub fn empty() -> Self {
71        // SAFETY: R_NilValue is always valid and represents empty dots
72        Dots { inner: SEXP::nil() }
73    }
74
75    /// Convert to a [`List`] without additional validation.
76    ///
77    /// This is a zero-cost conversion since the R wrapper already passes
78    /// `list(...)` to us. Use this when you trust the input or want
79    /// maximum performance.
80    ///
81    /// # Safety Note
82    ///
83    /// This is safe because the miniextendr macro always wraps `...` in
84    /// `list(...)` on the R side. However, if you're receiving a SEXP
85    /// from another source, use [`try_list`](Dots::try_list) instead.
86    ///
87    /// # Example
88    /// ```ignore
89    /// #[miniextendr]
90    /// pub fn process_dots(dots: ...) -> i32 {
91    ///     let list = dots.as_list();
92    ///     list.len() as i32
93    /// }
94    /// ```
95    #[inline]
96    pub fn as_list(&self) -> List {
97        // SAFETY: The R wrapper always passes list(...), which is a VECSXP.
98        // If this assumption is violated, we're in undefined behavior territory
99        // anyway, so wrapping in List is the safest reasonable choice.
100        unsafe { List::from_raw(self.inner) }
101    }
102
103    /// Convert to a [`List`], checking that the underlying SEXP is a list.
104    ///
105    /// Accepts a list (`VECSXP`) only, as `TryFromSexp for List` does; a
106    /// pairlist is refused (#1866). Use this when `inner` may not come from a
107    /// generated wrapper's `list(...)`.
108    ///
109    /// Names are not checked: R lets the caller repeat a name in `...`
110    /// (`f(a = 1, a = 2)`). To refuse that, check
111    /// [`List::first_duplicate_name`]; a [`typed`](Dots::typed) spec refuses
112    /// repeated names itself.
113    ///
114    /// # Errors
115    ///
116    /// Returns [`SexpTypeError`] if the SEXP is not a list.
117    ///
118    /// # Example
119    /// ```ignore
120    /// #[miniextendr]
121    /// pub fn count_named_dots(dots: ...) -> Result<i32, String> {
122    ///     let list = dots.try_list().map_err(|e| e.to_string())?;
123    ///     if let Some(name) = list.first_duplicate_name() {
124    ///         return Err(format!("argument {name:?} was given more than once"));
125    ///     }
126    ///     Ok(list.len() as i32)
127    /// }
128    /// ```
129    #[inline]
130    pub fn try_list(&self) -> Result<List, SexpTypeError> {
131        List::try_from_sexp(self.inner)
132    }
133
134    /// Get the number of elements in the dots list.
135    ///
136    /// This is equivalent to `dots.as_list().len()` but avoids
137    /// creating an intermediate List wrapper.
138    #[inline]
139    pub fn len(&self) -> isize {
140        self.inner.xlength()
141    }
142
143    /// Returns true if no arguments were passed to `...`.
144    #[inline]
145    pub fn is_empty(&self) -> bool {
146        self.len() == 0
147    }
148
149    /// Validate the dots against a typed list specification.
150    ///
151    /// This provides structured validation with clear error messages for
152    /// functions that expect specific named arguments via `...`.
153    ///
154    /// # Example
155    ///
156    /// ```ignore
157    /// use miniextendr_api::typed_list::{TypedListSpec, TypedEntry, TypeSpec};
158    ///
159    /// #[miniextendr]
160    /// pub fn process_args(dots: ...) -> Result<i32, String> {
161    ///     let spec = TypedListSpec::new(vec![
162    ///         TypedEntry::required("alpha", TypeSpec::Numeric(Some(4))),
163    ///         TypedEntry::optional("beta", TypeSpec::List(None)),
164    ///     ]);
165    ///
166    ///     let validated = dots.typed(spec).map_err(|e| e.to_string())?;
167    ///     let alpha: Vec<f64> = validated.get("alpha").map_err(|e| e.to_string())?;
168    ///     Ok(alpha.len() as i32)
169    /// }
170    /// ```
171    ///
172    /// # Errors
173    ///
174    /// Returns [`TypedListError`] if:
175    /// - The dots are not a valid list
176    /// - A required field is missing
177    /// - A field has the wrong type or length
178    /// - Extra fields exist when `allow_extra = false`
179    #[inline]
180    pub fn typed(&self, spec: TypedListSpec) -> Result<TypedList, TypedListError> {
181        let list = self.try_list().map_err(TypedListError::NotList)?;
182        validate_list(list, &spec)
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn dots_empty_creates_nil() {
192        let dots = Dots::empty();
193        assert_eq!(dots.inner, SEXP::nil());
194    }
195}