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