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