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