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}