Skip to main content

miniextendr_macros/
miniextendr_impl.rs

1//! # Impl-block Parsing and Wrapper Generation
2//!
3//! This module handles `#[miniextendr]` applied to inherent impl blocks,
4//! generating R wrappers for different class systems.
5//!
6//! ## Architecture Overview
7//!
8//! ```text
9//! ┌─────────────────────────────────────────────────────────────────────────┐
10//! │                         #[miniextendr(r6)]                              │
11//! │                         impl MyType { ... }                             │
12//! └─────────────────────────────────────────────────────────────────────────┘
13//!                                    │
14//!                                    ▼
15//! ┌─────────────────────────────────────────────────────────────────────────┐
16//! │                           PARSING PHASE                                 │
17//! │  ┌─────────────────┐    ┌─────────────────┐    ┌─────────────────────┐ │
18//! │  │   ImplAttrs     │    │  ParsedMethod   │    │    ParsedImpl       │ │
19//! │  │ - class_system  │    │ - ident         │───▶│ - type_ident        │ │
20//! │  │ - class_name    │    │ - receiver      │    │ - class_system      │ │
21//! │  └─────────────────┘    │ - sig           │    │ - methods[]         │ │
22//! │                         │ - doc_tags      │    │ - doc_tags          │ │
23//! │                         │ - method_attrs  │    └─────────────────────┘ │
24//! │                         └─────────────────┘                             │
25//! └─────────────────────────────────────────────────────────────────────────┘
26//!                                    │
27//!                                    ▼
28//! ┌─────────────────────────────────────────────────────────────────────────┐
29//! │                        CODE GENERATION PHASE                            │
30//! │                                                                         │
31//! │  For each method:                                                       │
32//! │  ┌─────────────────────────────────────────────────────────────────┐   │
33//! │  │ generate_c_wrapper_for_method()                                 │   │
34//! │  │   └─▶ CWrapperContext (shared builder from c_wrapper_builder)   │   │
35//! │  │         - thread strategy (main vs worker)                      │   │
36//! │  │         - SEXP→Rust conversion                                  │   │
37//! │  │         - return handling                                       │   │
38//! │  └─────────────────────────────────────────────────────────────────┘   │
39//! │                                                                         │
40//! │  For the whole impl:                                                    │
41//! │  ┌─────────────────────────────────────────────────────────────────┐   │
42//! │  │ generate_{class_system}_r_wrapper()                             │   │
43//! │  │   - generate_env_r_wrapper()   → Type$method(self, ...)         │   │
44//! │  │   - generate_r6_r_wrapper()    → R6Class with methods           │   │
45//! │  │   - generate_s3_r_wrapper()    → generic + method.Type          │   │
46//! │  │   - generate_s4_r_wrapper()    → setClass + setMethod           │   │
47//! │  │   - generate_s7_r_wrapper()    → new_class + method<-           │   │
48//! │  └─────────────────────────────────────────────────────────────────┘   │
49//! └─────────────────────────────────────────────────────────────────────────┘
50//!                                    │
51//!                                    ▼
52//! ┌─────────────────────────────────────────────────────────────────────────┐
53//! │                              OUTPUTS                                    │
54//! │  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────────────┐ │
55//! │  │ C wrapper fns   │  │ R wrapper code  │  │  R_CallMethodDef       │ │
56//! │  │ C_Type__method  │  │ (as const str)  │  │  registration entries  │ │
57//! │  └─────────────────┘  └─────────────────┘  └─────────────────────────┘ │
58//! └─────────────────────────────────────────────────────────────────────────┘
59//! ```
60//!
61//! ## Supported Class Systems
62//!
63//! | System | Syntax | R Pattern | Use Case |
64//! |--------|--------|-----------|----------|
65//! | **Env** | `#[miniextendr]` | `obj$method()` | Simple, environment-based dispatch |
66//! | **R6** | `#[miniextendr(r6)]` | `R6Class` with `$new()` | OOP with encapsulation |
67//! | **S3** | `#[miniextendr(s3)]` | `generic(obj)` dispatch | Idiomatic R generics |
68//! | **S4** | `#[miniextendr(s4)]` | `setClass`/`setMethod` | Formal OOP, multiple dispatch |
69//! | **S7** | `#[miniextendr(s7)]` | `new_class`/`new_generic` | Modern R OOP |
70//! | **vctrs** | `#[miniextendr(vctrs)]` | `new_vctr`/`new_rcrd`/`new_list_of` | vctrs-compatible vectors |
71//!
72//! ## Method Categorization
73//!
74//! Methods are categorized by their receiver type:
75//!
76//! | Receiver | [`ReceiverKind`] | Generated as |
77//! |----------|------------------|--------------|
78//! | `&self` | `Ref` | Instance method (immutable) |
79//! | `&mut self` | `RefMut` | Instance method (mutable, chainable) |
80//! | `self: &ExternalPtr<Self>` | `ExternalPtrRef` | Instance method (immutable, full ExternalPtr access) |
81//! | `self: &mut ExternalPtr<Self>` | `ExternalPtrRefMut` | Instance method (mutable, full ExternalPtr access) |
82//! | `self: ExternalPtr<Self>` | `ExternalPtrValue` | Instance method (owned ExternalPtr, full access) |
83//! | `self` | `Value` | Consuming method (not supported in v1) |
84//! | (none) | `None` | Static method or constructor |
85//!
86//! Special methods:
87//! - **Constructor**: Returns `Self`, marked with `#[miniextendr(constructor)]` or named `new`
88//! - **Finalizer**: R6 only, marked with `#[miniextendr(r6(finalize))]`
89//! - **Private**: R6 only, marked with `#[miniextendr(r6(private))]`
90//!
91//! ## Shared Builders
92//!
93//! This module uses shared infrastructure from:
94//! - [`crate::c_wrapper_builder`]: C wrapper generation with thread strategy
95//! - [`crate::r_wrapper_builder`]: R function signatures and `.Call()` args
96//! - [`crate::method_return_builder`]: Return value handling per class system
97//! - [`crate::roxygen`]: Documentation extraction from Rust doc comments
98//!
99//! ## Example
100//!
101//! ```ignore
102//! #[miniextendr(r6)]
103//! impl Counter {
104//!     fn new(value: i32) -> Self { Counter { value } }
105//!     fn get(&self) -> i32 { self.value }
106//!     fn increment(&mut self) { self.value += 1; }
107//! }
108//! ```
109//!
110//! Generates:
111//! - C wrappers: `C_Counter__new`, `C_Counter__get`, `C_Counter__increment`
112//! - R6Class with `initialize`, `get`, `increment` methods
113//! - Registration entries for R's `.Call()` interface
114
115use proc_macro2::TokenStream;
116use quote::{ToTokens, format_ident, quote};
117
118/// Check if a type is `ExternalPtr<...>` (possibly fully qualified).
119fn is_external_ptr_type(ty: &syn::Type) -> bool {
120    if let syn::Type::Path(type_path) = ty {
121        type_path
122            .path
123            .segments
124            .last()
125            .map(|seg| seg.ident == "ExternalPtr")
126            .unwrap_or(false)
127    } else {
128        false
129    }
130}
131
132/// Replace every occurrence of the `self` keyword/ident in a TokenStream
133/// with a replacement identifier. Does NOT touch `Self` (capital S).
134fn replace_self_in_tokens(
135    tokens: proc_macro2::TokenStream,
136    replacement: &str,
137) -> proc_macro2::TokenStream {
138    let replacement_ident = proc_macro2::Ident::new(replacement, proc_macro2::Span::call_site());
139    tokens
140        .into_iter()
141        .map(|tt| match tt {
142            proc_macro2::TokenTree::Ident(ref ident) if ident == "self" => {
143                proc_macro2::TokenTree::Ident(replacement_ident.clone())
144            }
145            proc_macro2::TokenTree::Group(group) => {
146                let new_stream = replace_self_in_tokens(group.stream(), replacement);
147                let mut new_group = proc_macro2::Group::new(group.delimiter(), new_stream);
148                new_group.set_span(group.span());
149                proc_macro2::TokenTree::Group(new_group)
150            }
151            other => other,
152        })
153        .collect()
154}
155
156/// Rewrite methods with ExternalPtr-based receivers so they compile on stable Rust
157/// (which lacks `arbitrary_self_types`).
158///
159/// Handles:
160/// - `self: &ExternalPtr<Self>` → `__miniextendr_self: &ExternalPtr<Self>`
161/// - `self: &mut ExternalPtr<Self>` → `__miniextendr_self: &mut ExternalPtr<Self>`
162/// - `self: ExternalPtr<Self>` → `__miniextendr_self: ExternalPtr<Self>`
163///
164/// Also replaces all `self` references in the method body with `__miniextendr_self`.
165fn rewrite_external_ptr_receivers(mut item_impl: syn::ItemImpl) -> syn::ItemImpl {
166    for item in &mut item_impl.items {
167        let syn::ImplItem::Fn(method) = item else {
168            continue;
169        };
170        let Some(syn::FnArg::Receiver(receiver)) = method.sig.inputs.first() else {
171            continue;
172        };
173        if receiver.colon_token.is_none() {
174            continue;
175        }
176
177        // Determine if this is an ExternalPtr receiver and build the replacement param.
178        let new_param: Option<syn::FnArg> =
179            if let syn::Type::Reference(type_ref) = receiver.ty.as_ref() {
180                // self: &ExternalPtr<Self> or self: &mut ExternalPtr<Self>
181                if is_external_ptr_type(&type_ref.elem) {
182                    let mutability = type_ref.mutability;
183                    let inner_ty = &type_ref.elem;
184                    Some(syn::parse_quote! {
185                        __miniextendr_self: &#mutability #inner_ty
186                    })
187                } else {
188                    None
189                }
190            } else if is_external_ptr_type(receiver.ty.as_ref()) {
191                // self: ExternalPtr<Self> (by value)
192                let inner_ty = &receiver.ty;
193                Some(syn::parse_quote! {
194                    __miniextendr_self: #inner_ty
195                })
196            } else {
197                None
198            };
199
200        let Some(new_param) = new_param else {
201            continue;
202        };
203
204        // Replace first parameter
205        let inputs: Vec<syn::FnArg> = method.sig.inputs.iter().cloned().collect();
206        let mut new_inputs: Vec<syn::FnArg> = Vec::with_capacity(inputs.len());
207        new_inputs.push(new_param);
208        new_inputs.extend(inputs.into_iter().skip(1));
209        method.sig.inputs = new_inputs.into_iter().collect();
210
211        // Replace `self` in method body
212        let old_body = method.block.clone();
213        let new_tokens = replace_self_in_tokens(old_body.into_token_stream(), "__miniextendr_self");
214        method.block =
215            syn::parse2(new_tokens).expect("failed to reparse method body after self replacement");
216    }
217    item_impl
218}
219
220/// Strip `#[miniextendr(...)]` attributes and roxygen doc tags from an impl block and
221/// all of its items (functions, constants, types, macros).
222///
223/// Called before re-emitting the original impl block so that proc-macro attributes
224/// do not appear in the compiler output. Returns the cleaned impl block.
225fn strip_miniextendr_attrs_from_impl(mut item_impl: syn::ItemImpl) -> syn::ItemImpl {
226    item_impl.attrs = crate::roxygen::strip_roxygen_from_attrs(&item_impl.attrs);
227    item_impl
228        .attrs
229        .retain(|attr| !attr.path().is_ident("miniextendr"));
230    for item in &mut item_impl.items {
231        match item {
232            syn::ImplItem::Fn(fn_item) => {
233                fn_item.attrs = crate::roxygen::strip_roxygen_from_attrs(&fn_item.attrs);
234                fn_item
235                    .attrs
236                    .retain(|attr| !attr.path().is_ident("miniextendr"));
237            }
238            syn::ImplItem::Const(const_item) => {
239                const_item.attrs = crate::roxygen::strip_roxygen_from_attrs(&const_item.attrs);
240                const_item
241                    .attrs
242                    .retain(|attr| !attr.path().is_ident("miniextendr"));
243            }
244            syn::ImplItem::Type(type_item) => {
245                type_item.attrs = crate::roxygen::strip_roxygen_from_attrs(&type_item.attrs);
246                type_item
247                    .attrs
248                    .retain(|attr| !attr.path().is_ident("miniextendr"));
249            }
250            syn::ImplItem::Macro(macro_item) => {
251                macro_item.attrs = crate::roxygen::strip_roxygen_from_attrs(&macro_item.attrs);
252                macro_item
253                    .attrs
254                    .retain(|attr| !attr.path().is_ident("miniextendr"));
255            }
256            _ => {}
257        }
258    }
259    item_impl
260}
261
262/// Class system flavor for wrapper generation.
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
264pub enum ClassSystem {
265    /// Environment-style with `$`/`[[` dispatch
266    Env,
267    /// R6::R6Class
268    R6,
269    /// S7::new_class
270    S7,
271    /// S3 structure() with class attribute
272    S3,
273    /// S4 setClass
274    S4,
275    /// vctrs-compatible S3 class (vctr, rcrd, or list_of)
276    Vctrs,
277}
278
279impl ClassSystem {
280    /// Convert to an identifier for token transport (e.g., in macro_rules! expansion).
281    pub fn to_ident(self) -> syn::Ident {
282        let name = match self {
283            ClassSystem::Env => "env",
284            ClassSystem::R6 => "r6",
285            ClassSystem::S7 => "s7",
286            ClassSystem::S3 => "s3",
287            ClassSystem::S4 => "s4",
288            ClassSystem::Vctrs => "vctrs",
289        };
290        syn::Ident::new(name, proc_macro2::Span::call_site())
291    }
292
293    /// Parse from an identifier (inverse of `to_ident`).
294    pub fn from_ident(ident: &syn::Ident) -> Option<Self> {
295        match ident.to_string().as_str() {
296            "env" => Some(ClassSystem::Env),
297            "r6" => Some(ClassSystem::R6),
298            "s7" => Some(ClassSystem::S7),
299            "s3" => Some(ClassSystem::S3),
300            "s4" => Some(ClassSystem::S4),
301            "vctrs" => Some(ClassSystem::Vctrs),
302            _ => None,
303        }
304    }
305}
306
307/// Case-insensitive parsing of class system names from strings.
308///
309/// Accepts: `"env"`, `"r6"`, `"s3"`, `"s4"`, `"s7"`, `"vctrs"` (any casing).
310impl std::str::FromStr for ClassSystem {
311    type Err = String;
312
313    fn from_str(s: &str) -> Result<Self, Self::Err> {
314        match s.to_lowercase().as_str() {
315            "env" => Ok(ClassSystem::Env),
316            "r6" => Ok(ClassSystem::R6),
317            "s7" => Ok(ClassSystem::S7),
318            "s3" => Ok(ClassSystem::S3),
319            "s4" => Ok(ClassSystem::S4),
320            "vctrs" => Ok(ClassSystem::Vctrs),
321            _ => Err(format!("unknown class system: {}", s)),
322        }
323    }
324}
325
326/// Kind of vctrs class being created.
327#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
328pub enum VctrsKind {
329    /// Simple vctr backed by a base vector (new_vctr)
330    #[default]
331    Vctr,
332    /// Record type with named fields (new_rcrd)
333    Rcrd,
334    /// Homogeneous list with ptype (new_list_of)
335    ListOf,
336}
337
338/// Case-insensitive parsing of vctrs kind names from strings.
339///
340/// Accepts: `"vctr"`, `"rcrd"` (or `"record"`), `"list_of"` (or `"listof"`).
341impl std::str::FromStr for VctrsKind {
342    type Err = String;
343
344    fn from_str(s: &str) -> Result<Self, Self::Err> {
345        match s.to_lowercase().as_str() {
346            "vctr" => Ok(VctrsKind::Vctr),
347            "rcrd" | "record" => Ok(VctrsKind::Rcrd),
348            "list_of" | "listof" => Ok(VctrsKind::ListOf),
349            _ => Err(format!(
350                "unknown vctrs kind: {} (expected vctr, rcrd, or list_of)",
351                s
352            )),
353        }
354    }
355}
356
357/// Attributes for vctrs class generation.
358#[derive(Debug, Clone, Default)]
359pub struct VctrsAttrs {
360    /// The vctrs kind (vctr, rcrd, list_of)
361    pub kind: VctrsKind,
362    /// Base type for vctr (e.g., "double", "integer", "character")
363    pub base: Option<String>,
364    /// Whether to inherit base type in class vector
365    pub inherit_base_type: Option<bool>,
366    /// Prototype type for list_of (R expression)
367    pub ptype: Option<String>,
368    /// Abbreviation for vec_ptype_abbr (for printing)
369    pub abbr: Option<String>,
370}
371
372/// Receiver kind for methods.
373#[derive(Debug, Clone, Copy, PartialEq, Eq)]
374pub enum ReceiverKind {
375    /// No env - static/associated function
376    None,
377    /// `&self` - immutable borrow
378    Ref,
379    /// `&mut self` - mutable borrow
380    RefMut,
381    /// `self` - consuming (not supported in v1)
382    Value,
383    /// `self: &ExternalPtr<Self>` — immutable borrow of the wrapping ExternalPtr
384    ExternalPtrRef,
385    /// `self: &mut ExternalPtr<Self>` — mutable borrow of the wrapping ExternalPtr
386    ExternalPtrRefMut,
387    /// `self: ExternalPtr<Self>` — owned ExternalPtr (not consuming the inner T)
388    ExternalPtrValue,
389}
390
391impl ReceiverKind {
392    /// Returns true if this is an instance method (has self).
393    pub fn is_instance(&self) -> bool {
394        matches!(
395            self,
396            ReceiverKind::Ref
397                | ReceiverKind::RefMut
398                | ReceiverKind::ExternalPtrRef
399                | ReceiverKind::ExternalPtrRefMut
400                | ReceiverKind::ExternalPtrValue
401        )
402    }
403
404    /// Returns true if this is a mutable instance receiver.
405    pub fn is_mut(&self) -> bool {
406        matches!(self, ReceiverKind::RefMut | ReceiverKind::ExternalPtrRefMut)
407    }
408}
409
410/// Parsed method from an impl block.
411///
412/// # Default Parameters
413///
414/// Default parameters are specified using method-level syntax:
415/// - `#[miniextendr(defaults(param = "value", ...))]` on the method
416///
417/// Note: Parameter-level `#[miniextendr(default = "...")]` syntax is only supported
418/// for standalone functions, not impl methods (Rust language limitation).
419///
420/// Defaults cannot be specified for `self` parameters (compile error).
421#[derive(Debug)]
422pub struct ParsedMethod {
423    /// The method's name (e.g., `new`, `get`, `set_value`).
424    pub ident: syn::Ident,
425    /// How this method receives `self`: `&self`, `&mut self`, by value, or not at all (static).
426    pub env: ReceiverKind,
427    /// Method signature with the `self` receiver stripped. Used for C wrapper generation
428    /// where `self` is handled separately as a SEXP parameter.
429    pub sig: syn::Signature,
430    /// Rust visibility of the method. Non-`pub` methods become private in R6;
431    /// only `pub` methods get `@export` in R wrappers.
432    pub vis: syn::Visibility,
433    /// Roxygen tag lines extracted from Rust doc comments
434    pub doc_tags: Vec<String>,
435    /// Per-method attributes for class system overrides. Also carries the
436    /// `match_arg(...)` / `choices(...)` / `several_ok` parameter annotations
437    /// via its `per_param_match_arg` / `per_param_choices` / `per_param_several_ok`
438    /// fields — see [`MethodAttrs`] for the parsing surface.
439    pub method_attrs: MethodAttrs,
440    /// Parameter default values from `#[miniextendr(default = "...")]`
441    pub param_defaults: std::collections::HashMap<String, String>,
442}
443
444/// R6-specific per-method markers, separated from [`MethodAttrs`] so the
445/// `r6` parser branch and R6 class generator own a self-contained bag.
446///
447/// All R6 boolean flags live here.  Using any of these markers under a
448/// non-R6 class system (`#[miniextendr(s3)]`, `s4`, `s7`, `env`) is a
449/// compile-time error caught by [`ParsedMethod::validate_method_attrs`].
450#[derive(Debug, Default)]
451pub struct R6MethodAttrs {
452    /// Mark as active binding getter (`#[miniextendr(r6(active))]`).
453    pub active: bool,
454    /// Span of the `r6(active)` marker — used for error reporting when the
455    /// marker is misused in a non-R6 class generator.
456    pub active_span: Option<proc_macro2::Span>,
457    /// R6 active-binding *setter* (paired with an `active` getter by `prop`).
458    pub setter: bool,
459    /// R6 active-binding property name (defaults to the method name).
460    pub prop: Option<String>,
461    /// Mark as private method (`#[miniextendr(r6(private))]`).
462    /// Also inferred from non-`pub` Rust visibility.
463    pub private: bool,
464    /// Span of the `r6(private)` marker — points the validator's diagnostic
465    /// at the offending marker rather than the method ident.
466    pub private_span: Option<proc_macro2::Span>,
467    /// Mark as finalizer (`#[miniextendr(r6(finalize))]`).
468    /// Also inferred when the method consumes `self` and does not return `Self`.
469    pub finalize: bool,
470    /// Span of the `r6(finalize)` marker — see `private_span`.
471    pub finalize_span: Option<proc_macro2::Span>,
472    /// Mark as R6 deep-clone handler (`#[miniextendr(r6(deep_clone))]`).
473    /// This method is wired into `private$deep_clone` in the R6Class definition.
474    pub deep_clone: bool,
475    /// Span of the `r6(deep_clone)` marker — see `private_span`.
476    pub deep_clone_span: Option<proc_macro2::Span>,
477}
478
479/// S7-specific per-method markers, separated from [`MethodAttrs`] so the S7
480/// class generator has a self-contained bag of its own state (property
481/// getters/setters, generic-dispatch controls, convert() wiring) and the other
482/// class generators don't have to look past them.
483///
484/// # Mapping from `s7(...)` attribute keys
485///
486/// | Attribute | Field |
487/// |-----------|-------|
488/// | `s7(getter)` | `getter: true` |
489/// | `s7(setter)` | `setter: true` |
490/// | `s7(prop = "name")` | `prop: Some("name")` |
491/// | `s7(default = "expr")` | `default: Some("expr")` |
492/// | `s7(validate)` | `validate: true` |
493/// | `s7(required)` | `required: true` |
494/// | `s7(frozen)` | `frozen: true` |
495/// | `s7(deprecated = "msg")` | `deprecated: Some("msg")` |
496/// | `s7(no_dots)` | `no_dots: true` |
497/// | `s7(dispatch = "x,y")` | `dispatch: Some("x,y")` |
498/// | `s7(fallback)` | `fallback: true` |
499/// | `s7(convert_from = "T")` | `convert_from: Some("T")` |
500/// | `s7(convert_to = "T")` | `convert_to: Some("T")` |
501#[derive(Debug, Default)]
502pub struct S7MethodAttrs {
503    pub getter: bool,
504    pub setter: bool,
505    pub prop: Option<String>,
506    pub default: Option<String>,
507    pub validate: bool,
508    pub required: bool,
509    pub frozen: bool,
510    pub deprecated: Option<String>,
511    pub no_dots: bool,
512    pub dispatch: Option<String>,
513    pub fallback: bool,
514    pub convert_from: Option<String>,
515    pub convert_to: Option<String>,
516}
517
518/// Per-method attributes for class system customization.
519#[derive(Debug, Default)]
520pub struct MethodAttrs {
521    /// Skip this method
522    pub ignore: bool,
523    /// Mark as constructor
524    pub constructor: bool,
525    /// R6-specific method markers. All R6 boolean flags live here.
526    /// Only consumed by the R6 class generator and R6-aware accessor methods
527    /// (`ParsedMethod::is_active`, `is_private`, `is_finalizer`).
528    pub r6: R6MethodAttrs,
529    /// Generate as `as.<class>()` S3 method (e.g., "data.frame", "list", "character").
530    ///
531    /// When set, generates an S3 method for R's `as.<class>()` generic:
532    /// ```r
533    /// as.data.frame.MyType <- function(x, ...) {
534    ///     .Call(C_MyType__as_data_frame, .call = match.call(), x)
535    /// }
536    /// ```
537    ///
538    /// Valid values: data.frame, list, character, numeric, double, integer,
539    /// logical, matrix, vector, factor, Date, POSIXct, complex, raw,
540    /// environment, function
541    pub as_coercion: Option<String>,
542    /// Span of `as = "..."` for error reporting.
543    pub as_coercion_span: Option<proc_macro2::Span>,
544    /// Override generic name for S3/S4/S7 methods.
545    ///
546    /// Use this to implement methods for existing generics (like `print`, `format`, `length`)
547    /// without creating a new generic. When set, the generated code:
548    /// - Uses the specified generic name instead of the method name
549    /// - Skips creating a new generic (assumes it already exists)
550    /// - Creates only the method implementation (e.g., `print.MyClass`)
551    ///
552    /// # Example
553    /// ```ignore
554    /// #[miniextendr(s3)]
555    /// impl MyType {
556    ///     #[miniextendr(generic = "print")]
557    ///     fn show(&self) -> String {
558    ///         format!("MyType: {}", self.value)
559    ///     }
560    /// }
561    /// ```
562    /// This generates `print.MyType` that calls the `show` method.
563    pub generic: Option<String>,
564    /// Override class suffix for S3 methods.
565    ///
566    /// Use this to implement double-dispatch methods (like vctrs coercion)
567    /// where the class suffix differs from the type name or contains multiple classes.
568    ///
569    /// # Example
570    /// ```ignore
571    /// #[miniextendr(s3(generic = "vec_ptype2", class = "my_vctr.my_vctr"))]
572    /// fn ptype2_self(x: Robj, y: Robj, dots: ...) -> Robj {
573    ///     // Return prototype
574    /// }
575    /// ```
576    /// This generates `vec_ptype2.my_vctr.my_vctr` for vctrs double-dispatch.
577    pub class: Option<String>,
578    /// Worker thread execution (default: auto-detect based on types)
579    pub worker: bool,
580    /// Force main thread execution (unsafe)
581    pub unsafe_main_thread: bool,
582    /// Enable R interrupt checking
583    pub check_interrupt: bool,
584    /// Enable coercion for this method's parameters
585    pub coerce: bool,
586    /// Enable RNG state management (GetRNGstate/PutRNGstate)
587    pub rng: bool,
588    /// Return `Result<T, E>` to R without unwrapping.
589    pub unwrap_in_r: bool,
590    /// Parameter defaults from `#[miniextendr(defaults(param = "value", ...))]`
591    pub defaults: std::collections::HashMap<String, String>,
592    /// Span of `defaults(...)` for error reporting.
593    pub defaults_span: Option<proc_macro2::Span>,
594    /// Per-parameter `match_arg` / `several_ok` / `choices` state for this
595    /// method, keyed by the Rust parameter name.
596    ///
597    /// Method-level (not parameter-level) because Rust's parser rejects
598    /// attribute macros on fn parameters inside impl items. Standalone
599    /// functions take the per-param syntax directly; impl methods spell the
600    /// same data through `#[miniextendr(match_arg(p1, p2))]`,
601    /// `#[miniextendr(match_arg_several_ok(p))]`, and
602    /// `#[miniextendr(choices(p = "a, b"))]` on the method attribute.
603    ///
604    /// Uses the shared [`ParamAttrs`](crate::miniextendr_fn::ParamAttrs)
605    /// struct — the `coerce` / `default` fields are unused on the impl path.
606    pub per_param: std::collections::HashMap<String, crate::miniextendr_fn::ParamAttrs>,
607    /// Span of `match_arg(...)` / `choices(...)` for error reporting.
608    pub match_arg_span: Option<proc_macro2::Span>,
609    /// S7-specific method markers. Only consumed by the S7 class generator;
610    /// all other generators ignore this field.
611    pub s7: S7MethodAttrs,
612    // region: Lifecycle support
613    /// Lifecycle specification for deprecation/experimental status on methods.
614    ///
615    /// Use `#[miniextendr(lifecycle = "deprecated")]` or
616    /// `#[miniextendr(lifecycle(stage = "deprecated", when = "0.4.0", with = "new_method()"))]`
617    /// on methods in impl blocks.
618    ///
619    /// # Example
620    /// ```ignore
621    /// #[miniextendr(r6)]
622    /// impl MyType {
623    ///     #[miniextendr(lifecycle = "deprecated")]
624    ///     pub fn old_method(&self) -> i32 { 0 }
625    /// }
626    /// ```
627    pub lifecycle: Option<crate::lifecycle::LifecycleSpec>,
628    /// vctrs protocol method override.
629    ///
630    /// Use `#[miniextendr(vctrs(format))]` to mark a method as implementing a vctrs
631    /// protocol S3 generic. The method will be generated as `format.<class>` instead
632    /// of the default Rust method name.
633    ///
634    /// Supported protocols: format, vec_proxy, vec_proxy_equal, vec_proxy_compare,
635    /// vec_proxy_order, vec_restore, obj_print_data, obj_print_header, obj_print_footer.
636    pub vctrs_protocol: Option<String>,
637    /// Override R method name.
638    ///
639    /// Use `#[miniextendr(r_name = "add_one")]` to give the R method a different name
640    /// than the Rust method. The C symbol is still derived from the Rust name.
641    /// Cannot be combined with `generic = "..."` on the same method.
642    pub r_name: Option<String>,
643    /// R code to inject at the very top of the method body (before all built-in checks).
644    pub r_entry: Option<String>,
645    /// R code to inject after all built-in checks, immediately before `.Call()`.
646    pub r_post_checks: Option<String>,
647    /// Register `on.exit()` cleanup code in the R method wrapper.
648    ///
649    /// Short form: `#[miniextendr(r_on_exit = "close(con)")]`
650    /// Long form: `#[miniextendr(r_on_exit(expr = "close(con)", add = false))]`
651    pub r_on_exit: Option<crate::miniextendr_fn::ROnExit>,
652    /// Mark this method as internal: adds `@keywords internal`, suppresses export.
653    ///
654    /// For R6 active bindings this emits `#' @field name (internal)` so the binding
655    /// stays satisfied for roxygen2 (which warns on undocumented R6 bindings even
656    /// when `@field name NULL` is present) but is clearly marked internal in the docs.
657    pub internal: bool,
658    /// Suppress export for this method without adding `@keywords internal`.
659    ///
660    /// For R6 active bindings this emits `#' @field name (internal)` (see `internal`
661    /// above for why we don't use roxygen2's `@field name NULL` opt-out).
662    pub noexport: bool,
663}
664
665/// Fully parsed `#[miniextendr]` impl block, ready for code generation.
666///
667/// Contains the type identity, chosen class system, all parsed methods, the original
668/// impl block (with miniextendr attrs stripped for re-emission), and all class-system-specific
669/// configuration options. Created by [`ParsedImpl::parse`] and consumed by the per-class-system
670/// R wrapper generators and [`generate_method_c_wrapper`].
671#[derive(Debug)]
672pub struct ParsedImpl {
673    /// The Rust type name being implemented (e.g., `Counter`).
674    pub type_ident: syn::Ident,
675    /// Which R class system to generate wrappers for.
676    pub class_system: ClassSystem,
677    /// Optional override for the R class name. When `None`, uses `type_ident` as the class name.
678    pub class_name: Option<String>,
679    /// Optional label for distinguishing multiple impl blocks of the same type.
680    pub label: Option<String>,
681    /// Roxygen tag lines extracted from `///` doc comments on the impl block.
682    /// Used for class-level documentation (e.g., the R6 class docstring or S3 type description).
683    pub doc_tags: Vec<String>,
684    /// All parsed methods in this impl block, in source order.
685    pub methods: Vec<ParsedMethod>,
686    /// The original impl block with `#[miniextendr]` and roxygen attrs stripped.
687    /// Re-emitted as-is so the Rust compiler sees the actual method implementations.
688    pub original_impl: syn::ItemImpl,
689    /// `#[cfg(...)]` attributes from the impl block, propagated to all generated items
690    /// (C wrappers, R wrapper constants, call def arrays) for conditional compilation.
691    pub cfg_attrs: Vec<syn::Attribute>,
692    /// vctrs-specific attributes (only used when class_system is Vctrs)
693    pub vctrs_attrs: VctrsAttrs,
694    /// R6 parent class name for inheritance (e.g., `"ParentClass"`).
695    /// Propagated from [`ImplAttrs::r6_inherit`].
696    pub r6_inherit: Option<String>,
697    /// R6 portable flag. When `Some(true)`, generates a portable R6 class.
698    /// Propagated from [`ImplAttrs::r6_portable`].
699    pub r6_portable: Option<bool>,
700    /// R6 cloneable flag. Controls whether `$clone()` is available on instances.
701    /// Propagated from [`ImplAttrs::r6_cloneable`].
702    pub r6_cloneable: Option<bool>,
703    /// R6 lock_objects flag. When `Some(true)`, prevents adding new fields after creation.
704    /// Propagated from [`ImplAttrs::r6_lock_objects`].
705    pub r6_lock_objects: Option<bool>,
706    /// R6 lock_class flag. When `Some(true)`, prevents modifying the class definition.
707    /// Propagated from [`ImplAttrs::r6_lock_class`].
708    pub r6_lock_class: Option<bool>,
709    /// S7 parent class name for inheritance (e.g., `"ParentClass"`).
710    /// Propagated from [`ImplAttrs::s7_parent`].
711    pub s7_parent: Option<String>,
712    /// When true, marks this as an abstract S7 class that cannot be instantiated.
713    /// Propagated from [`ImplAttrs::s7_abstract`].
714    pub s7_abstract: bool,
715    /// When true, auto-include sidecar `#[r_data]` field accessors in the class definition.
716    /// For R6: active bindings are added via `$set("active", ...)` after class creation.
717    /// For S7: properties are spliced from `.rdata_properties_{Type}` into `new_class()`.
718    pub r_data_accessors: bool,
719    /// Strict conversion mode: methods returning lossy types use checked conversions.
720    pub strict: bool,
721    /// Mark class as internal: adds `@keywords internal`, suppresses `@export`.
722    pub internal: bool,
723    /// Suppress `@export` without adding `@keywords internal`.
724    pub noexport: bool,
725    /// Deprecation warnings for `@param` tags found on the impl block.
726    /// Appended to the final TokenStream output.
727    pub param_warnings: proc_macro2::TokenStream,
728}
729
730/// Attributes parsed from `#[miniextendr(...)]` on an impl block.
731///
732/// These control which R class system to use, class naming, multi-impl labeling,
733/// and class-system-specific options (R6 inheritance, S7 parent, vctrs kind, etc.).
734///
735/// Parsed by the [`syn::parse::Parse`] implementation which handles all supported
736/// attribute formats like `#[miniextendr(r6, class = "Custom", label = "ops")]`.
737#[derive(Debug)]
738pub struct ImplAttrs {
739    /// Which R class system to generate wrappers for.
740    /// Defaults to `Env` unless overridden by feature flags (`default-r6`, `default-s7`).
741    pub class_system: ClassSystem,
742    /// Optional override for the R class name. When `None`, the Rust type name is used.
743    pub class_name: Option<String>,
744    /// Optional label for distinguishing multiple impl blocks of the same type.
745    ///
746    /// When a type has multiple `#[miniextendr]` impl blocks, each must have a
747    /// distinct label. The label is used in:
748    /// - Generated wrapper names (e.g., `C_Type_label__method`)
749    /// - Module registration (e.g., `impl Type as "label"`)
750    ///
751    /// Single impl blocks don't require labels.
752    pub label: Option<String>,
753    /// vctrs-specific attributes (only used when class_system is Vctrs)
754    pub vctrs_attrs: VctrsAttrs,
755    // endregion
756    // region: R6-specific configuration
757    /// R6 parent class for inheritance.
758    /// Use `#[miniextendr(r6(inherit = "ParentClass"))]` to specify the parent.
759    pub r6_inherit: Option<String>,
760    /// R6 portable flag. Default TRUE. Set to false for non-portable R6 classes.
761    pub r6_portable: Option<bool>,
762    /// R6 cloneable flag. Controls whether `$clone()` is available.
763    pub r6_cloneable: Option<bool>,
764    /// R6 lock_objects flag. Controls whether fields can be added after creation.
765    pub r6_lock_objects: Option<bool>,
766    /// R6 lock_class flag. Controls whether the class definition can be modified.
767    pub r6_lock_class: Option<bool>,
768    // endregion
769    // region: S7-specific configuration
770    /// S7 parent class for inheritance.
771    /// Use `#[miniextendr(s7(parent = "ParentClass"))]` to specify the parent.
772    pub s7_parent: Option<String>,
773    /// S7 abstract class flag. Abstract classes cannot be instantiated.
774    pub s7_abstract: bool,
775    // endregion
776    // region: Sidecar integration
777    /// When true, auto-include `#[r_data]` field accessors in the class definition.
778    /// For R6: active bindings via `$set("active", ...)` post-creation.
779    /// For S7: properties spliced from `.rdata_properties_{Type}`.
780    pub r_data_accessors: bool,
781    // endregion
782    // region: Strict conversion mode
783    /// When true, methods returning lossy types (i64/u64/isize/usize + Vec variants)
784    /// use `strict::checked_*()` instead of `IntoR::into_sexp()`, panicking on overflow.
785    pub strict: bool,
786    /// Mark class as internal: adds `@keywords internal`, suppresses `@export`.
787    pub internal: bool,
788    /// Suppress `@export` without adding `@keywords internal`.
789    pub noexport: bool,
790    /// When true on a trait impl (`impl Trait for Type`), the impl block is NOT
791    /// emitted (a blanket impl already provides it), but C wrappers and R wrappers
792    /// ARE generated from the method signatures in the body.
793    pub blanket: bool,
794    // endregion
795}
796
797impl syn::parse::Parse for ImplAttrs {
798    /// Parses `#[miniextendr(...)]` impl-level options.
799    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
800        let mut class_system = if cfg!(feature = "default-r6") {
801            ClassSystem::R6
802        } else if cfg!(feature = "default-s7") {
803            ClassSystem::S7
804        } else {
805            ClassSystem::Env
806        };
807        let mut class_system_span: Option<(&str, proc_macro2::Span)> = None;
808        let mut class_name = None;
809        let mut label = None;
810        let mut vctrs_attrs = VctrsAttrs::default();
811        let mut r6_inherit = None;
812        let mut r6_portable = None;
813        let mut r6_cloneable = None;
814        let mut r6_lock_objects = None;
815        let mut r6_lock_class = None;
816        let mut s7_parent = None;
817        let mut s7_abstract = false;
818        let mut r_data_accessors = false;
819        let mut strict: Option<bool> = None;
820        let mut internal = false;
821        let mut noexport = false;
822        let mut blanket = false;
823
824        // Parse attributes. The first identifier can be either:
825        // - A class system (env, r6, s3, s4, s7, vctrs)
826        // - A key in a key=value pair (class, label)
827        //
828        // Valid formats:
829        // - #[miniextendr]
830        // - #[miniextendr(r6)]
831        // - #[miniextendr(label = "foo")]
832        // - #[miniextendr(r6, label = "foo")]
833        // - #[miniextendr(r6, class = "CustomName", label = "foo")]
834        // - #[miniextendr(vctrs)]
835        // - #[miniextendr(vctrs(kind = "rcrd", base = "double", abbr = "my_abbr"))]
836        while !input.is_empty() {
837            let ident: syn::Ident = input.parse()?;
838            let ident_str = ident.to_string();
839
840            // Check if this is a key=value pair
841            if input.peek(syn::Token![=]) {
842                let _: syn::Token![=] = input.parse()?;
843                match ident_str.as_str() {
844                    "class" => {
845                        let value: syn::LitStr = input.parse()?;
846                        class_name = Some(value.value());
847                    }
848                    "label" => {
849                        let value: syn::LitStr = input.parse()?;
850                        label = Some(value.value());
851                    }
852                    _ => {
853                        return Err(syn::Error::new(
854                            ident.span(),
855                            format!(
856                                "unknown impl block option `{}`; expected one of: \
857                                 env, r6, s3, s4, s7, vctrs (class system), \
858                                 class = \"...\" (R class name), \
859                                 label = \"...\" (multi-impl label), \
860                                 strict (strict type conversion)",
861                                ident_str,
862                            ),
863                        ));
864                    }
865                }
866            } else if ident_str == "vctrs" {
867                // vctrs class system with optional nested attributes
868                if let Some((prev_name, _prev_span)) = class_system_span {
869                    return Err(syn::Error::new(
870                        ident.span(),
871                        format!(
872                            "multiple class systems specified (`{}` and `{}`); use only one of: env, r6, s3, s4, s7, vctrs",
873                            prev_name, ident_str
874                        ),
875                    ));
876                }
877                class_system_span = Some(("vctrs", ident.span()));
878                class_system = ClassSystem::Vctrs;
879
880                // Check for nested vctrs options: vctrs(kind = "rcrd", base = "double", ...)
881                if input.peek(syn::token::Paren) {
882                    let content;
883                    syn::parenthesized!(content in input);
884
885                    while !content.is_empty() {
886                        let key: syn::Ident = content.parse()?;
887                        let _: syn::Token![=] = content.parse()?;
888                        let key_str = key.to_string();
889
890                        match key_str.as_str() {
891                            "kind" => {
892                                let value: syn::LitStr = content.parse()?;
893                                vctrs_attrs.kind = value
894                                    .value()
895                                    .parse()
896                                    .map_err(|e| syn::Error::new(value.span(), e))?;
897                            }
898                            "base" => {
899                                let value: syn::LitStr = content.parse()?;
900                                vctrs_attrs.base = Some(value.value());
901                            }
902                            "inherit_base_type" => {
903                                let value: syn::LitBool = content.parse()?;
904                                vctrs_attrs.inherit_base_type = Some(value.value());
905                            }
906                            "ptype" => {
907                                let value: syn::LitStr = content.parse()?;
908                                vctrs_attrs.ptype = Some(value.value());
909                            }
910                            "abbr" => {
911                                let value: syn::LitStr = content.parse()?;
912                                vctrs_attrs.abbr = Some(value.value());
913                            }
914                            _ => {
915                                return Err(syn::Error::new(
916                                    key.span(),
917                                    format!(
918                                        "unknown vctrs option: {} (expected kind, base, inherit_base_type, ptype, abbr)",
919                                        key_str
920                                    ),
921                                ));
922                            }
923                        }
924
925                        // Consume trailing comma if present
926                        if content.peek(syn::Token![,]) {
927                            let _: syn::Token![,] = content.parse()?;
928                        }
929                    }
930                }
931            } else if ident_str == "r6" {
932                // R6 class system with optional nested attributes
933                // r6 or r6(inherit = "Parent", portable = false, cloneable, lock_class)
934                if let Some((prev_name, _prev_span)) = class_system_span {
935                    return Err(syn::Error::new(
936                        ident.span(),
937                        format!(
938                            "multiple class systems specified (`{}` and `{}`); use only one of: env, r6, s3, s4, s7, vctrs",
939                            prev_name, ident_str
940                        ),
941                    ));
942                }
943                class_system_span = Some(("r6", ident.span()));
944                class_system = ClassSystem::R6;
945
946                if input.peek(syn::token::Paren) {
947                    let content;
948                    syn::parenthesized!(content in input);
949
950                    while !content.is_empty() {
951                        let key: syn::Ident = content.parse()?;
952                        let key_str = key.to_string();
953
954                        match key_str.as_str() {
955                            "inherit" => {
956                                let _: syn::Token![=] = content.parse()?;
957                                let value: syn::LitStr = content.parse()?;
958                                r6_inherit = Some(value.value());
959                            }
960                            "portable" => {
961                                if content.peek(syn::Token![=]) {
962                                    let _: syn::Token![=] = content.parse()?;
963                                    let value: syn::LitBool = content.parse()?;
964                                    r6_portable = Some(value.value());
965                                } else {
966                                    r6_portable = Some(true);
967                                }
968                            }
969                            "cloneable" => {
970                                if content.peek(syn::Token![=]) {
971                                    let _: syn::Token![=] = content.parse()?;
972                                    let value: syn::LitBool = content.parse()?;
973                                    r6_cloneable = Some(value.value());
974                                } else {
975                                    r6_cloneable = Some(true);
976                                }
977                            }
978                            "lock_objects" => {
979                                if content.peek(syn::Token![=]) {
980                                    let _: syn::Token![=] = content.parse()?;
981                                    let value: syn::LitBool = content.parse()?;
982                                    r6_lock_objects = Some(value.value());
983                                } else {
984                                    r6_lock_objects = Some(true);
985                                }
986                            }
987                            "lock_class" => {
988                                if content.peek(syn::Token![=]) {
989                                    let _: syn::Token![=] = content.parse()?;
990                                    let value: syn::LitBool = content.parse()?;
991                                    r6_lock_class = Some(value.value());
992                                } else {
993                                    r6_lock_class = Some(true);
994                                }
995                            }
996                            "r_data_accessors" => {
997                                r_data_accessors = true;
998                            }
999                            _ => {
1000                                return Err(syn::Error::new(
1001                                    key.span(),
1002                                    format!(
1003                                        "unknown r6 option: {} (expected inherit, portable, cloneable, lock_objects, lock_class, r_data_accessors)",
1004                                        key_str
1005                                    ),
1006                                ));
1007                            }
1008                        }
1009
1010                        // Consume trailing comma if present
1011                        if content.peek(syn::Token![,]) {
1012                            let _: syn::Token![,] = content.parse()?;
1013                        }
1014                    }
1015                }
1016            } else if ident_str == "s7" {
1017                // S7 class system with optional nested attributes
1018                // s7 or s7(parent = "Parent", abstract)
1019                if let Some((prev_name, _prev_span)) = class_system_span {
1020                    return Err(syn::Error::new(
1021                        ident.span(),
1022                        format!(
1023                            "multiple class systems specified (`{}` and `{}`); use only one of: env, r6, s3, s4, s7, vctrs",
1024                            prev_name, ident_str
1025                        ),
1026                    ));
1027                }
1028                class_system_span = Some(("s7", ident.span()));
1029                class_system = ClassSystem::S7;
1030
1031                if input.peek(syn::token::Paren) {
1032                    let content;
1033                    syn::parenthesized!(content in input);
1034
1035                    while !content.is_empty() {
1036                        // Use parse_any to accept `abstract` (a reserved keyword)
1037                        use syn::ext::IdentExt;
1038                        let key = syn::Ident::parse_any(&content)?;
1039                        let key_str = key.to_string();
1040
1041                        match key_str.as_str() {
1042                            "parent" => {
1043                                let _: syn::Token![=] = content.parse()?;
1044                                let value: syn::LitStr = content.parse()?;
1045                                s7_parent = Some(value.value());
1046                            }
1047                            "abstract" => {
1048                                if content.peek(syn::Token![=]) {
1049                                    let _: syn::Token![=] = content.parse()?;
1050                                    let value: syn::LitBool = content.parse()?;
1051                                    s7_abstract = value.value();
1052                                } else {
1053                                    s7_abstract = true;
1054                                }
1055                            }
1056                            "r_data_accessors" => {
1057                                r_data_accessors = true;
1058                            }
1059                            _ => {
1060                                return Err(syn::Error::new(
1061                                    key.span(),
1062                                    format!(
1063                                        "unknown s7 option: {} (expected parent, abstract, r_data_accessors)",
1064                                        key_str
1065                                    ),
1066                                ));
1067                            }
1068                        }
1069
1070                        // Consume trailing comma if present
1071                        if content.peek(syn::Token![,]) {
1072                            let _: syn::Token![,] = content.parse()?;
1073                        }
1074                    }
1075                }
1076            } else if ident_str == "blanket" {
1077                blanket = true;
1078            } else if ident_str == "strict" {
1079                strict = Some(true);
1080            } else if ident_str == "no_strict" {
1081                strict = Some(false);
1082            } else if ident_str == "internal" {
1083                internal = true;
1084            } else if ident_str == "noexport" {
1085                noexport = true;
1086            } else {
1087                // This is a class system identifier
1088                let parsed_system: ClassSystem = ident_str
1089                    .parse()
1090                    .map_err(|e| syn::Error::new(ident.span(), e))?;
1091                if let Some((prev_name, _prev_span)) = class_system_span {
1092                    return Err(syn::Error::new(
1093                        ident.span(),
1094                        format!(
1095                            "multiple class systems specified (`{}` and `{}`); use only one of: env, r6, s3, s4, s7, vctrs",
1096                            prev_name, ident_str
1097                        ),
1098                    ));
1099                }
1100                class_system_span = Some((
1101                    match parsed_system {
1102                        ClassSystem::Env => "env",
1103                        ClassSystem::R6 => "r6",
1104                        ClassSystem::S3 => "s3",
1105                        ClassSystem::S4 => "s4",
1106                        ClassSystem::S7 => "s7",
1107                        ClassSystem::Vctrs => "vctrs",
1108                    },
1109                    ident.span(),
1110                ));
1111                class_system = parsed_system;
1112            }
1113
1114            // Consume trailing comma if present
1115            if input.peek(syn::Token![,]) {
1116                let _: syn::Token![,] = input.parse()?;
1117            }
1118        }
1119
1120        Ok(ImplAttrs {
1121            class_system,
1122            class_name,
1123            label,
1124            vctrs_attrs,
1125            r6_inherit,
1126            r6_portable,
1127            r6_cloneable,
1128            r6_lock_objects,
1129            r6_lock_class,
1130            s7_parent,
1131            s7_abstract,
1132            r_data_accessors,
1133            strict: strict.unwrap_or(cfg!(feature = "default-strict")),
1134            internal,
1135            noexport,
1136            blanket,
1137        })
1138    }
1139}
1140
1141impl ParsedMethod {
1142    /// Validate method attributes for the given class system.
1143    /// Returns an error if unsupported attributes are used.
1144    fn validate_method_attrs(
1145        attrs: &MethodAttrs,
1146        class_system: ClassSystem,
1147        span: proc_macro2::Span,
1148    ) -> syn::Result<()> {
1149        // R6-only boolean markers must not appear under any other class system.
1150        if class_system != ClassSystem::R6 {
1151            if attrs.r6.active {
1152                return Err(syn::Error::new(
1153                    attrs.r6.active_span.unwrap_or(span),
1154                    "`active` is only valid for R6 class systems",
1155                ));
1156            }
1157            if attrs.r6.private {
1158                return Err(syn::Error::new(
1159                    attrs.r6.private_span.unwrap_or(span),
1160                    "`private` is only valid for R6 class systems",
1161                ));
1162            }
1163            if attrs.r6.finalize {
1164                return Err(syn::Error::new(
1165                    attrs.r6.finalize_span.unwrap_or(span),
1166                    "`finalize` is only valid for R6 class systems",
1167                ));
1168            }
1169            if attrs.r6.deep_clone {
1170                return Err(syn::Error::new(
1171                    attrs.r6.deep_clone_span.unwrap_or(span),
1172                    "`deep_clone` is only valid for R6 class systems",
1173                ));
1174            }
1175        }
1176
1177        // convert_from and convert_to are mutually exclusive on the same method
1178        // - convert_from expects a static method (no &self, takes source type)
1179        // - convert_to expects an instance method (&self, returns target type)
1180        if attrs.s7.convert_from.is_some() && attrs.s7.convert_to.is_some() {
1181            return Err(syn::Error::new(
1182                span,
1183                "cannot specify both `convert_from` and `convert_to` on the same method; \
1184                 convert_from is for static methods, convert_to is for instance methods",
1185            ));
1186        }
1187
1188        // r_name and generic are mutually exclusive
1189        if attrs.r_name.is_some() && attrs.generic.is_some() {
1190            return Err(syn::Error::new(
1191                span,
1192                "`r_name` and `generic` cannot be used on the same method. \
1193                 Use `r_name` for a simple rename, or `generic`/`class` for S3/S4/S7 generic dispatch.",
1194            ));
1195        }
1196
1197        // Worker attribute is now supported on methods
1198        // (validation happens during wrapper generation based on return type)
1199
1200        Ok(())
1201    }
1202
1203    /// Split a comma-separated choices list (as given to `choices(param = "a, b, c")`)
1204    /// into individual trimmed entries. Surrounding double-quotes are tolerated so
1205    /// users can spell the list either way: `"a, b"` or `"\"a\", \"b\""`.
1206    fn split_choice_list(raw: &str) -> Vec<String> {
1207        raw.split(',')
1208            .map(|s| s.trim().trim_matches('"').to_string())
1209            .filter(|s| !s.is_empty())
1210            .collect()
1211    }
1212
1213    /// Parse method attributes in #[miniextendr(class_system(...))] format.
1214    ///
1215    /// Supported formats:
1216    /// - `#[miniextendr(r6(ignore, constructor, finalize, private, generic = "...")]`
1217    /// - `#[miniextendr(s3(ignore, constructor, generic = "..."))]`
1218    /// - `#[miniextendr(s7(ignore, constructor, generic = "..."))]`
1219    /// - etc.
1220    fn parse_method_attrs(attrs: &[syn::Attribute]) -> syn::Result<MethodAttrs> {
1221        use syn::spanned::Spanned;
1222        let mut method_attrs = MethodAttrs::default();
1223        // Use Option<bool> for fields that support feature defaults.
1224        let mut worker: Option<bool> = None;
1225        let mut unsafe_main_thread: Option<bool> = None;
1226        let mut coerce: Option<bool> = None;
1227
1228        for attr in attrs {
1229            // Parse new-style #[miniextendr(class_system(...))] attributes
1230            if !attr.path().is_ident("miniextendr") {
1231                continue;
1232            }
1233
1234            // Parse the nested content: miniextendr(class_system(options...)) or miniextendr(defaults(...))
1235            attr.parse_nested_meta(|meta| {
1236                // Note: "vctrs" is handled separately below for protocol method overrides
1237                let is_class_meta = meta.path.is_ident("env")
1238                    || meta.path.is_ident("r6")
1239                    || meta.path.is_ident("s7")
1240                    || meta.path.is_ident("s3")
1241                    || meta.path.is_ident("s4");
1242
1243                if is_class_meta {
1244                    // Parse the inner options: r6(ignore, constructor, ...)
1245                    meta.parse_nested_meta(|inner| {
1246                        if inner.path.is_ident("ignore") {
1247                            method_attrs.ignore = true;
1248                        } else if inner.path.is_ident("constructor") {
1249                            method_attrs.constructor = true;
1250                        } else if inner.path.is_ident("finalize") {
1251                            method_attrs.r6.finalize = true;
1252                            method_attrs.r6.finalize_span = Some(inner.path.span());
1253                        } else if inner.path.is_ident("private") {
1254                            method_attrs.r6.private = true;
1255                            method_attrs.r6.private_span = Some(inner.path.span());
1256                        } else if inner.path.is_ident("active") {
1257                            method_attrs.r6.active = true;
1258                            method_attrs.r6.active_span = Some(inner.path.span());
1259                        } else if inner.path.is_ident("setter") {
1260                            // Active binding setter: works for both R6 and S7
1261                            method_attrs.r6.setter = true;
1262                            method_attrs.s7.setter = true;
1263                        } else if inner.path.is_ident("worker") {
1264                            worker = Some(true);
1265                        } else if inner.path.is_ident("no_worker") {
1266                            worker = Some(false);
1267                        } else if inner.path.is_ident("main_thread") {
1268                            unsafe_main_thread = Some(true);
1269                        } else if inner.path.is_ident("no_main_thread") {
1270                            unsafe_main_thread = Some(false);
1271                        } else if inner.path.is_ident("check_interrupt") {
1272                            method_attrs.check_interrupt = true;
1273                        } else if inner.path.is_ident("coerce") {
1274                            coerce = Some(true);
1275                        } else if inner.path.is_ident("no_coerce") {
1276                            coerce = Some(false);
1277                        } else if inner.path.is_ident("rng") {
1278                            method_attrs.rng = true;
1279                        } else if inner.path.is_ident("unwrap_in_r") {
1280                            method_attrs.unwrap_in_r = true;
1281                        } else if inner.path.is_ident("generic") {
1282                            let _: syn::Token![=] = inner.input.parse()?;
1283                            let value: syn::LitStr = inner.input.parse()?;
1284                            method_attrs.generic = Some(value.value());
1285                        } else if inner.path.is_ident("class") {
1286                            let _: syn::Token![=] = inner.input.parse()?;
1287                            let value: syn::LitStr = inner.input.parse()?;
1288                            method_attrs.class = Some(value.value());
1289                        } else if inner.path.is_ident("getter") {
1290                            method_attrs.s7.getter = true;
1291                        } else if inner.path.is_ident("validate") {
1292                            method_attrs.s7.validate = true;
1293                        } else if inner.path.is_ident("prop") {
1294                            let _: syn::Token![=] = inner.input.parse()?;
1295                            let value: syn::LitStr = inner.input.parse()?;
1296                            let prop_value = value.value();
1297                            // Set both S7 and R6 prop - the class system will use the appropriate one
1298                            method_attrs.s7.prop = Some(prop_value.clone());
1299                            method_attrs.r6.prop = Some(prop_value);
1300                        } else if inner.path.is_ident("default") {
1301                            let _: syn::Token![=] = inner.input.parse()?;
1302                            let value: syn::LitStr = inner.input.parse()?;
1303                            method_attrs.s7.default = Some(value.value());
1304                        } else if inner.path.is_ident("required") {
1305                            method_attrs.s7.required = true;
1306                        } else if inner.path.is_ident("frozen") {
1307                            method_attrs.s7.frozen = true;
1308                        } else if inner.path.is_ident("deprecated") {
1309                            let _: syn::Token![=] = inner.input.parse()?;
1310                            let value: syn::LitStr = inner.input.parse()?;
1311                            method_attrs.s7.deprecated = Some(value.value());
1312                        } else if inner.path.is_ident("no_dots") {
1313                            method_attrs.s7.no_dots = true;
1314                        } else if inner.path.is_ident("dispatch") {
1315                            let _: syn::Token![=] = inner.input.parse()?;
1316                            let value: syn::LitStr = inner.input.parse()?;
1317                            method_attrs.s7.dispatch = Some(value.value());
1318                        } else if inner.path.is_ident("fallback") {
1319                            method_attrs.s7.fallback = true;
1320                        } else if inner.path.is_ident("convert_from") {
1321                            let _: syn::Token![=] = inner.input.parse()?;
1322                            let value: syn::LitStr = inner.input.parse()?;
1323                            method_attrs.s7.convert_from = Some(value.value());
1324                        } else if inner.path.is_ident("convert_to") {
1325                            let _: syn::Token![=] = inner.input.parse()?;
1326                            let value: syn::LitStr = inner.input.parse()?;
1327                            method_attrs.s7.convert_to = Some(value.value());
1328                        } else if inner.path.is_ident("deep_clone") {
1329                            method_attrs.r6.deep_clone = true;
1330                            method_attrs.r6.deep_clone_span = Some(inner.path.span());
1331                        } else if inner.path.is_ident("r_name") {
1332                            let _: syn::Token![=] = inner.input.parse()?;
1333                            let value: syn::LitStr = inner.input.parse()?;
1334                            let val = value.value();
1335                            if val.is_empty() {
1336                                return Err(syn::Error::new_spanned(value, "r_name must not be empty"));
1337                            }
1338                            method_attrs.r_name = Some(val);
1339                        } else if inner.path.is_ident("r_entry") {
1340                            let _: syn::Token![=] = inner.input.parse()?;
1341                            let value: syn::LitStr = inner.input.parse()?;
1342                            method_attrs.r_entry = Some(value.value());
1343                        } else if inner.path.is_ident("r_post_checks") {
1344                            let _: syn::Token![=] = inner.input.parse()?;
1345                            let value: syn::LitStr = inner.input.parse()?;
1346                            method_attrs.r_post_checks = Some(value.value());
1347                        } else if inner.path.is_ident("r_on_exit") {
1348                            if inner.input.peek(syn::Token![=]) {
1349                                // Short form: r_on_exit = "expr"
1350                                let _: syn::Token![=] = inner.input.parse()?;
1351                                let value: syn::LitStr = inner.input.parse()?;
1352                                method_attrs.r_on_exit = Some(crate::miniextendr_fn::ROnExit {
1353                                    expr: value.value(),
1354                                    add: true,
1355                                    after: true,
1356                                });
1357                            } else {
1358                                // Long form: r_on_exit(expr = "...", add = false, after = false)
1359                                let mut expr = None;
1360                                let mut add = true;
1361                                let mut after = true;
1362                                inner.parse_nested_meta(|meta| {
1363                                    if meta.path.is_ident("expr") {
1364                                        let _: syn::Token![=] = meta.input.parse()?;
1365                                        let value: syn::LitStr = meta.input.parse()?;
1366                                        expr = Some(value.value());
1367                                    } else if meta.path.is_ident("add") {
1368                                        let _: syn::Token![=] = meta.input.parse()?;
1369                                        let value: syn::LitBool = meta.input.parse()?;
1370                                        add = value.value;
1371                                    } else if meta.path.is_ident("after") {
1372                                        let _: syn::Token![=] = meta.input.parse()?;
1373                                        let value: syn::LitBool = meta.input.parse()?;
1374                                        after = value.value;
1375                                    } else {
1376                                        return Err(meta.error(
1377                                            "unknown r_on_exit option; expected `expr`, `add`, or `after`",
1378                                        ));
1379                                    }
1380                                    Ok(())
1381                                })?;
1382                                let expr = expr.ok_or_else(|| {
1383                                    inner.error("r_on_exit(...) requires `expr = \"...\"` specifying the R expression")
1384                                })?;
1385                                method_attrs.r_on_exit = Some(crate::miniextendr_fn::ROnExit { expr, add, after });
1386                            }
1387                        } else {
1388                            return Err(inner.error(
1389                                "unknown method option; expected one of: ignore, constructor, finalize, private, active, worker, no_worker, main_thread, no_main_thread, check_interrupt, coerce, no_coerce, rng, unwrap_in_r, generic, class, getter, setter, validate, prop, default, required, frozen, deprecated, no_dots, dispatch, fallback, convert_from, convert_to, deep_clone, r_on_exit"
1390                            ));
1391                        }
1392                        Ok(())
1393                    })?;
1394                } else if meta.path.is_ident("defaults") {
1395                    // Capture span for error reporting
1396                    method_attrs.defaults_span = Some(meta.path.span());
1397                    // Parse defaults(param = "value", param2 = "value2", ...)
1398                    meta.parse_nested_meta(|inner| {
1399                        // Get parameter name
1400                        let param_name = inner
1401                            .path
1402                            .get_ident()
1403                            .ok_or_else(|| inner.error("expected parameter name"))?
1404                            .to_string();
1405                        // Parse = "value"
1406                        let _: syn::Token![=] = inner.input.parse()?;
1407                        let value: syn::LitStr = inner.input.parse()?;
1408                        method_attrs.defaults.insert(param_name, value.value());
1409                        Ok(())
1410                    })?;
1411                } else if meta.path.is_ident("match_arg") {
1412                    // `match_arg(param1, param2, ...)` — scalar match_arg params.
1413                    method_attrs.match_arg_span.get_or_insert(meta.path.span());
1414                    meta.parse_nested_meta(|inner| {
1415                        let name = inner
1416                            .path
1417                            .get_ident()
1418                            .ok_or_else(|| inner.error("expected parameter name"))?
1419                            .to_string();
1420                        method_attrs
1421                            .per_param
1422                            .entry(name)
1423                            .or_default()
1424                            .match_arg = true;
1425                        Ok(())
1426                    })?;
1427                } else if meta.path.is_ident("match_arg_several_ok") {
1428                    // `match_arg_several_ok(param1, param2, ...)` — match_arg + several_ok,
1429                    // for Vec/slice/array/Box<[_]>-typed parameters.
1430                    method_attrs.match_arg_span.get_or_insert(meta.path.span());
1431                    meta.parse_nested_meta(|inner| {
1432                        let name = inner
1433                            .path
1434                            .get_ident()
1435                            .ok_or_else(|| inner.error("expected parameter name"))?
1436                            .to_string();
1437                        let entry = method_attrs.per_param.entry(name).or_default();
1438                        entry.match_arg = true;
1439                        entry.several_ok = true;
1440                        Ok(())
1441                    })?;
1442                } else if meta.path.is_ident("choices") {
1443                    // `choices(param = "a, b, c", param2 = "x, y")` — explicit string choice lists.
1444                    method_attrs.match_arg_span.get_or_insert(meta.path.span());
1445                    meta.parse_nested_meta(|inner| {
1446                        let name = inner
1447                            .path
1448                            .get_ident()
1449                            .ok_or_else(|| inner.error("expected parameter name"))?
1450                            .to_string();
1451                        let _: syn::Token![=] = inner.input.parse()?;
1452                        let value: syn::LitStr = inner.input.parse()?;
1453                        let choices = Self::split_choice_list(&value.value());
1454                        method_attrs.per_param.entry(name).or_default().choices = Some(choices);
1455                        Ok(())
1456                    })?;
1457                } else if meta.path.is_ident("choices_several_ok") {
1458                    // `choices_several_ok(param = "a, b, c")` — choices + several_ok.
1459                    method_attrs.match_arg_span.get_or_insert(meta.path.span());
1460                    meta.parse_nested_meta(|inner| {
1461                        let name = inner
1462                            .path
1463                            .get_ident()
1464                            .ok_or_else(|| inner.error("expected parameter name"))?
1465                            .to_string();
1466                        let _: syn::Token![=] = inner.input.parse()?;
1467                        let value: syn::LitStr = inner.input.parse()?;
1468                        let choices = Self::split_choice_list(&value.value());
1469                        let entry = method_attrs.per_param.entry(name).or_default();
1470                        entry.choices = Some(choices);
1471                        entry.several_ok = true;
1472                        Ok(())
1473                    })?;
1474                } else if meta.path.is_ident("unsafe") {
1475                    // Parse unsafe(main_thread) - same syntax as standalone functions
1476                    meta.parse_nested_meta(|inner| {
1477                        if inner.path.is_ident("main_thread") {
1478                            unsafe_main_thread = Some(true);
1479                        } else {
1480                            return Err(inner.error(
1481                                "unknown `unsafe(...)` option; only `main_thread` is supported",
1482                            ));
1483                        }
1484                        Ok(())
1485                    })?;
1486                } else if meta.path.is_ident("check_interrupt") {
1487                    method_attrs.check_interrupt = true;
1488                } else if meta.path.is_ident("coerce") {
1489                    coerce = Some(true);
1490                } else if meta.path.is_ident("no_coerce") {
1491                    coerce = Some(false);
1492                } else if meta.path.is_ident("rng") {
1493                    method_attrs.rng = true;
1494                } else if meta.path.is_ident("unwrap_in_r") {
1495                    method_attrs.unwrap_in_r = true;
1496                } else if meta.path.is_ident("as") {
1497                    // Parse as = "data.frame", as = "list", etc.
1498                    method_attrs.as_coercion_span = Some(meta.path.span());
1499                    let _: syn::Token![=] = meta.input.parse()?;
1500                    let value: syn::LitStr = meta.input.parse()?;
1501                    let coercion_type = value.value();
1502
1503                    // Validate the coercion type
1504                    const SUPPORTED_AS_TYPES: &[&str] = &[
1505                        "data.frame",
1506                        "list",
1507                        "character",
1508                        "numeric",
1509                        "double",
1510                        "integer",
1511                        "logical",
1512                        "matrix",
1513                        "vector",
1514                        "factor",
1515                        "Date",
1516                        "POSIXct",
1517                        "complex",
1518                        "raw",
1519                        "environment",
1520                        "function",
1521                        "tibble",
1522                        "data.table",
1523                        "array",
1524                        "ts",
1525                    ];
1526
1527                    if !SUPPORTED_AS_TYPES.contains(&coercion_type.as_str()) {
1528                        return Err(syn::Error::new(
1529                            value.span(),
1530                            format!(
1531                                "unsupported `as` type: \"{}\". Supported types: {}",
1532                                coercion_type,
1533                                SUPPORTED_AS_TYPES.join(", ")
1534                            ),
1535                        ));
1536                    }
1537
1538                    method_attrs.as_coercion = Some(coercion_type);
1539                } else if meta.path.is_ident("lifecycle") {
1540                    // lifecycle = "stage" or lifecycle(stage = "deprecated", when = "0.4.0", ...)
1541                    if meta.input.peek(syn::Token![=]) {
1542                        // lifecycle = "stage"
1543                        let _: syn::Token![=] = meta.input.parse()?;
1544                        let value: syn::LitStr = meta.input.parse()?;
1545                        let stage = crate::lifecycle::LifecycleStage::from_str(&value.value())
1546                            .ok_or_else(|| {
1547                                syn::Error::new(
1548                                    value.span(),
1549                                    "invalid lifecycle stage; expected one of: experimental, stable, superseded, soft-deprecated, deprecated, defunct",
1550                                )
1551                            })?;
1552                        method_attrs.lifecycle = Some(crate::lifecycle::LifecycleSpec::new(stage));
1553                    } else {
1554                        // lifecycle(stage = "deprecated", when = "0.4.0", ...)
1555                        let mut spec = crate::lifecycle::LifecycleSpec::default();
1556                        meta.parse_nested_meta(|inner| {
1557                            let key = inner.path.get_ident()
1558                                .ok_or_else(|| inner.error("expected identifier"))?
1559                                .to_string();
1560                            let _: syn::Token![=] = inner.input.parse()?;
1561                            let value: syn::LitStr = inner.input.parse()?;
1562                            match key.as_str() {
1563                                "stage" => {
1564                                    spec.stage = crate::lifecycle::LifecycleStage::from_str(&value.value())
1565                                        .ok_or_else(|| syn::Error::new(value.span(), "invalid lifecycle stage"))?;
1566                                }
1567                                "when" => spec.when = Some(value.value()),
1568                                "what" => spec.what = Some(value.value()),
1569                                "with" => spec.with = Some(value.value()),
1570                                "details" => spec.details = Some(value.value()),
1571                                "id" => spec.id = Some(value.value()),
1572                                _ => return Err(inner.error(
1573                                    "unknown lifecycle option; expected: stage, when, what, with, details, id"
1574                                )),
1575                            }
1576                            Ok(())
1577                        })?;
1578                        method_attrs.lifecycle = Some(spec);
1579                    }
1580                } else if meta.path.is_ident("vctrs") {
1581                    // vctrs protocol method: vctrs(format), vctrs(vec_proxy), etc.
1582                    meta.parse_nested_meta(|inner| {
1583                        let raw_name = inner.path.get_ident()
1584                            .ok_or_else(|| inner.error("expected protocol name"))?
1585                            .to_string();
1586
1587                        // Normalize short aliases to full protocol names
1588                        const PROTOCOL_ALIASES: &[(&str, &str)] = &[
1589                            ("print_data", "obj_print_data"),
1590                            ("print_header", "obj_print_header"),
1591                            ("print_footer", "obj_print_footer"),
1592                            ("proxy", "vec_proxy"),
1593                            ("proxy_equal", "vec_proxy_equal"),
1594                            ("proxy_compare", "vec_proxy_compare"),
1595                            ("proxy_order", "vec_proxy_order"),
1596                            ("restore", "vec_restore"),
1597                        ];
1598                        let protocol = PROTOCOL_ALIASES
1599                            .iter()
1600                            .find(|(alias, _)| *alias == raw_name)
1601                            .map(|(_, full)| full.to_string())
1602                            .unwrap_or_else(|| raw_name.to_string());
1603
1604                        const VALID_PROTOCOLS: &[&str] = &[
1605                            "format", "vec_proxy", "vec_proxy_equal", "vec_proxy_compare",
1606                            "vec_proxy_order", "vec_restore", "obj_print_data",
1607                            "obj_print_header", "obj_print_footer",
1608                        ];
1609                        if !VALID_PROTOCOLS.contains(&protocol.as_str()) {
1610                            return Err(inner.error(format!(
1611                                "unknown vctrs protocol: {}; expected one of: {}",
1612                                raw_name,
1613                                VALID_PROTOCOLS.join(", ")
1614                            )));
1615                        }
1616                        method_attrs.vctrs_protocol = Some(protocol);
1617                        Ok(())
1618                    })?;
1619                } else if meta.path.is_ident("r_name") {
1620                    let _: syn::Token![=] = meta.input.parse()?;
1621                    let value: syn::LitStr = meta.input.parse()?;
1622                    let val = value.value();
1623                    if val.is_empty() {
1624                        return Err(syn::Error::new_spanned(value, "r_name must not be empty"));
1625                    }
1626                    method_attrs.r_name = Some(val);
1627                } else if meta.path.is_ident("r_entry") {
1628                    let _: syn::Token![=] = meta.input.parse()?;
1629                    let value: syn::LitStr = meta.input.parse()?;
1630                    method_attrs.r_entry = Some(value.value());
1631                } else if meta.path.is_ident("r_post_checks") {
1632                    let _: syn::Token![=] = meta.input.parse()?;
1633                    let value: syn::LitStr = meta.input.parse()?;
1634                    method_attrs.r_post_checks = Some(value.value());
1635                } else if meta.path.is_ident("r_on_exit") {
1636                    if meta.input.peek(syn::Token![=]) {
1637                        // Short form: r_on_exit = "expr"
1638                        let _: syn::Token![=] = meta.input.parse()?;
1639                        let value: syn::LitStr = meta.input.parse()?;
1640                        method_attrs.r_on_exit = Some(crate::miniextendr_fn::ROnExit {
1641                            expr: value.value(),
1642                            add: true,
1643                            after: true,
1644                        });
1645                    } else {
1646                        // Long form: r_on_exit(expr = "...", add = false, after = false)
1647                        let mut expr = None;
1648                        let mut add = true;
1649                        let mut after = true;
1650                        meta.parse_nested_meta(|inner| {
1651                            if inner.path.is_ident("expr") {
1652                                let _: syn::Token![=] = inner.input.parse()?;
1653                                let value: syn::LitStr = inner.input.parse()?;
1654                                expr = Some(value.value());
1655                            } else if inner.path.is_ident("add") {
1656                                let _: syn::Token![=] = inner.input.parse()?;
1657                                let value: syn::LitBool = inner.input.parse()?;
1658                                add = value.value;
1659                            } else if inner.path.is_ident("after") {
1660                                let _: syn::Token![=] = inner.input.parse()?;
1661                                let value: syn::LitBool = inner.input.parse()?;
1662                                after = value.value;
1663                            } else {
1664                                return Err(inner.error(
1665                                    "unknown r_on_exit option; expected `expr`, `add`, or `after`",
1666                                ));
1667                            }
1668                            Ok(())
1669                        })?;
1670                        let expr = expr.ok_or_else(|| {
1671                            meta.error("r_on_exit(...) requires `expr = \"...\"` specifying the R expression")
1672                        })?;
1673                        method_attrs.r_on_exit = Some(crate::miniextendr_fn::ROnExit { expr, add, after });
1674                    }
1675                } else if meta.path.is_ident("noexport") {
1676                    method_attrs.noexport = true;
1677                } else if meta.path.is_ident("internal") {
1678                    method_attrs.internal = true;
1679                } else {
1680                    return Err(meta.error(
1681                        "unknown attribute; expected one of: env, r6, s3, s4, s7, vctrs, defaults, unsafe, check_interrupt, coerce, no_coerce, rng, unwrap_in_r, as, lifecycle, r_name, r_entry, r_post_checks, r_on_exit, noexport, internal"
1682                    ));
1683                }
1684                Ok(())
1685            })?;
1686        }
1687
1688        // Resolve feature defaults for fields not explicitly set
1689        method_attrs.worker = worker.unwrap_or(cfg!(feature = "default-worker"));
1690        method_attrs.unsafe_main_thread = unsafe_main_thread.unwrap_or(true);
1691        method_attrs.coerce = coerce.unwrap_or(cfg!(feature = "default-coerce"));
1692
1693        // Method-level `internal` / `noexport` are currently only honoured by the R6
1694        // active-binding doc path (emits `#' @field <name> (internal)` — the documented
1695        // roxygen2 8.0.0 `@field name NULL` opt-out doesn't actually silence
1696        // `r6_resolve_fields`'s "Undocumented R6 active binding" warning, so we use a
1697        // minimal real description instead). Accepting them silently elsewhere would be a
1698        // no-op surface: regular instance methods, static methods, and trait methods don't
1699        // currently consume these flags. Reject them at parse time so the failure is loud
1700        // rather than silent.
1701        // Class-level `#[miniextendr(internal)]` / `noexport` continue to work for whole
1702        // impl blocks via `parsed_impl.internal` / `parsed_impl.noexport`.
1703        if (method_attrs.internal || method_attrs.noexport) && !method_attrs.r6.active {
1704            return Err(syn::Error::new(
1705                proc_macro2::Span::call_site(),
1706                "method-level `internal` / `noexport` are currently only supported on R6 \
1707                 active bindings (use `#[miniextendr(r6(active), noexport)]`). For other \
1708                 method positions, set `internal` / `noexport` at the impl-block level \
1709                 (`#[miniextendr(s7, internal)] impl Foo { ... }`) instead.",
1710            ));
1711        }
1712
1713        Ok(method_attrs)
1714    }
1715
1716    /// Detect the [`ReceiverKind`] from a method's function signature.
1717    ///
1718    /// Inspects the first parameter to determine whether this is a static function
1719    /// (`None`), immutable borrow (`Ref`), mutable borrow (`RefMut`), consuming
1720    /// method (`Value`), or ExternalPtr receiver (`ExternalPtrRef`, `ExternalPtrRefMut`,
1721    /// `ExternalPtrValue`). Handles both standard receivers (`&self`, `&mut self`) and
1722    /// typed receivers (`self: &Self`, `self: ExternalPtr<Self>`, etc.).
1723    fn detect_env(sig: &syn::Signature) -> ReceiverKind {
1724        match sig.inputs.first() {
1725            Some(syn::FnArg::Receiver(r)) => {
1726                // Check for standard &self / &mut self
1727                if r.reference.is_some() {
1728                    if r.mutability.is_some() {
1729                        ReceiverKind::RefMut
1730                    } else {
1731                        ReceiverKind::Ref
1732                    }
1733                } else if r.colon_token.is_some() {
1734                    // Check for typed receiver (self: &Self, self: &mut Self,
1735                    // self: &ExternalPtr<Self>, self: &mut ExternalPtr<Self>)
1736                    if let syn::Type::Reference(type_ref) = r.ty.as_ref() {
1737                        if is_external_ptr_type(&type_ref.elem) {
1738                            if type_ref.mutability.is_some() {
1739                                ReceiverKind::ExternalPtrRefMut
1740                            } else {
1741                                ReceiverKind::ExternalPtrRef
1742                            }
1743                        } else if type_ref.mutability.is_some() {
1744                            ReceiverKind::RefMut
1745                        } else {
1746                            ReceiverKind::Ref
1747                        }
1748                    } else if is_external_ptr_type(r.ty.as_ref()) {
1749                        // self: ExternalPtr<Self> — owned ExternalPtr
1750                        ReceiverKind::ExternalPtrValue
1751                    } else {
1752                        // self: Box<Self>, self: Rc<Self>, etc. - treat as by value
1753                        ReceiverKind::Value
1754                    }
1755                } else {
1756                    ReceiverKind::Value
1757                }
1758            }
1759            _ => ReceiverKind::None,
1760        }
1761    }
1762
1763    /// Create a copy of the method signature with the `self` receiver removed.
1764    ///
1765    /// The C wrapper receives `self` as a separate SEXP argument and extracts it
1766    /// from an `ErasedExternalPtr`, so the receiver must not appear in the
1767    /// parameter list used for SEXP-to-Rust conversion codegen.
1768    fn sig_without_env(sig: &syn::Signature) -> syn::Signature {
1769        let mut sig = sig.clone();
1770        if let Some(syn::FnArg::Receiver(_)) = sig.inputs.first() {
1771            sig.inputs = sig.inputs.into_iter().skip(1).collect();
1772        }
1773        sig
1774    }
1775
1776    /// Parse a method from an impl item.
1777    ///
1778    /// Regular doc comments are auto-converted to `@description` for all class systems.
1779    pub fn from_impl_item(item: syn::ImplItemFn, _class_system: ClassSystem) -> syn::Result<Self> {
1780        use syn::spanned::Spanned;
1781        let env = Self::detect_env(&item.sig);
1782        let mut method_attrs = Self::parse_method_attrs(&item.attrs)?;
1783
1784        // match_arg / choices on impl methods: unlike standalone functions, Rust
1785        // doesn't accept `#[miniextendr(...)]` on method parameters inside an impl
1786        // (attribute macros aren't allowed there — "expected non-macro attribute").
1787        // The surface is instead method-level: `#[miniextendr(match_arg(p), choices(q = "a, b"))]`.
1788        // `parse_method_attrs` already filled the sets; validate that every named
1789        // param exists on the signature so typos fail at compile time.
1790        let sig_param_names: std::collections::HashSet<String> = item
1791            .sig
1792            .inputs
1793            .iter()
1794            .filter_map(|arg| match arg {
1795                syn::FnArg::Typed(pt) => match pt.pat.as_ref() {
1796                    syn::Pat::Ident(pat_ident) => Some(pat_ident.ident.to_string()),
1797                    _ => None,
1798                },
1799                _ => None,
1800            })
1801            .collect();
1802        for annotated in method_attrs.per_param.iter().filter_map(|(name, a)| {
1803            if a.match_arg || a.choices.is_some() {
1804                Some(name)
1805            } else {
1806                None
1807            }
1808        }) {
1809            if !sig_param_names.contains(annotated) {
1810                return Err(syn::Error::new(
1811                    method_attrs
1812                        .match_arg_span
1813                        .unwrap_or_else(|| item.sig.ident.span()),
1814                    format!("match_arg/choices references non-existent parameter `{annotated}`"),
1815                ));
1816            }
1817        }
1818        // Validate: no defaults on self parameter (any kind: &self, &mut self, self)
1819        if env != ReceiverKind::None && method_attrs.defaults.contains_key("self") {
1820            return Err(syn::Error::new(
1821                method_attrs
1822                    .defaults_span
1823                    .unwrap_or_else(|| item.sig.ident.span()),
1824                "cannot specify default for self parameter in defaults(...)",
1825            ));
1826        }
1827
1828        // Validate: all defaults reference existing parameters
1829        let param_names: std::collections::HashSet<String> = item
1830            .sig
1831            .inputs
1832            .iter()
1833            .filter_map(|input| {
1834                if let syn::FnArg::Typed(pat_type) = input
1835                    && let syn::Pat::Ident(pat_ident) = pat_type.pat.as_ref()
1836                {
1837                    Some(pat_ident.ident.to_string())
1838                } else {
1839                    None
1840                }
1841            })
1842            .collect();
1843
1844        let mut invalid_params: Vec<String> = method_attrs
1845            .defaults
1846            .keys()
1847            .filter(|key| *key != "self" && !param_names.contains(*key))
1848            .cloned()
1849            .collect();
1850        invalid_params.sort();
1851
1852        if !invalid_params.is_empty() {
1853            return Err(syn::Error::new(
1854                method_attrs
1855                    .defaults_span
1856                    .unwrap_or_else(|| item.sig.ident.span()),
1857                format!(
1858                    "defaults(...) references non-existent parameter(s): {}",
1859                    invalid_params.join(", ")
1860                ),
1861            ));
1862        }
1863
1864        // Validate type-based constraints on each parameter
1865        for input in &item.sig.inputs {
1866            let syn::FnArg::Typed(pat_type) = input else {
1867                continue;
1868            };
1869            let syn::Pat::Ident(pat_ident) = pat_type.pat.as_ref() else {
1870                continue;
1871            };
1872            let param_name = pat_ident.ident.to_string();
1873
1874            // Validate Missing nesting and Missing<Dots>
1875            crate::miniextendr_fn::validate_param_type(pat_type.ty.as_ref(), pat_type.ty.span())?;
1876
1877            // Validate: no defaults on Dots-type parameters
1878            if crate::miniextendr_fn::is_dots_type(pat_type.ty.as_ref())
1879                && method_attrs.defaults.contains_key(&param_name)
1880            {
1881                return Err(syn::Error::new(
1882                    method_attrs
1883                        .defaults_span
1884                        .unwrap_or_else(|| pat_ident.ident.span()),
1885                    format!(
1886                        "variadic (...) parameter `{}` cannot have a default value",
1887                        param_name
1888                    ),
1889                ));
1890            }
1891        }
1892
1893        // Extract lifecycle from #[deprecated] attribute if not already set via #[miniextendr(lifecycle = ...)]
1894        if method_attrs.lifecycle.is_none() {
1895            method_attrs.lifecycle = item
1896                .attrs
1897                .iter()
1898                .find_map(crate::lifecycle::parse_rust_deprecated);
1899        }
1900
1901        // Auto-convert regular doc comments to @description for all class systems
1902        let mut doc_tags = crate::roxygen::roxygen_tags_from_attrs_for_r6_method(&item.attrs);
1903
1904        // Inject lifecycle badge into method roxygen tags if present
1905        if let Some(ref spec) = method_attrs.lifecycle {
1906            crate::lifecycle::inject_lifecycle_badge(&mut doc_tags, spec);
1907        }
1908
1909        // Get parameter defaults from method-level #[miniextendr(defaults(...))] attribute
1910        let param_defaults = method_attrs.defaults.clone();
1911
1912        // Validate: Missing<T> parameters must not have defaults
1913        for arg in item.sig.inputs.iter() {
1914            if let syn::FnArg::Typed(pt) = arg
1915                && let syn::Pat::Ident(pat_ident) = pt.pat.as_ref()
1916            {
1917                let name = pat_ident.ident.to_string();
1918                if crate::r_wrapper_builder::is_missing_type(pt.ty.as_ref())
1919                    && param_defaults.contains_key(&name)
1920                {
1921                    let span = method_attrs.defaults_span.unwrap_or(item.sig.ident.span());
1922                    return Err(syn::Error::new(
1923                        span,
1924                        format!(
1925                            "`Missing<T>` parameter `{}` cannot have a default value. \
1926                             `Missing<T>` detects omitted arguments via `missing()` in R, \
1927                             which is incompatible with default values in the R function signature. \
1928                             Use `Option<T>` with a default instead.",
1929                            name
1930                        ),
1931                    ));
1932                }
1933            }
1934        }
1935
1936        // Validate: `self` by value (consuming) methods are not fully supported
1937        // They're either: constructor (returns Self), finalizer (marked or inferred), or error
1938        if env == ReceiverKind::Value {
1939            let returns_self = matches!(&item.sig.output, syn::ReturnType::Type(_, ty)
1940                if matches!(ty.as_ref(), syn::Type::Path(p)
1941                    if p.path.segments.last().map(|s| s.ident == "Self").unwrap_or(false)));
1942
1943            // Allow if: constructor (returns Self) or explicitly marked as finalize
1944            let is_allowed = returns_self || method_attrs.constructor || method_attrs.r6.finalize;
1945
1946            if !is_allowed {
1947                return Err(syn::Error::new(
1948                    item.sig.fn_token.span,
1949                    format!(
1950                        "method `{}` takes `self` by value (consuming), which is not fully supported.\n\
1951                         \n\
1952                         Methods that consume `self` cannot be called from R because R uses reference \
1953                         semantics via ExternalPtr - the R object would remain alive after the Rust \
1954                         value is consumed.\n\
1955                         \n\
1956                         Options:\n\
1957                         1. Use `&self` or `&mut self` instead of `self`\n\
1958                         2. If this is a finalizer (cleanup method), add `#[miniextendr(finalize)]`\n\
1959                         3. If this returns a new Self (builder pattern), add `#[miniextendr(constructor)]`",
1960                        item.sig.ident
1961                    ),
1962                ));
1963            }
1964        }
1965
1966        Ok(ParsedMethod {
1967            ident: item.sig.ident.clone(),
1968            env,
1969            sig: Self::sig_without_env(&item.sig),
1970            vis: item.vis,
1971            doc_tags,
1972            method_attrs,
1973            param_defaults,
1974        })
1975    }
1976
1977    /// Returns true if this method should be included in the class.
1978    pub fn should_include(&self) -> bool {
1979        // Skip ignored methods
1980        !self.method_attrs.ignore
1981    }
1982
1983    /// Returns true if this method should be private in R6.
1984    /// Inferred from Rust visibility: anything not `pub` is private.
1985    pub fn is_private(&self) -> bool {
1986        // Explicit attribute takes precedence
1987        if self.method_attrs.r6.private {
1988            return true;
1989        }
1990        // Infer from visibility: anything not `pub` is private
1991        !matches!(self.vis, syn::Visibility::Public(_))
1992    }
1993
1994    /// Returns true if this is likely a constructor.
1995    /// Inferred from: no env + named "new" + returns Self.
1996    pub fn is_constructor(&self) -> bool {
1997        self.method_attrs.constructor
1998            || (self.env == ReceiverKind::None && self.ident == "new" && self.returns_self())
1999    }
2000
2001    /// Returns true if this is likely a finalizer.
2002    /// Inferred from: consumes self (by value) + doesn't return Self.
2003    pub fn is_finalizer(&self) -> bool {
2004        self.method_attrs.r6.finalize || (self.env == ReceiverKind::Value && !self.returns_self())
2005    }
2006
2007    /// Returns true if this method should be an R6 active binding.
2008    /// Active bindings provide property-like access (obj$name instead of obj$name()).
2009    pub fn is_active(&self) -> bool {
2010        self.method_attrs.r6.active
2011    }
2012
2013    /// R-facing method name.
2014    ///
2015    /// Returns `r_name` if set, otherwise the Rust ident as a string.
2016    pub fn r_method_name(&self) -> String {
2017        self.method_attrs
2018            .r_name
2019            .clone()
2020            .unwrap_or_else(|| self.ident.to_string())
2021    }
2022
2023    /// C wrapper identifier for this method.
2024    ///
2025    /// Format: `C_{Type}__{method}` or `C_{Type}_{label}__{method}` if labeled.
2026    pub fn c_wrapper_ident(&self, type_ident: &syn::Ident, label: Option<&str>) -> syn::Ident {
2027        if let Some(label) = label {
2028            format_ident!("C_{}_{}_{}", type_ident, label, self.ident)
2029        } else {
2030            format_ident!("C_{}__{}", type_ident, self.ident)
2031        }
2032    }
2033
2034    /// Generate lifecycle prelude R code for this method, if lifecycle is specified.
2035    ///
2036    /// The `what` parameter describes the method in the format appropriate for the class system:
2037    /// - Env/R6: `"Type$method()"`
2038    /// - S3: `"method.Type()"`
2039    /// - S7: `"method()`" (dispatched generics)
2040    pub fn lifecycle_prelude(&self, what: &str) -> Option<String> {
2041        self.method_attrs
2042            .lifecycle
2043            .as_ref()
2044            .and_then(|spec| spec.r_prelude(what))
2045    }
2046
2047    /// Returns true if this method returns Self.
2048    pub fn returns_self(&self) -> bool {
2049        matches!(&self.sig.output, syn::ReturnType::Type(_, ty)
2050            if matches!(ty.as_ref(), syn::Type::Path(p)
2051                if p.path.segments.last().map(|s| s.ident == "Self").unwrap_or(false)))
2052    }
2053
2054    /// Returns true if this method has no return type (returns unit `()`).
2055    pub fn returns_unit(&self) -> bool {
2056        match &self.sig.output {
2057            syn::ReturnType::Default => true,
2058            syn::ReturnType::Type(_, ty) => {
2059                matches!(ty.as_ref(), syn::Type::Tuple(t) if t.elems.is_empty())
2060            }
2061        }
2062    }
2063}
2064
2065impl ParsedImpl {
2066    /// Parse an impl block with class system attribute.
2067    ///
2068    /// Note: Trait impls (`impl Trait for Type`) are handled by `expand_impl`
2069    /// before this function is called, so we only handle inherent impls here.
2070    pub fn parse(attrs: ImplAttrs, item_impl: syn::ItemImpl) -> syn::Result<Self> {
2071        // Extract type identifier
2072        let type_ident =
2073            match item_impl.self_ty.as_ref() {
2074                syn::Type::Path(p) => p.path.segments.last().map(|s| s.ident.clone()).ok_or_else(
2075                    || {
2076                        syn::Error::new_spanned(
2077                            &item_impl.self_ty,
2078                            "#[miniextendr] impl blocks require a named type (e.g., `impl MyType`)",
2079                        )
2080                    },
2081                )?,
2082                _ => {
2083                    return Err(syn::Error::new_spanned(
2084                        &item_impl.self_ty,
2085                        "#[miniextendr] impl blocks require a named struct type. \
2086                     Found a non-path type. Use `impl MyStruct { ... }` with a concrete struct.",
2087                    ));
2088                }
2089            };
2090
2091        // Reject all generics until codegen fully supports them.
2092        // The wrapper generation uses `type_ident` without generic args, which would
2093        // fail to compile or mis-resolve types for generic impls.
2094        // Lifetimes and type/const params are rejected for different reasons and get distinct messages.
2095        {
2096            let params = &item_impl.generics.params;
2097            let has_lifetime = params
2098                .iter()
2099                .any(|p| matches!(p, syn::GenericParam::Lifetime(_)));
2100            let has_type_or_const = params
2101                .iter()
2102                .any(|p| matches!(p, syn::GenericParam::Type(_) | syn::GenericParam::Const(_)));
2103
2104            if has_lifetime {
2105                return Err(syn::Error::new_spanned(
2106                    &item_impl.generics,
2107                    "#[miniextendr] impl blocks cannot have explicit lifetime parameters. \
2108                     The generated `extern \"C-unwind\" #[no_mangle]` C wrappers are \
2109                     incompatible with any generic parameter, including lifetimes. \
2110                     Use owned types (`Vec<T>` instead of `&[T]`, `String` instead of `&str`) \
2111                     or remove the explicit lifetime annotation.",
2112                ));
2113            }
2114            if has_type_or_const {
2115                return Err(syn::Error::new_spanned(
2116                    &item_impl.generics,
2117                    "generic impl blocks are not supported by #[miniextendr]. \
2118                     R's .Call interface requires monomorphic C symbols, so generic type \
2119                     parameters cannot be used. Remove the generic parameters and use a \
2120                     concrete type instead.",
2121                ));
2122            }
2123        }
2124
2125        // Reject unsupported attributes on the impl block
2126        for attr in &item_impl.attrs {
2127            if attr.path().is_ident("export_name") {
2128                return Err(syn::Error::new_spanned(
2129                    attr,
2130                    "#[export_name] is not supported with #[miniextendr]; \
2131                     the macro generates its own C symbol names",
2132                ));
2133            }
2134        }
2135
2136        // Parse methods and validate attributes
2137        let mut methods = Vec::new();
2138        for item in &item_impl.items {
2139            if let syn::ImplItem::Fn(fn_item) = item {
2140                let method = ParsedMethod::from_impl_item(fn_item.clone(), attrs.class_system)?;
2141                // Validate method attributes for this class system
2142                ParsedMethod::validate_method_attrs(
2143                    &method.method_attrs,
2144                    attrs.class_system,
2145                    fn_item.sig.ident.span(),
2146                )?;
2147                methods.push(method);
2148            }
2149        }
2150
2151        // MXL120: For vctrs impls, reject constructors that return Self or the named type,
2152        // and reject all instance-method receivers (&self, &mut self, self, self: ExternalPtr<Self>).
2153        //
2154        // The generated R wrapper passes the constructor result to vctrs::new_vctr() (or
2155        // new_rcrd/new_list_of), which requires a plain vector — not an ExternalPtr.
2156        // Returning Self produces an EXTPTRSXP which new_vctr rejects with
2157        // ".data must be a vector type".
2158        //
2159        // Instance-method receivers (&self, &mut self, etc.) are equally broken: the vctrs
2160        // S3 dispatch passes the R object (an S3-classed base vector — REALSXP, INTSXP, etc.)
2161        // as `self_sexp`. The C wrapper then calls `ErasedExternalPtr::from_sexp(self_sexp)`,
2162        // which panics because the base vector is not an ExternalPtr.  There is no Rust `Self`
2163        // stored anywhere — the vector payload IS the R object.  Instance methods must be
2164        // expressed as static methods receiving the vector data by parameter.
2165        if attrs.class_system == ClassSystem::Vctrs {
2166            for method in &methods {
2167                // Check 1: constructor return type
2168                let is_ctor = (method.method_attrs.constructor
2169                    || (method.env == ReceiverKind::None && method.ident == "new"))
2170                    && method.env != ReceiverKind::Ref
2171                    && method.env != ReceiverKind::RefMut;
2172                if is_ctor && vctrs_ctor_returns_self_or_type(&method.sig.output, &type_ident) {
2173                    return Err(syn::Error::new_spanned(
2174                        &method.sig.output,
2175                        format!(
2176                            "[MXL120] vctrs constructor `{}` must not return `Self` or `{}`.\n\
2177                             \n\
2178                             The generated R wrapper passes the constructor result to \
2179                             `vctrs::new_vctr()` (or `new_rcrd`/`new_list_of`), which requires a \
2180                             plain vector payload — not an ExternalPtr (`EXTPTRSXP`).\n\
2181                             \n\
2182                             Fix: return the vector payload directly instead of `Self`.\n\
2183                             For example, return `Vec<f64>` (for vctr), a `std::collections::HashMap` \
2184                             / named-list struct (for rcrd), or a `Vec<Vec<T>>` (for list_of).",
2185                            method.ident, type_ident
2186                        ),
2187                    ));
2188                }
2189
2190                // Check 2: instance-method receivers are not supported on vctrs impls
2191                if method.env.is_instance() {
2192                    let receiver_spelling = match method.env {
2193                        ReceiverKind::Ref => "&self",
2194                        ReceiverKind::RefMut => "&mut self",
2195                        ReceiverKind::Value => "self",
2196                        ReceiverKind::ExternalPtrRef => "self: &ExternalPtr<Self>",
2197                        ReceiverKind::ExternalPtrRefMut => "self: &mut ExternalPtr<Self>",
2198                        ReceiverKind::ExternalPtrValue => "self: ExternalPtr<Self>",
2199                        ReceiverKind::None => unreachable!(),
2200                    };
2201                    return Err(syn::Error::new_spanned(
2202                        &method.ident,
2203                        format!(
2204                            "[MXL120] vctrs impl method `{}` uses a `{}` receiver, which is not \
2205                             supported on `#[miniextendr(vctrs(...))]` impls.\n\
2206                             \n\
2207                             A vctrs object is an S3-classed base vector (REALSXP, INTSXP, etc.). \
2208                             There is no Rust `Self` stored inside the R SEXP — the vector payload \
2209                             IS the R object.  The C wrapper cannot reconstruct `Self` from a base \
2210                             vector, so calling an instance method would panic at runtime.\n\
2211                             \n\
2212                             Fix: convert this method to a static method whose parameters receive \
2213                             the vector data directly.  For example:\n\
2214                             \n\
2215                             // Before (broken):\n\
2216                             // pub fn value(&self) -> f64 {{ ... }}\n\
2217                             \n\
2218                             // After (correct):\n\
2219                             // pub fn value(amounts: Vec<f64>) -> Vec<f64> {{ ... }}",
2220                            method.ident, receiver_spelling
2221                        ),
2222                    ));
2223                }
2224            }
2225        }
2226
2227        // Extract cfg attributes
2228        let cfg_attrs: Vec<_> = item_impl
2229            .attrs
2230            .iter()
2231            .filter(|attr| attr.path().is_ident("cfg"))
2232            .cloned()
2233            .collect();
2234        let raw_doc_tags = crate::roxygen::roxygen_tags_from_attrs(&item_impl.attrs);
2235        let (doc_tags, param_warnings) = crate::roxygen::strip_method_tags(
2236            &raw_doc_tags,
2237            &type_ident.to_string(),
2238            item_impl.impl_token.span,
2239        );
2240
2241        Ok(ParsedImpl {
2242            type_ident,
2243            class_system: attrs.class_system,
2244            class_name: attrs.class_name,
2245            label: attrs.label,
2246            doc_tags,
2247            methods,
2248            // Strip miniextendr attributes (and roxygen tags) before re-emitting,
2249            // then rewrite ExternalPtr receivers for stable Rust compatibility.
2250            original_impl: rewrite_external_ptr_receivers(strip_miniextendr_attrs_from_impl(
2251                item_impl,
2252            )),
2253            cfg_attrs,
2254            vctrs_attrs: attrs.vctrs_attrs,
2255            r6_inherit: attrs.r6_inherit,
2256            r6_portable: attrs.r6_portable,
2257            r6_cloneable: attrs.r6_cloneable,
2258            r6_lock_objects: attrs.r6_lock_objects,
2259            r6_lock_class: attrs.r6_lock_class,
2260            s7_parent: attrs.s7_parent,
2261            s7_abstract: attrs.s7_abstract,
2262            r_data_accessors: attrs.r_data_accessors,
2263            strict: attrs.strict,
2264            internal: attrs.internal,
2265            noexport: attrs.noexport,
2266            param_warnings,
2267        })
2268    }
2269
2270    /// Get the class name (override or type name).
2271    pub fn class_name(&self) -> String {
2272        self.class_name
2273            .clone()
2274            .unwrap_or_else(|| self.type_ident.to_string())
2275    }
2276
2277    /// Get methods that should be included.
2278    pub fn included_methods(&self) -> impl Iterator<Item = &ParsedMethod> {
2279        self.methods.iter().filter(|m| m.should_include())
2280    }
2281
2282    /// Get the constructor method (fn new() -> Self), if included.
2283    /// Respects `#[...(ignore)]` and visibility filters.
2284    pub fn constructor(&self) -> Option<&ParsedMethod> {
2285        self.methods
2286            .iter()
2287            .find(|m| m.should_include() && self.is_method_constructor(m))
2288    }
2289
2290    /// Class-system-aware constructor detection.
2291    ///
2292    /// The default `ParsedMethod::is_constructor` requires the method to return
2293    /// `Self`. For vctrs impls that's too strict: the canonical vctrs
2294    /// constructor pattern returns the underlying vector payload (e.g.
2295    /// `Vec<f64>`) which `vctrs::new_vctr()` then wraps — returning `Self`
2296    /// would produce an `ExternalPtr` that `new_vctr` can't accept as `.data`.
2297    fn is_method_constructor(&self, m: &ParsedMethod) -> bool {
2298        if m.method_attrs.constructor {
2299            return true;
2300        }
2301        if m.env != ReceiverKind::None || m.ident != "new" {
2302            return false;
2303        }
2304        match self.class_system {
2305            ClassSystem::Vctrs => true,
2306            _ => m.returns_self(),
2307        }
2308    }
2309
2310    /// Get public instance methods (have env, not private, not active).
2311    pub fn public_instance_methods(&self) -> impl Iterator<Item = &ParsedMethod> {
2312        self.methods.iter().filter(|m| {
2313            m.should_include()
2314                && m.env.is_instance()
2315                && !m.is_constructor()
2316                && !m.is_finalizer()
2317                && !m.is_private()
2318                && !m.is_active()
2319        })
2320    }
2321
2322    /// Get private instance methods (have env, private visibility, not active).
2323    pub fn private_instance_methods(&self) -> impl Iterator<Item = &ParsedMethod> {
2324        self.methods.iter().filter(|m| {
2325            m.should_include()
2326                && m.env.is_instance()
2327                && !m.is_constructor()
2328                && !m.is_finalizer()
2329                && m.is_private()
2330                && !m.is_active()
2331        })
2332    }
2333
2334    /// Get active binding getter methods for R6 (have env, marked active, not setter).
2335    /// Active bindings provide property-like access (obj$name instead of obj$name()).
2336    pub fn active_instance_methods(&self) -> impl Iterator<Item = &ParsedMethod> {
2337        self.methods.iter().filter(|m| {
2338            m.should_include()
2339                && m.env.is_instance()
2340                && !m.is_constructor()
2341                && !m.is_finalizer()
2342                && m.is_active()
2343                && !m.method_attrs.r6.setter // Exclude setters
2344        })
2345    }
2346
2347    /// Get active binding setter methods for R6 (have env, marked as r6_setter).
2348    pub fn active_setter_methods(&self) -> impl Iterator<Item = &ParsedMethod> {
2349        self.methods
2350            .iter()
2351            .filter(|m| m.should_include() && m.env.is_instance() && m.method_attrs.r6.setter)
2352    }
2353
2354    /// Find the setter method for a given property name.
2355    pub fn find_setter_for_prop(&self, prop_name: &str) -> Option<&ParsedMethod> {
2356        self.active_setter_methods().find(|m| {
2357            // Match by explicit prop name or by method name with "set_" prefix removed
2358            if let Some(ref explicit_prop) = m.method_attrs.r6.prop {
2359                explicit_prop == prop_name
2360            } else {
2361                // Try to match by stripping "set_" prefix from method name
2362                let method_name = m.ident.to_string();
2363                method_name.strip_prefix("set_").unwrap_or(&method_name) == prop_name
2364            }
2365        })
2366    }
2367
2368    /// Get instance methods (have env) - includes both public and private.
2369    pub fn instance_methods(&self) -> impl Iterator<Item = &ParsedMethod> {
2370        self.methods.iter().filter(|m| {
2371            m.should_include() && m.env.is_instance() && !m.is_constructor() && !m.is_finalizer()
2372        })
2373    }
2374
2375    /// Get static methods (no env, not constructor, not finalizer).
2376    pub fn static_methods(&self) -> impl Iterator<Item = &ParsedMethod> {
2377        self.methods.iter().filter(|m| {
2378            m.should_include()
2379                && m.env == ReceiverKind::None
2380                && !self.is_method_constructor(m)
2381                && !m.is_finalizer()
2382        })
2383    }
2384
2385    /// Get methods with `#[miniextendr(as = "...")]` attribute.
2386    ///
2387    /// These generate S3 methods for R's `as.<class>()` generics like
2388    /// `as.data.frame.MyType`, `as.list.MyType`, etc.
2389    pub fn as_coercion_methods(&self) -> impl Iterator<Item = &ParsedMethod> {
2390        self.methods
2391            .iter()
2392            .filter(|m| m.should_include() && m.method_attrs.as_coercion.is_some())
2393    }
2394
2395    /// Get the finalizer method, if any.
2396    pub fn finalizer(&self) -> Option<&ParsedMethod> {
2397        self.methods
2398            .iter()
2399            .find(|m| m.should_include() && m.is_finalizer())
2400    }
2401
2402    /// Module constant identifier for R wrapper parts.
2403    ///
2404    /// Format: `R_WRAPPERS_IMPL_{TYPE}` or `R_WRAPPERS_IMPL_{TYPE}_{LABEL}` if labeled.
2405    pub fn r_wrappers_const_ident(&self) -> syn::Ident {
2406        let type_upper = self.type_ident.to_string().to_uppercase();
2407        if let Some(ref label) = self.label {
2408            let label_upper = label.to_uppercase();
2409            format_ident!("R_WRAPPERS_IMPL_{}_{}", type_upper, label_upper)
2410        } else {
2411            format_ident!("R_WRAPPERS_IMPL_{}", type_upper)
2412        }
2413    }
2414
2415    /// Returns the label if present.
2416    pub fn label(&self) -> Option<&str> {
2417        self.label.as_deref()
2418    }
2419}
2420
2421/// Generate a C-callable wrapper function for a single method in an impl block.
2422///
2423/// Produces a `#[no_mangle] extern "C"` function named `C_{Type}__{method}` that:
2424/// 1. Accepts SEXP arguments (including `self_sexp` for instance methods)
2425/// 2. Extracts `&self` / `&mut self` from an `ErasedExternalPtr` for instance methods
2426/// 3. Converts SEXP arguments to Rust types
2427/// 4. Calls the actual Rust method
2428/// 5. Converts the return value back to SEXP
2429///
2430/// Thread strategy is determined automatically: instance methods always run on the main
2431/// thread (because `self_ref` is a non-Send borrow), while static methods use the worker
2432/// thread unless `unsafe(main_thread)` is specified.
2433///
2434/// Also emits an `R_CallMethodDef` constant for R routine registration, and appends
2435/// generated R wrapper code fragments to the `r_wrappers_const` string constant.
2436///
2437/// # Arguments
2438///
2439/// * `parsed_impl` - The parsed impl block providing type identity, cfg attrs, and options
2440/// * `method` - The parsed method to generate a wrapper for
2441/// * `r_wrappers_const` - Identifier of the const that accumulates R wrapper code fragments
2442pub fn generate_method_c_wrapper(
2443    parsed_impl: &ParsedImpl,
2444    method: &ParsedMethod,
2445    r_wrappers_const: &syn::Ident,
2446) -> TokenStream {
2447    use crate::c_wrapper_builder::{CWrapperContext, ReturnHandling, ThreadStrategy};
2448
2449    let type_ident = &parsed_impl.type_ident;
2450    let method_ident = &method.ident;
2451    let c_ident = method.c_wrapper_ident(type_ident, parsed_impl.label());
2452
2453    // Determine thread strategy
2454    // Instance methods must use main thread because self_ref is a borrow that can't cross threads
2455    // Static methods use worker thread only when worker=true (set by explicit #[miniextendr(worker)]
2456    // or by the default-worker feature flag)
2457    let thread_strategy = if method.method_attrs.unsafe_main_thread || method.env.is_instance() {
2458        ThreadStrategy::MainThread
2459    } else if method.method_attrs.worker {
2460        ThreadStrategy::WorkerThread
2461    } else {
2462        ThreadStrategy::MainThread
2463    };
2464
2465    // Build rust argument names from the signature
2466    let rust_args: Vec<syn::Ident> = method
2467        .sig
2468        .inputs
2469        .iter()
2470        .filter_map(|arg| {
2471            if let syn::FnArg::Typed(pt) = arg
2472                && let syn::Pat::Ident(pat_ident) = pt.pat.as_ref()
2473            {
2474                Some(pat_ident.ident.clone())
2475            } else {
2476                None
2477            }
2478        })
2479        .collect();
2480
2481    // Generate self extraction for instance methods
2482    // SEXP is now Send+Sync, so this works for both main and worker threads
2483    let pre_call = if method.env.is_instance() {
2484        let self_extraction = match method.env {
2485            ReceiverKind::RefMut => {
2486                quote! {
2487                    let mut self_ptr = unsafe {
2488                        ::miniextendr_api::externalptr::ErasedExternalPtr::from_sexp(self_sexp)
2489                    };
2490                    let self_ref = self_ptr.downcast_mut::<#type_ident>()
2491                        .expect(concat!("expected ExternalPtr<", stringify!(#type_ident), ">"));
2492                }
2493            }
2494            ReceiverKind::Ref => {
2495                quote! {
2496                    let self_ptr = unsafe {
2497                        ::miniextendr_api::externalptr::ErasedExternalPtr::from_sexp(self_sexp)
2498                    };
2499                    let self_ref = self_ptr.downcast_ref::<#type_ident>()
2500                        .expect(concat!("expected ExternalPtr<", stringify!(#type_ident), ">"));
2501                }
2502            }
2503            ReceiverKind::ExternalPtrRef => {
2504                quote! {
2505                    let __self_ptr = unsafe {
2506                        ::miniextendr_api::externalptr::ExternalPtr::<#type_ident>::wrap_sexp(self_sexp)
2507                            .expect(concat!("expected ExternalPtr<", stringify!(#type_ident), ">"))
2508                    };
2509                }
2510            }
2511            ReceiverKind::ExternalPtrRefMut => {
2512                quote! {
2513                    let mut __self_ptr = unsafe {
2514                        ::miniextendr_api::externalptr::ExternalPtr::<#type_ident>::wrap_sexp(self_sexp)
2515                            .expect(concat!("expected ExternalPtr<", stringify!(#type_ident), ">"))
2516                    };
2517                }
2518            }
2519            ReceiverKind::ExternalPtrValue => {
2520                quote! {
2521                    let __self_ptr = unsafe {
2522                        ::miniextendr_api::externalptr::ExternalPtr::<#type_ident>::wrap_sexp(self_sexp)
2523                            .expect(concat!("expected ExternalPtr<", stringify!(#type_ident), ">"))
2524                    };
2525                }
2526            }
2527            _ => unreachable!(),
2528        };
2529        vec![self_extraction]
2530    } else {
2531        vec![]
2532    };
2533
2534    // Generate call expression
2535    let call_expr = match method.env {
2536        ReceiverKind::Ref | ReceiverKind::RefMut => {
2537            quote! { self_ref.#method_ident(#(#rust_args),*) }
2538        }
2539        ReceiverKind::ExternalPtrRef => {
2540            quote! { #type_ident::#method_ident(&__self_ptr, #(#rust_args),*) }
2541        }
2542        ReceiverKind::ExternalPtrRefMut => {
2543            quote! { #type_ident::#method_ident(&mut __self_ptr, #(#rust_args),*) }
2544        }
2545        ReceiverKind::ExternalPtrValue => {
2546            quote! { #type_ident::#method_ident(__self_ptr, #(#rust_args),*) }
2547        }
2548        ReceiverKind::None | ReceiverKind::Value => {
2549            quote! { #type_ident::#method_ident(#(#rust_args),*) }
2550        }
2551    };
2552
2553    // Determine return handling strategy
2554    let return_handling = if method.returns_self() {
2555        ReturnHandling::ExternalPtr
2556    } else if method.method_attrs.unwrap_in_r && output_is_result(&method.sig.output) {
2557        ReturnHandling::IntoR
2558    } else {
2559        crate::c_wrapper_builder::detect_return_handling(&method.sig.output)
2560    };
2561
2562    // Build the context using the builder
2563    let mut builder = CWrapperContext::builder(method_ident.clone(), c_ident)
2564        .r_wrapper_const(r_wrappers_const.clone())
2565        .inputs(method.sig.inputs.clone())
2566        .output(method.sig.output.clone())
2567        .pre_call(pre_call)
2568        .call_expr(call_expr)
2569        .thread_strategy(thread_strategy)
2570        .return_handling(return_handling)
2571        .cfg_attrs(parsed_impl.cfg_attrs.clone())
2572        .type_context(type_ident.clone());
2573
2574    if method.env.is_instance() {
2575        builder = builder.has_self();
2576    }
2577
2578    if method.method_attrs.coerce {
2579        builder = builder.coerce_all();
2580    }
2581
2582    if method.method_attrs.check_interrupt {
2583        builder = builder.check_interrupt();
2584    }
2585
2586    if method.method_attrs.rng {
2587        builder = builder.rng();
2588    }
2589
2590    if parsed_impl.strict {
2591        builder = builder.strict();
2592    }
2593
2594    // Forward match_arg + several_ok parameter names so `RustConversionBuilder` swaps
2595    // in `match_arg_vec_from_sexp` for the Vec/slice/array/Box<[_]> conversion path.
2596    // Scalar match_arg doesn't need this — R's match.arg() validated the choice and
2597    // `TryFromSexp for EnumType` (auto-generated by `#[derive(MatchArg)]`) decodes it.
2598    for (rust_name, attrs) in &method.method_attrs.per_param {
2599        if attrs.match_arg && attrs.several_ok {
2600            builder = builder.match_arg_several_ok(rust_name.clone());
2601        }
2602    }
2603
2604    let c_wrapper_and_def = builder.build().generate();
2605
2606    // Emit one `__match_arg_choices__<param>` helper fn + linkme registrations for each
2607    // match_arg-annotated parameter so the R wrapper can look up the enum's
2608    // `MatchArg::CHOICES` at call time (C extern) and the package-load write step can
2609    // substitute the placeholder default with the literal choices (distributed_slice).
2610    let match_arg_helpers = generate_method_match_arg_helpers(parsed_impl, method);
2611
2612    quote! {
2613        #c_wrapper_and_def
2614        #match_arg_helpers
2615    }
2616}
2617
2618/// Generate `__match_arg_choices__<param>` helper C wrappers plus the two linkme
2619/// registrations (`MX_CALL_DEFS` and `MX_MATCH_ARG_CHOICES`) per match_arg parameter.
2620///
2621/// Mirrors the standalone-fn emission in `lib.rs` so both surfaces resolve through the
2622/// same runtime paths — `C_*__match_arg_choices__*` is called from R's prelude, and
2623/// `MX_MATCH_ARG_CHOICES` drives write-time placeholder substitution when the cdylib
2624/// emits the final R wrapper file.
2625fn generate_method_match_arg_helpers(
2626    parsed_impl: &ParsedImpl,
2627    method: &ParsedMethod,
2628) -> TokenStream {
2629    if !method
2630        .method_attrs
2631        .per_param
2632        .values()
2633        .any(|a| a.match_arg || a.choices.is_some())
2634    {
2635        return TokenStream::new();
2636    }
2637
2638    let type_ident = &parsed_impl.type_ident;
2639    let c_ident = method.c_wrapper_ident(type_ident, parsed_impl.label());
2640    let c_ident_str = c_ident.to_string();
2641    let cfg_attrs = &parsed_impl.cfg_attrs;
2642
2643    let mut out = TokenStream::new();
2644
2645    for (rust_name, attrs) in method.method_attrs.per_param.iter() {
2646        if !attrs.match_arg {
2647            continue;
2648        }
2649        // Find the parameter type from the (already-normalized) signature.
2650        let Some(param_ty) = find_param_type(&method.sig.inputs, rust_name) else {
2651            continue;
2652        };
2653        // For several_ok, unwrap the container (Vec<Mode>, Box<[Mode]>, [Mode; N], &[Mode])
2654        // so the helper returns the inner enum's CHOICES, not the container type.
2655        let several_ok = attrs.several_ok;
2656        let choices_ty = if several_ok {
2657            crate::classify_several_ok_container(param_ty)
2658                .map(|(_, inner)| inner.clone())
2659                .unwrap_or_else(|| param_ty.clone())
2660        } else {
2661            param_ty.clone()
2662        };
2663
2664        let r_name = crate::r_wrapper_builder::normalize_r_arg_string(rust_name);
2665
2666        // C helper fn that R calls via .__mx_choices_<param> <- .Call(C_...)
2667        let helper_c_name_str = crate::match_arg_keys::choices_helper_c_name(&c_ident_str, &r_name);
2668        let helper_fn_ident = syn::Ident::new(&helper_c_name_str, proc_macro2::Span::call_site());
2669        let helper_def_ident =
2670            crate::match_arg_keys::choices_helper_def_ident(&c_ident_str, &r_name);
2671        let helper_c_name = syn::LitCStr::new(
2672            std::ffi::CString::new(helper_c_name_str.clone())
2673                .expect("valid C string")
2674                .as_c_str(),
2675            proc_macro2::Span::call_site(),
2676        );
2677
2678        // Placeholder that the write-time substitution pass replaces with the
2679        // literal `c("a", "b", ...)` default. Shape matches the standalone-fn convention.
2680        let placeholder = crate::r_class_formatter::match_arg_placeholder(&c_ident_str, &r_name);
2681        let entry_ident = syn::Ident::new(
2682            &format!(
2683                "match_arg_choices_entry_{}",
2684                crate::match_arg_keys::placeholder_ident_suffix(&placeholder)
2685            ),
2686            proc_macro2::Span::call_site(),
2687        );
2688        let doc_placeholder =
2689            crate::r_class_formatter::match_arg_param_doc_placeholder(&c_ident_str, &r_name);
2690        let doc_entry_ident = syn::Ident::new(
2691            &format!(
2692                "match_arg_param_doc_entry_{}",
2693                crate::match_arg_keys::placeholder_ident_suffix(&doc_placeholder)
2694            ),
2695            proc_macro2::Span::call_site(),
2696        );
2697
2698        let preferred_default = attrs
2699            .default
2700            .as_deref()
2701            .map(crate::match_arg_keys::extract_match_arg_default)
2702            .unwrap_or_default();
2703        let choices_entry_tokens = crate::match_arg_keys::choices_entry_tokens(
2704            cfg_attrs,
2705            &entry_ident,
2706            &placeholder,
2707            &choices_ty,
2708            &preferred_default,
2709        );
2710        let param_doc_entry_tokens = crate::match_arg_keys::param_doc_entry_tokens(
2711            cfg_attrs,
2712            &doc_entry_ident,
2713            &doc_placeholder,
2714            several_ok,
2715            &choices_ty,
2716        );
2717
2718        out.extend(quote! {
2719            #(#cfg_attrs)*
2720            #[allow(non_snake_case)]
2721            #[unsafe(no_mangle)]
2722            pub extern "C-unwind" fn #helper_fn_ident(
2723                __miniextendr_call: ::miniextendr_api::ffi::SEXP,
2724            ) -> ::miniextendr_api::ffi::SEXP {
2725                ::miniextendr_api::choices_sexp::<#choices_ty>()
2726            }
2727
2728            #(#cfg_attrs)*
2729            #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_CALL_DEFS), linkme(crate = ::miniextendr_api::linkme))]
2730            #[allow(non_upper_case_globals)]
2731            #[allow(non_snake_case)]
2732            static #helper_def_ident: ::miniextendr_api::ffi::R_CallMethodDef = unsafe {
2733                ::miniextendr_api::ffi::R_CallMethodDef {
2734                    name: #helper_c_name.as_ptr(),
2735                    fun: Some(::std::mem::transmute::<
2736                        unsafe extern "C-unwind" fn(
2737                            ::miniextendr_api::ffi::SEXP,
2738                        ) -> ::miniextendr_api::ffi::SEXP,
2739                        unsafe extern "C-unwind" fn() -> *mut ::std::os::raw::c_void,
2740                    >(#helper_fn_ident)),
2741                    numArgs: 1i32,
2742                }
2743            };
2744
2745            #choices_entry_tokens
2746            #param_doc_entry_tokens
2747        });
2748    }
2749
2750    out
2751}
2752
2753/// Find a parameter's Rust type from a stripped signature by identifier name.
2754fn find_param_type<'a>(
2755    inputs: &'a syn::punctuated::Punctuated<syn::FnArg, syn::Token![,]>,
2756    name: &str,
2757) -> Option<&'a syn::Type> {
2758    for arg in inputs {
2759        if let syn::FnArg::Typed(pt) = arg
2760            && let syn::Pat::Ident(pat_ident) = pt.pat.as_ref()
2761            && pat_ident.ident == name
2762        {
2763            return Some(pt.ty.as_ref());
2764        }
2765    }
2766    None
2767}
2768
2769/// Check whether a function's return type is syntactically `Result<_, _>`.
2770///
2771/// This performs a shallow name check on the last path segment -- it does not resolve
2772/// type aliases. Used to decide whether `unwrap_in_r` should strip the `Result` wrapper
2773/// before converting to SEXP.
2774fn output_is_result(output: &syn::ReturnType) -> bool {
2775    match output {
2776        syn::ReturnType::Type(_, ty) => matches!(
2777            ty.as_ref(),
2778            syn::Type::Path(p)
2779                if p.path
2780                    .segments
2781                    .last()
2782                    .map(|s| s.ident == "Result")
2783                    .unwrap_or(false)
2784        ),
2785        syn::ReturnType::Default => false,
2786    }
2787}
2788
2789/// Returns true if a vctrs constructor's return type is `Self`, `&Self`, `&mut Self`,
2790/// the named impl type, `Box<Self>`, `Result<Self, _>`, or `Result<NamedType, _>`.
2791///
2792/// These are all invalid for a vctrs constructor because the generated R wrapper
2793/// passes the return value to `vctrs::new_vctr()` / `new_rcrd()` / `new_list_of()`,
2794/// which require a plain vector payload — not an `ExternalPtr` (`EXTPTRSXP`).
2795fn vctrs_ctor_returns_self_or_type(output: &syn::ReturnType, type_ident: &syn::Ident) -> bool {
2796    let syn::ReturnType::Type(_, ty) = output else {
2797        return false;
2798    };
2799    ty_is_self_or_named(ty.as_ref(), type_ident)
2800}
2801
2802/// Recursively checks whether `ty` is `Self`, `&Self`, `&mut Self`, `Box<Self>`,
2803/// the named type, or `Result<(Self | NamedType), _>`.
2804fn ty_is_self_or_named(ty: &syn::Type, type_ident: &syn::Ident) -> bool {
2805    match ty {
2806        syn::Type::Path(p) => {
2807            let last = match p.path.segments.last() {
2808                Some(s) => s,
2809                None => return false,
2810            };
2811            // Plain `Self` or `TypeName`
2812            if last.ident == "Self" || last.ident == *type_ident {
2813                return true;
2814            }
2815            // `Result<Self, _>` or `Result<TypeName, _>`
2816            if last.ident == "Result"
2817                && let syn::PathArguments::AngleBracketed(ref args) = last.arguments
2818                && let Some(syn::GenericArgument::Type(first_ty)) = args.args.first()
2819            {
2820                return ty_is_self_or_named(first_ty, type_ident);
2821            }
2822            // `Box<Self>` or `Box<TypeName>`
2823            if last.ident == "Box"
2824                && let syn::PathArguments::AngleBracketed(ref args) = last.arguments
2825                && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
2826            {
2827                return ty_is_self_or_named(inner, type_ident);
2828            }
2829            false
2830        }
2831        // `&Self` or `&mut Self`
2832        syn::Type::Reference(r) => ty_is_self_or_named(r.elem.as_ref(), type_ident),
2833        _ => false,
2834    }
2835}
2836
2837// region: Class-system R wrapper generators (sub-modules)
2838
2839/// Environment-based class wrapper generator (`obj$method()` dispatch).
2840mod env_class;
2841/// R6 class wrapper generator (`R6Class` with `$new()`, active bindings, private methods).
2842mod r6_class;
2843/// S3 class wrapper generator (`structure()` + `generic.Class` dispatch).
2844mod s3_class;
2845/// S4 class wrapper generator (`setClass` / `setMethod` formal OOP).
2846mod s4_class;
2847/// S7 class wrapper generator (`new_class` / `new_generic` modern R OOP).
2848mod s7_class;
2849/// vctrs-compatible class wrapper generator (`new_vctr` / `new_rcrd` / `new_list_of`).
2850mod vctrs_class;
2851
2852pub(crate) use env_class::generate_env_r_wrapper;
2853pub(crate) use r6_class::generate_r6_r_wrapper;
2854pub(crate) use s3_class::generate_s3_r_wrapper;
2855pub(crate) use s4_class::generate_s4_r_wrapper;
2856pub(crate) use s7_class::generate_s7_r_wrapper;
2857#[cfg(test)]
2858use s7_class::rust_type_to_s7_class;
2859pub(crate) use vctrs_class::generate_vctrs_r_wrapper;
2860
2861/// Generate R S3 method wrappers for `as.<class>()` coercion methods.
2862///
2863/// For each method with `#[miniextendr(as = "...")]`, generates an S3 method like:
2864///
2865/// ```r
2866/// #' @export
2867/// #' @method as.data.frame MyType
2868/// as.data.frame.MyType <- function(x, ...) {
2869///     .Call(C_MyType__as_data_frame, .call = match.call(), x)
2870/// }
2871/// ```
2872///
2873/// This function is called by each class system generator to append the
2874/// `as.*` methods to the R wrapper output.
2875pub fn generate_as_coercion_methods(parsed_impl: &ParsedImpl) -> String {
2876    use crate::r_class_formatter::MethodContext;
2877
2878    let class_name = parsed_impl.class_name();
2879    let type_ident = &parsed_impl.type_ident;
2880
2881    // Check if class has @noRd - if so, skip documentation
2882    let class_doc_tags = &parsed_impl.doc_tags;
2883    let class_has_no_rd = crate::roxygen::has_roxygen_tag(class_doc_tags, "noRd");
2884    let class_has_internal = crate::roxygen::has_roxygen_tag(class_doc_tags, "keywords internal")
2885        || parsed_impl.internal;
2886    let should_export = !class_has_no_rd && !class_has_internal && !parsed_impl.noexport;
2887
2888    let mut lines = Vec::new();
2889
2890    for method in parsed_impl.as_coercion_methods() {
2891        // Get the coercion target (e.g., "data.frame", "list", "character")
2892        let coercion_target = match &method.method_attrs.as_coercion {
2893            Some(target) => target.clone(),
2894            None => continue,
2895        };
2896
2897        // Build method context for .Call generation
2898        let ctx = MethodContext::new(method, type_ident, parsed_impl.label());
2899
2900        // Normalize coercion target for R generic name
2901        // R has both as.numeric and as.double - they're equivalent, but we use the specified one
2902        // Some targets use non-standard S3 generic names (e.g., tibble uses as_tibble, not as.tibble)
2903        let r_generic = match coercion_target.as_str() {
2904            "numeric" => "as.numeric".to_string(),
2905            "double" => "as.double".to_string(),
2906            "tibble" => "as_tibble".to_string(),
2907            "ts" => "as.ts".to_string(),
2908            other => format!("as.{}", other),
2909        };
2910
2911        // S3 method name: as.data.frame.MyType
2912        let s3_method_name = format!("{}.{}", r_generic, class_name);
2913
2914        // Documentation
2915        if !class_has_no_rd {
2916            // Add documentation from the method
2917            if !method.doc_tags.is_empty() {
2918                crate::roxygen::push_roxygen_tags(&mut lines, &method.doc_tags);
2919            }
2920            lines.push(format!("#' @name {}", s3_method_name));
2921            lines.push(format!("#' @rdname {}", class_name));
2922            lines.push(crate::roxygen::method_source_tag(type_ident, &method.ident));
2923        }
2924
2925        // Export and method registration
2926        if should_export {
2927            lines.push("#' @export".to_string());
2928        }
2929        lines.push(format!("#' @method {} {}", r_generic, class_name));
2930
2931        // Function signature: always takes x and ... for S3 method compatibility
2932        // Additional parameters from the method are included
2933        let method_params =
2934            crate::r_wrapper_builder::build_r_formals_from_sig(&method.sig, &method.param_defaults);
2935        let formals = if method_params.is_empty() {
2936            "x, ...".to_string()
2937        } else {
2938            format!("x, {}, ...", method_params)
2939        };
2940
2941        lines.push(format!("{} <- function({}) {{", s3_method_name, formals));
2942
2943        // Build the .Call() invocation
2944        let call = ctx.instance_call("x");
2945        let strategy = crate::ReturnStrategy::for_method(method);
2946        let return_builder = crate::MethodReturnBuilder::new(call)
2947            .with_strategy(strategy)
2948            .with_class_name(class_name.clone());
2949        lines.extend(return_builder.build_s3_body());
2950
2951        lines.push("}".to_string());
2952        lines.push(String::new());
2953    }
2954
2955    lines.join("\n")
2956}
2957
2958/// Generate `impl As*` trait impls for methods with `#[miniextendr(as = "...")]`.
2959///
2960/// For each `as` coercion method, generates a forwarding trait impl:
2961/// ```ignore
2962/// impl ::miniextendr_api::as_coerce::AsDataFrame for MyType {
2963///     fn as_data_frame(&self) -> Result<::miniextendr_api::List, ::miniextendr_api::as_coerce::AsCoerceError> {
2964///         self.as_data_frame()  // inherent method preferred over trait method
2965///     }
2966/// }
2967/// ```
2968///
2969/// Skips methods with extra parameters beyond `&self` (trait methods have fixed signatures)
2970/// and skips non-standard targets (like "tibble", "data.table") that don't have corresponding traits.
2971pub fn generate_as_coercion_trait_impls(parsed_impl: &ParsedImpl) -> TokenStream {
2972    let type_ident = &parsed_impl.type_ident;
2973    let cfg_attrs = &parsed_impl.cfg_attrs;
2974
2975    let mut impls = Vec::new();
2976
2977    for method in parsed_impl.as_coercion_methods() {
2978        let coercion_target = match &method.method_attrs.as_coercion {
2979            Some(target) => target.as_str(),
2980            None => continue,
2981        };
2982
2983        // Skip methods with extra params beyond &self — trait methods have fixed &self-only signatures.
2984        // `sig.inputs` already has self stripped, so non-empty means extra params.
2985        if !method.sig.inputs.is_empty() {
2986            continue;
2987        }
2988
2989        // Skip non-instance methods (trait requires &self)
2990        if method.env != ReceiverKind::Ref {
2991            continue;
2992        }
2993
2994        // Map coercion target to (trait name, trait method name, return type tokens).
2995        // Only the 15 standard targets that have corresponding traits in as_coerce.
2996        let (trait_name, trait_method): (&str, &str) = match coercion_target {
2997            "data.frame" => ("AsDataFrame", "as_data_frame"),
2998            "list" => ("AsList", "as_list"),
2999            "character" => ("AsCharacter", "as_character"),
3000            "numeric" | "double" => ("AsNumeric", "as_numeric"),
3001            "integer" => ("AsInteger", "as_integer"),
3002            "logical" => ("AsLogical", "as_logical"),
3003            "matrix" => ("AsMatrix", "as_matrix"),
3004            "vector" => ("AsVector", "as_vector"),
3005            "factor" => ("AsFactor", "as_factor"),
3006            "Date" => ("AsDate", "as_date"),
3007            "POSIXct" => ("AsPOSIXct", "as_posixct"),
3008            "complex" => ("AsComplex", "as_complex"),
3009            "raw" => ("AsRaw", "as_raw"),
3010            "environment" => ("AsEnvironment", "as_environment"),
3011            "function" => ("AsFunction", "as_function"),
3012            _ => continue, // Non-standard targets (tibble, data.table, etc.)
3013        };
3014
3015        let trait_ident = syn::Ident::new(trait_name, proc_macro2::Span::call_site());
3016        let trait_method_ident = syn::Ident::new(trait_method, proc_macro2::Span::call_site());
3017        let user_method_ident = &method.ident;
3018
3019        // Return type: data.frame and list return Result<List, AsCoerceError>,
3020        // all others return Result<SEXP, AsCoerceError>
3021        let return_type = match coercion_target {
3022            "data.frame" | "list" => quote! {
3023                ::core::result::Result<::miniextendr_api::List, ::miniextendr_api::as_coerce::AsCoerceError>
3024            },
3025            _ => quote! {
3026                ::core::result::Result<::miniextendr_api::ffi::SEXP, ::miniextendr_api::as_coerce::AsCoerceError>
3027            },
3028        };
3029
3030        impls.push(quote! {
3031            #(#cfg_attrs)*
3032            impl ::miniextendr_api::as_coerce::#trait_ident for #type_ident {
3033                fn #trait_method_ident(&self) -> #return_type {
3034                    self.#user_method_ident()
3035                }
3036            }
3037        });
3038    }
3039
3040    quote! { #(#impls)* }
3041}
3042
3043/// Top-level entry point for expanding `#[miniextendr]` on impl blocks.
3044///
3045/// Dispatches between two cases:
3046/// 1. **Inherent impls** (`impl Type { ... }`): Parses [`ImplAttrs`] and [`ParsedImpl`],
3047///    then generates C wrappers, R wrapper code, `R_CallMethodDef` arrays, and
3048///    `as.<class>()` trait impls for the chosen class system.
3049/// 2. **Trait impls** (`impl Trait for Type { ... }`): Generates trait ABI vtables,
3050///    cross-package shims, and R wrappers via
3051///    [`expand_miniextendr_impl_trait`](crate::miniextendr_impl_trait::expand_miniextendr_impl_trait).
3052///
3053/// # Arguments
3054///
3055/// * `attr` - The token stream inside `#[miniextendr(...)]` (class system, options)
3056/// * `item` - The full `impl` block token stream
3057///
3058/// # Returns
3059///
3060/// A token stream containing the original impl block (with miniextendr attrs stripped),
3061/// C wrapper functions, an R wrapper string constant, a `R_CallMethodDef` array constant,
3062/// and any forwarding trait impls for `as.<class>()` coercion.
3063pub fn expand_impl(
3064    attr: proc_macro::TokenStream,
3065    item: proc_macro::TokenStream,
3066) -> proc_macro::TokenStream {
3067    let item_impl = match syn::parse::<syn::ItemImpl>(item.clone()) {
3068        Ok(i) => i,
3069        Err(e) => return e.into_compile_error().into(),
3070    };
3071
3072    // Check if this is a trait impl (impl Trait for Type)
3073    if item_impl.trait_.is_some() {
3074        // Delegate to trait ABI vtable generator
3075        return crate::miniextendr_impl_trait::expand_miniextendr_impl_trait(attr, item);
3076    }
3077
3078    // Otherwise, this is an inherent impl - parse class system attrs
3079    let attrs = match syn::parse::<ImplAttrs>(attr) {
3080        Ok(a) => a,
3081        Err(e) => return e.into_compile_error().into(),
3082    };
3083
3084    let parsed = match ParsedImpl::parse(attrs, item_impl) {
3085        Ok(p) => p,
3086        Err(e) => return e.into_compile_error().into(),
3087    };
3088
3089    // Generate constants for module registration (needed for doc links)
3090    let type_ident = &parsed.type_ident;
3091    let cfg_attrs = &parsed.cfg_attrs;
3092    let r_wrappers_const = parsed.r_wrappers_const_ident();
3093
3094    // Generate C wrappers for all included methods
3095    let c_wrappers: Vec<TokenStream> = parsed
3096        .included_methods()
3097        .map(|m| generate_method_c_wrapper(&parsed, m, &r_wrappers_const))
3098        .collect();
3099
3100    // Generate R wrapper string based on class system
3101    let mut r_wrapper_string = match parsed.class_system {
3102        ClassSystem::Env => generate_env_r_wrapper(&parsed),
3103        ClassSystem::R6 => generate_r6_r_wrapper(&parsed),
3104        ClassSystem::S3 => generate_s3_r_wrapper(&parsed),
3105        ClassSystem::S7 => generate_s7_r_wrapper(&parsed),
3106        ClassSystem::S4 => generate_s4_r_wrapper(&parsed),
3107        ClassSystem::Vctrs => generate_vctrs_r_wrapper(&parsed),
3108    };
3109
3110    // Append as.<class>() coercion methods (works with all class systems)
3111    let as_coercion_wrappers = generate_as_coercion_methods(&parsed);
3112    if !as_coercion_wrappers.is_empty() {
3113        r_wrapper_string.push_str("\n\n");
3114        r_wrapper_string.push_str(&as_coercion_wrappers);
3115    }
3116
3117    let original_impl = &parsed.original_impl;
3118
3119    // Generate forwarding trait impls for as.<class>() coercion methods
3120    let trait_impls = generate_as_coercion_trait_impls(&parsed);
3121
3122    let r_wrapper_str = crate::r_wrapper_raw_literal(&r_wrapper_string);
3123
3124    // Generate doc comment linking to R wrapper constant
3125    let r_wrapper_doc = format!(
3126        "See [`{}`] for the generated R wrapper code.",
3127        r_wrappers_const
3128    );
3129    let source_loc_doc = crate::source_location_doc(type_ident.span());
3130    let source_start = type_ident.span().start();
3131    let source_line_lit = syn::LitInt::new(&source_start.line.to_string(), type_ident.span());
3132    let source_col_lit =
3133        syn::LitInt::new(&(source_start.column + 1).to_string(), type_ident.span());
3134
3135    let param_warnings = &parsed.param_warnings;
3136
3137    // Build MX_CLASS_NAMES entry for cross-reference resolution.
3138    // r_class_name is the R-visible name (may differ from type_ident when
3139    // `class = "Override"` was set on the impl block).
3140    let r_class_name_str = parsed.class_name();
3141    let class_system_str = parsed.class_system.to_ident().to_string();
3142    let class_names_const = syn::Ident::new(
3143        &format!(
3144            "__mx_class_name_entry_{}",
3145            type_ident.to_string().to_lowercase()
3146        ),
3147        type_ident.span(),
3148    );
3149
3150    let expanded = quote! {
3151        // Original impl block with doc link to R wrapper
3152        #[doc = #r_wrapper_doc]
3153        #[doc = #source_loc_doc]
3154        #[doc = concat!("Generated from source file `", file!(), "`.")]
3155        #original_impl
3156
3157        // Warnings for @param tags on impl blocks
3158        #param_warnings
3159
3160        // C wrappers and call method defs
3161        #(#c_wrappers)*
3162
3163        // Forwarding trait impls for as.<class>() coercion methods
3164        #trait_impls
3165
3166        // R wrapper registration via distributed slice
3167        #(#cfg_attrs)*
3168        #[doc = concat!(
3169            "R wrapper code for impl block on `",
3170            stringify!(#type_ident),
3171            "`."
3172        )]
3173        #[doc = #source_loc_doc]
3174        #[doc = concat!("Generated from source file `", file!(), "`.")]
3175        #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_R_WRAPPERS), linkme(crate = ::miniextendr_api::linkme))]
3176        static #r_wrappers_const: ::miniextendr_api::registry::RWrapperEntry =
3177            ::miniextendr_api::registry::RWrapperEntry {
3178                priority: ::miniextendr_api::registry::RWrapperPriority::Class,
3179                source_file: file!(),
3180                content: concat!(
3181                    "# Generated from Rust impl `",
3182                    stringify!(#type_ident),
3183                    "` (",
3184                    file!(),
3185                    ":",
3186                    #source_line_lit,
3187                    ":",
3188                    #source_col_lit,
3189                    ")",
3190                    #r_wrapper_str
3191                ),
3192            };
3193
3194        // Class name registration for cross-reference placeholder resolution.
3195        // Maps the Rust type name to the R-visible class name at link time.
3196        #(#cfg_attrs)*
3197        #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_CLASS_NAMES), linkme(crate = ::miniextendr_api::linkme))]
3198        #[allow(non_upper_case_globals)]
3199        #[allow(non_snake_case)]
3200        static #class_names_const: ::miniextendr_api::registry::ClassNameEntry =
3201            ::miniextendr_api::registry::ClassNameEntry {
3202                rust_type: stringify!(#type_ident),
3203                r_class_name: #r_class_name_str,
3204                class_system: #class_system_str,
3205            };
3206    };
3207
3208    expanded.into()
3209}
3210
3211#[cfg(test)]
3212mod tests;
3213// endregion