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