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