Skip to main content

miniextendr_macros/
lib.rs

1//! # miniextendr-macros - Procedural macros for Rust <-> R interop
2//!
3//! This crate provides the procedural macros that power miniextendr's code
4//! generation. Most users should depend on `miniextendr-api` and use its
5//! re-exports, but this crate can be used directly when you only need macros.
6//!
7//! Primary macros and derives:
8//! - `#[miniextendr]` on functions, impl blocks, trait defs, and trait impls.
9//! - `#[r_ffi_checked]` for main-thread routing of C-ABI wrappers.
10//! - Derives: `ExternalPtr`, `RNativeType`, ALTREP derives, `RFactor`.
11//! - Helpers: `typed_list` for typed list builders.
12//!
13//! R wrapper generation is driven by Rust doc comments (roxygen tags are
14//! extracted). The `document` binary collects these wrappers and writes
15//! `R/miniextendr_wrappers.R` during package build.
16//!
17//! ## Quick start
18//!
19//! ```ignore
20//! use miniextendr_api::miniextendr;
21//!
22//! #[miniextendr]
23//! fn add(a: i32, b: i32) -> i32 {
24//!     a + b
25//! }
26//! ```
27//!
28//! ## Macro expansion pipeline
29//!
30//! ### Overview
31//!
32//! ```text
33//! ┌──────────────────────────────────────────────────────────────────────────┐
34//! │                         #[miniextendr] on fn                             │
35//! │                                                                          │
36//! │  1. Parse: syn::ItemFn → MiniextendrFunctionParsed                       │
37//! │  2. Analyze return type (Result<T>, Option<T>, raw SEXP, etc.)           │
38//! │  3. Generate:                                                            │
39//! │     ├── C wrapper: extern "C-unwind" fn C_<name>(call: SEXP, ...) → SEXP │
40//! │     ├── R wrapper: const R_WRAPPER_<NAME>: &str = "..."                  │
41//! │     └── Registration: const call_method_def_<name>: R_CallMethodDef      │
42//! │  4. Original function preserved (with added attributes)                  │
43//! └──────────────────────────────────────────────────────────────────────────┘
44//!
45//! ┌──────────────────────────────────────────────────────────────────────────┐
46//! │                    #[miniextendr(env|r6|s3|s4|s7)] on impl               │
47//! │                                                                          │
48//! │  1. Parse: syn::ItemImpl → extract methods                               │
49//! │  2. For each method:                                                     │
50//! │     ├── Generate C wrapper (handles self parameter)                      │
51//! │     ├── Generate R method wrapper string                                 │
52//! │     └── Generate registration entry                                      │
53//! │  3. Generate class definition code per class system:                     │
54//! │     ├── env: new.env() + method assignment                               │
55//! │     ├── r6: R6Class() definition                                         │
56//! │     ├── s3: S3 generics + methods                                        │
57//! │     ├── s4: setClass() + setMethod()                                     │
58//! │     └── s7: new_class() definition                                       │
59//! │  4. Emit const with combined R code                                      │
60//! └──────────────────────────────────────────────────────────────────────────┘
61//!
62//! ┌──────────────────────────────────────────────────────────────────────────┐
63//! │                         #[miniextendr] on trait                          │
64//! │                                                                          │
65//! │  1. Parse: syn::ItemTrait → extract method signatures                    │
66//! │  2. Generate:                                                            │
67//! │     ├── Trait tag constant: const TAG_<TRAIT>: mx_tag = ...              │
68//! │     ├── Vtable struct: struct __vtable_<Trait> { ... }                   │
69//! │     └── CCalls table: static MX_CCALL_<TRAIT>: [...] = ...               │
70//! │  3. Original trait preserved                                             │
71//! └──────────────────────────────────────────────────────────────────────────┘
72//!
73//! ┌──────────────────────────────────────────────────────────────────────────┐
74//! │                    #[miniextendr] impl Trait for Type                    │
75//! │                                                                          │
76//! │  1. Parse: syn::ItemImpl (trait impl)                                    │
77//! │  2. Generate:                                                            │
78//! │     ├── Vtable instance: static __VTABLE_<TRAIT>_FOR_<TYPE>: ...         │
79//! │     ├── Wrapper struct: struct __MxWrapper<Type> { erased, data }        │
80//! │     ├── Query function: fn __mx_query_<type>(tag) → vtable ptr           │
81//! │     └── Base vtable: static __MX_BASE_VTABLE_<TYPE>: ...                 │
82//! │  3. Original impl preserved                                              │
83//! └──────────────────────────────────────────────────────────────────────────┘
84//!
85//! ```
86//!
87//! ### Key Modules
88//!
89//! | Module | Purpose |
90//! |--------|---------|
91//! | `miniextendr_fn` | Function parsing and attribute handling |
92//! | `c_wrapper_builder` | C wrapper generation (`extern "C-unwind"`) |
93//! | `r_wrapper_builder` | R wrapper code generation |
94//! | `rust_conversion_builder` | Rust→SEXP return value conversion |
95//! | `miniextendr_impl` | `impl Type` block processing |
96//! | `r_class_formatter` | Class system code generation (env/r6/s3/s4/s7) |
97//! | `miniextendr_trait` | Trait ABI metadata generation |
98//! | `miniextendr_impl_trait` | `impl Trait for Type` vtable generation |
99//! | `altrep` / `altrep_derive` | ALTREP struct derivation |
100//! | `externalptr_derive` | `#[derive(ExternalPtr)]` |
101//! | `roxygen` | Roxygen doc comment handling |
102//!
103//! ### Generated Symbol Naming
104//!
105//! For a function `my_func`:
106//! - C wrapper: `C_my_func`
107//! - R wrapper const: `R_WRAPPER_MY_FUNC`
108//! - Registration: `call_method_def_my_func`
109//!
110//! For a type `MyType` with trait `Counter`:
111//! - Vtable: `__VTABLE_COUNTER_FOR_MYTYPE`
112//! - Wrapper: `__MxWrapperMyType`
113//! - Query: `__mx_query_mytype`
114//!
115//! ## Return Type Handling
116//!
117//! The `return_type_analysis` module determines how to convert Rust returns to SEXP:
118//!
119//! | Rust Type | Strategy | R Result |
120//! |-----------|----------|----------|
121//! | `T: IntoR` | `.into_sexp()` | Converted value |
122//! | `Result<T, E>` | Unwrap or R error | Value or error |
123//! | `Option<T>` | `Some` → value, `None` → `NULL` | Value or NULL |
124//! | `SEXP` | Pass through | Raw SEXP |
125//! | `()` | Invisible NULL | `invisible(NULL)` |
126//!
127//! Use `#[miniextendr(unwrap_in_r)]` to return `Result<T, E>` to R without unwrapping.
128//!
129//! ## Thread Strategy
130//!
131//! By default, `#[miniextendr]` functions run on a **worker thread** for clean panic handling.
132//! The macro automatically switches to **main thread** when it detects:
133//!
134//! - Function takes or returns `SEXP`
135//! - Function uses variadic dots (`...`)
136//! - `check_interrupt` attribute is set
137//!
138//! ### When to use `#[miniextendr(unsafe(main_thread))]`
139//!
140//! Only use this attribute when your function calls R API **directly** using
141//! `_unchecked` variants (bypassing `with_r_thread`) and the macro can't auto-detect it.
142//! This is rare:
143//!
144//! ```rust,ignore
145//! // NEEDED: Calls _unchecked R API, but signature doesn't show it
146//! #[miniextendr(unsafe(main_thread))]
147//! fn call_r_api_internally() -> i32 {
148//!     // _unchecked variants assume we're on main thread
149//!     unsafe { miniextendr_api::ffi::Rf_ScalarInteger_unchecked(42); }
150//!     42
151//! }
152//!
153//! // NOT NEEDED: Macro auto-detects SEXP return
154//! #[miniextendr]
155//! fn returns_sexp() -> SEXP { /* ... */ }
156//!
157//! // NOT NEEDED: ExternalPtr is Send, can cross thread boundary
158//! #[miniextendr]
159//! fn returns_extptr() -> ExternalPtr<MyType> { /* ... */ }
160//! ```
161//!
162//! **Note**: `ExternalPtr<T>` is `Send` - it can be returned from worker thread functions.
163//! All R API operations on ExternalPtr are serialized through `with_r_thread`.
164//!
165//! ## Class Systems
166//!
167//! The `r_class_formatter` module generates R code for different class systems:
168//!
169//! | System | Generated R Code | Self Parameter |
170//! |--------|------------------|----------------|
171//! | `env` | `new.env()` with methods | `self` environment |
172//! | `r6` | `R6Class()` | `self` environment |
173//! | `s3` | `structure()` + generics | First argument |
174//! | `s4` | `setClass()` + `setMethod()` | First argument |
175//! | `s7` | `new_class()` | `self` property |
176
177// miniextendr-macros procedural macros
178
179mod altrep;
180mod c_wrapper_builder;
181mod list_macro;
182mod miniextendr_fn;
183mod typed_list;
184use crate::miniextendr_fn::{MiniextendrFnAttrs, MiniextendrFunctionParsed};
185mod miniextendr_impl;
186mod r_wrapper_builder;
187/// Builder utilities for formatting R wrapper arguments and calls.
188pub(crate) use r_wrapper_builder::RArgumentBuilder;
189mod rust_conversion_builder;
190/// Helper for generating Rust→R conversion code for return values.
191pub(crate) use rust_conversion_builder::RustConversionBuilder;
192mod method_return_builder;
193/// Helpers for shaping method return handling (R vs Rust wrapper code).
194pub(crate) use method_return_builder::{MethodReturnBuilder, ReturnStrategy};
195mod altrep_derive;
196mod dataframe_derive;
197mod lifecycle;
198mod list_derive;
199mod r_class_formatter;
200mod r_preconditions;
201mod return_type_analysis;
202mod roxygen;
203
204// Trait ABI support modules
205mod externalptr_derive;
206mod miniextendr_impl_trait;
207mod miniextendr_trait;
208mod typed_external_macro;
209
210// Factor support
211mod factor_derive;
212mod match_arg_derive;
213
214// Struct/enum dispatch for #[miniextendr] on structs and enums
215mod struct_enum_dispatch;
216
217// vctrs support
218#[cfg(feature = "vctrs")]
219mod vctrs_derive;
220
221pub(crate) use miniextendr_macros_core::{call_method_def_ident_for, r_wrapper_const_ident_for};
222
223// Feature default mutual exclusivity guards
224#[cfg(all(feature = "default-r6", feature = "default-s7"))]
225compile_error!("`default-r6` and `default-s7` are mutually exclusive");
226// Note: default-main-thread was removed — main thread is now the hardcoded default.
227// default-worker still opts into worker thread execution.
228
229// normalize_r_arg_ident is now provided by r_wrapper_builder module
230
231/// Extract `#[cfg(...)]` attributes from a list of attributes.
232///
233/// These should be propagated to generated items so they are conditionally
234/// compiled along with the original function.
235fn extract_cfg_attrs(attrs: &[syn::Attribute]) -> Vec<syn::Attribute> {
236    attrs
237        .iter()
238        .filter(|attr| attr.path().is_ident("cfg"))
239        .cloned()
240        .collect()
241}
242
243/// Format a human-readable source location note from a syntax span.
244///
245/// Column is reported as 1-based for consistency with editor displays.
246pub(crate) fn source_location_doc(span: proc_macro2::Span) -> String {
247    let start = span.start();
248    format!(
249        "Generated from source location line {}, column {}.",
250        start.line,
251        start.column + 1
252    )
253}
254
255/// Build a `TokenStream` containing a raw string literal from an R wrapper string.
256pub(crate) fn r_wrapper_raw_literal(s: &str) -> proc_macro2::TokenStream {
257    use std::str::FromStr;
258    let raw = format!("r#\"\n{}\n\"#", s);
259    proc_macro2::TokenStream::from_str(&raw).expect("valid raw string literal")
260}
261
262/// Returns the first generic type argument from a path segment.
263fn first_type_argument(seg: &syn::PathSegment) -> Option<&syn::Type> {
264    nth_type_argument(seg, 0)
265}
266
267/// Returns the second generic type argument from a path segment.
268fn second_type_argument(seg: &syn::PathSegment) -> Option<&syn::Type> {
269    nth_type_argument(seg, 1)
270}
271
272/// Returns the `n`-th generic type argument from a path segment.
273fn nth_type_argument(seg: &syn::PathSegment, n: usize) -> Option<&syn::Type> {
274    if let syn::PathArguments::AngleBracketed(ab) = &seg.arguments {
275        let mut count = 0;
276        for arg in ab.args.iter() {
277            if let syn::GenericArgument::Type(ty) = arg {
278                if count == n {
279                    return Some(ty);
280                }
281                count += 1;
282            }
283        }
284    }
285    None
286}
287
288#[inline]
289/// Returns true if `ty` is syntactically `SEXP`.
290fn is_sexp_type(ty: &syn::Type) -> bool {
291    matches!(ty, syn::Type::Path(p) if p
292        .path
293        .segments
294        .last()
295        .map(|s| s.ident == "SEXP")
296        .unwrap_or(false))
297}
298
299/// Known vctrs S3 generics that need `@importFrom vctrs` for roxygen registration.
300///
301/// This list includes all generics exported by vctrs that users might implement
302/// S3 methods for when creating custom vector types.
303const VCTRS_GENERICS: &[&str] = &[
304    // Core proxy/restore (required for most custom types)
305    "vec_proxy",
306    "vec_restore",
307    // Type coercion (required for vec_c, vec_rbind, etc.)
308    "vec_ptype2",
309    "vec_cast",
310    // Equality/comparison/ordering proxies
311    "vec_proxy_equal",
312    "vec_proxy_compare",
313    "vec_proxy_order",
314    // Printing/formatting
315    "vec_ptype_abbr",
316    "vec_ptype_full",
317    "obj_print_data",
318    "obj_print_footer",
319    "obj_print_header",
320    // str() output
321    "obj_str_data",
322    "obj_str_footer",
323    "obj_str_header",
324    // Arithmetic (for numeric-like types)
325    "vec_arith",
326    "vec_math",
327    // Other
328    "vec_ptype_finalise",
329    "vec_cbind_frame_ptype",
330    // List-of conversion
331    "as_list_of",
332];
333
334/// Check if a generic name is a vctrs generic that needs `@importFrom vctrs`.
335#[inline]
336fn is_vctrs_generic(generic: &str) -> bool {
337    VCTRS_GENERICS.contains(&generic)
338}
339
340/// Export Rust items to R.
341///
342/// `#[miniextendr]` can be applied to:
343/// - `fn` items (generate C + R wrappers)
344/// - `impl` blocks (generate R class methods)
345/// - `trait` items (generate trait ABI metadata)
346/// - ALTREP wrapper structs (generate `RegisterAltrep` impls)
347///
348/// # Functions
349///
350/// ```ignore
351/// use miniextendr_api::miniextendr;
352///
353/// #[miniextendr]
354/// fn add(a: i32, b: i32) -> i32 { a + b }
355/// ```
356///
357/// This produces a C wrapper `C_add` and an R wrapper `add()`.
358/// Registration is automatic via linkme distributed slices.
359///
360/// ## `extern "C-unwind"`
361///
362/// If the function is declared `extern "C-unwind"` and exported with
363/// `#[no_mangle]` (2021), `#[unsafe(no_mangle)]` (2024), or `#[export_name = "..."]`,
364/// the function itself is the C symbol and the R wrapper is prefixed with
365/// `unsafe_` to signal bypassed safety (no worker isolation or conversion).
366///
367/// ## Variadics (`...`)
368///
369/// Use `...` as the last argument. The Rust parameter becomes `_dots: &Dots`.
370/// Use `name @ ...` to give it a custom name (e.g., `args @ ...` → `args: &Dots`).
371///
372/// ### Typed Dots Validation
373///
374/// Use `#[miniextendr(dots = typed_list!(...))]` to automatically validate dots
375/// and create a `dots_typed` variable with typed accessors:
376///
377/// ```ignore
378/// #[miniextendr(dots = typed_list!(x => numeric(), y => integer(), z? => character()))]
379/// pub fn my_func(...) -> String {
380///     let x: f64 = dots_typed.get("x").expect("x");
381///     let y: i32 = dots_typed.get("y").expect("y");
382///     let z: Option<String> = dots_typed.get_opt("z").expect("z");
383///     format!("x={}, y={}", x, y)
384/// }
385/// ```
386///
387/// Type specs: `numeric()`, `integer()`, `logical()`, `character()`, `list()`,
388/// `raw()`, `complex()`, or `"class_name"` for class inheritance checks.
389/// Add `(n)` for exact length: `numeric(4)`. Use `?` suffix for optional fields.
390/// Use `@exact;` prefix for strict mode (reject extra fields).
391///
392/// ## Attributes
393///
394/// - `#[miniextendr(unsafe(main_thread))]` — run on R's main thread (bypass worker)
395/// - `#[miniextendr(invisible)]` / `#[miniextendr(visible)]` — control return visibility
396/// - `#[miniextendr(check_interrupt)]` — check for user interrupt after call
397/// - `#[miniextendr(coerce)]` — coerce R type before conversion (also usable per-parameter)
398/// - `#[miniextendr(strict)]` — reject lossy conversions for i64/u64/isize/usize
399/// - `#[miniextendr(unwrap_in_r)]` — return `Result<T, E>` to R without unwrapping
400/// - `#[miniextendr(dots = typed_list!(...))]` — validate dots, create `dots_typed`
401/// - `#[miniextendr(internal)]` — adds `@keywords internal` to R wrapper
402/// - `#[miniextendr(noexport)]` — suppresses `@export` from R wrapper
403///
404/// # Impl blocks (class systems)
405///
406/// Apply `#[miniextendr(env|r6|s7|s3|s4)]` to an `impl Type` block.
407/// Use `#[miniextendr(label = "...")]` to disambiguate multiple impl blocks
408/// on the same type.
409/// Registration is automatic.
410///
411/// ## R6 Active Bindings
412///
413/// For R6 classes, use `#[miniextendr(r6(active))]` on methods to create
414/// active bindings (computed properties accessed without parentheses):
415///
416/// ```ignore
417/// use miniextendr_api::miniextendr;
418///
419/// pub struct Rectangle {
420///     width: f64,
421///     height: f64,
422/// }
423///
424/// #[miniextendr(r6)]
425/// impl Rectangle {
426///     pub fn new(width: f64, height: f64) -> Self {
427///         Self { width, height }
428///     }
429///
430///     /// Returns the area (width * height).
431///     #[miniextendr(r6(active))]
432///     pub fn area(&self) -> f64 {
433///         self.width * self.height
434///     }
435///
436///     /// Regular method (requires parentheses).
437///     pub fn scale(&mut self, factor: f64) {
438///         self.width *= factor;
439///         self.height *= factor;
440///     }
441/// }
442/// ```
443///
444/// In R:
445/// ```r
446/// r <- Rectangle$new(3, 4)
447/// r$area        # 12 (active binding - no parentheses!)
448/// r$scale(2)    # Regular method call
449/// r$area        # 24
450/// ```
451///
452/// Active bindings must be getter-only methods taking only `&self`.
453///
454/// ## S7 Properties
455///
456/// For S7 classes, use `#[miniextendr(s7(getter))]` and `#[miniextendr(s7(setter))]`
457/// to create computed properties accessed via `@`:
458///
459/// ```ignore
460/// use miniextendr_api::{miniextendr, ExternalPtr};
461///
462/// #[derive(ExternalPtr)]
463/// pub struct Range {
464///     start: f64,
465///     end: f64,
466/// }
467///
468/// #[miniextendr(s7)]
469/// impl Range {
470///     pub fn new(start: f64, end: f64) -> Self {
471///         Self { start, end }
472///     }
473///
474///     /// Computed property (read-only): length of the range.
475///     #[miniextendr(s7(getter))]
476///     pub fn length(&self) -> f64 {
477///         self.end - self.start
478///     }
479///
480///     /// Dynamic property getter.
481///     #[miniextendr(s7(getter, prop = "midpoint"))]
482///     pub fn get_midpoint(&self) -> f64 {
483///         (self.start + self.end) / 2.0
484///     }
485///
486///     /// Dynamic property setter.
487///     #[miniextendr(s7(setter, prop = "midpoint"))]
488///     pub fn set_midpoint(&mut self, value: f64) {
489///         let half = self.length() / 2.0;
490///         self.start = value - half;
491///         self.end = value + half;
492///     }
493/// }
494/// ```
495///
496/// In R:
497/// ```r
498/// r <- Range(0, 10)
499/// r@length     # 10 (computed, read-only)
500/// r@midpoint   # 5 (dynamic property)
501/// r@midpoint <- 20  # Adjusts start/end to center at 20
502/// ```
503///
504/// ### Property Attributes
505///
506/// - `#[miniextendr(s7(getter))]` - Read-only computed property
507/// - `#[miniextendr(s7(getter, prop = "name"))]` - Named property getter
508/// - `#[miniextendr(s7(setter, prop = "name"))]` - Named property setter
509/// - `#[miniextendr(s7(getter, default = "0.0"))]` - Property with default value
510/// - `#[miniextendr(s7(getter, required))]` - Required property (error if not provided)
511/// - `#[miniextendr(s7(getter, frozen))]` - Property that can only be set once
512/// - `#[miniextendr(s7(getter, deprecated = "Use X instead"))]` - Deprecated property
513/// - `#[miniextendr(s7(validate))]` - Validator function for property
514///
515/// ## S7 Generic Dispatch Control
516///
517/// Control how S7 generics are created:
518///
519/// - `#[miniextendr(s7(no_dots))]` - Create strict generic without `...`
520/// - `#[miniextendr(s7(dispatch = "x,y"))]` - Multi-dispatch on multiple arguments
521/// - `#[miniextendr(s7(fallback))]` - Register method for `class_any` (catch-all).
522///   The generated R wrapper uses `tryCatch(x@.ptr, error = function(e) x)` to
523///   safely extract the self argument, so non-miniextendr objects won't crash with
524///   a slot-access error. Instead, incompatible objects produce a Rust type-conversion
525///   error when the method tries to interpret the argument as `&Self`.
526///
527/// ```ignore
528/// #[miniextendr(s7)]
529/// impl MyClass {
530///     /// Strict generic: function(x) instead of function(x, ...)
531///     #[miniextendr(s7(no_dots))]
532///     pub fn strict_method(&self) -> i32 { 42 }
533///
534///     /// Fallback method dispatched on class_any.
535///     /// Calling this on a non-MyClass object produces a type-conversion error,
536///     /// not a slot-access crash.
537///     #[miniextendr(s7(fallback))]
538///     pub fn describe(&self) -> String { "generic description".into() }
539/// }
540/// ```
541///
542/// ## S7 Type Conversion (`convert`)
543///
544/// Use `convert_from` and `convert_to` to enable S7's `convert()` for type coercion:
545///
546/// ```ignore
547/// use miniextendr_api::{miniextendr, ExternalPtr};
548///
549/// #[derive(ExternalPtr)]
550/// pub struct Celsius { value: f64 }
551///
552/// #[derive(ExternalPtr)]
553/// pub struct Fahrenheit { value: f64 }
554///
555/// #[miniextendr(s7)]
556/// impl Fahrenheit {
557///     pub fn new(value: f64) -> Self { Self { value } }
558///
559///     /// Convert FROM Celsius TO Fahrenheit.
560///     /// Usage: S7::convert(celsius_obj, Fahrenheit)
561///     #[miniextendr(s7(convert_from = "Celsius"))]
562///     pub fn from_celsius(c: ExternalPtr<Celsius>) -> Self {
563///         Fahrenheit { value: c.value * 9.0 / 5.0 + 32.0 }
564///     }
565///
566///     /// Convert FROM Fahrenheit TO Celsius.
567///     /// Usage: S7::convert(fahrenheit_obj, Celsius)
568///     #[miniextendr(s7(convert_to = "Celsius"))]
569///     pub fn to_celsius(&self) -> Celsius {
570///         Celsius { value: (self.value - 32.0) * 5.0 / 9.0 }
571///     }
572/// }
573/// ```
574///
575/// In R:
576/// ```r
577/// c <- Celsius(100)
578/// f <- S7::convert(c, Fahrenheit)  # Uses convert_from
579/// c2 <- S7::convert(f, Celsius)    # Uses convert_to
580/// ```
581///
582/// **Note:** Classes must be defined before they can be referenced in convert methods.
583/// Define the "from" class before the "to" class to avoid forward reference issues.
584///
585/// # Traits (ABI)
586///
587/// Apply `#[miniextendr]` to a trait to generate ABI metadata, then use
588/// `#[miniextendr] impl Trait for Type`. Registration is automatic.
589///
590/// # ALTREP
591///
592/// Apply `#[miniextendr(class = "...", base = "...")]` to a one-field
593/// wrapper struct. Registration is automatic.
594#[proc_macro_attribute]
595pub fn miniextendr(
596    attr: proc_macro::TokenStream,
597    item: proc_macro::TokenStream,
598) -> proc_macro::TokenStream {
599    // Try to parse as function first
600    if syn::parse::<syn::ItemFn>(item.clone()).is_ok() {
601        // Continue with function handling below
602    } else if syn::parse::<syn::ItemImpl>(item.clone()).is_ok() {
603        // Delegate to impl block parser
604        return miniextendr_impl::expand_impl(attr, item);
605    } else if syn::parse::<syn::ItemTrait>(item.clone()).is_ok() {
606        // Delegate to trait ABI generator
607        return miniextendr_trait::expand_trait(attr, item);
608    } else {
609        // Delegate to struct/enum dispatch (handles ALTREP, ExternalPtr, list, dataframe, factor, match_arg)
610        return struct_enum_dispatch::expand_struct_or_enum(attr, item);
611    }
612
613    let MiniextendrFnAttrs {
614        force_main_thread,
615        force_worker,
616        force_invisible,
617        check_interrupt,
618        coerce_all,
619        rng,
620        unwrap_in_r,
621        return_pref,
622        s3_generic,
623        s3_class,
624        dots_spec,
625        dots_span,
626        lifecycle,
627        strict,
628        internal,
629        noexport,
630        export,
631        doc,
632        error_in_r,
633        c_symbol,
634        r_name: fn_r_name,
635        r_entry,
636        r_post_checks,
637        r_on_exit,
638    } = syn::parse_macro_input!(attr as MiniextendrFnAttrs);
639
640    let mut parsed = syn::parse_macro_input!(item as MiniextendrFunctionParsed);
641
642    // Reject async functions
643    if let Some(asyncness) = &parsed.item().sig.asyncness {
644        return syn::Error::new_spanned(
645            asyncness,
646            "async functions are not supported by #[miniextendr]; \
647             R's C API is synchronous and incompatible with async executors",
648        )
649        .into_compile_error()
650        .into();
651    }
652
653    // Validate: reject generic functions (extern "C-unwind" + #[no_mangle] incompatible with generics)
654    if !parsed.item().sig.generics.params.is_empty() {
655        let err = syn::Error::new_spanned(
656            &parsed.item().sig.generics,
657            "#[miniextendr] functions cannot have generic type parameters. \
658             Generic functions are incompatible with `extern \"C-unwind\"` and `#[no_mangle]` \
659             required for R FFI. Consider using trait objects or monomorphization instead.",
660        );
661        return err.into_compile_error().into();
662    }
663
664    parsed.add_track_caller_if_needed();
665    parsed.add_inline_never_if_needed();
666
667    // Extract commonly used values
668    let uses_internal_c_wrapper = parsed.uses_internal_c_wrapper();
669    let call_method_def = parsed.call_method_def_ident();
670    let c_ident = if let Some(ref sym) = c_symbol {
671        syn::Ident::new(sym, parsed.c_wrapper_ident().span())
672    } else {
673        parsed.c_wrapper_ident()
674    };
675    let r_wrapper_generator = parsed.r_wrapper_const_ident();
676
677    use syn::spanned::Spanned;
678
679    // Extract references to parsed components
680    let rust_ident = parsed.ident();
681    let inputs = parsed.inputs();
682    let output = parsed.output();
683    let abi = parsed.abi();
684    let attrs = parsed.attrs();
685    let vis = parsed.vis();
686    let generics = parsed.generics();
687    let has_dots = parsed.has_dots();
688    let named_dots = parsed.named_dots().cloned();
689
690    // Check for @title/@description conflicts with implicit values (doc-lint feature)
691    // Skip when `doc` attribute overrides the roxygen — implicit docs are irrelevant then.
692    let doc_lint_warnings = if doc.is_some() {
693        proc_macro2::TokenStream::new()
694    } else {
695        crate::roxygen::doc_conflict_warnings(attrs, rust_ident.span())
696    };
697
698    let rust_arg_count = inputs.len();
699    let registered_arg_count = if uses_internal_c_wrapper {
700        rust_arg_count + 1
701    } else {
702        rust_arg_count
703    };
704    let num_args = syn::LitInt::new(&registered_arg_count.to_string(), inputs.span());
705
706    // name of the C-wrapper
707    let c_ident_name = syn::LitCStr::new(
708        std::ffi::CString::new(c_ident.to_string())
709            .expect("couldn't create a C-string for the C wrapper name")
710            .as_c_str(),
711        rust_ident.span(),
712    );
713    // registration of the C-wrapper
714    // these are needed to transmute fn-item to fn-pointer correctly.
715    let mut func_ptr_def: Vec<syn::Type> = Vec::new();
716    if uses_internal_c_wrapper {
717        func_ptr_def.push(syn::parse_quote!(::miniextendr_api::ffi::SEXP));
718    }
719    func_ptr_def.extend((0..inputs.len()).map(|_| syn::parse_quote!(::miniextendr_api::ffi::SEXP)));
720
721    // calling the rust function with
722    let rust_inputs: Vec<syn::Ident> = inputs
723        .iter()
724        .filter_map(|arg| {
725            if let syn::FnArg::Typed(pt) = arg
726                && let syn::Pat::Ident(p) = pt.pat.as_ref()
727            {
728                return Some(p.ident.clone());
729            }
730            None
731        })
732        .collect();
733    // dbg!(&rust_inputs);
734
735    // Hygiene: Use call_site() for internal variable names that should be visible to
736    // procedural macro machinery but not create unhygienic references to user code.
737    // call_site() = span of the macro invocation (#[miniextendr])
738    let call_param_ident = syn::Ident::new("__miniextendr_call", proc_macro2::Span::call_site());
739    let mut c_wrapper_inputs: Vec<syn::FnArg> = Vec::new();
740    if uses_internal_c_wrapper {
741        c_wrapper_inputs.push(syn::parse_quote!(#call_param_ident: ::miniextendr_api::ffi::SEXP));
742    }
743    for arg in inputs.iter() {
744        match arg {
745            syn::FnArg::Receiver(receiver) => {
746                let err = syn::Error::new_spanned(
747                    receiver,
748                    "self parameter not allowed in standalone functions; \
749                     use #[miniextendr(env|r6|s3|s4|s7)] on impl blocks instead",
750                );
751                return err.into_compile_error().into();
752            }
753            syn::FnArg::Typed(pt) => {
754                let pat = &pt.pat;
755                match pat.as_ref() {
756                    syn::Pat::Ident(pat_ident) => {
757                        let mut pat_ident = pat_ident.clone();
758                        pat_ident.mutability = None;
759                        pat_ident.by_ref = None;
760                        let ident = pat_ident;
761                        c_wrapper_inputs
762                            .push(syn::parse_quote!(#ident: ::miniextendr_api::ffi::SEXP));
763                    }
764                    syn::Pat::Wild(_) => {
765                        unreachable!(
766                            "wildcard patterns should have been transformed to synthetic identifiers"
767                        )
768                    }
769                    _ => {
770                        let err = syn::Error::new_spanned(
771                            pat,
772                            "unsupported pattern in function argument; only simple identifiers are supported",
773                        );
774                        return err.into_compile_error().into();
775                    }
776                }
777            }
778        }
779    }
780    // dbg!(&wrapper_inputs);
781    let mut pre_call_statements: Vec<proc_macro2::TokenStream> = Vec::new();
782    if check_interrupt {
783        pre_call_statements.push(quote::quote! {
784            unsafe { ::miniextendr_api::ffi::R_CheckUserInterrupt(); }
785        });
786    }
787
788    // Validate dots_spec usage (actual injection happens later in the function body)
789    if dots_spec.is_some() && !has_dots {
790        let err = syn::Error::new(
791            dots_span.unwrap_or_else(proc_macro2::Span::call_site),
792            "#[miniextendr(dots = typed_list!(...))] requires a `...` parameter in the function signature",
793        );
794        return err.into_compile_error().into();
795    }
796
797    // Build conversion builder with coercion settings
798    let mut conversion_builder = RustConversionBuilder::new();
799    if strict {
800        conversion_builder = conversion_builder.with_strict();
801    }
802    if error_in_r {
803        conversion_builder = conversion_builder.with_error_in_r();
804    }
805    if coerce_all {
806        conversion_builder = conversion_builder.with_coerce_all();
807    }
808    for input in inputs.iter() {
809        if let syn::FnArg::Typed(pt) = input
810            && let syn::Pat::Ident(pat_ident) = pt.pat.as_ref()
811        {
812            let param_name = pat_ident.ident.to_string();
813            if parsed.has_coerce_attr(&param_name) {
814                conversion_builder = conversion_builder.with_coerce_param(param_name);
815            }
816        }
817    }
818
819    // Generate conversion statements (split for worker thread compatibility)
820    // pre_closure: runs on main thread, produces owned values to move
821    // in_closure: runs inside worker closure, creates borrows from moved storage
822    let (pre_closure_stmts, in_closure_stmts): (Vec<_>, Vec<_>) = inputs
823        .iter()
824        .zip(rust_inputs.iter())
825        .filter_map(|(arg, sexp_ident)| {
826            if let syn::FnArg::Typed(pat_type) = arg {
827                Some(conversion_builder.build_conversion_split(pat_type, sexp_ident))
828            } else {
829                None
830            }
831        })
832        .fold(
833            (Vec::new(), Vec::new()),
834            |(mut pre, mut in_c), (owned, borrowed)| {
835                pre.extend(owned);
836                in_c.extend(borrowed);
837                (pre, in_c)
838            },
839        );
840    // For main thread paths (no split needed), flatten both into closure_statements
841    let closure_statements: Vec<_> = pre_closure_stmts
842        .iter()
843        .chain(in_closure_stmts.iter())
844        .cloned()
845        .collect();
846
847    // Hygiene: Use mixed_site() for internal variables that need to reference both
848    // macro-generated items (quote!) and user-provided items from the original function.
849    // mixed_site() = allows capturing both hygiene contexts for cross-context references.
850    let rust_result_ident =
851        syn::Ident::new("__miniextendr_rust_result", proc_macro2::Span::mixed_site());
852
853    // Analyze return type to determine:
854    // - Whether it returns SEXP (affects thread strategy)
855    // - Whether result should be invisible
856    // - How to convert Rust → SEXP
857    // - Post-call processing (unwrap Option/Result)
858    let return_analysis = return_type_analysis::analyze_return_type(
859        output,
860        &rust_result_ident,
861        rust_ident,
862        return_pref,
863        unwrap_in_r,
864        strict,
865        error_in_r,
866    );
867
868    let returns_sexp = return_analysis.returns_sexp;
869    let is_invisible_return_type = return_analysis.is_invisible;
870    let return_expression = return_analysis.return_expression;
871    let post_call_statements = return_analysis.post_call_statements;
872
873    // Apply explicit visibility override from #[miniextendr(invisible)] or #[miniextendr(visible)]
874    let is_invisible_return_type = force_invisible.unwrap_or(is_invisible_return_type);
875
876    // Check if any input parameter is SEXP (not Send, must stay on main thread)
877    let has_sexp_inputs = inputs.iter().any(|arg| {
878        if let syn::FnArg::Typed(pat_type) = arg {
879            is_sexp_type(pat_type.ty.as_ref())
880        } else {
881            false
882        }
883    });
884
885    // ═══════════════════════════════════════════════════════════════════════════
886    // Thread Strategy Selection
887    // ═══════════════════════════════════════════════════════════════════════════
888    //
889    // miniextendr supports two execution strategies:
890    //
891    // 1. **Main Thread Strategy** (with_r_unwind_protect) — DEFAULT
892    //    - All code runs on R's main thread
893    //    - Required when SEXP types are involved (not Send)
894    //    - Required for R API calls (Rf_*, R_*)
895    //    - Panic handling via R_UnwindProtect (Rust destructors run correctly)
896    //    - Combined with error_in_r: errors returned as tagged SEXP values,
897    //      R wrapper raises structured condition objects
898    //    - Simpler execution model, better R integration
899    //
900    // 2. **Worker Thread Strategy** (run_on_worker + catch_unwind) — OPT-IN
901    //    - Argument conversion on main thread (SEXP → Rust types)
902    //    - Function execution on dedicated worker thread (clean panic isolation)
903    //    - Result conversion on main thread (Rust types → SEXP)
904    //    - Panic handling via catch_unwind (prevents unwinding across FFI boundary)
905    //    - Opt in with #[miniextendr(worker)]
906    //    - ExternalPtr<T> is Send: can be returned from worker thread functions
907    //    - R API calls from worker use with_r_thread (serialized to main thread)
908    //
909    // Default: Main thread (safer with error_in_r, simpler execution model)
910    // Override: Use worker thread with #[miniextendr(worker)]
911    //
912    // Thread strategy:
913    // - Main thread is always used unless force_worker is set
914    // - force_worker cannot override hard requirements for main thread
915    // - Hard requirements: returns_sexp, has_sexp_inputs, has_dots, check_interrupt
916    let requires_main_thread = returns_sexp || has_sexp_inputs || has_dots || check_interrupt;
917    let use_main_thread = !force_worker || requires_main_thread;
918
919    // Suppress unused variable warnings — force_main_thread is now the default,
920    // and force_worker is consumed in use_main_thread above.
921    let _ = (force_main_thread, force_worker);
922    // RNG state management tokens
923    let (rng_get, rng_put) = if rng {
924        (
925            quote::quote! { unsafe { ::miniextendr_api::ffi::GetRNGstate(); } },
926            quote::quote! { unsafe { ::miniextendr_api::ffi::PutRNGstate(); } },
927        )
928    } else {
929        (
930            proc_macro2::TokenStream::new(),
931            proc_macro2::TokenStream::new(),
932        )
933    };
934    let source_loc_doc = source_location_doc(rust_ident.span());
935
936    // Select unwind protection function: error_in_r returns tagged error values on panic,
937    // standard mode raises R errors via Rf_errorcall.
938    let unwind_protect_fn = if error_in_r {
939        quote::quote! { ::miniextendr_api::unwind_protect::with_r_unwind_protect_error_in_r }
940    } else {
941        quote::quote! { ::miniextendr_api::unwind_protect::with_r_unwind_protect }
942    };
943
944    let c_wrapper = if abi.is_some() {
945        proc_macro2::TokenStream::new()
946    } else if use_main_thread {
947        // SEXP-returning or Dots-taking functions: use with_r_unwind_protect on main thread
948        let c_wrapper_doc = format!(
949            "C wrapper for [`{}`] (main thread). See [`{}`] for R wrapper.",
950            rust_ident, r_wrapper_generator
951        );
952        if rng {
953            // RNG variant: wrap in catch_unwind so we can call PutRNGstate before error handling.
954            // rng always implies error_in_r (validated at parse time), so we always return
955            // a tagged error value on panic instead of resume_unwind.
956            let rng_panic_handler = quote::quote! {
957                ::miniextendr_api::error_value::make_rust_error_value(
958                    &::miniextendr_api::unwind_protect::panic_payload_to_string(&*payload),
959                    "panic",
960                    Some(#call_param_ident),
961                )
962            };
963            quote::quote! {
964                #[doc = #c_wrapper_doc]
965                #[doc = concat!("Wraps Rust function `", stringify!(#rust_ident), "`.")]
966                #[doc = #source_loc_doc]
967                #[doc = concat!("Generated from source file `", file!(), "`.")]
968                #[unsafe(no_mangle)]
969                #vis extern "C-unwind" fn #c_ident #generics(#(#c_wrapper_inputs),*) -> ::miniextendr_api::ffi::SEXP {
970                    #rng_get
971                    let __result = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| {
972                        #(#pre_call_statements)*
973                        #unwind_protect_fn(
974                            || {
975                                #(#closure_statements)*
976                                let #rust_result_ident = #rust_ident(#(#rust_inputs),*);
977                                #(#post_call_statements)*
978                                #return_expression
979                            },
980                            Some(#call_param_ident),
981                        )
982                    }));
983                    #rng_put
984                    match __result {
985                        Ok(sexp) => sexp,
986                        Err(payload) => { #rng_panic_handler },
987                    }
988                }
989            }
990        } else {
991            // Non-RNG variant: direct call to with_r_unwind_protect
992            quote::quote! {
993                #[doc = #c_wrapper_doc]
994                #[doc = concat!("Wraps Rust function `", stringify!(#rust_ident), "`.")]
995                #[doc = #source_loc_doc]
996                #[doc = concat!("Generated from source file `", file!(), "`.")]
997                #[unsafe(no_mangle)]
998                #vis extern "C-unwind" fn #c_ident #generics(#(#c_wrapper_inputs),*) -> ::miniextendr_api::ffi::SEXP {
999                    #(#pre_call_statements)*
1000
1001                    #unwind_protect_fn(
1002                        || {
1003                            #(#closure_statements)*
1004                            let #rust_result_ident = #rust_ident(#(#rust_inputs),*);
1005                            #(#post_call_statements)*
1006                            #return_expression
1007                        },
1008                        Some(#call_param_ident),
1009                    )
1010                }
1011            }
1012        }
1013    } else {
1014        // Pure Rust functions: use worker thread strategy.
1015        // Emit a compile-time check that the `worker-thread` feature is enabled.
1016        // Without it, `run_on_worker` is a stub that runs inline (silent degradation).
1017        // Check both `worker-thread` (direct) and `default-worker` (implies worker-thread
1018        // via miniextendr-api, but the user crate may only have the latter in its features).
1019        let worker_feature_check = {
1020            let fn_name = rust_ident.to_string();
1021            let msg = format!(
1022                "`#[miniextendr(worker)]` on `{fn_name}` requires the `worker-thread` cargo feature. \
1023                 Add `worker-thread = [\"miniextendr-api/worker-thread\"]` to your [features] in Cargo.toml."
1024            );
1025            quote::quote! {
1026                #[cfg(not(any(feature = "worker-thread", feature = "default-worker")))]
1027                compile_error!(#msg);
1028            }
1029        };
1030        let c_wrapper_doc = format!(
1031            "C wrapper for [`{}`] (worker thread). See [`{}`] for R wrapper.",
1032            rust_ident, r_wrapper_generator
1033        );
1034        let worker_panic_handler = if error_in_r {
1035            quote::quote! {
1036                ::miniextendr_api::error_value::make_rust_error_value(
1037                    &::miniextendr_api::unwind_protect::panic_payload_to_string(&*payload),
1038                    "panic",
1039                    Some(#call_param_ident),
1040                )
1041            }
1042        } else {
1043            quote::quote! {
1044                ::miniextendr_api::worker::panic_message_to_r_error(
1045                    ::miniextendr_api::unwind_protect::panic_payload_to_string(&*payload),
1046                    None,
1047                )
1048            }
1049        };
1050        if error_in_r {
1051            // error_in_r: run_on_worker returns Result; Err → tagged error value
1052            quote::quote! {
1053                #worker_feature_check
1054
1055                #[doc = #c_wrapper_doc]
1056                #[doc = concat!("Wraps Rust function `", stringify!(#rust_ident), "`.")]
1057                #[doc = #source_loc_doc]
1058                #[doc = concat!("Generated from source file `", file!(), "`.")]
1059                #[unsafe(no_mangle)]
1060                #vis extern "C-unwind" fn #c_ident #generics(#(#c_wrapper_inputs),*) -> ::miniextendr_api::ffi::SEXP {
1061                    #rng_get
1062                    let __miniextendr_panic_result = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(move || {
1063                        #(#pre_call_statements)*
1064                        #(#pre_closure_stmts)*
1065
1066                        match ::miniextendr_api::worker::run_on_worker(move || {
1067                            #(#in_closure_stmts)*
1068                            let #rust_result_ident = #rust_ident(#(#rust_inputs),*);
1069                            #(#post_call_statements)*
1070                            #rust_result_ident
1071                        }) {
1072                            Ok(#rust_result_ident) => {
1073                                #unwind_protect_fn(
1074                                    move || #return_expression,
1075                                    None,
1076                                )
1077                            }
1078                            Err(__panic_msg) => {
1079                                ::miniextendr_api::error_value::make_rust_error_value(
1080                                    &__panic_msg, "panic", Some(#call_param_ident),
1081                                )
1082                            }
1083                        }
1084                    }));
1085                    #rng_put
1086                    match __miniextendr_panic_result {
1087                        Ok(sexp) => sexp,
1088                        Err(payload) => { #worker_panic_handler },
1089                    }
1090                }
1091            }
1092        } else {
1093            // run_on_worker returns Result; Err → R error via Rf_error
1094            quote::quote! {
1095                #worker_feature_check
1096
1097                #[doc = #c_wrapper_doc]
1098                #[doc = concat!("Wraps Rust function `", stringify!(#rust_ident), "`.")]
1099                #[doc = #source_loc_doc]
1100                #[doc = concat!("Generated from source file `", file!(), "`.")]
1101                #[unsafe(no_mangle)]
1102                #vis extern "C-unwind" fn #c_ident #generics(#(#c_wrapper_inputs),*) -> ::miniextendr_api::ffi::SEXP {
1103                    #rng_get
1104                    let __miniextendr_panic_result = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(move || {
1105                        #(#pre_call_statements)*
1106                        #(#pre_closure_stmts)*
1107
1108                        match ::miniextendr_api::worker::run_on_worker(move || {
1109                            #(#in_closure_stmts)*
1110                            let #rust_result_ident = #rust_ident(#(#rust_inputs),*);
1111                            #(#post_call_statements)*
1112                            #rust_result_ident
1113                        }) {
1114                            Ok(#rust_result_ident) => {
1115                                #unwind_protect_fn(
1116                                    move || #return_expression,
1117                                    None,
1118                                )
1119                            }
1120                            Err(__panic_msg) => {
1121                                ::miniextendr_api::worker::panic_message_to_r_error(__panic_msg, None)
1122                            }
1123                        }
1124                    }));
1125                    #rng_put
1126                    match __miniextendr_panic_result {
1127                        Ok(sexp) => sexp,
1128                        Err(payload) => { #worker_panic_handler },
1129                    }
1130                }
1131            }
1132        }
1133    };
1134
1135    // check the validity of the provided C-function!
1136    if abi.is_some() {
1137        // check that #[no_mangle] or #[unsafe(no_mangle)] or #[export_name] is present!
1138        let has_no_mangle = attrs.iter().any(|attr| {
1139            attr.path().is_ident("no_mangle")
1140                || attr
1141                    .parse_nested_meta(|meta| {
1142                        if meta.path.is_ident("no_mangle") {
1143                            Err(meta.error("found #[no_mangle]"))
1144                        } else {
1145                            Ok(())
1146                        }
1147                    })
1148                    .is_err()
1149        });
1150
1151        let has_export_name = attrs.iter().any(|attr| attr.path().is_ident("export_name"));
1152
1153        if !has_no_mangle && !has_export_name {
1154            return syn::Error::new(
1155                attrs
1156                    .first()
1157                    .map(|attr| attr.span())
1158                    .unwrap_or_else(|| abi.span()),
1159                "extern \"C-unwind\" functions need a visible C symbol for R's .Call interface. \
1160                 Add one of:\n  \
1161                 - `#[unsafe(no_mangle)]` (Rust 2024 edition)\n  \
1162                 - `#[no_mangle]` (Rust 2021 edition)\n  \
1163                 - `#[export_name = \"my_symbol\"]` (custom symbol name)",
1164            )
1165            .into_compile_error()
1166            .into();
1167        }
1168
1169        // Validate return type is SEXP for extern "C-unwind" functions
1170        match output {
1171            non_return_type @ syn::ReturnType::Default => {
1172                return syn::Error::new(
1173                    non_return_type.span(),
1174                    "extern \"C-unwind\" functions used with #[miniextendr] must return SEXP. \
1175                     Add `-> miniextendr_api::ffi::SEXP` as the return type. \
1176                     If you want automatic type conversion, remove `extern \"C-unwind\"` and let \
1177                     the macro generate the C wrapper.",
1178                )
1179                .into_compile_error()
1180                .into();
1181            }
1182            syn::ReturnType::Type(_rarrow, output_type) => match output_type.as_ref() {
1183                syn::Type::Path(type_path) => {
1184                    if let Some(path_to_sexp) = type_path.path.segments.last().map(|x| &x.ident)
1185                        && path_to_sexp != "SEXP"
1186                    {
1187                        return syn::Error::new(
1188                            path_to_sexp.span(),
1189                            format!(
1190                                "extern \"C-unwind\" functions must return SEXP, found `{}`. \
1191                                 R's .Call interface expects SEXP return values. \
1192                                 Change the return type to `miniextendr_api::ffi::SEXP`, or remove \
1193                                 `extern \"C-unwind\"` to let the macro handle type conversion.",
1194                                path_to_sexp,
1195                            ),
1196                        )
1197                        .into_compile_error()
1198                        .into();
1199                    }
1200                }
1201                _ => {
1202                    return syn::Error::new(
1203                        output_type.span(),
1204                        "extern \"C-unwind\" functions must return SEXP. \
1205                         R's .Call interface expects SEXP return values. \
1206                         Change the return type to `miniextendr_api::ffi::SEXP`, or remove \
1207                         `extern \"C-unwind\"` to let the macro handle type conversion.",
1208                    )
1209                    .into_compile_error()
1210                    .into();
1211                }
1212            },
1213        }
1214
1215        // Validate all input types are SEXP for extern "C-unwind" functions.
1216        // R's .Call interface passes all arguments as SEXP, so accepting other types is UB.
1217        // Also reject variadic (...) signatures which are not valid for .Call.
1218        for input in inputs.iter() {
1219            match input {
1220                syn::FnArg::Receiver(recv) => {
1221                    return syn::Error::new_spanned(
1222                        recv,
1223                        "extern \"C-unwind\" functions cannot have a `self` parameter. \
1224                         R's .Call interface only accepts SEXP arguments. \
1225                         Use `#[miniextendr(env|r6|s3|s4|s7)]` on an impl block for methods.",
1226                    )
1227                    .into_compile_error()
1228                    .into();
1229                }
1230                syn::FnArg::Typed(pat_type) => {
1231                    // Check if this is a variadic pattern (...)
1232                    if let syn::Pat::Rest(_) = pat_type.pat.as_ref() {
1233                        return syn::Error::new_spanned(
1234                            pat_type,
1235                            "extern functions cannot use variadic (...) - .Call passes fixed arguments",
1236                        )
1237                        .into_compile_error()
1238                        .into();
1239                    }
1240
1241                    // Validate type is SEXP
1242                    let is_sexp = match pat_type.ty.as_ref() {
1243                        syn::Type::Path(type_path) => type_path
1244                            .path
1245                            .segments
1246                            .last()
1247                            .is_some_and(|seg| seg.ident == "SEXP"),
1248                        _ => false,
1249                    };
1250
1251                    if !is_sexp {
1252                        // Check if the type looks like &Dots or Dots
1253                        let is_dots_type =
1254                            if let syn::Type::Reference(type_ref) = pat_type.ty.as_ref() {
1255                                if let syn::Type::Path(inner) = type_ref.elem.as_ref() {
1256                                    inner
1257                                        .path
1258                                        .segments
1259                                        .last()
1260                                        .is_some_and(|seg| seg.ident == "Dots")
1261                                } else {
1262                                    false
1263                                }
1264                            } else if let syn::Type::Path(type_path) = pat_type.ty.as_ref() {
1265                                type_path
1266                                    .path
1267                                    .segments
1268                                    .last()
1269                                    .is_some_and(|seg| seg.ident == "Dots")
1270                            } else {
1271                                false
1272                            };
1273
1274                        let msg = if is_dots_type {
1275                            "extern functions cannot use Dots; use `...` syntax in non-extern #[miniextendr] functions instead"
1276                        } else {
1277                            "extern function parameters must be SEXP - .Call passes all arguments as SEXP"
1278                        };
1279                        return syn::Error::new_spanned(&pat_type.ty, msg)
1280                            .into_compile_error()
1281                            .into();
1282                    }
1283                }
1284            }
1285        }
1286    }
1287
1288    // region: R wrappers generation in `fn`
1289    // Build R formal parameters and call arguments using shared builder
1290    let mut arg_builder = RArgumentBuilder::new(inputs);
1291    if has_dots {
1292        arg_builder = arg_builder.with_dots(named_dots.clone().map(|id| id.to_string()));
1293    }
1294    // Add user-specified parameter defaults (Missing<T> defaults handled via body prelude)
1295    let mut merged_defaults = parsed.param_defaults().clone();
1296    // Add NULL default for match_arg params that don't already have an explicit default
1297    for match_arg_param in parsed.match_arg_params() {
1298        let r_name = r_wrapper_builder::normalize_r_arg_ident(&syn::Ident::new(
1299            match_arg_param,
1300            proc_macro2::Span::call_site(),
1301        ))
1302        .to_string();
1303        merged_defaults
1304            .entry(r_name)
1305            .or_insert_with(|| "NULL".to_string());
1306    }
1307    // Add c("a", "b", "c") default for choices params (idiomatic R match.arg pattern)
1308    for (param_name, choices) in parsed.choices_params() {
1309        let r_name = r_wrapper_builder::normalize_r_arg_ident(&syn::Ident::new(
1310            param_name,
1311            proc_macro2::Span::call_site(),
1312        ))
1313        .to_string();
1314        let quoted: Vec<String> = choices.iter().map(|c| format!("\"{}\"", c)).collect();
1315        merged_defaults
1316            .entry(r_name)
1317            .or_insert_with(|| format!("c({})", quoted.join(", ")));
1318    }
1319    arg_builder = arg_builder.with_defaults(merged_defaults);
1320
1321    let r_formals = arg_builder.build_formals();
1322    let mut r_call_args_strs = arg_builder.build_call_args_vec();
1323
1324    // Prepend .call parameter if using internal C wrapper
1325    if uses_internal_c_wrapper {
1326        r_call_args_strs.insert(0, ".call = match.call()".to_string());
1327    }
1328
1329    // Build the R body string consistently
1330    let c_ident_str = c_ident.to_string();
1331    let call_args_joined = r_call_args_strs.join(", ");
1332    let call_expr = if r_call_args_strs.is_empty() {
1333        format!(".Call({})", c_ident_str)
1334    } else {
1335        format!(".Call({}, {})", c_ident_str, call_args_joined)
1336    };
1337    let r_wrapper_return_str = if error_in_r {
1338        // error_in_r mode: capture result, check for error value, raise R condition
1339        let final_return = if is_invisible_return_type {
1340            "invisible(.val)"
1341        } else {
1342            ".val"
1343        };
1344        crate::method_return_builder::error_in_r_standalone_body(&call_expr, final_return)
1345    } else if !is_invisible_return_type {
1346        call_expr
1347    } else {
1348        format!("invisible({})", call_expr)
1349    };
1350    // Determine R function name and S3-specific comments
1351    let is_s3_method = s3_generic.is_some() || s3_class.is_some();
1352    let r_wrapper_ident_str: String;
1353    let s3_method_comment: String;
1354
1355    if is_s3_method {
1356        // For S3 methods, function name is generic.class
1357        // generic defaults to Rust function name if not specified
1358        let generic = s3_generic.clone().unwrap_or_else(|| rust_ident.to_string());
1359        // s3_class is guaranteed to be Some here because MiniextendrFnAttrs::parse
1360        // validates that s3(...) always has class specified
1361        let class = s3_class.as_ref().expect("s3_class validated at parse time");
1362        r_wrapper_ident_str = format!("{}.{}", generic, class);
1363        // Add @importFrom for vctrs generics so roxygen registers the dependency
1364        let import_comment = if is_vctrs_generic(&generic) {
1365            format!("#' @importFrom vctrs {}\n", generic)
1366        } else {
1367            String::new()
1368        };
1369        s3_method_comment = format!("{}#' @method {} {}\n", import_comment, generic, class);
1370    } else if let Some(ref custom_name) = fn_r_name {
1371        r_wrapper_ident_str = custom_name.clone();
1372        s3_method_comment = String::new();
1373    } else if abi.is_some() {
1374        r_wrapper_ident_str = format!("unsafe_{}", rust_ident);
1375        s3_method_comment = String::new();
1376    } else {
1377        r_wrapper_ident_str = rust_ident.to_string();
1378        s3_method_comment = String::new();
1379    };
1380
1381    // Stable, consistent R formatting style: brace on same line, body indented, closing brace on its own line
1382    // r_formals is already a joined string from build_formals()
1383    let formals_joined = r_formals;
1384    let mut roxygen_tags = if let Some(ref doc_text) = doc {
1385        // Custom doc override: each line becomes a separate roxygen tag entry
1386        doc_text.lines().map(|l| l.to_string()).collect()
1387    } else {
1388        crate::roxygen::roxygen_tags_from_attrs(attrs)
1389    };
1390
1391    // Determine lifecycle: explicit attr > #[deprecated] extraction
1392    let lifecycle_spec = lifecycle.or_else(|| {
1393        attrs
1394            .iter()
1395            .find_map(crate::lifecycle::parse_rust_deprecated)
1396    });
1397
1398    // Inject lifecycle badge into roxygen tags if present
1399    if let Some(ref spec) = lifecycle_spec {
1400        crate::lifecycle::inject_lifecycle_badge(&mut roxygen_tags, spec);
1401    }
1402
1403    // Auto-generate @param tags for choices params (unless user already wrote one)
1404    for arg in inputs.iter() {
1405        if let syn::FnArg::Typed(pt) = arg
1406            && let syn::Pat::Ident(pat_ident) = pt.pat.as_ref()
1407        {
1408            let rust_name = pat_ident.ident.to_string();
1409            if let Some(choices) = parsed.choices_for_param(&rust_name) {
1410                let r_name = r_wrapper_builder::normalize_r_arg_ident(&pat_ident.ident).to_string();
1411                // Only inject if user didn't already write @param for this param
1412                let has_user_param = roxygen_tags
1413                    .iter()
1414                    .any(|t| t.trim_start().starts_with(&format!("@param {}", r_name)));
1415                if !has_user_param {
1416                    let quoted: Vec<String> =
1417                        choices.iter().map(|c| format!("\"{}\"", c)).collect();
1418                    roxygen_tags.push(format!("@param {} One of {}.", r_name, quoted.join(", ")));
1419                }
1420            }
1421        }
1422    }
1423
1424    // Auto-generate @param for any function parameter that doesn't have one yet.
1425    // This prevents R CMD check warnings about undocumented arguments.
1426    // Skip dots params (they become `...` in R formals, not a named param).
1427    for arg in inputs.iter() {
1428        if let syn::FnArg::Typed(pt) = arg
1429            && let syn::Pat::Ident(pat_ident) = pt.pat.as_ref()
1430        {
1431            // Skip dots parameters — they map to `...` in R, which can't have @param
1432            if parsed.is_dots_param(&pat_ident.ident) {
1433                continue;
1434            }
1435            let r_name = r_wrapper_builder::normalize_r_arg_ident(&pat_ident.ident).to_string();
1436            let has_param = roxygen_tags
1437                .iter()
1438                .any(|t| t.trim_start().starts_with(&format!("@param {r_name}")));
1439            if !has_param {
1440                roxygen_tags.push(format!("@param {r_name} (no documentation available)"));
1441            }
1442        }
1443    }
1444
1445    // Ensure a @title exists when we have auto-generated tags (e.g., @param from choices)
1446    // but the auto-title logic in roxygen_tags_from_attrs didn't fire (because has_any_tags
1447    // was false at that point — choices @param tags are added after extraction).
1448    // Prefer the implicit title from doc comments; fall back to the function name.
1449    if !roxygen_tags.is_empty() && !crate::roxygen::has_roxygen_tag(&roxygen_tags, "title") {
1450        let title = crate::roxygen::implicit_title_from_attrs(attrs)
1451            .unwrap_or_else(|| rust_ident.to_string().replace('_', " "));
1452        roxygen_tags.insert(0, format!("@title {}", title));
1453    }
1454
1455    let roxygen_tags_str = crate::roxygen::format_roxygen_tags(&roxygen_tags);
1456    let has_export_tag = crate::roxygen::has_roxygen_tag(&roxygen_tags, "export");
1457    let has_no_rd_tag = crate::roxygen::has_roxygen_tag(&roxygen_tags, "noRd");
1458    let has_internal_tag = crate::roxygen::has_roxygen_tag(&roxygen_tags, "keywords internal");
1459    // Add roxygen comments: @source for traceability, @export if public
1460    let source_comment = format!(
1461        "#' @source Generated by miniextendr from Rust fn `{}`\n",
1462        rust_ident
1463    );
1464    // Inject @keywords internal if #[miniextendr(internal)] and not already present
1465    let internal_comment = if internal && !has_internal_tag {
1466        "#' @keywords internal\n"
1467    } else {
1468        ""
1469    };
1470    // S3 methods need both @method (for registration) AND @export (for NAMESPACE)
1471    // Don't auto-export functions marked with @noRd, @keywords internal, or attr flags
1472    // #[miniextendr(export)] forces @export even on non-pub functions
1473    let export_comment = if (matches!(vis, syn::Visibility::Public(_)) || export)
1474        && !has_export_tag
1475        && !has_no_rd_tag
1476        && !has_internal_tag
1477        && !internal
1478        && !noexport
1479    {
1480        "#' @export\n".to_string()
1481    } else {
1482        String::new()
1483    };
1484    // Generate match.arg prelude for parameters with #[miniextendr(match_arg)]
1485    // Collect (r_param_name, rust_type) for each match_arg param
1486    let match_arg_param_info: Vec<(String, &syn::Type)> = inputs
1487        .iter()
1488        .filter_map(|arg| {
1489            if let syn::FnArg::Typed(pt) = arg
1490                && let syn::Pat::Ident(pat_ident) = pt.pat.as_ref()
1491            {
1492                let rust_name = pat_ident.ident.to_string();
1493                if parsed.has_match_arg_attr(&rust_name) {
1494                    let r_name =
1495                        r_wrapper_builder::normalize_r_arg_ident(&pat_ident.ident).to_string();
1496                    return Some((r_name, pt.ty.as_ref()));
1497                }
1498            }
1499            None
1500        })
1501        .collect();
1502
1503    let match_arg_prelude = if match_arg_param_info.is_empty() {
1504        String::new()
1505    } else {
1506        let mut lines = Vec::new();
1507        for (r_param, _) in &match_arg_param_info {
1508            // choices helper call
1509            let choices_c_name = format!(
1510                "C_{}__match_arg_choices__{}",
1511                c_ident.to_string().trim_start_matches("C_"),
1512                r_param
1513            );
1514            lines.push(format!(
1515                ".__mx_choices_{param} <- .Call({choices_c}, .call = match.call())",
1516                param = r_param,
1517                choices_c = choices_c_name,
1518            ));
1519            // factor → character normalization
1520            lines.push(format!(
1521                "{param} <- if (is.factor({param})) as.character({param}) else {param}",
1522                param = r_param,
1523            ));
1524            // match.arg validation
1525            lines.push(format!(
1526                "{param} <- base::match.arg({param}, .__mx_choices_{param})",
1527                param = r_param,
1528            ));
1529        }
1530        lines.join("\n  ")
1531    };
1532
1533    // Generate idiomatic match.arg prelude for choices params
1534    // These use the simpler pattern: `param <- match.arg(param)` (no C helper call needed)
1535    let choices_prelude = {
1536        let mut lines = Vec::new();
1537        for arg in inputs.iter() {
1538            if let syn::FnArg::Typed(pt) = arg
1539                && let syn::Pat::Ident(pat_ident) = pt.pat.as_ref()
1540            {
1541                let rust_name = pat_ident.ident.to_string();
1542                if parsed.choices_for_param(&rust_name).is_some() {
1543                    let r_name =
1544                        r_wrapper_builder::normalize_r_arg_ident(&pat_ident.ident).to_string();
1545                    lines.push(format!("{r_name} <- match.arg({r_name})"));
1546                }
1547            }
1548        }
1549        if lines.is_empty() {
1550            String::new()
1551        } else {
1552            lines.join("\n  ")
1553        }
1554    };
1555
1556    // Generate lifecycle prelude if needed
1557    let lifecycle_prelude = lifecycle_spec
1558        .as_ref()
1559        .and_then(|spec| spec.r_prelude(&r_wrapper_ident_str));
1560
1561    // Generate R-side precondition checks (stopifnot + fallback precheck calls)
1562    // Skip both match_arg and choices params (already validated by match.arg)
1563    let mut skip_params = parsed.match_arg_params().clone();
1564    for param_name in parsed.choices_params().keys() {
1565        let r_name = r_wrapper_builder::normalize_r_arg_ident(&syn::Ident::new(
1566            param_name,
1567            proc_macro2::Span::call_site(),
1568        ))
1569        .to_string();
1570        skip_params.insert(r_name);
1571    }
1572    let precondition_output = r_preconditions::build_precondition_checks(inputs, &skip_params);
1573    let precondition_prelude = if precondition_output.static_checks.is_empty() {
1574        String::new()
1575    } else {
1576        precondition_output.static_checks.join("\n  ")
1577    };
1578
1579    // Generate Missing<T> prelude: `if (missing(param)) param <- quote(expr=)`
1580    let missing_prelude = {
1581        let lines = r_wrapper_builder::build_missing_prelude(inputs, parsed.param_defaults());
1582        if lines.is_empty() {
1583            String::new()
1584        } else {
1585            lines.join("\n  ")
1586        }
1587    };
1588
1589    // Combine all preludes: r_entry, on.exit, missing defaults, lifecycle, static preconditions, match.arg, choices, r_post_checks
1590    let on_exit_str = r_on_exit.as_ref().map(|oe| oe.to_r_code());
1591    let combined_prelude = {
1592        let mut parts = Vec::new();
1593        if let Some(ref entry) = r_entry {
1594            parts.push(entry.as_str());
1595        }
1596        if let Some(ref s) = on_exit_str {
1597            parts.push(s.as_str());
1598        }
1599        if !missing_prelude.is_empty() {
1600            parts.push(missing_prelude.as_str());
1601        }
1602        if let Some(ref lc) = lifecycle_prelude {
1603            parts.push(lc.as_str());
1604        }
1605        if !precondition_prelude.is_empty() {
1606            parts.push(&precondition_prelude);
1607        }
1608        if !match_arg_prelude.is_empty() {
1609            parts.push(&match_arg_prelude);
1610        }
1611        if !choices_prelude.is_empty() {
1612            parts.push(&choices_prelude);
1613        }
1614        if let Some(ref post) = r_post_checks {
1615            parts.push(post.as_str());
1616        }
1617        if parts.is_empty() {
1618            None
1619        } else {
1620            Some(parts.join("\n  "))
1621        }
1622    };
1623
1624    let r_wrapper_string = if let Some(prelude) = combined_prelude {
1625        format!(
1626            "{}{}{}{}{}{} <- function({}) {{\n  {}\n  {}\n}}",
1627            roxygen_tags_str,
1628            source_comment,
1629            s3_method_comment,
1630            internal_comment,
1631            export_comment,
1632            r_wrapper_ident_str,
1633            formals_joined,
1634            prelude,
1635            r_wrapper_return_str
1636        )
1637    } else {
1638        format!(
1639            "{}{}{}{}{}{} <- function({}) {{\n  {}\n}}",
1640            roxygen_tags_str,
1641            source_comment,
1642            s3_method_comment,
1643            internal_comment,
1644            export_comment,
1645            r_wrapper_ident_str,
1646            formals_joined,
1647            r_wrapper_return_str
1648        )
1649    };
1650    // Use a raw string literal for better readability in macro expansion
1651    let r_wrapper_str = r_wrapper_raw_literal(&r_wrapper_string);
1652
1653    // endregion
1654
1655    let abi = abi
1656        .cloned()
1657        .unwrap_or_else(|| syn::parse_quote!(extern "C-unwind"));
1658
1659    // Extract cfg attributes to apply to generated items
1660    let cfg_attrs = extract_cfg_attrs(parsed.attrs());
1661
1662    // Generate doc strings with links
1663    let r_wrapper_doc = format!(
1664        "R wrapper code for [`{}`], calls [`{}`].",
1665        rust_ident, c_ident
1666    );
1667    let call_method_def_doc = format!(
1668        "R call method definition for [`{}`] (C wrapper: [`{}`]).",
1669        rust_ident, c_ident
1670    );
1671    let call_method_def_example = format!(
1672        "Value: `R_CallMethodDef {{ name: \"{}\", numArgs: {}, fun: <DL_FUNC> }}`",
1673        c_ident, num_args
1674    );
1675    let source_start = rust_ident.span().start();
1676    let source_line_lit = syn::LitInt::new(&source_start.line.to_string(), rust_ident.span());
1677    let source_col_lit =
1678        syn::LitInt::new(&(source_start.column + 1).to_string(), rust_ident.span());
1679
1680    // Get the normalized item for output, with roxygen tags stripped from docs.
1681    // Roxygen tags are for R documentation and shouldn't appear in rustdoc.
1682    let mut original_item = parsed.item_without_roxygen();
1683    // Strip only the miniextendr attributes; keep everything else.
1684    original_item
1685        .attrs
1686        .retain(|attr| !attr.path().is_ident("miniextendr"));
1687
1688    // Inject dots_typed binding into function body if dots = typed_list!(...) was specified
1689    if let Some(ref spec_tokens) = dots_spec {
1690        let dots_param = named_dots.clone().unwrap_or_else(|| {
1691            syn::Ident::new("__miniextendr_dots", proc_macro2::Span::call_site())
1692        });
1693        let validation_stmt: syn::Stmt = syn::parse_quote! {
1694            let dots_typed = #dots_param.typed(#spec_tokens)
1695                .expect("dots validation failed");
1696        };
1697        original_item.block.stmts.insert(0, validation_stmt);
1698    }
1699
1700    let original_item = original_item;
1701
1702    // Generate match_arg choices helper C wrappers and R_CallMethodDef entries
1703    let mut match_arg_helper_def_idents: Vec<syn::Ident> = Vec::new();
1704    let match_arg_helpers: Vec<proc_macro2::TokenStream> = match_arg_param_info
1705        .iter()
1706        .map(|(r_param, param_ty)| {
1707            let helper_fn_name = format!(
1708                "C_{}__match_arg_choices__{}",
1709                c_ident.to_string().trim_start_matches("C_"),
1710                r_param
1711            );
1712            let helper_fn_ident = syn::Ident::new(&helper_fn_name, proc_macro2::Span::call_site());
1713            let helper_def_ident = syn::Ident::new(
1714                &format!("call_method_def_{}", helper_fn_name),
1715                proc_macro2::Span::call_site(),
1716            );
1717            match_arg_helper_def_idents.push(helper_def_ident.clone());
1718            let helper_c_name = syn::LitCStr::new(
1719                std::ffi::CString::new(helper_fn_name.clone())
1720                    .expect("valid C string")
1721                    .as_c_str(),
1722                proc_macro2::Span::call_site(),
1723            );
1724            quote::quote! {
1725                #(#cfg_attrs)*
1726                #[allow(non_snake_case)]
1727                #[unsafe(no_mangle)]
1728                pub extern "C-unwind" fn #helper_fn_ident(
1729                    __miniextendr_call: ::miniextendr_api::ffi::SEXP,
1730                ) -> ::miniextendr_api::ffi::SEXP {
1731                    ::miniextendr_api::choices_sexp::<#param_ty>()
1732                }
1733
1734                #(#cfg_attrs)*
1735                #[::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_CALL_DEFS)]
1736                #[linkme(crate = ::miniextendr_api::linkme)]
1737                #[allow(non_upper_case_globals)]
1738                #[allow(non_snake_case)]
1739                static #helper_def_ident: ::miniextendr_api::ffi::R_CallMethodDef = unsafe {
1740                    ::miniextendr_api::ffi::R_CallMethodDef {
1741                        name: #helper_c_name.as_ptr(),
1742                        fun: Some(std::mem::transmute::<
1743                            unsafe extern "C-unwind" fn(
1744                                ::miniextendr_api::ffi::SEXP,
1745                            ) -> ::miniextendr_api::ffi::SEXP,
1746                            unsafe extern "C-unwind" fn() -> *mut ::std::os::raw::c_void,
1747                        >(#helper_fn_ident)),
1748                        numArgs: 1i32,
1749                    }
1750                };
1751            }
1752        })
1753        .collect();
1754
1755    // Generate doc comment linking to C wrapper and R wrapper constant
1756    let fn_r_wrapper_doc = format!(
1757        "See [`{}`] for C wrapper, [`{}`] for R wrapper.",
1758        c_ident, r_wrapper_generator
1759    );
1760
1761    let expanded: proc_macro::TokenStream = quote::quote! {
1762        // rust function with doc link to R wrapper
1763        #[doc = #fn_r_wrapper_doc]
1764        #original_item
1765
1766        // C wrapper
1767        #(#cfg_attrs)*
1768        #c_wrapper
1769
1770        // R wrapper (self-registers via distributed_slice)
1771        #(#cfg_attrs)*
1772        #[doc = #r_wrapper_doc]
1773        #[doc = concat!("Wraps Rust function `", stringify!(#rust_ident), "`.")]
1774        #[doc = #source_loc_doc]
1775        #[doc = concat!("Generated from source file `", file!(), "`.")]
1776        #[::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_R_WRAPPERS)]
1777                #[linkme(crate = ::miniextendr_api::linkme)]
1778        #[allow(non_upper_case_globals)]
1779        #[allow(non_snake_case)]
1780        static #r_wrapper_generator: ::miniextendr_api::registry::RWrapperEntry =
1781            ::miniextendr_api::registry::RWrapperEntry {
1782                priority: ::miniextendr_api::registry::RWrapperPriority::Function,
1783                source_file: file!(),
1784                content: concat!(
1785                    "# Generated from Rust fn `",
1786                    stringify!(#rust_ident),
1787                    "` (",
1788                    file!(),
1789                    ":",
1790                    #source_line_lit,
1791                    ":",
1792                    #source_col_lit,
1793                    ")",
1794                    #r_wrapper_str
1795                ),
1796            };
1797
1798        // registration of C wrapper in R (self-registers via distributed_slice)
1799        #(#cfg_attrs)*
1800        #[doc = #call_method_def_doc]
1801        #[doc = #call_method_def_example]
1802        #[doc = concat!("Wraps Rust function `", stringify!(#rust_ident), "`.")]
1803        #[doc = #source_loc_doc]
1804        #[doc = concat!("Generated from source file `", file!(), "`.")]
1805        #[::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_CALL_DEFS)]
1806                #[linkme(crate = ::miniextendr_api::linkme)]
1807        #[allow(non_upper_case_globals)]
1808        #[allow(non_snake_case)]
1809        static #call_method_def: ::miniextendr_api::ffi::R_CallMethodDef = unsafe {
1810            ::miniextendr_api::ffi::R_CallMethodDef {
1811                name: #c_ident_name.as_ptr(),
1812                // Cast to DL_FUNC (generic function pointer) for storage in R's registration table.
1813                // R will cast back to the appropriate signature when calling.
1814                fun: Some(std::mem::transmute::<
1815                    unsafe #abi fn(#(#func_ptr_def),*) -> ::miniextendr_api::ffi::SEXP,
1816                    unsafe #abi fn() -> *mut ::std::os::raw::c_void
1817                >(#c_ident)),
1818                numArgs: #num_args,
1819            }
1820        };
1821
1822        // match_arg choices helpers (C wrappers + R_CallMethodDef entries)
1823        // Each helper's call_method_def self-registers via distributed_slice
1824        #(#match_arg_helpers)*
1825
1826        // doc-lint warnings (if any)
1827        #doc_lint_warnings
1828    }
1829    .into();
1830
1831    expanded
1832}
1833
1834/// Generate thread-safe wrappers for R FFI functions.
1835///
1836/// Apply this to an `extern "C-unwind"` block to generate wrappers that ensure
1837/// R API calls happen on R's main thread.
1838///
1839/// # Behavior
1840///
1841/// All non-variadic functions are routed to the main thread via `with_r_thread`
1842/// when called from a worker thread. The return value is wrapped in `Sendable`
1843/// and sent back to the caller. This applies to both value-returning functions
1844/// (SEXP, i32, etc.) and pointer-returning functions (`*const T`, `*mut T`).
1845///
1846/// Pointer-returning functions (like `INTEGER`, `REAL`) are safe to route because
1847/// the underlying SEXP must be GC-protected by the caller, and R's GC only runs
1848/// during R API calls which are serialized through `with_r_thread`.
1849///
1850/// # Initialization Requirement
1851///
1852/// `miniextendr_runtime_init()` must be called before using any wrapped function.
1853/// Calling before initialization will panic with a descriptive error message.
1854///
1855/// # Limitations
1856///
1857/// - Variadic functions are passed through unchanged (no wrapper)
1858/// - Statics are passed through unchanged
1859/// - Functions with `#[link_name]` are passed through unchanged
1860///
1861/// # Example
1862///
1863/// ```ignore
1864/// #[r_ffi_checked]
1865/// unsafe extern "C-unwind" {
1866///     // Routed to main thread via with_r_thread when called from worker
1867///     pub fn Rf_ScalarInteger(arg1: i32) -> SEXP;
1868///     pub fn INTEGER(x: SEXP) -> *mut i32;
1869/// }
1870/// ```
1871#[proc_macro_attribute]
1872pub fn r_ffi_checked(
1873    _attr: proc_macro::TokenStream,
1874    item: proc_macro::TokenStream,
1875) -> proc_macro::TokenStream {
1876    let foreign_mod = syn::parse_macro_input!(item as syn::ItemForeignMod);
1877
1878    let foreign_mod_attrs = &foreign_mod.attrs;
1879    let abi = &foreign_mod.abi;
1880    let mut unchecked_items = Vec::new();
1881    let mut checked_wrappers = Vec::new();
1882
1883    for item in &foreign_mod.items {
1884        match item {
1885            syn::ForeignItem::Fn(fn_item) => {
1886                let is_variadic = fn_item.sig.variadic.is_some();
1887
1888                // Check if function already has #[link_name] - if so, pass through unchanged
1889                let has_link_name = fn_item
1890                    .attrs
1891                    .iter()
1892                    .any(|attr| attr.path().is_ident("link_name"));
1893
1894                if is_variadic || has_link_name {
1895                    // Pass through variadic functions and functions with explicit link_name unchanged
1896                    unchecked_items.push(item.clone());
1897                } else {
1898                    // Generate checked wrapper for non-variadic functions
1899                    let vis = &fn_item.vis;
1900                    let fn_name = &fn_item.sig.ident;
1901                    let fn_name_str = fn_name.to_string();
1902                    let unchecked_name = quote::format_ident!("{}_unchecked", fn_name);
1903                    let unchecked_name_str = unchecked_name.to_string();
1904                    let inputs = &fn_item.sig.inputs;
1905                    let output = &fn_item.sig.output;
1906                    // Filter out link_name attributes (already checked above, but be safe)
1907                    let attrs: Vec<_> = fn_item
1908                        .attrs
1909                        .iter()
1910                        .filter(|attr| !attr.path().is_ident("link_name"))
1911                        .collect();
1912                    let checked_doc = format!(
1913                        "Checked wrapper for `{}`. Calls `{}` and routes through `with_r_thread`.",
1914                        fn_name_str, unchecked_name_str
1915                    );
1916                    let checked_doc_lit = syn::LitStr::new(&checked_doc, fn_name.span());
1917                    let source_loc_doc = crate::source_location_doc(fn_name.span());
1918                    let source_loc_doc_lit = syn::LitStr::new(&source_loc_doc, fn_name.span());
1919
1920                    // Generate the unchecked FFI binding with #[link_name]
1921                    // Same visibility as the checked variant
1922                    let link_name = syn::LitStr::new(&fn_name_str, fn_name.span());
1923                    let unchecked_fn: syn::ForeignItem = syn::parse_quote! {
1924                        #(#attrs)*
1925                        #[doc = concat!("Unchecked FFI binding for `", stringify!(#fn_name), "`.")]
1926                        #[doc = #source_loc_doc_lit]
1927                        #[doc = concat!("Generated from source file `", file!(), "`.")]
1928                        #[link_name = #link_name]
1929                        #vis fn #unchecked_name(#inputs) #output;
1930                    };
1931                    unchecked_items.push(unchecked_fn);
1932
1933                    // Generate a checked wrapper function
1934                    let arg_names: Vec<_> = inputs
1935                        .iter()
1936                        .filter_map(|arg| {
1937                            if let syn::FnArg::Typed(pat_type) = arg
1938                                && let syn::Pat::Ident(pat_ident) = pat_type.pat.as_ref()
1939                            {
1940                                Some(pat_ident.ident.clone())
1941                            } else {
1942                                None
1943                            }
1944                        })
1945                        .collect();
1946
1947                    let is_never = matches!(output, syn::ReturnType::Type(_, ty) if matches!(**ty, syn::Type::Never(_)));
1948
1949                    let wrapper = if is_never {
1950                        // Never-returning functions (like Rf_error)
1951                        quote::quote! {
1952                            #(#attrs)*
1953                            #[doc = #checked_doc_lit]
1954                            #[doc = #source_loc_doc_lit]
1955                            #[doc = concat!("Generated from source file `", file!(), "`.")]
1956                            #[inline(always)]
1957                            #[allow(non_snake_case)]
1958                            #vis unsafe fn #fn_name(#inputs) #output {
1959                                ::miniextendr_api::worker::with_r_thread(move || unsafe {
1960                                    #unchecked_name(#(#arg_names),*)
1961                                })
1962                            }
1963                        }
1964                    } else {
1965                        // Normal functions - route via with_r_thread
1966                        quote::quote! {
1967                            #(#attrs)*
1968                            #[doc = #checked_doc_lit]
1969                            #[doc = #source_loc_doc_lit]
1970                            #[doc = concat!("Generated from source file `", file!(), "`.")]
1971                            #[inline(always)]
1972                            #[allow(non_snake_case)]
1973                            #vis unsafe fn #fn_name(#inputs) #output {
1974                                let result = ::miniextendr_api::worker::with_r_thread(move || {
1975                                    ::miniextendr_api::worker::Sendable(unsafe {
1976                                        #unchecked_name(#(#arg_names),*)
1977                                    })
1978                                });
1979                                result.0
1980                            }
1981                        }
1982                    };
1983                    checked_wrappers.push(wrapper);
1984                }
1985            }
1986            _ => {
1987                // Pass through statics and other items unchanged
1988                unchecked_items.push(item.clone());
1989            }
1990        }
1991    }
1992
1993    let expanded = quote::quote! {
1994        #(#foreign_mod_attrs)*
1995        unsafe #abi {
1996            #(#unchecked_items)*
1997        }
1998
1999        #(#checked_wrappers)*
2000    };
2001
2002    expanded.into()
2003}
2004
2005/// Derive macro for implementing `RNativeType` on a newtype wrapper.
2006///
2007/// This allows newtype wrappers around R native types to work with `Vec<T>`,
2008/// `&[T]` conversions and the `Coerce<R>` traits.
2009/// The inner type must implement `RNativeType`.
2010///
2011/// # Supported Struct Forms
2012///
2013/// Both tuple structs and single-field named structs are supported:
2014///
2015/// ```ignore
2016/// use miniextendr_api::RNativeType;
2017///
2018/// // Tuple struct (most common)
2019/// #[derive(Clone, Copy, RNativeType)]
2020/// struct UserId(i32);
2021///
2022/// // Named single-field struct
2023/// #[derive(Clone, Copy, RNativeType)]
2024/// struct Temperature { celsius: f64 }
2025/// ```
2026///
2027/// # Generated Code
2028///
2029/// For `struct UserId(i32)`, this generates:
2030///
2031/// ```ignore
2032/// impl RNativeType for UserId {
2033///     const SEXP_TYPE: SEXPTYPE = <i32 as RNativeType>::SEXP_TYPE;
2034///
2035///     unsafe fn dataptr_mut(sexp: SEXP) -> *mut Self {
2036///         <i32 as RNativeType>::dataptr_mut(sexp).cast()
2037///     }
2038/// }
2039/// ```
2040///
2041/// # Using the Newtype with Coerce
2042///
2043/// Once `RNativeType` is derived, you can implement `Coerce` to/from the newtype:
2044///
2045/// ```ignore
2046/// impl Coerce<UserId> for i32 {
2047///     fn coerce(self) -> UserId { UserId(self) }
2048/// }
2049///
2050/// let id: UserId = 42.coerce();
2051/// ```
2052///
2053/// # Requirements
2054///
2055/// - Must be a newtype struct (exactly one field, tuple or named)
2056/// - The inner type must implement `RNativeType` (`i32`, `f64`, `RLogical`, `u8`, `Rcomplex`)
2057/// - Should also derive `Copy` (required by `RNativeType: Copy`)
2058#[proc_macro_derive(RNativeType)]
2059pub fn derive_rnative_type(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2060    let input = syn::parse_macro_input!(input as syn::DeriveInput);
2061    let name = &input.ident;
2062    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
2063
2064    // Extract inner type and constructor — must be a newtype (single field)
2065    let (inner_ty, elt_ctor): (syn::Type, proc_macro2::TokenStream) = match &input.data {
2066        syn::Data::Struct(data) => match &data.fields {
2067            syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
2068                let ty = fields.unnamed.first().unwrap().ty.clone();
2069                let ctor = quote::quote! { Self(val) };
2070                (ty, ctor)
2071            }
2072            syn::Fields::Named(fields) if fields.named.len() == 1 => {
2073                let field = fields.named.first().unwrap();
2074                let ty = field.ty.clone();
2075                let field_name = field.ident.as_ref().unwrap();
2076                let ctor = quote::quote! { Self { #field_name: val } };
2077                (ty, ctor)
2078            }
2079            _ => {
2080                return syn::Error::new_spanned(
2081                    name,
2082                    "#[derive(RNativeType)] requires a newtype struct with exactly one field",
2083                )
2084                .into_compile_error()
2085                .into();
2086            }
2087        },
2088        _ => {
2089            return syn::Error::new_spanned(name, "#[derive(RNativeType)] only works on structs")
2090                .into_compile_error()
2091                .into();
2092        }
2093    };
2094
2095    let expanded = quote::quote! {
2096        impl #impl_generics ::miniextendr_api::ffi::RNativeType for #name #ty_generics #where_clause {
2097            const SEXP_TYPE: ::miniextendr_api::ffi::SEXPTYPE =
2098                <#inner_ty as ::miniextendr_api::ffi::RNativeType>::SEXP_TYPE;
2099
2100            #[inline]
2101            unsafe fn dataptr_mut(sexp: ::miniextendr_api::ffi::SEXP) -> *mut Self {
2102                // Newtype is repr(transparent), so we can cast the pointer
2103                unsafe {
2104                    <#inner_ty as ::miniextendr_api::ffi::RNativeType>::dataptr_mut(sexp).cast()
2105                }
2106            }
2107
2108            #[inline]
2109            fn elt(sexp: ::miniextendr_api::ffi::SEXP, i: isize) -> Self {
2110                let val = <#inner_ty as ::miniextendr_api::ffi::RNativeType>::elt(sexp, i);
2111                #elt_ctor
2112            }
2113        }
2114
2115    };
2116
2117    expanded.into()
2118}
2119
2120/// Derive macro for implementing `TypedExternal` on a type.
2121///
2122/// This makes the type compatible with `ExternalPtr<T>` for storing in R's external pointers.
2123///
2124/// # Basic Usage
2125///
2126/// ```ignore
2127/// use miniextendr_api::TypedExternal;
2128///
2129/// #[derive(ExternalPtr)]
2130/// struct MyData {
2131///     value: i32,
2132/// }
2133///
2134/// // Now you can use ExternalPtr<MyData>
2135/// let ptr = ExternalPtr::new(MyData { value: 42 });
2136/// ```
2137///
2138/// # Trait ABI
2139///
2140/// Trait dispatch wrappers are automatically generated:
2141///
2142/// ```ignore
2143/// use miniextendr_api::miniextendr;
2144///
2145/// #[derive(ExternalPtr)]
2146/// struct MyCounter {
2147///     value: i32,
2148/// }
2149///
2150/// #[miniextendr]
2151/// impl Counter for MyCounter {
2152///     fn value(&self) -> i32 { self.value }
2153///     fn increment(&mut self) { self.value += 1; }
2154/// }
2155/// ```
2156///
2157/// This generates additional infrastructure for type-erased trait dispatch:
2158/// - `__MxWrapperMyCounter` - Type-erased wrapper struct
2159/// - `__MX_BASE_VTABLE_MYCOUNTER` - Base vtable with drop/query
2160/// - `__mx_wrap_mycounter()` - Constructor returning `*mut mx_erased`
2161///
2162/// # Generated Code (Basic)
2163///
2164/// For a type `MyData` without traits:
2165///
2166/// ```ignore
2167/// impl TypedExternal for MyData {
2168///     const TYPE_NAME: &'static str = "MyData";
2169///     const TYPE_NAME_CSTR: &'static [u8] = b"MyData\0";
2170/// }
2171/// ```
2172#[proc_macro_derive(ExternalPtr, attributes(externalptr, r_data))]
2173pub fn derive_external_ptr(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2174    let input = syn::parse_macro_input!(input as syn::DeriveInput);
2175
2176    externalptr_derive::derive_external_ptr(input)
2177        .unwrap_or_else(|e| e.into_compile_error())
2178        .into()
2179}
2180
2181/// Derive macro for ALTREP integer vector data types.
2182///
2183/// Auto-implements `AltrepLen`, `AltIntegerData`, and calls `impl_altinteger_from_data!`.
2184///
2185/// # Attributes
2186///
2187/// - `#[altrep(len = "field_name")]` - Specify length field (auto-detects "len" or "length")
2188/// - `#[altrep(elt = "field_name")]` - For constant vectors, specify which field provides elements
2189/// - `#[altrep(dataptr)]` - Pass `dataptr` option to low-level macro
2190/// - `#[altrep(serialize)]` - Pass `serialize` option to low-level macro
2191/// - `#[altrep(subset)]` - Pass `subset` option to low-level macro
2192/// - `#[altrep(no_lowlevel)]` - Skip automatic `impl_altinteger_from_data!` call
2193///
2194/// # Example (Constant Vector - Zero Boilerplate!)
2195///
2196/// ```ignore
2197/// #[derive(ExternalPtr, AltrepInteger)]
2198/// #[altrep(elt = "value")]  // All elements return this field
2199/// pub struct ConstantIntData {
2200///     value: i32,
2201///     len: usize,
2202/// }
2203///
2204/// // That's it! 3 lines instead of 30!
2205/// // AltrepLen, AltIntegerData, and low-level impls are auto-generated
2206///
2207/// #[miniextendr(class = "ConstantInt")]
2208/// pub struct ConstantIntClass(pub ConstantIntData);
2209/// ```
2210///
2211/// # Example (Custom elt() - Override One Method)
2212///
2213/// ```ignore
2214/// #[derive(ExternalPtr, AltrepInteger)]
2215/// pub struct ArithSeqData {
2216///     start: i32,
2217///     step: i32,
2218///     len: usize,
2219/// }
2220///
2221/// // Auto-generates AltrepLen and stub AltIntegerData
2222/// // Just override elt() for custom logic:
2223/// impl AltIntegerData for ArithSeqData {
2224///     fn elt(&self, i: usize) -> i32 {
2225///         self.start + (i as i32) * self.step
2226///     }
2227/// }
2228/// ```
2229#[proc_macro_derive(AltrepInteger, attributes(altrep))]
2230pub fn derive_altrep_integer(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2231    let input = syn::parse_macro_input!(input as syn::DeriveInput);
2232    altrep_derive::derive_altrep_integer(input)
2233        .unwrap_or_else(|e| e.into_compile_error())
2234        .into()
2235}
2236
2237/// Derive macro for ALTREP real vector data types.
2238///
2239/// Auto-implements `AltrepLen` and `AltRealData` traits.
2240/// Supports the same `#[altrep(...)]` attributes as [`AltrepInteger`](derive@AltrepInteger).
2241#[proc_macro_derive(AltrepReal, attributes(altrep))]
2242pub fn derive_altrep_real(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2243    let input = syn::parse_macro_input!(input as syn::DeriveInput);
2244    altrep_derive::derive_altrep_real(input)
2245        .unwrap_or_else(|e| e.into_compile_error())
2246        .into()
2247}
2248
2249/// Derive macro for ALTREP logical vector data types.
2250///
2251/// Auto-implements `AltrepLen` and `AltLogicalData` traits.
2252/// Supports the same `#[altrep(...)]` attributes as [`AltrepInteger`](derive@AltrepInteger).
2253#[proc_macro_derive(AltrepLogical, attributes(altrep))]
2254pub fn derive_altrep_logical(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2255    let input = syn::parse_macro_input!(input as syn::DeriveInput);
2256    altrep_derive::derive_altrep_logical(input)
2257        .unwrap_or_else(|e| e.into_compile_error())
2258        .into()
2259}
2260
2261/// Derive macro for ALTREP raw vector data types.
2262///
2263/// Auto-implements `AltrepLen` and `AltRawData` traits.
2264/// Supports the same `#[altrep(...)]` attributes as [`AltrepInteger`](derive@AltrepInteger).
2265#[proc_macro_derive(AltrepRaw, attributes(altrep))]
2266pub fn derive_altrep_raw(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2267    let input = syn::parse_macro_input!(input as syn::DeriveInput);
2268    altrep_derive::derive_altrep_raw(input)
2269        .unwrap_or_else(|e| e.into_compile_error())
2270        .into()
2271}
2272
2273/// Derive macro for ALTREP string vector data types.
2274///
2275/// Auto-implements `AltrepLen` and `AltStringData` traits.
2276/// Supports the same `#[altrep(...)]` attributes as [`AltrepInteger`](derive@AltrepInteger).
2277#[proc_macro_derive(AltrepString, attributes(altrep))]
2278pub fn derive_altrep_string(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2279    let input = syn::parse_macro_input!(input as syn::DeriveInput);
2280    altrep_derive::derive_altrep_string(input)
2281        .unwrap_or_else(|e| e.into_compile_error())
2282        .into()
2283}
2284
2285/// Derive macro for ALTREP complex vector data types.
2286///
2287/// Auto-implements `AltrepLen` and `AltComplexData` traits.
2288/// Supports the same `#[altrep(...)]` attributes as [`AltrepInteger`](derive@AltrepInteger).
2289#[proc_macro_derive(AltrepComplex, attributes(altrep))]
2290pub fn derive_altrep_complex(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2291    let input = syn::parse_macro_input!(input as syn::DeriveInput);
2292    altrep_derive::derive_altrep_complex(input)
2293        .unwrap_or_else(|e| e.into_compile_error())
2294        .into()
2295}
2296
2297/// Derive macro for ALTREP list vector data types.
2298///
2299/// Auto-implements `AltrepLen` and `AltListData` traits.
2300/// Supports the same `#[altrep(...)]` attributes as [`AltrepInteger`](derive@AltrepInteger),
2301/// except `dataptr` and `subset` which are not supported for list ALTREP.
2302#[proc_macro_derive(AltrepList, attributes(altrep))]
2303pub fn derive_altrep_list(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2304    let input = syn::parse_macro_input!(input as syn::DeriveInput);
2305    altrep_derive::derive_altrep_list(input)
2306        .unwrap_or_else(|e| e.into_compile_error())
2307        .into()
2308}
2309
2310/// Derive ALTREP registration for a data struct.
2311///
2312/// Generates `TypedExternal`, `AltrepClass`, `RegisterAltrep`, `IntoR`,
2313/// linkme registration entry, and `Ref`/`Mut` accessor types.
2314///
2315/// The struct must already have low-level ALTREP traits implemented.
2316/// For most use cases, prefer a family-specific derive:
2317/// `#[derive(AltrepInteger)]`, `#[derive(AltrepReal)]`, etc.
2318/// Use `#[altrep(manual)]` on a family derive to skip data trait generation
2319/// when you provide your own `AltrepLen` + `Alt*Data` impls.
2320///
2321/// # Attributes
2322///
2323/// - `#[altrep(class = "Name")]` — custom ALTREP class name (defaults to struct name)
2324///
2325/// # Example
2326///
2327/// ```ignore
2328/// // Prefer family derives with manual:
2329/// #[derive(AltrepInteger)]
2330/// #[altrep(manual, class = "MyCustom", serialize)]
2331/// struct MyData { ... }
2332///
2333/// impl AltrepLen for MyData { ... }
2334/// impl AltIntegerData for MyData { ... }
2335/// ```
2336#[proc_macro_derive(Altrep, attributes(altrep))]
2337pub fn derive_altrep(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2338    let input = syn::parse_macro_input!(input as syn::DeriveInput);
2339    altrep::derive_altrep(input)
2340        .unwrap_or_else(|e| e.into_compile_error())
2341        .into()
2342}
2343
2344/// Derive `IntoList` for a struct (Rust → R list).
2345///
2346/// - Named structs → named R list: `list(x = 1L, y = 2L)`
2347/// - Tuple structs → unnamed R list: `list(1L, 2L)`
2348/// - Fields annotated `#[into_list(ignore)]` are skipped
2349#[proc_macro_derive(IntoList, attributes(into_list))]
2350pub fn derive_into_list(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2351    let input = syn::parse_macro_input!(input as syn::DeriveInput);
2352    list_derive::derive_into_list(input)
2353        .unwrap_or_else(|e| e.into_compile_error())
2354        .into()
2355}
2356
2357/// Derive `TryFromList` for a struct (R list → Rust).
2358///
2359/// - Named structs: extract by field name
2360/// - Tuple structs: extract by position (0, 1, 2, ...)
2361/// - Fields annotated `#[into_list(ignore)]` are not read and are initialized with `Default::default()`
2362#[proc_macro_derive(TryFromList, attributes(into_list))]
2363pub fn derive_try_from_list(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2364    let input = syn::parse_macro_input!(input as syn::DeriveInput);
2365    list_derive::derive_try_from_list(input)
2366        .unwrap_or_else(|e| e.into_compile_error())
2367        .into()
2368}
2369
2370/// Derive `PrefersList`: when a type implements both `IntoList` and `ExternalPtr`,
2371/// this selects list as the default `IntoR` conversion.
2372///
2373/// Without a Prefer* derive, types that implement multiple conversion paths
2374/// will get a compile error due to conflicting `IntoR` impls.
2375///
2376/// # Example
2377///
2378/// ```ignore
2379/// #[derive(IntoList, PreferList)]
2380/// struct Config { verbose: bool, threads: i32 }
2381/// // IntoR produces list(verbose = TRUE, threads = 4L)
2382/// ```
2383#[proc_macro_derive(PreferList)]
2384pub fn derive_prefer_list(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2385    let input = syn::parse_macro_input!(input as syn::DeriveInput);
2386    list_derive::derive_prefer_list(input)
2387        .unwrap_or_else(|e| e.into_compile_error())
2388        .into()
2389}
2390
2391/// Derive `PreferDataFrame`: when a type implements both `IntoDataFrame` (via `DataFrameRow`)
2392/// and other conversion paths, this selects data.frame as the default `IntoR` conversion.
2393///
2394/// # Example
2395///
2396/// ```ignore
2397/// #[derive(DataFrameRow, PreferDataFrame)]
2398/// struct Obs { time: f64, value: f64 }
2399/// // IntoR produces data.frame(time = ..., value = ...)
2400/// ```
2401#[proc_macro_derive(PreferDataFrame)]
2402pub fn derive_prefer_data_frame(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2403    let input = syn::parse_macro_input!(input as syn::DeriveInput);
2404    list_derive::derive_prefer_data_frame(input)
2405        .unwrap_or_else(|e| e.into_compile_error())
2406        .into()
2407}
2408
2409/// Derive `PreferExternalPtr`: when a type implements both `ExternalPtr` and
2410/// other conversion paths (e.g., `IntoList`), this selects `ExternalPtr` wrapping
2411/// as the default `IntoR` conversion.
2412///
2413/// # Example
2414///
2415/// ```ignore
2416/// #[derive(ExternalPtr, IntoList, PreferExternalPtr)]
2417/// struct Model { weights: Vec<f64> }
2418/// // IntoR wraps as ExternalPtr (opaque R object), not list
2419/// ```
2420#[proc_macro_derive(PreferExternalPtr)]
2421pub fn derive_prefer_externalptr(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2422    let input = syn::parse_macro_input!(input as syn::DeriveInput);
2423    list_derive::derive_prefer_externalptr(input)
2424        .unwrap_or_else(|e| e.into_compile_error())
2425        .into()
2426}
2427
2428/// Derive `PreferRNativeType`: when a newtype wraps an `RNativeType` and also
2429/// implements other conversions, this selects the native R vector conversion
2430/// as the default `IntoR` path.
2431///
2432/// # Example
2433///
2434/// ```ignore
2435/// #[derive(Copy, Clone, RNativeType, PreferRNativeType)]
2436/// struct Meters(f64);
2437/// // IntoR produces a numeric scalar, not an ExternalPtr
2438/// ```
2439#[proc_macro_derive(PreferRNativeType)]
2440pub fn derive_prefer_rnative(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2441    let input = syn::parse_macro_input!(input as syn::DeriveInput);
2442    list_derive::derive_prefer_rnative(input)
2443        .unwrap_or_else(|e| e.into_compile_error())
2444        .into()
2445}
2446
2447/// Derive `DataFrameRow`: generates a companion `*DataFrame` type with collection fields,
2448/// plus `IntoR` / `TryFromSexp` / `IntoDataFrame` impls for seamless R data.frame conversion.
2449///
2450/// # Example
2451///
2452/// ```ignore
2453/// #[derive(DataFrameRow)]
2454/// struct Measurement {
2455///     time: f64,
2456///     value: f64,
2457/// }
2458///
2459/// // Generates MeasurementDataFrame { time: Vec<f64>, value: Vec<f64> }
2460/// // plus conversion impls
2461/// ```
2462///
2463/// # Struct-level attributes
2464///
2465/// - `#[dataframe(name = "CustomDf")]` — custom name for the generated DataFrame type
2466/// - `#[dataframe(align)]` — pad shorter columns with NA to match longest
2467/// - `#[dataframe(tag = "my_tag")]` — attach a tag attribute to the data.frame
2468/// - `#[dataframe(conflicts = "string")]` — resolve conflicting column types as strings
2469///
2470/// # Field-level attributes
2471///
2472/// - `#[dataframe(skip)]` — omit this field from the DataFrame
2473/// - `#[dataframe(rename = "col")]` — custom column name
2474/// - `#[dataframe(as_list)]` — keep collection as single list column (no expansion)
2475/// - `#[dataframe(expand)]` / `#[dataframe(unnest)]` — expand collection into suffixed columns
2476/// - `#[dataframe(width = N)]` — pin expansion width (shorter rows get NA)
2477#[proc_macro_derive(DataFrameRow, attributes(dataframe))]
2478pub fn derive_dataframe_row(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2479    let input = syn::parse_macro_input!(input as syn::DeriveInput);
2480    dataframe_derive::derive_dataframe_row(input)
2481        .unwrap_or_else(|e| e.into_compile_error())
2482        .into()
2483}
2484
2485/// Derive `RFactor`: enables conversion between Rust enums and R factors.
2486///
2487/// # Usage
2488///
2489/// ```ignore
2490/// #[derive(Copy, Clone, RFactor)]
2491/// enum Color {
2492///     Red,
2493///     Green,
2494///     Blue,
2495/// }
2496/// ```
2497///
2498/// # Attributes
2499///
2500/// - `#[r_factor(rename = "name")]` - Rename a variant's level string
2501/// - `#[r_factor(rename_all = "snake_case")]` - Rename all variants (snake_case, kebab-case, lower, upper)
2502#[proc_macro_derive(RFactor, attributes(r_factor))]
2503pub fn derive_r_factor(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2504    let input = syn::parse_macro_input!(input as syn::DeriveInput);
2505    factor_derive::derive_r_factor(input)
2506        .unwrap_or_else(|e| e.into_compile_error())
2507        .into()
2508}
2509
2510/// Derive `MatchArg`: enables conversion between Rust enums and R character strings
2511/// with `match.arg` semantics (partial matching, informative errors).
2512///
2513/// # Usage
2514///
2515/// ```ignore
2516/// #[derive(Copy, Clone, MatchArg)]
2517/// enum Mode {
2518///     Fast,
2519///     Safe,
2520///     Debug,
2521/// }
2522/// ```
2523///
2524/// # Attributes
2525///
2526/// - `#[match_arg(rename = "name")]` - Rename a variant's choice string
2527/// - `#[match_arg(rename_all = "snake_case")]` - Rename all variants (snake_case, kebab-case, lower, upper)
2528///
2529/// # Generated Implementations
2530///
2531/// - `MatchArg` - Choice metadata and bidirectional conversion
2532/// - `TryFromSexp` - Convert R STRSXP/factor to enum (with partial matching)
2533/// - `IntoR` - Convert enum to R character scalar
2534#[proc_macro_derive(MatchArg, attributes(match_arg))]
2535pub fn derive_match_arg(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2536    let input = syn::parse_macro_input!(input as syn::DeriveInput);
2537    match_arg_derive::derive_match_arg(input)
2538        .unwrap_or_else(|e| e.into_compile_error())
2539        .into()
2540}
2541
2542/// Derive `Vctrs`: enables creating vctrs-compatible S3 vector classes from Rust structs.
2543///
2544/// # Usage
2545///
2546/// ```ignore
2547/// #[derive(Vctrs)]
2548/// #[vctrs(class = "percent", base = "double")]
2549/// pub struct Percent {
2550///     data: Vec<f64>,
2551/// }
2552/// ```
2553///
2554/// # Attributes
2555///
2556/// - `#[vctrs(class = "name")]` - R class name (required)
2557/// - `#[vctrs(base = "type")]` - Base type: double, integer, logical, character, raw, list, record
2558/// - `#[vctrs(abbr = "abbr")]` - Abbreviation for `vec_ptype_abbr`
2559/// - `#[vctrs(inherit_base = true|false)]` - Whether to include base type in class vector
2560///
2561/// # Generated Implementations
2562///
2563/// - `VctrsClass` - Metadata trait for vctrs class information
2564/// - `VctrsRecord` (for `base = "record"`) - Field names for record types
2565#[cfg(feature = "vctrs")]
2566#[proc_macro_derive(Vctrs, attributes(vctrs))]
2567pub fn derive_vctrs(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2568    let input = syn::parse_macro_input!(input as syn::DeriveInput);
2569    vctrs_derive::derive_vctrs(input)
2570        .unwrap_or_else(|e| e.into_compile_error())
2571        .into()
2572}
2573
2574/// Create a `TypedListSpec` for validating `...` arguments or lists.
2575///
2576/// This macro provides ergonomic syntax for defining typed list specifications
2577/// that can be used with `Dots::typed()` to validate the structure of
2578/// `...` arguments passed from R.
2579///
2580/// # Syntax
2581///
2582/// ```text
2583/// typed_list!(
2584///     name => type_spec,    // required field with type
2585///     name? => type_spec,   // optional field with type
2586///     name,                 // required field, any type
2587///     name?,                // optional field, any type
2588/// )
2589/// ```
2590///
2591/// For strict mode (no extra fields allowed):
2592/// ```text
2593/// typed_list!(@exact; name => type_spec, ...)
2594/// ```
2595///
2596/// # Type Specifications
2597///
2598/// ## Base types (with optional length)
2599/// - `numeric()` / `numeric(4)` - Real/double vector
2600/// - `integer()` / `integer(4)` - Integer vector
2601/// - `logical()` / `logical(4)` - Logical vector
2602/// - `character()` / `character(4)` - Character vector
2603/// - `raw()` / `raw(4)` - Raw vector
2604/// - `complex()` / `complex(4)` - Complex vector
2605/// - `list()` / `list(4)` - List (VECSXP)
2606///
2607/// ## Special types
2608/// - `data_frame()` - Data frame
2609/// - `factor()` - Factor
2610/// - `matrix()` - Matrix
2611/// - `array()` - Array
2612/// - `function()` - Function
2613/// - `environment()` - Environment
2614/// - `null()` - NULL only
2615/// - `any()` - Any type
2616///
2617/// ## String literals
2618/// - `"numeric"`, `"integer"`, etc. - Same as call syntax
2619/// - `"data.frame"` - Data frame (alias)
2620/// - `"MyClass"` - Any other string is treated as a class name (uses `Rf_inherits`)
2621///
2622/// # Examples
2623///
2624/// ## Basic usage
2625///
2626/// ```ignore
2627/// use miniextendr_api::{miniextendr, typed_list, Dots};
2628///
2629/// #[miniextendr]
2630/// pub fn process_args(dots: ...) -> Result<i32, String> {
2631///     let args = dots.typed(typed_list!(
2632///         alpha => numeric(4),
2633///         beta => list(),
2634///         gamma? => "character",
2635///     )).map_err(|e| e.to_string())?;
2636///
2637///     let alpha: Vec<f64> = args.get("alpha").map_err(|e| e.to_string())?;
2638///     Ok(alpha.len() as i32)
2639/// }
2640/// ```
2641///
2642/// ## Strict mode
2643///
2644/// ```ignore
2645/// // Reject any extra named fields
2646/// let args = dots.typed(typed_list!(@exact;
2647///     x => numeric(),
2648///     y => numeric(),
2649/// ))?;
2650/// ```
2651///
2652/// ## Class checking
2653///
2654/// ```ignore
2655/// // Check for specific R class (uses Rf_inherits semantics)
2656/// let args = dots.typed(typed_list!(
2657///     data => "data.frame",
2658///     model => "lm",
2659/// ))?;
2660/// ```
2661///
2662/// ## Attribute sugar
2663///
2664/// Instead of calling `.typed()` manually, you can use `typed_list!` directly in the
2665/// `#[miniextendr]` attribute for automatic validation:
2666///
2667/// ```ignore
2668/// #[miniextendr(dots = typed_list!(x => numeric(), y => numeric()))]
2669/// pub fn my_func(...) -> String {
2670///     // `dots_typed` is automatically created and validated
2671///     let x: f64 = dots_typed.get("x").expect("x");
2672///     let y: f64 = dots_typed.get("y").expect("y");
2673///     format!("x={}, y={}", x, y)
2674/// }
2675/// ```
2676///
2677/// This injects validation at the start of the function body:
2678/// ```ignore
2679/// let dots_typed = _dots.typed(typed_list!(...)).expect("dots validation failed");
2680/// ```
2681///
2682/// See the [`#[miniextendr]`](macro@miniextendr) attribute documentation for more details.
2683///
2684#[proc_macro]
2685pub fn typed_list(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2686    let parsed = syn::parse_macro_input!(input as typed_list::TypedListInput);
2687    typed_list::expand_typed_list(parsed).into()
2688}
2689
2690/// Construct an R list from Rust values.
2691///
2692/// This macro provides a convenient way to create R lists in Rust code,
2693/// using R-like syntax. Values are converted to R objects via the [`IntoR`] trait.
2694///
2695/// # Syntax
2696///
2697/// ```ignore
2698/// // Named entries (like R's list())
2699/// list!(
2700///     alpha = 1,
2701///     beta = "hello",
2702///     "my-name" = vec![1, 2, 3],
2703/// )
2704///
2705/// // Unnamed entries
2706/// list!(1, "hello", vec![1, 2, 3])
2707///
2708/// // Mixed (unnamed entries get empty string names)
2709/// list!(alpha = 1, 2, beta = "hello")
2710///
2711/// // Empty list
2712/// list!()
2713/// ```
2714///
2715/// # Examples
2716///
2717/// ```ignore
2718/// use miniextendr_api::{list, IntoR};
2719///
2720/// // Create a named list
2721/// let my_list = list!(
2722///     x = 42,
2723///     y = "hello world",
2724///     z = vec![1.0, 2.0, 3.0],
2725/// );
2726///
2727/// // In R this is equivalent to:
2728/// // list(x = 42L, y = "hello world", z = c(1, 2, 3))
2729/// ```
2730///
2731/// [`IntoR`]: https://docs.rs/miniextendr-api/latest/miniextendr_api/into_r/trait.IntoR.html
2732#[proc_macro]
2733pub fn list(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2734    let parsed = syn::parse_macro_input!(input as list_macro::ListInput);
2735    list_macro::expand_list(parsed).into()
2736}
2737
2738/// Internal proc macro used by TPIE (Trait-Provided Impl Expansion).
2739///
2740/// Called by `__mx_impl_<Trait>!` macro_rules macros generated by `#[miniextendr]` on traits.
2741/// Do not call directly.
2742#[proc_macro]
2743#[doc(hidden)]
2744pub fn __mx_trait_impl_expand(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2745    miniextendr_impl_trait::expand_tpie(input)
2746}
2747
2748/// Generate `TypedExternal` and `IntoExternalPtr` impls for a concrete monomorphization
2749/// of a generic type.
2750///
2751/// Since `#[derive(ExternalPtr)]` rejects generic types, use this macro to generate
2752/// the necessary impls for a specific type instantiation.
2753///
2754/// # Example
2755///
2756/// ```ignore
2757/// struct Wrapper<T> { inner: T }
2758///
2759/// impl_typed_external!(Wrapper<i32>);
2760/// impl_typed_external!(Wrapper<String>);
2761/// ```
2762#[proc_macro]
2763pub fn impl_typed_external(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2764    match typed_external_macro::impl_typed_external(input.into()) {
2765        Ok(tokens) => tokens.into(),
2766        Err(err) => err.into_compile_error().into(),
2767    }
2768}
2769
2770/// Generate the `R_init_*` entry point for a miniextendr R package.
2771///
2772/// This macro consolidates all package initialization into a single line.
2773/// It generates an `extern "C-unwind"` function that R calls when loading
2774/// the shared library.
2775///
2776/// # Usage
2777///
2778/// ```ignore
2779/// // Auto-detects package name from CARGO_CRATE_NAME (recommended):
2780/// miniextendr_api::miniextendr_init!();
2781///
2782/// // Or specify explicitly (for edge cases):
2783/// miniextendr_api::miniextendr_init!(mypkg);
2784/// ```
2785///
2786/// The generated function calls `miniextendr_api::init::package_init` which
2787/// handles panic hooks, runtime init, locale assertion, ALTREP setup, trait ABI
2788/// registration, routine registration, and symbol locking.
2789#[proc_macro]
2790pub fn miniextendr_init(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2791    let pkg_name: syn::Ident = if input.is_empty() {
2792        // Auto-detect from CARGO_CRATE_NAME (set by cargo during compilation)
2793        let name = std::env::var("CARGO_CRATE_NAME").unwrap_or_else(|_| {
2794            panic!(
2795                "CARGO_CRATE_NAME not set. Either pass the package name explicitly: \
2796                 miniextendr_init!(mypkg), or ensure you're building with cargo."
2797            )
2798        });
2799        syn::Ident::new(&name, proc_macro2::Span::call_site())
2800    } else {
2801        syn::parse_macro_input!(input as syn::Ident)
2802    };
2803    let fn_name = syn::Ident::new(&format!("R_init_{}", pkg_name), pkg_name.span());
2804
2805    // Build a byte string literal with NUL terminator for the package name.
2806    let mut name_bytes = pkg_name.to_string().into_bytes();
2807    name_bytes.push(0);
2808    let byte_lit = syn::LitByteStr::new(&name_bytes, pkg_name.span());
2809
2810    let expanded = quote::quote! {
2811        #[unsafe(no_mangle)]
2812        pub unsafe extern "C-unwind" fn #fn_name(
2813            dll: *mut ::miniextendr_api::ffi::DllInfo,
2814        ) {
2815            unsafe {
2816                // SAFETY: byte literal is a valid NUL-terminated string produced by the macro.
2817                let pkg_name = ::std::ffi::CStr::from_bytes_with_nul_unchecked(#byte_lit);
2818                ::miniextendr_api::init::package_init(dll, pkg_name);
2819            }
2820        }
2821
2822        /// Linker anchor: stub.c references this symbol to force the linker to pull
2823        /// in the user crate's archive member from the staticlib. With codegen-units = 1,
2824        /// this single member contains all linkme distributed_slice entries.
2825        /// The name is package-independent so stub.c doesn't need configure substitution.
2826        #[unsafe(no_mangle)]
2827        pub static miniextendr_force_link: ::std::ffi::c_char = 0;
2828    };
2829
2830    expanded.into()
2831}
2832
2833#[cfg(test)]
2834mod tests;