Skip to main content

miniextendr_macros/
dots_policy.rs

1//! `#[miniextendr(dots = ...)]`: `typed_list!(...)`, or what the function's
2//! `...` is for (#2157).
3//!
4//! An S3 method takes `...` because its generic does, and R hands it every
5//! argument the method has no formal for. A method that reads none of them
6//! loses a misspelt one without a word: `confint(fit, levle = 0.9)` returns
7//! the 95% interval. The role says which arguments `...` takes, and the
8//! generated R wrapper refuses the others before it evaluates anything:
9//!
10//! | `dots =` | `...` takes | the Rust function |
11//! |---|---|---|
12//! | `empty` | nothing | reads no dots |
13//! | `empty(allow = ["optional"])` | the listed names | ignores them |
14//! | `forward(allow = ["digits"])` | the listed names | passes them on to another R function |
15//! | `data` | anything, unchecked | reads them |
16//!
17//! An argument the function reads is a parameter, not a name in `...`: R then
18//! matches it by name or by position, as for any function.
19//!
20//! An unnamed argument in `...` is one of the generic's arguments given by
21//! position (#2185). Under `forward` it passes: the function the dots go on to
22//! matches it by position itself. Under `empty` nothing reads it, so only the
23//! count matters: up to the number of allowed names not given by name are
24//! taken, and the rest refused.
25//!
26//! `class = "..."` (or `class = ["...", ...]`) puts the package's classes in
27//! front of the refusal's own.
28//!
29//! The check is one statement, `if (...length()) .miniextendr_check_dots(...)`
30//! ([`DotsPolicy::r_check`]), after the lifecycle notice and before the
31//! argument checks, so a misspelt argument is reported before a missing one
32//! (`predict(fit, nwedata = nd)` names `nwedata`, not the missing `newdata`).
33//! It reads `...length()` and `...names()` only, so no argument is forced. The
34//! helper is in the wrappers preamble (`miniextendr_api::registry`): one
35//! classed argument error for every refused argument, with the formal or
36//! listed name each one most likely meant.
37//!
38//! Generated methods that have no attribute make the `empty` check too
39//! ([`DotsPolicy::empty`], #2174): the `[[`, `as.list()` and `[[<-` field
40//! methods of `s3(r_data_accessors)` on a bare pointer, with the type's
41//! `field_error` classes, and an env class's `[[`.
42
43use proc_macro2::{Span, TokenStream};
44use syn::spanned::Spanned;
45
46/// What the function's `...` takes (see the [module docs](self)).
47#[derive(#[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for DotsRole { }
#[automatically_derived]
impl ::core::clone::Clone for DotsRole {
    #[inline]
    fn clone(&self) -> Self { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for DotsRole { }Copy, #[automatically_derived]
impl ::core::fmt::Debug for DotsRole {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::write_str(f,
            match self {
                DotsRole::Empty => "Empty",
                DotsRole::Forward => "Forward",
                DotsRole::Data => "Data",
            })
    }
}Debug, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for DotsRole { }
#[automatically_derived]
impl ::core::cmp::PartialEq for DotsRole {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        ::core::intrinsics::discriminant_value(self) ==
            ::core::intrinsics::discriminant_value(other)
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for DotsRole { }Eq)]
48pub(crate) enum DotsRole {
49    /// `empty` / `empty(allow = [..])`: nothing, or names the function ignores.
50    Empty,
51    /// `forward(allow = [..])`: the names the function passes on.
52    Forward,
53    /// `data`: anything; no check.
54    Data,
55}
56
57impl DotsRole {
58    /// The spelling in the attribute, for diagnostics.
59    pub(crate) fn name(self) -> &'static str {
60        match self {
61            Self::Empty => "empty",
62            Self::Forward => "forward",
63            Self::Data => "data",
64        }
65    }
66}
67
68/// A parsed `dots = empty | empty(..) | forward(..) | data`.
69#[derive(#[automatically_derived]
impl ::core::clone::Clone for DotsPolicy {
    #[inline]
    fn clone(&self) -> Self {
        DotsPolicy {
            role: ::core::clone::Clone::clone(&self.role),
            allow: ::core::clone::Clone::clone(&self.allow),
            class: ::core::clone::Clone::clone(&self.class),
            class_expr: ::core::clone::Clone::clone(&self.class_expr),
            span: ::core::clone::Clone::clone(&self.span),
        }
    }
}Clone, #[automatically_derived]
impl ::core::fmt::Debug for DotsPolicy {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field5_finish(f, "DotsPolicy",
            "role", &self.role, "allow", &self.allow, "class", &self.class,
            "class_expr", &self.class_expr, "span", &&self.span)
    }
}Debug)]
70pub(crate) struct DotsPolicy {
71    pub(crate) role: DotsRole,
72    /// The names `...` takes, in order.
73    pub(crate) allow: Vec<String>,
74    /// The package's classes for the refusal, in front of its own.
75    pub(crate) class: Vec<String>,
76    /// The package's classes as an R expression the wrapper evaluates, in
77    /// place of `class`: the field methods pass the type's `field_error`
78    /// classes, a variable of the derive's helpers (#2174). The attribute
79    /// never sets it.
80    pub(crate) class_expr: Option<String>,
81    /// The attribute value, for diagnostics about the signature.
82    pub(crate) span: Span,
83}
84
85/// The value of `dots = ...`.
86pub(crate) enum DotsAttr {
87    /// `typed_list!(...)`, as written.
88    TypedList(TokenStream),
89    /// A role for `...`.
90    Policy(DotsPolicy),
91}
92
93const EXPECTED: &str = "dots expects `typed_list!(...)`, `empty`, `empty(allow = [\"name\", ...])`, \
94                        `forward(allow = [\"name\", ...])` or `data`";
95
96/// Parse the value of `dots = ...`.
97pub(crate) fn parse_dots_attr(value: &syn::Expr) -> syn::Result<DotsAttr> {
98    match value {
99        syn::Expr::Macro(expr_macro) if expr_macro.mac.path.is_ident("typed_list") => {
100            Ok(DotsAttr::TypedList({
    let mut _s = ::quote::__private::TokenStream::new();
    ::quote::ToTokens::to_tokens(&expr_macro, &mut _s);
    _s
}quote::quote!(#expr_macro)))
101        }
102        syn::Expr::Path(path) => match role_of(&path.path) {
103            Some(DotsRole::Forward) => Err(syn::Error::new_spanned(
104                path,
105                "`forward` passes the names it lists: `forward(allow = [\"name\", ...])`; \
106                 `data` passes every argument",
107            )),
108            Some(role) => Ok(DotsAttr::Policy(DotsPolicy {
109                role,
110                allow: Vec::new(),
111                class: Vec::new(),
112                class_expr: None,
113                span: value.span(),
114            })),
115            None => Err(syn::Error::new_spanned(path, EXPECTED)),
116        },
117        syn::Expr::Call(call) => {
118            let syn::Expr::Path(func) = call.func.as_ref() else {
119                return Err(syn::Error::new_spanned(&call.func, EXPECTED));
120            };
121            let Some(role) = role_of(&func.path) else {
122                return Err(syn::Error::new_spanned(func, EXPECTED));
123            };
124            if role == DotsRole::Data {
125                return Err(syn::Error::new_spanned(
126                    call,
127                    "`data` takes no options: it passes every argument unchecked",
128                ));
129            }
130            let (allow, class) = parse_options(call)?;
131            if role == DotsRole::Forward && allow.is_empty() {
132                return Err(syn::Error::new_spanned(
133                    call,
134                    "`forward` passes the names it lists: `forward(allow = [\"name\", ...])`; \
135                     `data` passes every argument",
136                ));
137            }
138            Ok(DotsAttr::Policy(DotsPolicy {
139                role,
140                allow,
141                class,
142                class_expr: None,
143                span: value.span(),
144            }))
145        }
146        _ => Err(syn::Error::new_spanned(value, EXPECTED)),
147    }
148}
149
150fn role_of(path: &syn::Path) -> Option<DotsRole> {
151    let ident = path.get_ident()?;
152    match ident.to_string().as_str() {
153        "empty" => Some(DotsRole::Empty),
154        "forward" => Some(DotsRole::Forward),
155        "data" => Some(DotsRole::Data),
156        _ => None,
157    }
158}
159
160/// The `allow = ..` and `class = ..` options of `empty(..)` / `forward(..)`,
161/// each at most once. Every problem is reported at once.
162fn parse_options(call: &syn::ExprCall) -> syn::Result<(Vec<String>, Vec<String>)> {
163    let mut errors: Option<syn::Error> = None;
164    let mut push = |err: syn::Error| match &mut errors {
165        Some(errors) => errors.combine(err),
166        None => errors = Some(err),
167    };
168    let mut allow: Option<Vec<String>> = None;
169    let mut class: Option<Vec<String>> = None;
170    for arg in &call.args {
171        let syn::Expr::Assign(assign) = arg else {
172            push(syn::Error::new_spanned(
173                arg,
174                "expected `allow = [\"name\", ...]` or `class = \"...\"`",
175            ));
176            continue;
177        };
178        let key = match assign.left.as_ref() {
179            syn::Expr::Path(p) => p.path.get_ident().map(ToString::to_string),
180            _ => None,
181        };
182        let slot = match key.as_deref() {
183            Some("allow") => &mut allow,
184            Some("class") => &mut class,
185            _ => {
186                push(syn::Error::new_spanned(
187                    &assign.left,
188                    "unknown option; expected `allow = [\"name\", ...]` or `class = \"...\"`",
189                ));
190                continue;
191            }
192        };
193        if slot.is_some() {
194            push(syn::Error::new_spanned(
195                &assign.left,
196                ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("`{0}` is given twice",
                key.unwrap_or_default()))
    })format!("`{}` is given twice", key.unwrap_or_default()),
197            ));
198            continue;
199        }
200        match string_list(&assign.right, key.as_deref() == Some("allow")) {
201            Ok(values) => *slot = Some(values),
202            Err(err) => push(err),
203        }
204    }
205    match errors {
206        Some(errors) => Err(errors),
207        None => Ok((allow.unwrap_or_default(), class.unwrap_or_default())),
208    }
209}
210
211/// A string, or a non-empty array of distinct strings, none empty or holding
212/// a NUL. Names (`allow`) can't be `...` either.
213fn string_list(expr: &syn::Expr, names: bool) -> syn::Result<Vec<String>> {
214    let items: Vec<&syn::Expr> = match expr {
215        syn::Expr::Array(array) if !array.elems.is_empty() => array.elems.iter().collect(),
216        syn::Expr::Array(array) => {
217            return Err(syn::Error::new_spanned(
218                array,
219                "the list is empty; give at least one string",
220            ));
221        }
222        other => ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [other]))vec![other],
223    };
224    let mut errors: Option<syn::Error> = None;
225    let mut push = |err: syn::Error| match &mut errors {
226        Some(errors) => errors.combine(err),
227        None => errors = Some(err),
228    };
229    let mut values: Vec<String> = Vec::with_capacity(items.len());
230    for item in items {
231        let syn::Expr::Lit(syn::ExprLit {
232            lit: syn::Lit::Str(lit),
233            ..
234        }) = item
235        else {
236            push(syn::Error::new_spanned(item, "expected a string literal"));
237            continue;
238        };
239        let value = lit.value();
240        if value.is_empty() {
241            push(syn::Error::new_spanned(lit, "the string is empty"));
242        } else if value.contains('\0') {
243            push(syn::Error::new_spanned(
244                lit,
245                "an R string cannot hold a NUL",
246            ));
247        } else if names && value == "..." {
248            push(syn::Error::new_spanned(
249                lit,
250                "`...` is not an argument name; list the names `...` takes",
251            ));
252        } else if values.contains(&value) {
253            push(syn::Error::new_spanned(
254                lit,
255                ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("\"{0}\" is listed twice", value))
    })format!("\"{value}\" is listed twice"),
256            ));
257        } else {
258            values.push(value);
259        }
260    }
261    match errors {
262        Some(errors) => Err(errors),
263        None => Ok(values),
264    }
265}
266
267impl DotsPolicy {
268    /// `empty`, for generated code that has no attribute, with the refusal's
269    /// classes as an R expression ([`class_expr`](Self::class_expr)): the
270    /// field methods of `s3(r_data_accessors)` and an env class's `[[`
271    /// (#2174).
272    pub(crate) fn empty(class_expr: Option<String>) -> Self {
273        DotsPolicy {
274            role: DotsRole::Empty,
275            allow: Vec::new(),
276            class: Vec::new(),
277            class_expr,
278            span: Span::call_site(),
279        }
280    }
281
282    /// Refuse a role the signature can't have, spanned on the attribute:
283    /// `forward` and `data` hand the arguments to the Rust function, so it
284    /// takes `...` (`_: ...`, `&Dots` or `LazyDots`); `wrapper_dots` says
285    /// whether the R wrapper has a `...` formal for `empty` to check.
286    pub(crate) fn check_signature(&self, rust_dots: bool, wrapper_dots: bool) -> syn::Result<()> {
287        match self.role {
288            DotsRole::Forward | DotsRole::Data if !rust_dots => Err(syn::Error::new(
289                self.span,
290                ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("`dots = {0}` passes arguments to the function, which takes none: add a `...` parameter (`_: ...`, `&Dots` or `LazyDots`)",
                self.role.name()))
    })format!(
291                    "`dots = {}` passes arguments to the function, which takes none: add a \
292                     `...` parameter (`_: ...`, `&Dots` or `LazyDots`)",
293                    self.role.name()
294                ),
295            )),
296            DotsRole::Empty if !wrapper_dots => Err(syn::Error::new(
297                self.span,
298                "`dots = empty` checks the R wrapper's `...`, and this one has none: R already \
299                 refuses an argument the function has no formal for",
300            )),
301            _ => Ok(()),
302        }
303    }
304
305    /// The wrapper statement that refuses what `...` doesn't take, for a
306    /// wrapper with `formals` (an R formals list); `call` is the wrapper's
307    /// [`r_check_call`](crate::r_wrapper_builder::CallAttribution::r_check_call).
308    /// `None` for `data`, and for a wrapper without a `...` formal, which R
309    /// keeps from receiving any extra argument.
310    ///
311    /// The formals other than `...` and the generated `.call`, `.mx_self` and
312    /// `.ptr` are what the helper suggests from, after the allowed names.
313    pub(crate) fn r_check(&self, formals: &str, call: Option<&str>) -> Option<String> {
314        if self.role == DotsRole::Data {
315            return None;
316        }
317        let names: Vec<&str> = crate::roxygen::split_r_formals(formals)
318            .into_iter()
319            .map(crate::roxygen::formal_name)
320            .filter(|name| !name.is_empty())
321            .collect();
322        if !names.contains(&"...") {
323            return None;
324        }
325        let formals: Vec<String> = names
326            .into_iter()
327            .filter(|name| !#[allow(non_exhaustive_omitted_patterns)] match *name {
    "..." | ".call" | ".mx_self" | ".ptr" => true,
    _ => false,
}matches!(*name, "..." | ".call" | ".mx_self" | ".ptr"))
328            .map(str::to_string)
329            .collect();
330        let mut args = ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        ["...names()".to_string(), "...length()".to_string(),
                r_chr(&formals)]))vec![
331            "...names()".to_string(),
332            "...length()".to_string(),
333            r_chr(&formals),
334        ];
335        if !self.allow.is_empty() {
336            args.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("allow = {0}", r_chr(&self.allow)))
    })format!("allow = {}", r_chr(&self.allow)));
