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