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