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 ¶m_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