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