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