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