Skip to main content

miniextendr_macros/
miniextendr_fn.rs

1//! Function signature parsing for `#[miniextendr]`.
2//!
3//! This module handles parsing and normalizing Rust function signatures for the
4//! `#[miniextendr]` attribute macro. It provides:
5//!
6//! - [`MiniextendrFunctionParsed`]: Parsed function with normalization and codegen helpers
7//! - [`MiniextendrFnAttrs`]: Parsed `#[miniextendr(...)]` attribute options
8//! - [`CoercionMapping`]: Type coercion analysis for automatic R→Rust conversion
9
10use crate::r_wrapper_const_ident_for;
11
12// region: Coercion analysis
13
14/// Result of coercion analysis for a type.
15/// Contains the R native type to extract from SEXP and the target type to coerce to.
16pub(crate) enum CoercionMapping {
17    /// Scalar coercion: extract R native type, coerce to target.
18    Scalar {
19        /// The R-native scalar type to extract from the SEXP (e.g., `i32` for R integers,
20        /// `f64` for R reals). This is the type that R stores internally.
21        r_native: proc_macro2::TokenStream,
22        /// The Rust target type to coerce into (e.g., `u16`, `bool`, `f32`).
23        target: proc_macro2::TokenStream,
24    },
25    /// Vec coercion: extract R native slice, coerce element-wise to `Vec<target>`.
26    Vec {
27        /// The R-native element type of the source slice (e.g., `i32` for integer vectors,
28        /// `f64` for real vectors).
29        r_native_elem: proc_macro2::TokenStream,
30        /// The Rust target element type for the resulting `Vec` (e.g., `u16`, `bool`, `f32`).
31        target_elem: proc_macro2::TokenStream,
32    },
33}
34
35impl CoercionMapping {
36    /// Determines the coercion mapping for a Rust type, if it needs coercion from
37    /// an R-native type.
38    ///
39    /// Returns `None` if the type is already R-native (`i32`, `f64`, `String`, etc.)
40    /// or is not a recognized coercible type.
41    ///
42    /// # Recognized coercions
43    ///
44    /// - **Scalar integer-like** (`u16`, `i16`, `i8`, `u32`, `u64`, `i64`, `isize`, `usize`):
45    ///   coerced from `i32` (R's native integer type).
46    /// - **Scalar `bool`**: coerced from `i32` (R's logical vectors use `i32` internally).
47    /// - **Scalar `f32`**: coerced from `f64` (R's native real type).
48    /// - **`Vec<T>`** variants: element-wise coercion from the corresponding R-native slice type.
49    pub(crate) fn from_type(ty: &syn::Type) -> Option<Self> {
50        match ty {
51            syn::Type::Path(type_path) => {
52                let seg = type_path.path.segments.last()?;
53                let type_name = seg.ident.to_string();
54
55                // Check for Vec<T> types
56                if type_name == "Vec" {
57                    if let syn::PathArguments::AngleBracketed(args) = &seg.arguments
58                        && let Some(syn::GenericArgument::Type(syn::Type::Path(inner_path))) =
59                            args.args.first()
60                    {
61                        let inner_name = inner_path.path.segments.last()?.ident.to_string();
62                        return match inner_name.as_str() {
63                            // Vec<integer-like> from &[i32]
64                            "u16" | "i16" | "i8" | "u32" | "u64" | "i64" | "isize" | "usize" => {
65                                let target_elem: proc_macro2::TokenStream =
66                                    inner_name.parse().ok()?;
67                                Some(Self::Vec {
68                                    r_native_elem: quote::quote!(i32),
69                                    target_elem,
70                                })
71                            }
72                            // Vec<bool> from &[i32] (R logical vectors use i32)
73                            "bool" => Some(Self::Vec {
74                                r_native_elem: quote::quote!(i32),
75                                target_elem: quote::quote!(bool),
76                            }),
77                            // Vec<f32> from &[f64]
78                            "f32" => Some(Self::Vec {
79                                r_native_elem: quote::quote!(f64),
80                                target_elem: quote::quote!(f32),
81                            }),
82                            _ => None,
83                        };
84                    }
85                    return None;
86                }
87
88                // Check for scalar types
89                match type_name.as_str() {
90                    // Integer-like types from i32
91                    "u16" | "i16" | "i8" | "u32" | "u64" | "i64" | "isize" | "usize" => {
92                        let target: proc_macro2::TokenStream = type_name.parse().ok()?;
93                        Some(Self::Scalar {
94                            r_native: quote::quote!(i32),
95                            target,
96                        })
97                    }
98                    // bool from i32 (R logical vectors use i32 internally)
99                    "bool" => Some(Self::Scalar {
100                        r_native: quote::quote!(i32),
101                        target: quote::quote!(bool),
102                    }),
103                    // f32 from f64
104                    "f32" => Some(Self::Scalar {
105                        r_native: quote::quote!(f64),
106                        target: quote::quote!(f32),
107                    }),
108                    // R-native types or unknown - no coercion
109                    _ => None,
110                }
111            }
112            _ => None,
113        }
114    }
115}
116
117// endregion
118
119// region: Type inspection helpers
120
121/// Check if a type path ends with the given identifier (e.g., "Dots", "Missing").
122///
123/// Handles fully-qualified paths like `miniextendr_api::dots::Dots` as well as
124/// bare `Dots`.
125fn type_ends_with(ty: &syn::Type, name: &str) -> bool {
126    match ty {
127        syn::Type::Path(tp) => tp
128            .path
129            .segments
130            .last()
131            .map(|s| s.ident == name)
132            .unwrap_or(false),
133        syn::Type::Reference(r) => type_ends_with(&r.elem, name),
134        _ => false,
135    }
136}
137
138/// Check if a type is `Dots` or `&Dots` (the variadic `...` parameter type).
139pub(crate) fn is_dots_type(ty: &syn::Type) -> bool {
140    type_ends_with(ty, "Dots")
141}
142
143/// Result of normalizing Rust variadic syntax (`...`) into an explicit `&Dots`
144/// parameter.
145#[derive(Debug, Clone)]
146pub(crate) struct VariadicDots {
147    /// Whether the original signature used Rust variadic syntax.
148    pub has_dots: bool,
149    /// User-provided variadic identifier, e.g. `dots` in `dots: ...`.
150    pub named_dots: Option<syn::Ident>,
151}
152
153/// Replace Rust variadic syntax with a trailing `&miniextendr_api::dots::Dots`
154/// parameter so downstream codegen never emits a non-extern variadic Rust fn.
155pub(crate) fn rewrite_variadic_dots(sig: &mut syn::Signature) -> syn::Result<VariadicDots> {
156    use syn::spanned::Spanned;
157
158    let has_dots = sig.variadic.is_some();
159    let named_dots = if has_dots {
160        let dots = sig.variadic.as_ref().unwrap();
161        if let Some(named_dots) = dots.pat.as_ref() {
162            if let syn::Pat::Ident(named_dots_ident) = named_dots.0.as_ref() {
163                Some(named_dots_ident.ident.clone())
164            } else {
165                return Err(syn::Error::new(
166                    named_dots.0.span(),
167                    "variadic pattern must be a simple identifier (e.g. `dots: ...`) or unnamed `...`",
168                ));
169            }
170        } else {
171            None
172        }
173    } else {
174        None
175    };
176
177    if has_dots {
178        sig.variadic = None;
179        sig.inputs
180            .push(if let Some(named_dots) = named_dots.as_ref() {
181                syn::parse_quote!(#named_dots: &::miniextendr_api::dots::Dots)
182            } else {
183                // Cannot use `_` as a variable name, so unnamed `...` needs a
184                // stable synthetic binding that does not collide with user args.
185                for arg in &sig.inputs {
186                    let syn::FnArg::Typed(pat_type) = arg else {
187                        continue;
188                    };
189                    if let syn::Pat::Ident(pat_ident) = pat_type.pat.as_ref()
190                        && pat_ident.ident == "__miniextendr_dots"
191                    {
192                        return Err(syn::Error::new(
193                            pat_ident.ident.span(),
194                            "parameter named `__miniextendr_dots` conflicts with implicit dots parameter; use named dots like `my_dots: ...` instead",
195                        ));
196                    }
197                }
198                syn::parse_quote!(__miniextendr_dots: &::miniextendr_api::dots::Dots)
199            });
200    }
201
202    Ok(VariadicDots {
203        has_dots,
204        named_dots,
205    })
206}
207
208/// Return the identifier for a trailing `Dots` / `&Dots` parameter, if present.
209pub(crate) fn trailing_dots_ident(
210    inputs: &syn::punctuated::Punctuated<syn::FnArg, syn::token::Comma>,
211) -> Option<syn::Ident> {
212    let syn::FnArg::Typed(pat_type) = inputs.last()? else {
213        return None;
214    };
215    if !is_dots_type(pat_type.ty.as_ref()) {
216        return None;
217    }
218    let syn::Pat::Ident(pat_ident) = pat_type.pat.as_ref() else {
219        return None;
220    };
221    Some(pat_ident.ident.clone())
222}
223
224/// Check if a type is `Missing<T>`.
225pub(crate) fn is_missing_type(ty: &syn::Type) -> bool {
226    type_ends_with(ty, "Missing")
227}
228
229/// Check if a type is a vector-like type that `several_ok` can populate.
230///
231/// Accepts `Vec<T>`, `Box<[T]>`, `&[T]` / `&mut [T]`, and `[T; N]`. Rejects
232/// scalar types (like `Mode`, `String`, `&str`) so `several_ok` — which
233/// produces a multi-element R character vector via
234/// `match.arg(..., several.ok = TRUE)` — fails at compile time instead of
235/// deserialization time.
236pub(crate) fn is_vector_like_type(ty: &syn::Type) -> bool {
237    match ty {
238        syn::Type::Path(tp) => {
239            let Some(seg) = tp.path.segments.last() else {
240                return false;
241            };
242            if seg.ident == "Vec" {
243                return true;
244            }
245            if seg.ident == "Box" {
246                let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
247                    return false;
248                };
249                return matches!(
250                    args.args.first(),
251                    Some(syn::GenericArgument::Type(syn::Type::Slice(_)))
252                );
253            }
254            false
255        }
256        syn::Type::Reference(r) => matches!(&*r.elem, syn::Type::Slice(_)),
257        syn::Type::Slice(_) => true,
258        syn::Type::Array(_) => true,
259        _ => false,
260    }
261}
262
263/// Extract the inner type `T` from `Missing<T>`, if the type is `Missing<T>`.
264///
265/// Returns `None` if the type is not `Missing<T>` or has no generic argument.
266pub(crate) fn get_missing_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
267    let syn::Type::Path(tp) = ty else {
268        return None;
269    };
270    let seg = tp.path.segments.last()?;
271    if seg.ident != "Missing" {
272        return None;
273    }
274    let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
275        return None;
276    };
277    if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
278        Some(inner)
279    } else {
280        None
281    }
282}
283
284/// Validate a parameter's type for `Missing` and `Dots` conflicts.
285///
286/// Returns `Err` if:
287/// - `Missing<Missing<T>>` (nested Missing)
288/// - `Missing<Dots>` or `Missing<&Dots>`
289pub(crate) fn validate_param_type(ty: &syn::Type, span: proc_macro2::Span) -> syn::Result<()> {
290    if let Some(inner) = get_missing_inner_type(ty) {
291        if is_missing_type(inner) {
292            return Err(syn::Error::new(
293                span,
294                "Missing<T> cannot be nested; use Missing<T> with the inner type directly",
295            ));
296        }
297        if is_dots_type(inner) {
298            return Err(syn::Error::new(
299                span,
300                "Missing<T> cannot wrap Dots; variadic parameters (...) are always present when called",
301            ));
302        }
303    }
304    Ok(())
305}
306
307/// Validate per-parameter attribute conflicts.
308///
309/// Returns `Err` if:
310/// - `coerce` + `match_arg` on the same parameter
311/// - `coerce` + `choices(...)` on the same parameter
312/// - `choices(...)` + explicit `default` on the same parameter
313/// - `default` on a `&Dots` parameter
314pub(crate) fn validate_per_param_attr_conflicts(
315    attr: &PerParamMiniextendrAttr,
316    param_name: &str,
317    is_dots: bool,
318    ty: Option<&syn::Type>,
319    span: proc_macro2::Span,
320) -> syn::Result<()> {
321    if attr.has_coerce && attr.has_match_arg {
322        return Err(syn::Error::new(
323            span,
324            format!(
325                "cannot combine coerce and match_arg on parameter `{}`; \
326                 coerce converts the R type while match_arg validates string values",
327                param_name
328            ),
329        ));
330    }
331    if attr.has_coerce && attr.choices.is_some() {
332        return Err(syn::Error::new(
333            span,
334            format!(
335                "cannot combine coerce and choices on parameter `{}`; \
336                 coerce converts the R type while choices validates string values",
337                param_name
338            ),
339        ));
340    }
341    if attr.choices.is_some() && attr.default_value.is_some() {
342        return Err(syn::Error::new(
343            span,
344            format!(
345                "cannot combine choices() and default on parameter `{}`; \
346                 choices auto-generates its default from the first choice value",
347                param_name
348            ),
349        ));
350    }
351    if attr.has_several_ok && attr.choices.is_none() && !attr.has_match_arg {
352        return Err(syn::Error::new(
353            span,
354            format!(
355                "several_ok requires choices() or match_arg on parameter `{}`; \
356                 several_ok enables multi-value match.arg which needs a choice list",
357                param_name
358            ),
359        ));
360    }
361    if attr.has_several_ok
362        && let Some(ty) = ty
363    {
364        // Unwrap Missing<T> so several_ok is allowed on optional vector params.
365        let check_ty = get_missing_inner_type(ty).unwrap_or(ty);
366        if !is_vector_like_type(check_ty) {
367            return Err(syn::Error::new(
368                span,
369                format!(
370                    "several_ok requires a vector type on parameter `{}`; \
371                     several_ok enables multi-value match.arg which returns a character vector. \
372                     Use `Vec<T>`, `Box<[T]>`, `&[T]`, or `[T; N]` instead of a scalar type",
373                    param_name
374                ),
375            ));
376        }
377    }
378    if is_dots && attr.default_value.is_some() {
379        return Err(syn::Error::new(
380            span,
381            format!(
382                "variadic (...) parameter `{}` cannot have a default value",
383                param_name
384            ),
385        ));
386    }
387    if let Some(ty) = ty
388        && is_missing_type(ty)
389        && attr.default_value.is_some()
390    {
391        return Err(syn::Error::new(
392            span,
393            format!(
394                "`Missing<T>` parameter `{}` cannot have a default value. \
395                 `Missing<T>` detects omitted arguments via `missing()` in R, \
396                 which is incompatible with default values in the R function signature. \
397                 Use `Option<T>` with `#[miniextendr(default = \"...\")]` instead.",
398                param_name
399            ),
400        ));
401    }
402    Ok(())
403}
404
405// endregion
406
407// region: Per-parameter attribute parsing
408
409/// Parsed per-parameter `#[miniextendr(...)]` attribute content.
410///
411/// A single attribute can contain multiple items, e.g.
412/// `#[miniextendr(match_arg, default = "Safe")]`.
413#[derive(Default)]
414pub(crate) struct PerParamMiniextendrAttr {
415    /// Whether `coerce` was present, enabling automatic type coercion for this parameter
416    /// (e.g., `i32` to `u16`, `f64` to `f32`).
417    pub has_coerce: bool,
418    /// Whether `match_arg` was present, generating R `match.arg()` validation for
419    /// string parameters against a set of allowed values.
420    pub has_match_arg: bool,
421    /// Default value from `default = "..."`, if present. The tuple contains the default
422    /// value string and the attribute span (for error reporting).
423    pub default_value: Option<(String, proc_macro2::Span)>,
424    /// Choices for string parameters: `#[miniextendr(choices("a", "b", "c"))]`.
425    pub choices: Option<Vec<String>>,
426    /// Whether `several_ok` was present, enabling multi-value `match.arg(several.ok = TRUE)`.
427    /// Only valid with `choices(...)` or `match_arg`.
428    pub has_several_ok: bool,
429}
430
431/// Parse all per-parameter options from a `#[miniextendr(...)]` attribute.
432///
433/// Handles mixed content like `#[miniextendr(match_arg, default = "\"Safe\"")]`
434/// and `#[miniextendr(choices("a", "b", "c"))]`.
435///
436/// Returns `None` if `attr` is not a `#[miniextendr(...)]` attribute, if it cannot
437/// be parsed, or if it contains only function-level options (like `strict`) with
438/// no per-parameter options.
439///
440/// # Arguments
441///
442/// * `attr` - A `syn::Attribute` to inspect. Only attributes with path `miniextendr`
443///   are considered.
444pub(crate) fn parse_per_param_attr(attr: &syn::Attribute) -> Option<PerParamMiniextendrAttr> {
445    use syn::spanned::Spanned;
446    if !attr.path().is_ident("miniextendr") {
447        return None;
448    }
449
450    let syn::Meta::List(meta_list) = &attr.meta else {
451        return None;
452    };
453
454    let mut result = PerParamMiniextendrAttr::default();
455    let mut is_per_param = false;
456
457    let metas = match meta_list
458        .parse_args_with(syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated)
459    {
460        Ok(m) => m,
461        Err(_) => return None,
462    };
463
464    for meta in &metas {
465        match meta {
466            syn::Meta::Path(path) => {
467                if path.is_ident("coerce") {
468                    result.has_coerce = true;
469                    is_per_param = true;
470                } else if path.is_ident("match_arg") {
471                    result.has_match_arg = true;
472                    is_per_param = true;
473                } else if path.is_ident("several_ok") {
474                    result.has_several_ok = true;
475                    is_per_param = true;
476                }
477                // Other paths (like `strict`) are function-level, ignore here
478            }
479            syn::Meta::NameValue(nv) => {
480                if nv.path.is_ident("default")
481                    && let syn::Expr::Lit(syn::ExprLit {
482                        lit: syn::Lit::Str(lit_str),
483                        ..
484                    }) = &nv.value
485                {
486                    result.default_value = Some((lit_str.value(), attr.span()));
487                    is_per_param = true;
488                }
489                // Other name-value pairs are function-level, ignore here
490            }
491            syn::Meta::List(list) => {
492                if list.path.is_ident("choices") {
493                    // Parse choices("a", "b", "c") — a comma-separated list of string literals
494                    let choice_lits = match list.parse_args_with(
495                        syn::punctuated::Punctuated::<syn::LitStr, syn::Token![,]>::parse_terminated,
496                    ) {
497                        Ok(lits) => lits,
498                        Err(_) => continue,
499                    };
500                    let choices: Vec<String> = choice_lits.iter().map(|l| l.value()).collect();
501                    result.choices = Some(choices);
502                    is_per_param = true;
503                }
504                // Other list forms are function-level, ignore here
505            }
506        }
507    }
508
509    if !is_per_param {
510        return None;
511    }
512    Some(result)
513}
514
515/// Returns `true` if `attr` is a `#[miniextendr(...)]` attribute containing `coerce`.
516///
517/// The `coerce` flag may be combined with other per-parameter options (e.g.,
518/// `#[miniextendr(coerce, default = "0")]`).
519pub(crate) fn is_miniextendr_coerce_attr(attr: &syn::Attribute) -> bool {
520    parse_per_param_attr(attr).is_some_and(|a| a.has_coerce)
521}
522
523/// Returns `true` if `attr` is a `#[miniextendr(...)]` attribute containing `match_arg`.
524///
525/// The `match_arg` flag may be combined with other per-parameter options (e.g.,
526/// `#[miniextendr(match_arg, choices("a", "b"))]`).
527pub(crate) fn is_miniextendr_match_arg_attr(attr: &syn::Attribute) -> bool {
528    parse_per_param_attr(attr).is_some_and(|a| a.has_match_arg)
529}
530
531/// Returns `true` if `attr` is a `#[miniextendr(...)]` attribute containing `choices(...)`.
532///
533/// The `choices(...)` option may be combined with other per-parameter options (e.g.,
534/// `#[miniextendr(match_arg, choices("a", "b"))]`).
535pub(crate) fn is_miniextendr_choices_attr(attr: &syn::Attribute) -> bool {
536    parse_per_param_attr(attr).is_some_and(|a| a.choices.is_some())
537}
538
539/// Returns `true` if `attr` is a `#[miniextendr(...)]` attribute containing `several_ok`.
540pub(crate) fn is_miniextendr_several_ok_attr(attr: &syn::Attribute) -> bool {
541    parse_per_param_attr(attr).is_some_and(|a| a.has_several_ok)
542}
543
544/// Extracts the list of choice strings from a `#[miniextendr(choices("a", "b", "c"))]` attribute.
545///
546/// Returns `None` if the attribute does not contain `choices(...)` or is not a
547/// `#[miniextendr(...)]` attribute.
548pub(crate) fn parse_choices_attr(attr: &syn::Attribute) -> Option<Vec<String>> {
549    parse_per_param_attr(attr).and_then(|a| a.choices)
550}
551
552/// Extracts the default value from a `#[miniextendr(default = "...")]` attribute.
553///
554/// Returns `Some((default_value, attr_span))` if the attribute contains a `default` option.
555/// The span is used for error reporting when the default references a non-existent parameter.
556pub(crate) fn parse_default_attr(attr: &syn::Attribute) -> Option<(String, proc_macro2::Span)> {
557    parse_per_param_attr(attr).and_then(|a| a.default_value)
558}
559// endregion
560
561// region: Function parsing
562
563/// Parsed + normalized Rust function item for `#[miniextendr]`.
564///
565/// This performs signature normalization that the wrapper generator depends on:
566/// - `...` → a final `&miniextendr_api::dots::Dots` argument
567/// - `_` wildcard patterns → synthetic identifiers (`__unused0`, `__unused1`, ...)
568/// - Destructuring patterns (tuple, struct) → synthetic identifiers with let-binding in body
569/// - consumes `#[miniextendr(coerce)]` parameter attributes and records which params had it
570pub(crate) struct MiniextendrFunctionParsed {
571    /// The normalized function item (with dots transformed, wildcards renamed).
572    item: syn::ItemFn,
573    /// Whether the original function had `...` (variadic).
574    has_dots: bool,
575    /// If dots were named (e.g., `my_dots: ...`), the identifier.
576    named_dots: Option<syn::Ident>,
577    /// All per-parameter `#[miniextendr(...)]` options (coerce, match_arg,
578    /// default, choices, several_ok), keyed by the (possibly synthesized) Rust
579    /// parameter name. Replaces five parallel `HashSet` / `HashMap` fields.
580    per_param: std::collections::HashMap<String, ParamAttrs>,
581}
582
583/// Collapsed per-parameter attribute state for a single function parameter.
584///
585/// Built during parsing from `#[miniextendr(coerce | match_arg | several_ok |
586/// default = "…" | choices("…"))]` on the argument. Accessors on
587/// [`MiniextendrFunctionParsed`] query this struct rather than looking
588/// through multiple side-tables.
589#[derive(Default, Debug, Clone)]
590pub(crate) struct ParamAttrs {
591    pub coerce: bool,
592    pub match_arg: bool,
593    pub several_ok: bool,
594    pub choices: Option<Vec<String>>,
595    pub default: Option<String>,
596}
597
598/// Parses a Rust `fn` item from a token stream, performing all normalizations
599/// required by the `#[miniextendr]` codegen pipeline.
600///
601/// # Normalizations performed
602///
603/// 1. **Variadic (`...`) rewriting**: Replaces Rust variadic syntax with a typed
604///    `&miniextendr_api::dots::Dots` parameter. Named dots (`my_dots: ...`) preserve
605///    the user's identifier; unnamed `...` becomes `__miniextendr_dots`.
606/// 2. **Wildcard pattern renaming**: `_` parameter patterns become `__unused0`,
607///    `__unused1`, etc., so they can be passed by name to the C wrapper.
608/// 3. **Destructuring expansion**: Tuple/struct destructuring patterns are replaced
609///    with synthetic identifiers (`__param_0`, ...) and a `let` binding is prepended
610///    to the function body.
611/// 4. **Per-parameter attribute consumption**: `#[miniextendr(coerce)]`,
612///    `#[miniextendr(match_arg)]`, `#[miniextendr(default = "...")]`, and
613///    `#[miniextendr(choices(...))]` are consumed from parameters and recorded in
614///    the corresponding `per_param_*` fields.
615/// 5. **Validation**: Rejects `#[export_name]` on non-extern functions, rejects
616///    unsupported parameter patterns, and validates that defaults reference existing
617///    parameter names.
618impl syn::parse::Parse for MiniextendrFunctionParsed {
619    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
620        use syn::spanned::Spanned;
621
622        let mut item: syn::ItemFn = input.parse()?;
623
624        // dots support: parse variadic name (if any) and replace `...` with `&Dots`.
625        let dots_info = rewrite_variadic_dots(&mut item.sig)?;
626        let has_dots = dots_info.has_dots;
627        let named_dots = dots_info.named_dots;
628
629        // Reject #[export_name] for regular functions (not extern "C-unwind").
630        // For extern functions, #[export_name] can be used as an alternative to #[no_mangle].
631        let is_extern = item.sig.abi.is_some();
632        if !is_extern {
633            for attr in &item.attrs {
634                if attr.path().is_ident("export_name") {
635                    return Err(syn::Error::new_spanned(
636                        attr,
637                        "#[export_name] is not supported with #[miniextendr] on regular functions; \
638                         use `#[miniextendr(c_symbol = \"...\")]` to customize the C symbol name. \
639                         For extern \"C-unwind\" functions, #[export_name] is allowed.",
640                    ));
641                }
642            }
643        }
644
645        // Transform `_` wildcard patterns to synthetic identifiers, and consume
646        // per-parameter `#[miniextendr(coerce)]`, `#[miniextendr(default = "...")]`,
647        // and `#[miniextendr(choices(...))]` attributes.
648        let mut per_param: std::collections::HashMap<String, ParamAttrs> =
649            std::collections::HashMap::new();
650        let mut per_param_default_spans: std::collections::HashMap<String, proc_macro2::Span> =
651            std::collections::HashMap::new();
652        let mut unused_counter = 0usize;
653        let mut pattern_destructures: Vec<(Box<syn::Pat>, syn::Ident)> = Vec::new();
654        for arg in &mut item.sig.inputs {
655            let syn::FnArg::Typed(pat_type) = arg else {
656                // Self parameters are not allowed in standalone functions.
657                // Users should use #[miniextendr(env|r6|s3|s4|s7)] on impl blocks instead.
658                // The error is raised in lib.rs c_wrapper_inputs generation.
659                continue;
660            };
661
662            let had_coerce_attr = pat_type.attrs.iter().any(is_miniextendr_coerce_attr);
663            let had_match_arg_attr = pat_type.attrs.iter().any(is_miniextendr_match_arg_attr);
664            let had_several_ok = pat_type.attrs.iter().any(is_miniextendr_several_ok_attr);
665            let default_with_span = pat_type.attrs.iter().find_map(parse_default_attr);
666            let had_choices = pat_type.attrs.iter().find_map(parse_choices_attr);
667
668            // Remove miniextendr attributes from parameters (coerce, match_arg, choices, several_ok, default)
669            pat_type.attrs.retain(|attr| {
670                !is_miniextendr_coerce_attr(attr)
671                    && !is_miniextendr_match_arg_attr(attr)
672                    && !is_miniextendr_choices_attr(attr)
673                    && !is_miniextendr_several_ok_attr(attr)
674                    && parse_default_attr(attr).is_none()
675            });
676
677            // Validate type-based constraints (Missing nesting, Missing<Dots>)
678            validate_param_type(pat_type.ty.as_ref(), pat_type.ty.span())?;
679
680            // Resolve the Rust parameter name — either the user's identifier,
681            // or a synthesized one for wildcard / destructuring patterns.
682            let param_name: String = match pat_type.pat.as_ref() {
683                syn::Pat::Ident(pat_ident) => pat_ident.ident.to_string(),
684                syn::Pat::Wild(_) => {
685                    let synthetic_name = format!("__unused{}", unused_counter);
686                    unused_counter += 1;
687                    let synthetic_ident = syn::Ident::new(&synthetic_name, pat_type.pat.span());
688                    *pat_type.pat = syn::Pat::Ident(syn::PatIdent {
689                        attrs: vec![],
690                        by_ref: None,
691                        mutability: None,
692                        ident: synthetic_ident,
693                        subpat: None,
694                    });
695                    synthetic_name
696                }
697                syn::Pat::Tuple(_) | syn::Pat::TupleStruct(_) | syn::Pat::Struct(_) => {
698                    let synthetic_name = format!("__param_{}", unused_counter);
699                    unused_counter += 1;
700                    let synthetic_ident = syn::Ident::new(&synthetic_name, pat_type.pat.span());
701                    let original_pat = pat_type.pat.clone();
702                    *pat_type.pat = syn::Pat::Ident(syn::PatIdent {
703                        attrs: vec![],
704                        by_ref: None,
705                        mutability: None,
706                        ident: synthetic_ident.clone(),
707                        subpat: None,
708                    });
709                    pattern_destructures.push((original_pat, synthetic_ident));
710                    synthetic_name
711                }
712                _ => {
713                    return Err(syn::Error::new(
714                        pat_type.pat.span(),
715                        "miniextendr parameters must be identifiers or destructuring patterns (tuple, struct)",
716                    ));
717                }
718            };
719            let param_name_for_validation = param_name.clone();
720
721            // Record per-parameter attrs in one entry instead of five side-tables.
722            if had_coerce_attr
723                || had_match_arg_attr
724                || had_several_ok
725                || had_choices.is_some()
726                || default_with_span.is_some()
727            {
728                let entry = per_param.entry(param_name.clone()).or_default();
729                if had_coerce_attr {
730                    entry.coerce = true;
731                }
732                if had_match_arg_attr {
733                    entry.match_arg = true;
734                }
735                if had_several_ok {
736                    entry.several_ok = true;
737                }
738                if let Some(choices) = had_choices.clone() {
739                    entry.choices = Some(choices);
740                }
741                if let Some((default, span)) = default_with_span.clone() {
742                    entry.default = Some(default);
743                    per_param_default_spans.insert(param_name, span);
744                }
745            }
746
747            // Validate per-parameter attribute conflicts (coerce+match_arg, coerce+choices, etc.)
748            let per_param_combined = PerParamMiniextendrAttr {
749                has_coerce: had_coerce_attr,
750                has_match_arg: had_match_arg_attr,
751                default_value: default_with_span,
752                choices: had_choices,
753                has_several_ok: had_several_ok,
754            };
755            validate_per_param_attr_conflicts(
756                &per_param_combined,
757                &param_name_for_validation,
758                is_dots_type(pat_type.ty.as_ref()),
759                Some(pat_type.ty.as_ref()),
760                pat_type.ty.span(),
761            )?;
762        }
763
764        // Insert destructuring let-bindings for pattern parameters at the start of the function body
765        for (pat, ident) in pattern_destructures.iter().rev() {
766            item.block.stmts.insert(
767                0,
768                syn::parse_quote! {
769                    let #pat = #ident;
770                },
771            );
772        }
773
774        // Validate: all defaults reference existing parameters
775        let param_names: std::collections::HashSet<String> = item
776            .sig
777            .inputs
778            .iter()
779            .filter_map(|input| {
780                if let syn::FnArg::Typed(pat_type) = input
781                    && let syn::Pat::Ident(pat_ident) = pat_type.pat.as_ref()
782                {
783                    Some(pat_ident.ident.to_string())
784                } else {
785                    None
786                }
787            })
788            .collect();
789
790        let mut invalid_params: Vec<String> = per_param
791            .iter()
792            .filter_map(|(name, attrs)| {
793                if attrs.default.is_some() && !param_names.contains(name) {
794                    Some(name.clone())
795                } else {
796                    None
797                }
798            })
799            .collect();
800        invalid_params.sort();
801
802        if !invalid_params.is_empty() {
803            // Use the span of the first invalid param's attribute for the error
804            let error_span = invalid_params
805                .first()
806                .and_then(|p| per_param_default_spans.get(p).copied())
807                .unwrap_or_else(|| item.sig.ident.span());
808            return Err(syn::Error::new(
809                error_span,
810                format!(
811                    "default attribute(s) reference non-existent parameter(s): {}",
812                    invalid_params.join(", ")
813                ),
814            ));
815        }
816
817        Ok(Self {
818            item,
819            has_dots,
820            named_dots,
821            per_param,
822        })
823    }
824}
825
826/// Accessors and codegen helpers for [`MiniextendrFunctionParsed`].
827///
828/// Accessors are split into two groups:
829/// - **Parsed metadata**: dots, coerce, match_arg, choices, and defaults from
830///   per-parameter `#[miniextendr(...)]` attributes.
831/// - **Signature components**: attrs, vis, abi, ident, generics, inputs, output
832///   from the normalized `syn::ItemFn`.
833///
834/// Codegen helpers produce identifiers and perform mutations needed by the
835/// `#[miniextendr]` expansion pipeline.
836impl MiniextendrFunctionParsed {
837    // region: Accessors for parsed metadata
838
839    /// Whether the original function had `...` (variadic).
840    pub(crate) fn has_dots(&self) -> bool {
841        self.has_dots
842    }
843
844    /// If dots were named (e.g., `my_dots: ...`), returns the identifier.
845    pub(crate) fn named_dots(&self) -> Option<&syn::Ident> {
846        self.named_dots.as_ref()
847    }
848
849    /// Check if a parameter is the dots (`...`) param.
850    /// After parsing, dots are rewritten to `&Dots` — this checks the original name.
851    pub(crate) fn is_dots_param(&self, ident: &syn::Ident) -> bool {
852        if !self.has_dots {
853            return false;
854        }
855        // Named dots: check if ident matches the original name (e.g., `dots`, `my_dots`)
856        if let Some(ref named) = self.named_dots {
857            return ident == named;
858        }
859        // Unnamed dots: the variadic was replaced with `_dots` as the param name
860        ident == "_dots"
861    }
862
863    /// Check if a parameter name had `#[miniextendr(coerce)]` attribute.
864    pub(crate) fn has_coerce_attr(&self, param_name: &str) -> bool {
865        self.per_param.get(param_name).is_some_and(|a| a.coerce)
866    }
867
868    /// Check if a parameter name had `#[miniextendr(match_arg)]` attribute.
869    pub(crate) fn has_match_arg_attr(&self, param_name: &str) -> bool {
870        self.per_param.get(param_name).is_some_and(|a| a.match_arg)
871    }
872
873    /// Iterator over parameter names annotated with `#[miniextendr(match_arg)]`.
874    pub(crate) fn match_arg_params(&self) -> impl Iterator<Item = &String> {
875        self.per_param
876            .iter()
877            .filter_map(|(name, a)| if a.match_arg { Some(name) } else { None })
878    }
879
880    /// Get the choices for a parameter, if any.
881    pub(crate) fn choices_for_param(&self, param_name: &str) -> Option<&[String]> {
882        self.per_param
883            .get(param_name)
884            .and_then(|a| a.choices.as_deref())
885    }
886
887    /// Iterator over parameter names annotated with `#[miniextendr(choices(…))]`,
888    /// together with their choice lists.
889    pub(crate) fn choices_params(&self) -> impl Iterator<Item = (&String, &Vec<String>)> {
890        self.per_param
891            .iter()
892            .filter_map(|(name, a)| a.choices.as_ref().map(|c| (name, c)))
893    }
894
895    /// Check if a parameter has `several_ok` (multi-value match.arg).
896    pub(crate) fn has_several_ok(&self, param_name: &str) -> bool {
897        self.per_param.get(param_name).is_some_and(|a| a.several_ok)
898    }
899
900    /// Returns all parameter defaults as an owned map from parameter name to
901    /// default value string (the raw R expression used in the wrapper formals,
902    /// e.g. `"NULL"`, `"TRUE"`, `"\"Safe\""`).
903    pub(crate) fn param_defaults(&self) -> std::collections::HashMap<String, String> {
904        self.per_param
905            .iter()
906            .filter_map(|(name, a)| a.default.as_ref().map(|d| (name.clone(), d.clone())))
907            .collect()
908    }
909    // endregion
910
911    // region: Accessors for signature components
912
913    /// Original attributes on the function item (doc comments, cfgs, etc.).
914    pub(crate) fn attrs(&self) -> &[syn::Attribute] {
915        &self.item.attrs
916    }
917
918    /// Visibility of the function (`pub`, `pub(crate)`, or private).
919    pub(crate) fn vis(&self) -> &syn::Visibility {
920        &self.item.vis
921    }
922
923    /// Explicit ABI, if the function was declared `extern "C-unwind"`.
924    pub(crate) fn abi(&self) -> Option<&syn::Abi> {
925        self.item.sig.abi.as_ref()
926    }
927
928    /// Function identifier after normalization.
929    pub(crate) fn ident(&self) -> &syn::Ident {
930        &self.item.sig.ident
931    }
932
933    /// Generic parameters on the function signature.
934    pub(crate) fn generics(&self) -> &syn::Generics {
935        &self.item.sig.generics
936    }
937
938    /// Function inputs after normalization (dots rewritten, wildcards renamed).
939    pub(crate) fn inputs(&self) -> &syn::punctuated::Punctuated<syn::FnArg, syn::Token![,]> {
940        &self.item.sig.inputs
941    }
942
943    /// Function return type.
944    pub(crate) fn output(&self) -> &syn::ReturnType {
945        &self.item.sig.output
946    }
947
948    /// The normalized function item (with original doc comments).
949    pub(crate) fn item(&self) -> &syn::ItemFn {
950        &self.item
951    }
952
953    /// The normalized function item with roxygen tags stripped from doc comments.
954    ///
955    /// This is used for emitting the Rust function without R-specific documentation
956    /// tags (e.g., `@param`, `@examples`) that don't belong in rustdoc.
957    pub(crate) fn item_without_roxygen(&self) -> syn::ItemFn {
958        let mut item = self.item.clone();
959        item.attrs = crate::roxygen::strip_roxygen_from_attrs(&item.attrs);
960        item
961    }
962    // endregion
963
964    // region: Codegen helpers
965
966    /// Returns `true` if this function needs an internal C wrapper (`C_<crate>_<name>` function).
967    ///
968    /// Rust-ABI functions (no explicit `extern`) need a generated `extern "C-unwind"` wrapper
969    /// that handles SEXP conversion and error propagation. Functions already declared as
970    /// `extern "C-unwind"` are passed through directly without wrapping.
971    pub(crate) fn uses_internal_c_wrapper(&self) -> bool {
972        self.abi().is_none()
973    }
974
975    /// Returns the identifier for the generated `const &str` holding the R wrapper code.
976    ///
977    /// The R wrapper is a string constant containing the R function definition that
978    /// calls `.Call(C_<crate>_<name>, ...)`. It is collected via linkme distributed slices to
979    /// produce the `R/miniextendr-wrappers.R` file.
980    pub(crate) fn r_wrapper_const_ident(&self) -> syn::Ident {
981        r_wrapper_const_ident_for(self.ident())
982    }
983
984    /// Returns the identifier for the C-callable entry point.
985    ///
986    /// - **Rust ABI functions**: Returns `C_<crate>_<name>` (the generated wrapper
987    ///   function, crate-prefixed for webR cross-package symbol uniqueness — #1273).
988    /// - **`extern "C-unwind"` functions**: Returns the function's own name, or the
989    ///   value from `#[export_name = "..."]` if present. The user owns these symbols,
990    ///   including their cross-package uniqueness under webR.
991    pub(crate) fn c_wrapper_ident(&self) -> syn::Ident {
992        if self.uses_internal_c_wrapper() {
993            crate::naming::bare_fn_c_wrapper_ident(self.ident())
994        } else {
995            // For extern functions, check for #[export_name = "..."]
996            self.export_name_ident()
997                .unwrap_or_else(|| self.ident().clone())
998        }
999    }
1000
1001    /// Extracts the custom symbol name from `#[export_name = "..."]`, if present.
1002    ///
1003    /// Only meaningful for `extern "C-unwind"` functions, where `#[export_name]` is
1004    /// allowed as an alternative to `#[no_mangle]`. Returns `None` if no such attribute exists.
1005    pub(crate) fn export_name_ident(&self) -> Option<syn::Ident> {
1006        for attr in &self.item.attrs {
1007            if attr.path().is_ident("export_name")
1008                && let syn::Meta::NameValue(meta) = &attr.meta
1009                && let syn::Expr::Lit(syn::ExprLit {
1010                    lit: syn::Lit::Str(lit_str),
1011                    ..
1012                }) = &meta.value
1013            {
1014                return Some(syn::Ident::new(&lit_str.value(), lit_str.span()));
1015            }
1016        }
1017        None
1018    }
1019
1020    /// Add `#[inline(never)]` if no `#[inline(...)]` attribute is present.
1021    /// Only for Rust ABI functions - extern "C-unwind" functions are passed through as-is.
1022    ///
1023    /// Preventing inlining ensures:
1024    /// - Worker-dispatched functions retain a distinct call frame
1025    /// - Panic handling and unwinding retain the intended boundary
1026    /// - Stack traces show the actual function name
1027    pub(crate) fn add_inline_never_if_needed(&mut self) {
1028        let has_explicit_abi = self.item.sig.abi.is_some();
1029        let has_inline = self
1030            .item
1031            .attrs
1032            .iter()
1033            .any(|attr| attr.path().is_ident("inline"));
1034        if !has_inline && !has_explicit_abi {
1035            self.item.attrs.push(syn::parse_quote!(#[inline(never)]));
1036        }
1037    }
1038    // endregion
1039}
1040// endregion
1041
1042// region: Attribute parsing
1043
1044/// Parse the value of a `name = "..."` meta item as a string literal.
1045///
1046/// Returns a compile error spanning the offending token when the RHS is not a
1047/// `&str` literal. `field` is used in the diagnostic (e.g. `"c_symbol"`).
1048fn parse_lit_str(nv: &syn::MetaNameValue, field: &str) -> syn::Result<String> {
1049    match &nv.value {
1050        syn::Expr::Lit(syn::ExprLit {
1051            lit: syn::Lit::Str(lit),
1052            ..
1053        }) => Ok(lit.value()),
1054        syn::Expr::Lit(expr_lit) => Err(syn::Error::new_spanned(
1055            &expr_lit.lit,
1056            format!("{field} expects a string literal"),
1057        )),
1058        other => Err(syn::Error::new_spanned(
1059            other,
1060            format!("{field} expects a string literal"),
1061        )),
1062    }
1063}
1064
1065/// Comma-separated list of all fn-level boolean flags, for error messages.
1066///
1067/// Kept as a single constant so the three "unknown option" error paths (Path,
1068/// NameValue bool, parenthesized bool) all read from the same list and can't
1069/// drift.
1070const FN_BOOL_FLAGS_HELP: &str = "invisible, visible, check_interrupt, worker, no_worker, coerce, no_coerce, \
1071     rng, unwrap_in_r, strict, no_strict, \
1072     no_preconditions, no_call_attribution, fast, no_fast, \
1073     internal, noexport, export";
1074
1075/// Comma-separated list of fn-level nested options, for error messages.
1076const FN_NESTED_OPTIONS_HELP: &str = "`s3(...)`, `lifecycle(...)`, `defaults(...)`";
1077
1078/// Parsed arguments for the `#[miniextendr(...)]` attribute on functions.
1079///
1080/// This is intentionally a small, "data-only" struct that:
1081/// - Owns the parsing rules for the attribute
1082/// - Produces a normalized, easy-to-consume representation for codegen
1083///
1084/// # Accepted flags
1085///
1086/// - `invisible` / `visible`: control whether the generated R wrapper returns invisibly
1087/// - `check_interrupt`: insert `R_CheckUserInterrupt()` before calling Rust
1088/// - `worker`: opt into worker-thread execution (default is main thread)
1089/// - `coerce`: enable automatic coercion for supported parameter types
1090/// - `rng`: enable RNG state management (GetRNGstate/PutRNGstate)
1091/// - `unwrap_in_r`: return `Result<T, E>` to R without unwrapping
1092/// - `prefer = "auto" | "list" | "externalptr" | "vector"`: prefer a specific `IntoR` path
1093/// - `no_preconditions`: drop the R-side `stopifnot(...)` block. `TryFromSexp`
1094///   still raises on bad input; the message comes from Rust rather than R.
1095///   Saves ~300 ns per assertion (~600 ns per scalar arg, ~1230 ns for a 1-arg
1096///   numeric scalar fn). Hot-path opt-in. Opt out with `no_fast` when `fast-default`
1097///   is enabled.
1098/// - `no_call_attribution`: emit `.call = NULL` instead of `.call = match.call()`
1099///   in the generated R wrapper. The error fallback `sys.call()` preserves
1100///   wrapper-invocation attribution (positional args instead of named). Saves
1101///   ~1200 ns per call regardless of arg count.
1102/// - `fast`: shorthand for `no_preconditions + no_call_attribution`. The
1103///   biggest single-knob wrapper speedup.
1104/// - `no_fast`: explicit opt-out of both knobs (useful when `fast-default`
1105///   feature is enabled crate-wide to restore full error UX for a specific fn).
1106///
1107/// See `analysis/scaffolding-deep-findings-2026-05-20.md` for the measurement
1108/// underlying these options (~13× speedup possible on the wrapper layer).
1109///
1110/// # Note
1111///
1112/// Unknown flags are rejected with a compile error to avoid silently ignoring typos.
1113#[derive(Default)]
1114pub(crate) struct MiniextendrFnAttrs {
1115    /// Force execution on worker thread (set by `worker`).
1116    pub(crate) force_worker: bool,
1117    /// Override visibility; `Some(true)` makes the wrapper return invisibly, `Some(false)` forces visibility.
1118    pub(crate) force_invisible: Option<bool>,
1119    /// Insert `R_CheckUserInterrupt()` before calling the Rust function.
1120    pub(crate) check_interrupt: bool,
1121    /// Enable automatic coercion for all parameters that support it.
1122    pub(crate) coerce_all: bool,
1123    /// Enable RNG state management (GetRNGstate/PutRNGstate).
1124    pub(crate) rng: bool,
1125    /// Return `Result<T, E>` to R without unwrapping.
1126    pub(crate) unwrap_in_r: bool,
1127    /// Skip emission of the R-side `stopifnot(...)` precondition block.
1128    ///
1129    /// `TryFromSexp` already raises a typed Rust error on mismatched input,
1130    /// so the information isn't lost — just routed through the Rust error
1131    /// message rather than R's stopifnot text. Useful for hot paths where the
1132    /// per-call precondition cost (~300 ns per assertion, ~600 ns per arg for
1133    /// numeric scalars) dominates over actual work.
1134    ///
1135    /// Set by `#[miniextendr(no_preconditions)]` or implied by `fast`.
1136    /// Use `no_fast` to opt out when `fast-default` is enabled.
1137    pub(crate) no_preconditions: bool,
1138    /// Emit `.call = NULL` instead of `.call = match.call()` in the generated
1139    /// R wrapper.
1140    ///
1141    /// `match.call()` costs ~1200 ns per call (fixed, independent of arg
1142    /// count) but is only consulted on the error path by
1143    /// `.miniextendr_raise_condition`. When `.call = NULL`, the helper falls
1144    /// back to `sys.call()` which surfaces the same wrapper invocation
1145    /// (positional args instead of named).
1146    ///
1147    /// Set by `#[miniextendr(no_call_attribution)]` or implied by `fast`.
1148    /// Use `no_fast` to opt out when `fast-default` is enabled.
1149    pub(crate) no_call_attribution: bool,
1150    /// Preferred return conversion: forces `AsList`/`AsExternalPtr`/`AsRNative` wrapping
1151    /// of the return value before `IntoR::into_sexp` is called.
1152    pub(crate) return_pref: ReturnPref,
1153    /// Span of the `prefer = ...` attribute, for error reporting when the return type
1154    /// falls into a codegen category (`Option<T>`, `Result<T, E>`, `()`, `Self`, raw
1155    /// `SEXP`, ...) that can't honor it.
1156    pub(crate) return_pref_span: Option<proc_macro2::Span>,
1157    /// S3 generic name (if this function is an S3 method).
1158    ///
1159    /// Use `#[miniextendr(s3(generic = "vec_proxy", class = "my_vctr"))]` to mark a function
1160    /// as an S3 method for an existing generic.
1161    pub(crate) s3_generic: Option<String>,
1162    /// S3 class suffix for the method (e.g., "my_vctr" or "my_vctr.my_vctr" for double-dispatch).
1163    pub(crate) s3_class: Option<String>,
1164    /// Typed list validation spec for dots parameter.
1165    ///
1166    /// Use `#[miniextendr(dots = typed_list!(...))]` to automatically validate dots
1167    /// at the start of the function and bind the result to `dots_typed`.
1168    pub(crate) dots_spec: Option<proc_macro2::TokenStream>,
1169    /// Span of the `dots = ...` attribute for error reporting.
1170    pub(crate) dots_span: Option<proc_macro2::Span>,
1171    /// Lifecycle specification for deprecation/experimental status.
1172    pub(crate) lifecycle: Option<crate::lifecycle::LifecycleSpec>,
1173    /// Strict output conversion: panic instead of lossy widening for i64/u64/isize/usize.
1174    pub(crate) strict: bool,
1175    /// Mark as internal: adds `@keywords internal`, suppresses `@export`.
1176    pub(crate) internal: bool,
1177    /// Suppress `@export` without adding `@keywords internal`.
1178    pub(crate) noexport: bool,
1179    /// Force `@export` even on non-pub functions. Antidote to `noexport`.
1180    pub(crate) export: bool,
1181    /// Custom roxygen documentation override.
1182    ///
1183    /// When set, replaces auto-extracted roxygen from Rust doc comments.
1184    /// Each `\n` in the string becomes a separate `#'` line.
1185    pub(crate) doc: Option<String>,
1186    /// Custom C symbol name for the generated wrapper.
1187    ///
1188    /// Overrides the default `C_<crate>_<fn_name>` naming convention. The value is used
1189    /// verbatim (no crate prefix) — the author owns cross-package uniqueness on webR (#1273).
1190    /// Must be a valid C identifier (alphanumeric + underscore, starting with letter or underscore).
1191    pub(crate) c_symbol: Option<String>,
1192    /// Override R wrapper function name.
1193    ///
1194    /// Use `#[miniextendr(r_name = "is.my_type")]` to give the R wrapper a different name
1195    /// than the Rust function. The C symbol is still derived from the Rust name.
1196    /// Cannot be combined with `s3(generic/class)` — use `generic`/`class` for S3 naming.
1197    pub(crate) r_name: Option<String>,
1198    /// R code to inject at the very top of the wrapper body (before all built-in checks).
1199    ///
1200    /// Use `#[miniextendr(r_entry = "x <- as.integer(x)")]` to run R code before
1201    /// missing-default handling, lifecycle checks, stopifnot, and match.arg.
1202    /// Multi-line via `\n`. No validation of R syntax.
1203    pub(crate) r_entry: Option<String>,
1204    /// R code to inject after all built-in checks, immediately before `.Call()`.
1205    ///
1206    /// Use `#[miniextendr(r_post_checks = "message('calling rust')")]` to run R code
1207    /// after all precondition checks but before the Rust function is invoked.
1208    /// Multi-line via `\n`. No validation of R syntax.
1209    pub(crate) r_post_checks: Option<String>,
1210    /// Register `on.exit()` cleanup code in the R wrapper.
1211    ///
1212    /// Short form: `#[miniextendr(r_on_exit = "close(con)")]` → `on.exit(close(con), add = TRUE)`
1213    ///
1214    /// Long form: `#[miniextendr(r_on_exit(expr = "close(con)", add = false))]`
1215    ///
1216    /// Defaults: `add = TRUE`, `after = TRUE`. Injected after `r_entry`, before other checks.
1217    pub(crate) r_on_exit: Option<ROnExit>,
1218}
1219
1220/// Parsed `r_on_exit` attribute for `on.exit()` cleanup code in R wrappers.
1221///
1222/// Two forms:
1223/// - Short: `r_on_exit = "expr"` → `ROnExit { expr, add: true, after: true }`
1224/// - Long: `r_on_exit(expr = "...", add = false, after = false)`
1225///
1226/// Defaults match R conventions for composable code: `add = TRUE`, `after = TRUE`.
1227#[derive(Debug, Clone)]
1228pub(crate) struct ROnExit {
1229    pub expr: String,
1230    pub add: bool,
1231    pub after: bool,
1232}
1233
1234impl ROnExit {
1235    /// Generate the R `on.exit(...)` call string.
1236    ///
1237    /// - `add = FALSE` (R default): `on.exit(expr)`
1238    /// - `add = TRUE, after = TRUE`: `on.exit(expr, add = TRUE)`
1239    /// - `add = TRUE, after = FALSE`: `on.exit(expr, add = TRUE, after = FALSE)`
1240    pub fn to_r_code(&self) -> String {
1241        if !self.add {
1242            format!("on.exit({})", self.expr)
1243        } else if !self.after {
1244            format!("on.exit({}, add = TRUE, after = FALSE)", self.expr)
1245        } else {
1246            format!("on.exit({}, add = TRUE)", self.expr)
1247        }
1248    }
1249}
1250
1251#[derive(Clone, Copy, Default)]
1252/// Preferred return-conversion path for `IntoR`.
1253pub(crate) enum ReturnPref {
1254    /// Use the default `IntoR` implementation for the type.
1255    #[default]
1256    Auto,
1257    /// Force list conversion via the `AsList` wrapper.
1258    List,
1259    /// Force external pointer conversion via the `AsExternalPtr` wrapper.
1260    ExternalPtr,
1261    /// Force native vector/scalar conversion via the `AsRNative` wrapper.
1262    Native,
1263}
1264
1265/// Parses the comma-separated option list inside `#[miniextendr(...)]`.
1266///
1267/// Supports three syntactic forms for each option:
1268/// - **Bare identifier**: `#[miniextendr(invisible)]`
1269/// - **Name-value**: `#[miniextendr(prefer = "list")]` or `#[miniextendr(invisible = true)]`
1270/// - **Nested list**: `#[miniextendr(s3(generic = "...", class = "..."))]`
1271///
1272/// Options with negated forms (`no_worker`, `no_coerce`, `no_strict`) explicitly
1273/// disable the corresponding flag, which is useful for overriding feature-based
1274/// defaults.
1275///
1276/// An empty input (plain `#[miniextendr]`) resolves all options to their feature-based
1277/// defaults (e.g., `worker-default`, `coerce-default`, `strict-default`).
1278///
1279/// # Errors
1280///
1281/// Returns a compile error for:
1282/// - Unknown option names (prevents silent typos)
1283/// - Mutually exclusive options (`internal` + `noexport`)
1284/// - Invalid values for key-value options (e.g., bad `prefer` or `c_symbol`)
1285/// - Missing required sub-options (e.g., `s3(...)` without `class`)
1286impl syn::parse::Parse for MiniextendrFnAttrs {
1287    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
1288        use syn::spanned::Spanned;
1289        // Use Option<bool> for fields that support feature defaults.
1290        // None = not explicitly set → resolve from cfg!(feature = "...") at end.
1291        let mut force_worker: Option<bool> = None;
1292        let mut force_invisible: Option<bool> = None;
1293        let mut check_interrupt = false;
1294        let mut coerce_all: Option<bool> = None;
1295        let mut rng = false;
1296        let mut unwrap_in_r = false;
1297        let mut no_preconditions: Option<bool> = None;
1298        let mut no_call_attribution: Option<bool> = None;
1299        let mut return_pref = ReturnPref::Auto;
1300        let mut return_pref_span: Option<proc_macro2::Span> = None;
1301        let mut s3_generic = None;
1302        let mut s3_class = None;
1303        let mut dots_spec = None;
1304        let mut dots_span = None;
1305        let mut lifecycle = None;
1306        let mut strict: Option<bool> = None;
1307        let mut internal = false;
1308        let mut noexport = false;
1309        let mut export = false;
1310        let mut doc = None;
1311        let mut c_symbol = None;
1312        let mut r_name = None;
1313        let mut r_entry = None;
1314        let mut r_post_checks = None;
1315        let mut r_on_exit = None;
1316
1317        // Empty input (`#[miniextendr]`) → skip the parse loop and fall through
1318        // to the single Ok(Self {...}) at the bottom; every local is already
1319        // seeded with its default value above.
1320        let metas = if input.is_empty() {
1321            syn::punctuated::Punctuated::new()
1322        } else {
1323            syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated(input)?
1324        };
1325
1326        for meta in metas {
1327            match meta {
1328                // Simple identifiers: invisible, visible, check_interrupt, coerce, worker, rng
1329                syn::Meta::Path(path) => {
1330                    if let Some(ident) = path.get_ident() {
1331                        if ident == "invisible" {
1332                            force_invisible = Some(true);
1333                        } else if ident == "visible" {
1334                            force_invisible = Some(false);
1335                        } else if ident == "check_interrupt" {
1336                            check_interrupt = true;
1337                        } else if ident == "coerce" {
1338                            coerce_all = Some(true);
1339                        } else if ident == "no_coerce" {
1340                            coerce_all = Some(false);
1341                        } else if ident == "rng" {
1342                            rng = true;
1343                        } else if ident == "unwrap_in_r" {
1344                            unwrap_in_r = true;
1345                        } else if ident == "worker" {
1346                            force_worker = Some(true);
1347                        } else if ident == "no_worker" {
1348                            force_worker = Some(false);
1349                        } else if ident == "strict" {
1350                            strict = Some(true);
1351                        } else if ident == "no_strict" {
1352                            strict = Some(false);
1353                        } else if ident == "no_preconditions" {
1354                            no_preconditions = Some(true);
1355                        } else if ident == "no_call_attribution" {
1356                            no_call_attribution = Some(true);
1357                        } else if ident == "fast" {
1358                            // Bundle alias: drop the two biggest R-side
1359                            // overheads in the generated wrapper.
1360                            no_preconditions = Some(true);
1361                            no_call_attribution = Some(true);
1362                        } else if ident == "no_fast" {
1363                            // Explicit opt-out: restore full error UX even when
1364                            // `fast-default` feature is enabled crate-wide.
1365                            no_preconditions = Some(false);
1366                            no_call_attribution = Some(false);
1367                        } else if ident == "internal" {
1368                            internal = true;
1369                        } else if ident == "noexport" {
1370                            noexport = true;
1371                        } else if ident == "export" {
1372                            export = true;
1373                        } else {
1374                            return Err(syn::Error::new_spanned(
1375                                ident,
1376                                format!(
1377                                    "unknown `#[miniextendr]` option; expected one of: {FN_BOOL_FLAGS_HELP}"
1378                                ),
1379                            ));
1380                        }
1381                    }
1382                }
1383                syn::Meta::NameValue(nv) => {
1384                    // Check for boolean flag options: option = true / option = false
1385                    if let syn::Expr::Lit(syn::ExprLit {
1386                        lit: syn::Lit::Bool(lit_bool),
1387                        ..
1388                    }) = &nv.value
1389                    {
1390                        let val = lit_bool.value;
1391                        if let Some(ident) = nv.path.get_ident() {
1392                            if ident == "invisible" {
1393                                force_invisible = Some(val);
1394                            } else if ident == "visible" {
1395                                force_invisible = Some(!val);
1396                            } else if ident == "check_interrupt" {
1397                                check_interrupt = val;
1398                            } else if ident == "worker" {
1399                                force_worker = Some(val);
1400                            } else if ident == "no_worker" {
1401                                force_worker = Some(!val);
1402                            } else if ident == "coerce" {
1403                                coerce_all = Some(val);
1404                            } else if ident == "no_coerce" {
1405                                coerce_all = Some(!val);
1406                            } else if ident == "rng" {
1407                                rng = val;
1408                            } else if ident == "unwrap_in_r" {
1409                                unwrap_in_r = val;
1410                            } else if ident == "strict" {
1411                                strict = Some(val);
1412                            } else if ident == "no_strict" {
1413                                strict = Some(!val);
1414                            } else if ident == "no_preconditions" {
1415                                no_preconditions = Some(val);
1416                            } else if ident == "no_call_attribution" {
1417                                no_call_attribution = Some(val);
1418                            } else if ident == "fast" {
1419                                no_preconditions = Some(val);
1420                                no_call_attribution = Some(val);
1421                            } else if ident == "no_fast" {
1422                                no_preconditions = Some(!val);
1423                                no_call_attribution = Some(!val);
1424                            } else if ident == "internal" {
1425                                internal = val;
1426                            } else if ident == "noexport" {
1427                                noexport = val;
1428                            } else if ident == "export" {
1429                                export = val;
1430                            } else {
1431                                return Err(syn::Error::new_spanned(
1432                                    ident,
1433                                    format!(
1434                                        "unknown `#[miniextendr]` option `{ident}`; expected one of: \
1435                                         {FN_BOOL_FLAGS_HELP}"
1436                                    ),
1437                                ));
1438                            }
1439                            continue;
1440                        }
1441                    }
1442
1443                    if nv.path.is_ident("prefer") {
1444                        let v = parse_lit_str(&nv, "prefer")?;
1445                        return_pref_span = Some(nv.span());
1446                        return_pref = match v.as_str() {
1447                            "list" => ReturnPref::List,
1448                            "externalptr" => ReturnPref::ExternalPtr,
1449                            "vector" | "native" => ReturnPref::Native,
1450                            "auto" => ReturnPref::Auto,
1451                            _ => {
1452                                return Err(syn::Error::new_spanned(
1453                                    &nv.value,
1454                                    "prefer must be one of: auto, list, externalptr, vector/native",
1455                                ));
1456                            }
1457                        };
1458                    } else if nv.path.is_ident("dots") {
1459                        // dots = typed_list!(...) - capture the macro invocation
1460                        // Store span for error reporting
1461                        dots_span = Some(nv.path.span());
1462                        if let syn::Expr::Macro(expr_macro) = &nv.value {
1463                            if expr_macro.mac.path.is_ident("typed_list") {
1464                                // Capture the entire macro invocation as TokenStream
1465                                dots_spec = Some(quote::quote!(#expr_macro));
1466                            } else {
1467                                return Err(syn::Error::new_spanned(
1468                                    &expr_macro.mac.path,
1469                                    "dots expects `typed_list!(...)` macro",
1470                                ));
1471                            }
1472                        } else {
1473                            return Err(syn::Error::new_spanned(
1474                                &nv.value,
1475                                "dots expects `typed_list!(...)` macro",
1476                            ));
1477                        }
1478                    } else if nv.path.is_ident("lifecycle") {
1479                        // lifecycle = "stage"
1480                        if let Some(spec) = crate::lifecycle::parse_lifecycle_attr(
1481                            &syn::Meta::NameValue(nv.clone()),
1482                        )? {
1483                            lifecycle = Some(spec);
1484                        }
1485                    } else if nv.path.is_ident("doc") {
1486                        doc = Some(parse_lit_str(&nv, "doc")?);
1487                    } else if nv.path.is_ident("c_symbol") {
1488                        let val = parse_lit_str(&nv, "c_symbol")?;
1489                        if val.is_empty()
1490                            || (!val.starts_with(|c: char| c.is_ascii_alphabetic())
1491                                && !val.starts_with('_'))
1492                        {
1493                            return Err(syn::Error::new_spanned(
1494                                &nv.value,
1495                                "c_symbol must be a valid C identifier",
1496                            ));
1497                        }
1498                        if !val.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
1499                            return Err(syn::Error::new_spanned(
1500                                &nv.value,
1501                                "c_symbol must be a valid C identifier (alphanumeric and underscore only)",
1502                            ));
1503                        }
1504                        c_symbol = Some(val);
1505                    } else if nv.path.is_ident("r_name") {
1506                        let val = parse_lit_str(&nv, "r_name")?;
1507                        if val.is_empty() {
1508                            return Err(syn::Error::new_spanned(
1509                                &nv.value,
1510                                "r_name must not be empty",
1511                            ));
1512                        }
1513                        r_name = Some(val);
1514                    } else if nv.path.is_ident("r_entry") {
1515                        r_entry = Some(parse_lit_str(&nv, "r_entry")?);
1516                    } else if nv.path.is_ident("r_post_checks") {
1517                        r_post_checks = Some(parse_lit_str(&nv, "r_post_checks")?);
1518                    } else if nv.path.is_ident("r_on_exit") {
1519                        // Short form: r_on_exit = "expr" → on.exit(expr, add = TRUE)
1520                        r_on_exit = Some(ROnExit {
1521                            expr: parse_lit_str(&nv, "r_on_exit")?,
1522                            add: true,
1523                            after: true,
1524                        });
1525                    } else {
1526                        let key_name = nv
1527                            .path
1528                            .get_ident()
1529                            .map(|i| i.to_string())
1530                            .unwrap_or_default();
1531                        return Err(syn::Error::new_spanned(
1532                            nv,
1533                            format!(
1534                                "unknown `#[miniextendr]` key-value option `{}`. \
1535                                 Key-value options are: `prefer = \"...\"`, `dots = typed_list!(...)`, \
1536                                 `lifecycle = \"...\"`, `doc = \"...\"`, `c_symbol = \"...\"`, \
1537                                 `r_name = \"...\"`, `r_entry = \"...\"`, `r_post_checks = \"...\"`, \
1538                                 `r_on_exit = \"...\"`",
1539                                key_name,
1540                            ),
1541                        ));
1542                    }
1543                }
1544                syn::Meta::List(list) => {
1545                    if list.path.is_ident("defaults") {
1546                        // Ignore defaults(...) - it's handled by impl method parsing
1547                        // This allows #[miniextendr(defaults(...))] on impl methods
1548                    } else if list.path.is_ident("lifecycle") {
1549                        // lifecycle(stage = "deprecated", when = "0.4.0", ...)
1550                        if let Some(spec) =
1551                            crate::lifecycle::parse_lifecycle_attr(&syn::Meta::List(list.clone()))?
1552                        {
1553                            lifecycle = Some(spec);
1554                        }
1555                    } else if list.path.is_ident("s3") {
1556                        // Parse s3(generic = "...", class = "...")
1557                        list.parse_nested_meta(|meta| {
1558                            if meta.path.is_ident("generic") {
1559                                let _: syn::Token![=] = meta.input.parse()?;
1560                                let value: syn::LitStr = meta.input.parse()?;
1561                                s3_generic = Some(value.value());
1562                            } else if meta.path.is_ident("class") {
1563                                let _: syn::Token![=] = meta.input.parse()?;
1564                                let value: syn::LitStr = meta.input.parse()?;
1565                                s3_class = Some(value.value());
1566                            } else {
1567                                return Err(
1568                                    meta.error("unknown s3 option; expected `generic` or `class`")
1569                                );
1570                            }
1571                            Ok(())
1572                        })?;
1573                        // Validate: s3 requires class (generic can default to function name)
1574                        if s3_class.is_none() {
1575                            return Err(syn::Error::new_spanned(
1576                                &list,
1577                                "s3(...) requires `class = \"...\"` to specify the S3 class suffix; \
1578                                 `generic` is optional and defaults to the function name",
1579                            ));
1580                        }
1581                    } else if list.path.is_ident("r_on_exit") {
1582                        // Long form: r_on_exit(expr = "...", add = false, after = false)
1583                        let mut expr = None;
1584                        let mut add = true;
1585                        let mut after = true;
1586                        list.parse_nested_meta(|meta| {
1587                            if meta.path.is_ident("expr") {
1588                                let _: syn::Token![=] = meta.input.parse()?;
1589                                let value: syn::LitStr = meta.input.parse()?;
1590                                expr = Some(value.value());
1591                            } else if meta.path.is_ident("add") {
1592                                let _: syn::Token![=] = meta.input.parse()?;
1593                                let value: syn::LitBool = meta.input.parse()?;
1594                                add = value.value;
1595                            } else if meta.path.is_ident("after") {
1596                                let _: syn::Token![=] = meta.input.parse()?;
1597                                let value: syn::LitBool = meta.input.parse()?;
1598                                after = value.value;
1599                            } else {
1600                                return Err(meta.error(
1601                                    "unknown r_on_exit option; expected `expr`, `add`, or `after`",
1602                                ));
1603                            }
1604                            Ok(())
1605                        })?;
1606                        let expr = expr.ok_or_else(|| {
1607                            syn::Error::new_spanned(
1608                                &list,
1609                                "r_on_exit(...) requires `expr = \"...\"` specifying the R expression",
1610                            )
1611                        })?;
1612                        r_on_exit = Some(ROnExit { expr, add, after });
1613                    } else if let Some(ident) = list.path.get_ident() {
1614                        // Bool-flag parenthesized form (e.g. `strict(true)`) is not
1615                        // supported — write `strict` alone or `strict = true` instead.
1616                        let opt_name = ident.to_string();
1617                        return Err(syn::Error::new_spanned(
1618                            &list,
1619                            format!(
1620                                "`{opt_name}` does not accept parenthesized arguments. \
1621                                 Use `{opt_name}` alone or `{opt_name} = true/false`.",
1622                            ),
1623                        ));
1624                    } else {
1625                        // path(something) where path is not a single ident
1626                        return Err(syn::Error::new_spanned(
1627                            list,
1628                            format!(
1629                                "unrecognized nested option. Nested options are: {FN_NESTED_OPTIONS_HELP}"
1630                            ),
1631                        ));
1632                    }
1633                }
1634            }
1635        }
1636
1637        // Validate: `internal` and `noexport` are redundant together
1638        if internal && noexport {
1639            return Err(syn::Error::new(
1640                proc_macro2::Span::call_site(),
1641                "`internal` and `noexport` cannot be used together. \
1642                 `internal` already suppresses @export and also adds @keywords internal. \
1643                 Use `internal` alone to mark as internal, or `noexport` alone to only suppress export.",
1644            ));
1645        }
1646
1647        // Validate: `export` conflicts with `noexport` and `internal`
1648        if export && noexport {
1649            return Err(syn::Error::new(
1650                proc_macro2::Span::call_site(),
1651                "`export` and `noexport` are contradictory.",
1652            ));
1653        }
1654        if export && internal {
1655            return Err(syn::Error::new(
1656                proc_macro2::Span::call_site(),
1657                "`export` and `internal` are contradictory.",
1658            ));
1659        }
1660
1661        // Validate: `r_name` is incompatible with S3 naming (`s3(generic/class)`)
1662        if r_name.is_some() && (s3_generic.is_some() || s3_class.is_some()) {
1663            return Err(syn::Error::new(
1664                proc_macro2::Span::call_site(),
1665                "`r_name` cannot be used with `s3(generic = ..., class = ...)`. \
1666                 S3 method names are always `generic.class`. Use `generic` and `class` instead.",
1667            ));
1668        }
1669
1670        Ok(Self {
1671            force_worker: force_worker.unwrap_or(cfg!(feature = "worker-default")),
1672            force_invisible,
1673            check_interrupt,
1674            coerce_all: coerce_all.unwrap_or(cfg!(feature = "coerce-default")),
1675            rng,
1676            unwrap_in_r,
1677            no_preconditions: no_preconditions.unwrap_or(cfg!(feature = "fast-default")),
1678            no_call_attribution: no_call_attribution.unwrap_or(cfg!(feature = "fast-default")),
1679            return_pref,
1680            return_pref_span,
1681            s3_generic,
1682            s3_class,
1683            dots_spec,
1684            dots_span,
1685            lifecycle,
1686            strict: strict.unwrap_or(cfg!(feature = "strict-default")),
1687            internal,
1688            noexport,
1689            export,
1690            doc,
1691            c_symbol,
1692            r_name,
1693            r_entry,
1694            r_post_checks,
1695            r_on_exit,
1696        })
1697    }
1698}
1699// endregion