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