337        }
338        if self.role == DotsRole::Forward {
339            args.push("forward = TRUE".to_string());
340        }
341        if let Some(class) = &self.class_expr {
342            args.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("class = {0}", class))
    })format!("class = {class}"));
343        } else if !self.class.is_empty() {
344            args.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("class = {0}", r_chr(&self.class)))
    })format!("class = {}", r_chr(&self.class)));
345        }
346        if let Some(call) = call {
347            args.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("call = {0}", call))
    })format!("call = {call}"));
348        }
349        Some(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("if (...length()) .miniextendr_check_dots({0})",
                args.join(", ")))
    })format!(
350            "if (...length()) .miniextendr_check_dots({})",
351            args.join(", ")
352        ))
353    }
354}
355
356/// An R character vector literal: `character()`, `"a"` or `c("a", "b")`.
357fn r_chr(values: &[String]) -> String {
358    let quoted: Vec<String> = values
359        .iter()
360        .map(|v| ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("\"{0}\"",
                crate::naming::r_string_escape(v)))
    })format!("\"{}\"", crate::naming::r_string_escape(v)))
361        .collect();
362    match quoted.as_slice() {
363        [] => "character()".to_string(),
364        [single] => single.clone(),
365        _ => ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("c({0})", quoted.join(", ")))
    })format!("c({})", quoted.join(", ")),
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372
373    fn policy(value: syn::Expr) -> DotsPolicy {
374        match parse_dots_attr(&value) {
375            Ok(DotsAttr::Policy(policy)) => policy,
376            Ok(DotsAttr::TypedList(_)) => panic!("parsed as typed_list!"),
377            Err(err) => panic!("{err}"),
378        }
379    }
380
381    fn error(value: syn::Expr) -> String {
382        match parse_dots_attr(&value) {
383            Err(err) => err
384                .into_iter()
385                .map(|e| e.to_string())
386                .collect::<Vec<_>>()
387                .join(" | "),
388            Ok(_) => panic!("parsed"),
389        }
390    }
391
392    #[test]
393    fn spellings() {
394        let p = policy(syn::parse_quote!(empty));
395        assert_eq!(
396            (p.role, p.allow.len(), p.class.len()),
397            (DotsRole::Empty, 0, 0)
398        );
399        let p = policy(syn::parse_quote!(empty()));
400        assert_eq!((p.role, p.allow.len()), (DotsRole::Empty, 0));
401        let p = policy(syn::parse_quote!(empty(allow = ["row.names", "optional"])));
402        assert_eq!(p.allow, ["row.names", "optional"]);
403        let p = policy(syn::parse_quote!(forward(
404            allow = "digits",
405            class = "pkg_unused"
406        )));
407        assert_eq!(
408            (p.role, p.allow, p.class),
409            (
410                DotsRole::Forward,
411                vec!["digits".into()],
412                vec!["pkg_unused".into()]
413            )
414        );
415        let p = policy(syn::parse_quote!(data));
416        assert_eq!(p.role, DotsRole::Data);
417        assert!(matches!(
418            parse_dots_attr(&syn::parse_quote!(typed_list!(x => numeric()))),
419            Ok(DotsAttr::TypedList(_))
420        ));
421    }
422
423    #[test]
424    fn refusals_are_batched() {
425        let msg = error(syn::parse_quote!(empty(
426            allow = ["a", "", "a", 1, "..."],
427            class = []
428        )));
429        assert!(msg.contains("the string is empty"), "{msg}");
430        assert!(msg.contains("\"a\" is listed twice"), "{msg}");
431        assert!(msg.contains("expected a string literal"), "{msg}");
432        assert!(msg.contains("`...` is not an argument name"), "{msg}");
433        assert!(msg.contains("the list is empty"), "{msg}");
434        assert!(error(syn::parse_quote!(forward)).contains("`forward` passes the names it lists"));
435        assert!(
436            error(syn::parse_quote!(forward())).contains("`forward` passes the names it lists")
437        );
438        assert!(error(syn::parse_quote!(data(allow = "x"))).contains("`data` takes no options"));
439        assert!(error(syn::parse_quote!(empty(allow = "a", allow = "b"))).contains("given twice"));
440        assert!(error(syn::parse_quote!(empty(keep = "a"))).contains("unknown option"));
441        assert!(error(syn::parse_quote!(nothing)).contains("dots expects"));
442        assert!(error(syn::parse_quote!(other_list!(x))).contains("dots expects"));
443    }
444
445    #[test]
446    fn r_check_reads_names_only() {
447        let p = policy(syn::parse_quote!(empty));
448        assert_eq!(
449            p.r_check("object, parm, level = 0.95, ...", None).unwrap(),
450            "if (...length()) .miniextendr_check_dots(...names(), ...length(), \
451             c(\"object\", \"parm\", \"level\"))"
452        );
453        let p = policy(syn::parse_quote!(empty(
454            allow = ["optional"],
455            class = ["a\"b"]
456        )));
457        assert_eq!(
458            p.r_check(".mx_self, x, ..., .call = NULL", Some("environment()"))
459                .unwrap(),
460            "if (...length()) .miniextendr_check_dots(...names(), ...length(), \"x\", \
461             allow = \"optional\", class = \"a\\\"b\", call = environment())"
462        );
463        // No `...` formal: R refuses extra arguments itself.
464        assert!(p.r_check("x, y", None).is_none());
465        // `forward` lets unnamed arguments through.
466        assert_eq!(
467            policy(syn::parse_quote!(forward(allow = ["digits", "nsmall"])))
468                .r_check("x, ...", None)
469                .unwrap(),
470            "if (...length()) .miniextendr_check_dots(...names(), ...length(), \"x\", \
471             allow = c(\"digits\", \"nsmall\"), forward = TRUE)"
472        );
473        // `data` checks nothing.
474        assert!(
475            policy(syn::parse_quote!(data))
476                .r_check("x, ...", None)
477                .is_none()
478        );
479        // A default holding a comma doesn't split a formal.
480        assert_eq!(
481            policy(syn::parse_quote!(empty))
482                .r_check("x, mode = c(\"a\", \"b\"), ...", None)
483                .unwrap(),
484            "if (...length()) .miniextendr_check_dots(...names(), ...length(), c(\"x\", \"mode\"))"
485        );
486    }
487
488    #[test]
489    fn r_check_takes_classes_as_an_r_expression() {
490        // The field methods pass the type's `field_error` classes, a variable
491        // of the derive's helpers, unquoted (#2174).
492        let p = DotsPolicy::empty(Some(".rdata_error_class_T".to_string()));
493        assert_eq!(
494            p.r_check("x, i, ..., exact = TRUE", Some("environment()"))
495                .unwrap(),
496            "if (...length()) .miniextendr_check_dots(...names(), ...length(), \
497             c(\"x\", \"i\", \"exact\"), class = .rdata_error_class_T, call = environment())"
498        );
499        assert_eq!(
500            DotsPolicy::empty(None).r_check("x, ...", None).unwrap(),
501            "if (...length()) .miniextendr_check_dots(...names(), ...length(), \"x\")"
502        );
503    }
504}