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(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}