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