Skip to main content

miniextendr_macros/miniextendr_impl_trait/
vtable.rs

1//! Vtable static generation, C wrapper generation, and method attribute parsing for trait impls.
2//!
3//! This module contains the core codegen for `#[miniextendr]` on `impl Trait for Type` blocks.
4//! It produces the vtable static constant, C-callable wrapper functions for each method and
5//! associated constant, and delegates to [`super::r_wrappers`] for R wrapper code generation.
6
7use proc_macro2::TokenStream;
8use quote::format_ident;
9use syn::ItemImpl;
10
11use super::r_wrappers::{TraitWrapperOpts, generate_trait_r_wrapper};
12use super::{TraitConst, TraitMethod, type_to_uppercase_name};
13use crate::miniextendr_impl::ClassSystem;
14
15/// Generate the vtable static, C wrappers, R wrappers, and call defs for a trait implementation.
16///
17/// This is the main entry point for trait impl codegen. For a given
18/// `impl Trait for Type { ... }` block, it produces:
19///
20/// - The cleaned original impl block (with `#[miniextendr]` attrs stripped from methods)
21/// - A `static __VTABLE_{CRATE}_{TRAIT}_FOR_{TYPE}: {Trait}VTable` constant
22/// - C wrapper functions and `R_CallMethodDef` entries for each method and const
23/// - R wrapper code string (class-system specific)
24/// - Two `const` items: `{TYPE}_{TRAIT}_CALL_DEFS` and `R_WRAPPERS_{TYPE}_{TRAIT}_IMPL`
25///
26/// # Arguments
27///
28/// - `impl_item`: The parsed `impl Trait for Type` block
29/// - `trait_path`: Full path to the trait (e.g., `crate::Counter`)
30/// - `concrete_type`: The implementing type (e.g., `MyCounter`)
31/// - `class_system`: Which R class system (env, r6, s3, s4, s7) to generate wrappers for
32/// - `blanket`: If true, skip emitting the impl block (a blanket impl already provides it)
33/// - `internal`: If true, add `@keywords internal` to R documentation
34/// - `noexport`: If true, suppress `@export` in R documentation
35pub(super) fn generate_vtable_static(
36    impl_item: &ItemImpl,
37    trait_path: &syn::Path,
38    concrete_type: &syn::Type,
39    class_system: ClassSystem,
40    blanket: bool,
41    internal: bool,
42    noexport: bool,
43) -> TokenStream {
44    // Extract trait name for naming
45    let Some(trait_name) = trait_path.segments.last().map(|s| &s.ident) else {
46        return syn::Error::new_spanned(trait_path, "trait path must have at least one segment")
47            .into_compile_error();
48    };
49
50    // Extract type args from trait path's last segment
51    // e.g., for `RExtend<i32>`, extract `[i32]`; for `RMakeIter<i32, IterableVecIter>`, extract `[i32, IterableVecIter]`
52    let trait_type_args: Vec<syn::Type> = trait_path
53        .segments
54        .last()
55        .and_then(|seg| {
56            if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
57                Some(
58                    args.args
59                        .iter()
60                        .filter_map(|arg| {
61                            if let syn::GenericArgument::Type(ty) = arg {
62                                Some(ty.clone())
63                            } else {
64                                None
65                            }
66                        })
67                        .collect(),
68                )
69            } else {
70                None
71            }
72        })
73        .unwrap_or_default();
74
75    // Extract type identifier (for simple types)
76    let type_ident = match concrete_type {
77        syn::Type::Path(type_path) => {
78            let Some(last_seg) = type_path.path.segments.last() else {
79                return syn::Error::new_spanned(
80                    concrete_type,
81                    "type path must have at least one segment",
82                )
83                .into_compile_error();
84            };
85            last_seg.ident.clone()
86        }
87        _ => format_ident!("Unknown"),
88    };
89
90    // Extract type name for naming (simplified - handles Path types)
91    let type_name_str = type_to_uppercase_name(concrete_type);
92    let trait_name_upper = trait_name.to_string().to_uppercase();
93    let trait_name_lower = trait_name.to_string().to_lowercase();
94
95    // Generate names (crate-prefixed vtable static — #1273 — routed through the
96    // shared naming.rs helper so this can't drift from the other two
97    // reconstruction sites in `miniextendr_impl_trait.rs`)
98    let vtable_static_name = crate::naming::vtable_static_ident(&trait_name_upper, &type_name_str);
99    let vtable_type_name = format_ident!("{}VTable", trait_name);
100
101    // Build path to vtable builder function
102    // If trait is `foo::Counter`, builder is `foo::__counter_build_vtable`
103    // Strip type args from the path (builder uses turbofish instead)
104    let mut builder_path = trait_path.clone();
105    if let Some(last) = builder_path.segments.last_mut() {
106        last.ident = format_ident!("__{}_build_vtable", trait_name_lower);
107        last.arguments = syn::PathArguments::None;
108    }
109
110    // Build the vtable type path (same module as trait)
111    // Strip type args (vtable type is not generic)
112    let mut vtable_type_path = trait_path.clone();
113    if let Some(last) = vtable_type_path.segments.last_mut() {
114        last.ident = vtable_type_name.clone();
115        last.arguments = syn::PathArguments::None;
116    }
117
118    // Parse methods and consts from the impl block
119    let all_methods = match extract_methods(impl_item) {
120        Ok(m) => m,
121        Err(e) => return e.into_compile_error(),
122    };
123    let consts = extract_consts(impl_item);
124
125    // Separate skipped methods: skipped methods are kept in the emitted impl block
126    // but excluded from C wrappers, R wrappers, call defs, and vtable shims.
127    let methods: Vec<&TraitMethod> = all_methods.iter().filter(|m| !m.skip).collect();
128
129    // Generate C wrappers and call defs for each non-skipped method
130    let method_c_wrappers: Vec<TokenStream> = methods
131        .iter()
132        .map(|m| generate_trait_method_c_wrapper(m, &type_ident, trait_name, trait_path))
133        .collect();
134
135    // Generate C wrappers for consts
136    let const_c_wrappers: Vec<TokenStream> = consts
137        .iter()
138        .map(|c| generate_trait_const_c_wrapper(c, &type_ident, trait_name, trait_path))
139        .collect();
140
141    // Combine C wrappers
142    let c_wrappers: Vec<TokenStream> = method_c_wrappers
143        .into_iter()
144        .chain(const_c_wrappers)
145        .collect();
146
147    // Check if impl block has @noRd doc comment (strip @param from class-level tags)
148    let raw_impl_tags = crate::roxygen::roxygen_tags_from_attrs(&impl_item.attrs);
149    let (impl_doc_tags, param_warnings) = crate::roxygen::strip_method_tags(
150        &raw_impl_tags,
151        &type_ident.to_string(),
152        crate::roxygen::next_impl_tag_block_id(),
153        impl_item.impl_token.span,
154    );
155    let class_has_no_rd = crate::roxygen::has_roxygen_tag(&impl_doc_tags, "noRd");
156
157    // Generate R wrapper code string based on class system (only non-skipped methods)
158    let methods_owned: Vec<TraitMethod> = methods.iter().map(|m| (*m).clone()).collect();
159    let r_wrapper_string = match generate_trait_r_wrapper(
160        &type_ident,
161        trait_name,
162        &methods_owned,
163        &consts,
164        TraitWrapperOpts {
165            class_system,
166            class_has_no_rd,
167            internal,
168            noexport,
169        },
170    ) {
171        Ok(s) => s,
172        Err(e) => return e.into_compile_error(),
173    };
174
175    // Generate constant name for R wrapper registration
176    let r_wrappers_const = format_ident!(
177        "R_WRAPPERS_{}_{}_IMPL",
178        type_ident.to_string().to_uppercase(),
179        trait_name_upper
180    );
181
182    // Generate trait dispatch entry name
183    let dispatch_entry_name = format_ident!(
184        "__MX_DISPATCH_{}_{}_FOR_{}",
185        trait_name_upper,
186        type_ident.to_string().to_uppercase(),
187        type_name_str
188    );
189
190    // Build TAG path for the trait (same module as trait, e.g. counter::TAG_COUNTER)
191    let mut trait_tag_path = trait_path.clone();
192    if let Some(last) = trait_tag_path.segments.last_mut() {
193        last.ident = format_ident!("TAG_{}", trait_name_upper);
194        last.arguments = syn::PathArguments::None;
195    }
196
197    // Format R wrapper as raw string literal
198    let r_wrapper_str = crate::r_wrapper_raw_literal(&r_wrapper_string);
199    let source_loc_doc = crate::source_location_doc(type_ident.span());
200    let source_start = type_ident.span().start();
201    let source_line_lit = syn::LitInt::new(&source_start.line.to_string(), type_ident.span());
202    let source_col_lit =
203        syn::LitInt::new(&(source_start.column + 1).to_string(), type_ident.span());
204
205    // Strip #[miniextendr(...)] attrs from methods before emitting,
206    // so they don't trigger another macro expansion.
207    //
208    // Skip emitting the impl block when:
209    // - Body is empty (no methods, no consts) — blanket impl provides it
210    // - `blanket` flag is set — a blanket impl exists, methods are only for
211    //   C wrapper signature extraction, not for actual trait implementation
212    let has_items = !all_methods.is_empty() || !consts.is_empty();
213    let clean_impl_tokens = if has_items && !blanket {
214        let mut clean_impl = impl_item.clone();
215        for item in &mut clean_impl.items {
216            if let syn::ImplItem::Fn(method) = item {
217                method
218                    .attrs
219                    .retain(|attr| !attr.path().is_ident("miniextendr"));
220            }
221        }
222        quote::quote! { #clean_impl }
223    } else {
224        quote::quote! {}
225    };
226
227    // For generic traits (with type args like <i32>), generate concrete vtable shims
228    // and inline vtable construction. For non-generic traits, use the builder function.
229    //
230    // Both paths emit the static as `pub` with `#[unsafe(no_mangle)]` so that a separate
231    // compilation unit (e.g. a WASM codegen path) can reference it by name.
232    let vtable_static_tokens = if trait_type_args.is_empty() {
233        // Non-generic: use the builder function generated at the trait definition site
234        quote::quote! {
235            #[unsafe(no_mangle)]
236            pub static #vtable_static_name: #vtable_type_path =
237                #builder_path::<#concrete_type>();
238        }
239    } else {
240        // Generic: generate concrete vtable shims and inline construction
241        // Only non-skipped methods go into vtable shims
242        let methods_for_vtable: Vec<TraitMethod> = methods.iter().map(|m| (*m).clone()).collect();
243        let concrete_shims = generate_concrete_vtable_shims(
244            &methods_for_vtable,
245            &type_ident,
246            trait_name,
247            trait_path,
248            concrete_type,
249        );
250        let vtable_inits: Vec<TokenStream> = methods
251            .iter()
252            .filter(|m| m.has_self)
253            .map(|m| {
254                let name = &m.ident;
255                let shim_name = crate::naming::vtshim_ident(&type_ident, trait_name, &m.ident);
256                quote::quote! { #name: #shim_name }
257            })
258            .collect();
259        quote::quote! {
260            #concrete_shims
261            #[unsafe(no_mangle)]
262            pub static #vtable_static_name: #vtable_type_path = #vtable_type_path {
263                #(#vtable_inits),*
264            };
265        }
266    };
267
268    quote::quote! {
269        // Pass through the original impl block (with method attrs stripped)
270        // — omitted when the body is empty (blanket impl covers it)
271        #clean_impl_tokens
272
273        // Warnings for @param tags on trait impl blocks
274        #param_warnings
275
276        #[doc = concat!(
277            "Vtable for `",
278            stringify!(#concrete_type),
279            "` implementing `",
280            stringify!(#trait_path),
281            "`."
282        )]
283        #[doc = "Generated by `#[miniextendr]` on the trait impl block."]
284        #[doc = #source_loc_doc]
285        #[doc = concat!("Generated from source file `", file!(), "`.")]
286        #[doc(hidden)]
287        #vtable_static_tokens
288
289        // C wrappers and call method defs for trait methods
290        #(#c_wrappers)*
291
292        // R wrapper registration via distributed slice
293        #[doc = concat!(
294            "R wrapper code for `",
295            stringify!(#type_ident),
296            "` implementing `",
297            stringify!(#trait_name),
298            "`."
299        )]
300        #[doc = #source_loc_doc]
301        #[doc = concat!("Generated from source file `", file!(), "`.")]
302        #[doc(hidden)]
303        #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_R_WRAPPERS), linkme(crate = ::miniextendr_api::linkme))]
304        static #r_wrappers_const: ::miniextendr_api::registry::RWrapperEntry =
305            ::miniextendr_api::registry::RWrapperEntry {
306                priority: ::miniextendr_api::registry::RWrapperPriority::TraitImpl,
307                source_file: file!(),
308                content: concat!(
309                    "# Generated from Rust impl `",
310                    stringify!(#trait_name),
311                    "` for `",
312                    stringify!(#type_ident),
313                    "` (",
314                    file!(),
315                    ":",
316                    #source_line_lit,
317                    ":",
318                    #source_col_lit,
319                    ")",
320                    #r_wrapper_str
321                ),
322            };
323
324        // Trait dispatch entry for universal_query
325        #[doc = concat!(
326            "Trait dispatch entry: `",
327            stringify!(#trait_name),
328            "` for `",
329            stringify!(#type_ident),
330            "`."
331        )]
332        #[doc = #source_loc_doc]
333        #[doc = concat!("Generated from source file `", file!(), "`.")]
334        #[doc(hidden)]
335        #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_TRAIT_DISPATCH), linkme(crate = ::miniextendr_api::linkme))]
336        static #dispatch_entry_name: ::miniextendr_api::registry::TraitDispatchEntry =
337            ::miniextendr_api::registry::TraitDispatchEntry {
338                concrete_tag: ::miniextendr_api::abi::mx_tag_from_path(
339                    concat!(module_path!(), "::", stringify!(#type_ident))
340                ),
341                trait_tag: #trait_tag_path,
342                vtable: unsafe {
343                    // SAFETY: vtable is a static reference valid for program lifetime
344                    ::std::ptr::from_ref(&#vtable_static_name).cast::<::std::os::raw::c_void>()
345                },
346                vtable_symbol: stringify!(#vtable_static_name),
347            };
348    }
349}
350
351/// Generate concrete vtable shims for a generic trait impl.
352///
353/// For generic traits (e.g., `RExtend<T>`), shims and the vtable builder function
354/// cannot be generated at the trait definition site because where clauses like
355/// `Vec<T>: TryFromSexp` cause recursive trait resolution overflow. Instead, we
356/// generate fully monomorphized shims at the impl site where `T` is known (e.g., `T = i32`).
357///
358/// Each instance method gets a concrete `unsafe extern "C"` shim named
359/// `__vtshim_{crate}_{Type}__{Trait}__{method}` (crate-prefixed for webR
360/// cross-package symbol uniqueness — #1273) that:
361/// 1. Checks argument arity
362/// 2. Wraps everything in `with_r_unwind_protect`
363/// 3. Extracts SEXP arguments to concrete Rust types
364/// 4. Calls the method via fully-qualified syntax `<Type as Trait>::method()`
365/// 5. Converts the result back to SEXP
366///
367/// Static methods are skipped (not part of the vtable).
368fn generate_concrete_vtable_shims(
369    methods: &[TraitMethod],
370    type_ident: &syn::Ident,
371    trait_name: &syn::Ident,
372    trait_path: &syn::Path,
373    concrete_type: &syn::Type,
374) -> TokenStream {
375    let mut shims = Vec::new();
376
377    for method in methods {
378        if !method.has_self {
379            continue; // Static methods not in vtable
380        }
381
382        let method_ident = &method.ident;
383        let shim_name = crate::naming::vtshim_ident(type_ident, trait_name, method_ident);
384
385        // Count non-self parameters
386        let param_count = method
387            .sig
388            .inputs
389            .iter()
390            .filter(|a| !matches!(a, syn::FnArg::Receiver(_)))
391            .count();
392        let expected_argc = param_count as i32;
393
394        // Generate argument extraction (concrete types, no generics)
395        let arg_extractions: Vec<TokenStream> = method
396            .sig
397            .inputs
398            .iter()
399            .filter(|a| !matches!(a, syn::FnArg::Receiver(_)))
400            .enumerate()
401            .map(|(i, arg)| {
402                if let syn::FnArg::Typed(pt) = arg {
403                    let name = if let syn::Pat::Ident(pat_ident) = pt.pat.as_ref() {
404                        pat_ident.ident.clone()
405                    } else {
406                        format_ident!("arg{}", i)
407                    };
408                    let name_str = name.to_string();
409
410                    // Handle &Self params: extract ExternalPtr<ConcreteType>
411                    if is_self_ref_type(&pt.ty) {
412                        let extptr_name = format_ident!("__extptr_{}", name);
413                        quote::quote! {
414                            let #extptr_name: ::miniextendr_api::ExternalPtr<#concrete_type> = unsafe {
415                                ::miniextendr_api::trait_abi::extract_arg(argc, argv, #i, #name_str)
416                            };
417                            let #name = &*#extptr_name;
418                        }
419                    } else {
420                        let ty = &pt.ty;
421                        quote::quote! {
422                            let #name: #ty = unsafe {
423                                ::miniextendr_api::trait_abi::extract_arg(argc, argv, #i, #name_str)
424                            };
425                        }
426                    }
427                } else {
428                    quote::quote! {}
429                }
430            })
431            .collect();
432
433        // Collect param names for the method call
434        let param_names: Vec<syn::Ident> = method
435            .sig
436            .inputs
437            .iter()
438            .filter(|a| !matches!(a, syn::FnArg::Receiver(_)))
439            .enumerate()
440            .map(|(i, arg)| {
441                if let syn::FnArg::Typed(pt) = arg
442                    && let syn::Pat::Ident(pat_ident) = pt.pat.as_ref()
443                {
444                    return pat_ident.ident.clone();
445                }
446                format_ident!("arg{}", i)
447            })
448            .collect();
449
450        // Generate method call using fully-qualified syntax to avoid ambiguity
451        // with generic trait paths like `RExtend<i32>::method()` where `<` would
452        // be parsed as a comparison operator in expression position.
453        let method_call = if method.is_mut {
454            quote::quote! {
455                let self_ref = unsafe { &mut *data.cast::<#concrete_type>() };
456                <#concrete_type as #trait_path>::#method_ident(self_ref, #(#param_names),*)
457            }
458        } else {
459            quote::quote! {
460                let self_ref = unsafe { &*data.cast::<#concrete_type>().cast_const() };
461                <#concrete_type as #trait_path>::#method_ident(self_ref, #(#param_names),*)
462            }
463        };
464
465        // Generate result conversion
466        let has_return = match &method.sig.output {
467            syn::ReturnType::Default => false,
468            syn::ReturnType::Type(_, ty) => {
469                !matches!(ty.as_ref(), syn::Type::Tuple(t) if t.elems.is_empty())
470            }
471        };
472        let result_conversion = if has_return {
473            quote::quote! {
474                unsafe { ::miniextendr_api::trait_abi::to_sexp(result) }
475            }
476        } else {
477            quote::quote! {
478                let _ = result;
479                unsafe { ::miniextendr_api::trait_abi::nil() }
480            }
481        };
482
483        let method_name_str = format!("{}::{}", trait_name, method_ident);
484
485        shims.push(quote::quote! {
486            #[doc(hidden)]
487            #[allow(non_snake_case)]
488            unsafe extern "C" fn #shim_name(
489                data: *mut ::std::os::raw::c_void,
490                argc: i32,
491                argv: *const ::miniextendr_api::SEXP,
492            ) -> ::miniextendr_api::SEXP {
493                unsafe {
494                    ::miniextendr_api::trait_abi::check_arity(argc, #expected_argc, #method_name_str);
495                }
496                // with_r_unwind_protect_shim: returns tagged error SEXP on panic
497                // so the View method wrapper can re-panic via repanic_if_rust_error,
498                // allowing rust_* class layering (issue #345).
499                ::miniextendr_api::unwind_protect::with_r_unwind_protect_shim(|| {
500                    #(#arg_extractions)*
501                    let result = { #method_call };
502                    #result_conversion
503                })
504            }
505        });
506    }
507
508    quote::quote! { #(#shims)* }
509}
510
511/// Extract all methods from a trait impl block as [`TraitMethod`] structs.
512///
513/// Parses each `ImplItem::Fn` to determine receiver type, mutability,
514/// `#[miniextendr(...)]` attributes (coerce, skip, r_name, defaults, etc.),
515/// and roxygen `@param` tags from doc comments.
516fn extract_methods(impl_item: &ItemImpl) -> syn::Result<Vec<TraitMethod>> {
517    let mut methods = Vec::new();
518    for item in &impl_item.items {
519        if let syn::ImplItem::Fn(method) = item {
520            // Check receiver type
521            let (has_self, is_mut) = method.sig.inputs.first().map_or((false, false), |arg| {
522                if let syn::FnArg::Receiver(r) = arg {
523                    (true, r.mutability.is_some())
524                } else {
525                    (false, false)
526                }
527            });
528            let attrs = parse_trait_method_attrs(&method.attrs)?;
529
530            // Extract @param tags from method doc comments
531            let all_tags = crate::roxygen::roxygen_tags_from_attrs(&method.attrs);
532            let param_tags: Vec<String> = all_tags
533                .into_iter()
534                .filter(|tag| tag.starts_with("@param"))
535                .collect();
536
537            methods.push(TraitMethod {
538                ident: method.sig.ident.clone(),
539                sig: method.sig.clone(),
540                has_self,
541                is_mut,
542                worker: attrs.worker,
543                unsafe_main_thread: attrs.unsafe_main_thread,
544                coerce: attrs.coerce,
545                check_interrupt: attrs.check_interrupt,
546                rng: attrs.rng,
547                unwrap_in_r: attrs.unwrap_in_r,
548                param_defaults: attrs.defaults,
549                param_tags,
550                skip: attrs.skip,
551                r_name: attrs.r_name,
552                strict: attrs.strict,
553                lifecycle: attrs.lifecycle,
554                r_entry: attrs.r_entry,
555                r_post_checks: attrs.r_post_checks,
556                r_on_exit: attrs.r_on_exit,
557                no_shortcut: attrs.no_shortcut,
558                per_param: attrs.per_param,
559            });
560        }
561    }
562    Ok(methods)
563}
564
565/// Parsed `#[miniextendr(...)]` attributes for a single trait method.
566///
567/// Extracted from method-level attributes to control C wrapper behavior,
568/// threading, and R wrapper generation.
569struct TraitMethodAttrs {
570    /// Dispatch to worker thread. Set by explicit `#[miniextendr(worker)]` or `worker-default` feature.
571    worker: bool,
572    /// Force execution on R's main thread (overrides explicit or feature-selected worker dispatch).
573    unsafe_main_thread: bool,
574    /// Enable `Rf_coerceVector` for all parameters.
575    coerce: bool,
576    /// Call `R_CheckUserInterrupt` before the method body.
577    check_interrupt: bool,
578    /// Wrap the call in `GetRNGstate`/`PutRNGstate` for reproducible random number generation.
579    rng: bool,
580    /// Return `Result<T, E>` to R without unwrapping (R wrapper receives the result variant).
581    unwrap_in_r: bool,
582    /// Exclude this method from all generated wrappers (C, R, vtable shims).
583    skip: bool,
584    /// Parameter default values: keys are parameter names, values are R expressions.
585    defaults: std::collections::HashMap<String, String>,
586    /// Override the R-facing method name.
587    r_name: Option<String>,
588    /// Strict output conversion: panic instead of lossy widening for i64/u64/isize/usize.
589    strict: bool,
590    /// Lifecycle specification for deprecation/experimental status.
591    lifecycle: Option<crate::lifecycle::LifecycleSpec>,
592    /// R code to inject at the very top of the wrapper body.
593    r_entry: Option<String>,
594    /// R code to inject after all checks, immediately before `.Call()`.
595    r_post_checks: Option<String>,
596    /// Register `on.exit()` cleanup code in the R wrapper.
597    r_on_exit: Option<crate::miniextendr_fn::ROnExit>,
598    /// Opt out of the S7 fast-dispatch shortcut (`#[miniextendr(s7(no_shortcut))]`).
599    no_shortcut: bool,
600    /// Per-parameter `match_arg`/`choices`/`several_ok` attributes, keyed by
601    /// Rust parameter name. See `TraitMethod::per_param`.
602    per_param: std::collections::HashMap<String, crate::miniextendr_fn::ParamAttrs>,
603}
604
605/// Parse `#[miniextendr(...)]` attributes from a trait method.
606///
607/// Supports two syntax styles:
608/// - **Flat**: `#[miniextendr(worker, coerce, rng)]`
609/// - **Nested class-system**: `#[miniextendr(env(worker, coerce))]`
610///
611/// Both styles can coexist. The `worker` flag controls whether static methods
612/// dispatch to the worker thread (defaults to `cfg!(feature = "worker-default")`).
613fn parse_trait_method_attrs(attrs: &[syn::Attribute]) -> syn::Result<TraitMethodAttrs> {
614    let mut worker = false;
615    let mut unsafe_main_thread = false;
616    let mut coerce = false;
617    let mut check_interrupt = false;
618    let mut rng = false;
619    let mut unwrap_in_r = false;
620    let mut skip = false;
621    let mut strict = false;
622    let mut defaults = std::collections::HashMap::new();
623    let mut r_name: Option<String> = None;
624    let mut lifecycle: Option<crate::lifecycle::LifecycleSpec> = None;
625    let mut r_entry: Option<String> = None;
626    let mut r_post_checks: Option<String> = None;
627    let mut r_on_exit: Option<crate::miniextendr_fn::ROnExit> = None;
628    let mut no_shortcut = false;
629    let mut per_param: std::collections::HashMap<String, crate::miniextendr_fn::ParamAttrs> =
630        std::collections::HashMap::new();
631
632    for attr in attrs {
633        if !attr.path().is_ident("miniextendr") {
634            continue;
635        }
636
637        attr.parse_nested_meta(|meta| {
638            let is_class_meta = meta.path.is_ident("env")
639                || meta.path.is_ident("r6")
640                || meta.path.is_ident("s7")
641                || meta.path.is_ident("s3")
642                || meta.path.is_ident("s4");
643
644            if is_class_meta {
645                meta.parse_nested_meta(|inner| {
646                    if inner.path.is_ident("worker") {
647                        worker = true;
648                    } else if inner.path.is_ident("main_thread") {
649                        unsafe_main_thread = true;
650                    } else if inner.path.is_ident("coerce") {
651                        coerce = true;
652                    } else if inner.path.is_ident("check_interrupt") {
653                        check_interrupt = true;
654                    } else if inner.path.is_ident("unwrap_in_r") {
655                        unwrap_in_r = true;
656                    } else if inner.path.is_ident("no_shortcut") {
657                        no_shortcut = true;
658                    } else {
659                        return Err(inner.error(
660                            "unknown nested option; expected `worker`, `main_thread`, `coerce`, \
661                             `check_interrupt`, `unwrap_in_r`, or `no_shortcut`",
662                        ));
663                    }
664                    Ok(())
665                })?;
666            } else if meta.path.is_ident("worker") {
667                worker = true;
668            } else if meta.path.is_ident("main_thread") {
669                unsafe_main_thread = true;
670            } else if meta.path.is_ident("coerce") {
671                coerce = true;
672            } else if meta.path.is_ident("check_interrupt") {
673                check_interrupt = true;
674            } else if meta.path.is_ident("rng") {
675                rng = true;
676            } else if meta.path.is_ident("unwrap_in_r") {
677                unwrap_in_r = true;
678            } else if meta.path.is_ident("skip") {
679                skip = true;
680            } else if meta.path.is_ident("no_shortcut") {
681                no_shortcut = true;
682            } else if meta.path.is_ident("r_name") {
683                let value: syn::LitStr = meta.value()?.parse()?;
684                r_name = Some(value.value());
685            } else if meta.path.is_ident("strict") {
686                strict = true;
687            } else if meta.path.is_ident("defaults") {
688                // Parse defaults(param = "value", param2 = "value2", ...)
689                meta.parse_nested_meta(|inner| {
690                    let param_name = inner
691                        .path
692                        .get_ident()
693                        .map(|i| i.to_string())
694                        .unwrap_or_default();
695                    let value: syn::LitStr = inner.value()?.parse()?;
696                    defaults.insert(param_name, value.value());
697                    Ok(())
698                })?;
699            } else if meta.path.is_ident("lifecycle") {
700                if meta.input.peek(syn::Token![=]) {
701                    // lifecycle = "stage"
702                    let _: syn::Token![=] = meta.input.parse()?;
703                    let value: syn::LitStr = meta.input.parse()?;
704                    let stage = crate::lifecycle::LifecycleStage::from_str(&value.value())
705                        .ok_or_else(|| {
706                            syn::Error::new(
707                                value.span(),
708                                "invalid lifecycle stage; expected one of: experimental, stable, superseded, soft-deprecated, deprecated, defunct",
709                            )
710                        })?;
711                    lifecycle = Some(crate::lifecycle::LifecycleSpec::new(stage));
712                } else {
713                    // lifecycle(stage = "deprecated", when = "0.4.0", ...)
714                    let mut spec = crate::lifecycle::LifecycleSpec::default();
715                    meta.parse_nested_meta(|inner| {
716                        let key = inner.path.get_ident()
717                            .ok_or_else(|| inner.error("expected identifier"))?
718                            .to_string();
719                        let _: syn::Token![=] = inner.input.parse()?;
720                        let value: syn::LitStr = inner.input.parse()?;
721                        match key.as_str() {
722                            "stage" => {
723                                spec.stage = crate::lifecycle::LifecycleStage::from_str(&value.value())
724                                    .ok_or_else(|| syn::Error::new(value.span(), "invalid lifecycle stage"))?;
725                            }
726                            "when" => spec.when = Some(value.value()),
727                            "what" => spec.what = Some(value.value()),
728                            "with" => spec.with = Some(value.value()),
729                            "details" => spec.details = Some(value.value()),
730                            "id" => spec.id = Some(value.value()),
731                            _ => return Err(inner.error(
732                                "unknown lifecycle option; expected: stage, when, what, with, details, id"
733                            )),
734                        }
735                        Ok(())
736                    })?;
737                    lifecycle = Some(spec);
738                }
739            } else if meta.path.is_ident("r_entry") {
740                let _: syn::Token![=] = meta.input.parse()?;
741                let value: syn::LitStr = meta.input.parse()?;
742                r_entry = Some(value.value());
743            } else if meta.path.is_ident("r_post_checks") {
744                let _: syn::Token![=] = meta.input.parse()?;
745                let value: syn::LitStr = meta.input.parse()?;
746                r_post_checks = Some(value.value());
747            } else if meta.path.is_ident("r_on_exit") {
748                if meta.input.peek(syn::Token![=]) {
749                    // Short form: r_on_exit = "expr"
750                    let _: syn::Token![=] = meta.input.parse()?;
751                    let value: syn::LitStr = meta.input.parse()?;
752                    r_on_exit = Some(crate::miniextendr_fn::ROnExit {
753                        expr: value.value(),
754                        add: true,
755                        after: true,
756                    });
757                } else {
758                    // Long form: r_on_exit(expr = "...", add = false, after = false)
759                    let mut expr = None;
760                    let mut add = true;
761                    let mut after = true;
762                    meta.parse_nested_meta(|inner| {
763                        if inner.path.is_ident("expr") {
764                            let _: syn::Token![=] = inner.input.parse()?;
765                            let value: syn::LitStr = inner.input.parse()?;
766                            expr = Some(value.value());
767                        } else if inner.path.is_ident("add") {
768                            let _: syn::Token![=] = inner.input.parse()?;
769                            let value: syn::LitBool = inner.input.parse()?;
770                            add = value.value;
771                        } else if inner.path.is_ident("after") {
772                            let _: syn::Token![=] = inner.input.parse()?;
773                            let value: syn::LitBool = inner.input.parse()?;
774                            after = value.value;
775                        } else {
776                            return Err(inner.error(
777                                "unknown r_on_exit option; expected `expr`, `add`, or `after`",
778                            ));
779                        }
780                        Ok(())
781                    })?;
782                    let expr = expr.ok_or_else(|| {
783                        meta.error("r_on_exit(...) requires `expr = \"...\"` specifying the R expression")
784                    })?;
785                    r_on_exit = Some(crate::miniextendr_fn::ROnExit { expr, add, after });
786                }
787            } else if meta.path.is_ident("choices") {
788                // `choices(param = "a, b, c", param2 = "x, y")` — explicit string choice lists.
789                //
790                // NOTE: `match_arg`/`match_arg_several_ok` (choices derived from a
791                // `#[derive(MatchArg)]` enum's `CHOICES` const) are intentionally NOT
792                // supported here yet. The inherent-impl path additionally emits a
793                // `__match_arg_choices__<param>` C helper + `MX_MATCH_ARG_CHOICES`
794                // registration (`generate_method_match_arg_helpers` in
795                // `miniextendr_impl.rs`) that resolves the write-time placeholder
796                // formal default into the enum's literal choice list — trait methods
797                // have no equivalent C-wrapper generation yet, so accepting `match_arg`
798                // here would silently ship a formal default that never resolves
799                // (`object '.__MX_MATCH_ARG_CHOICES_...__' not found` at call time).
800                // `choices(...)` needs no such resolution (the literal list is baked in
801                // at macro-expansion time), so it's safe to support today. Tracked in
802                // the trait-method-emitter follow-up issue for full match_arg parity.
803                meta.parse_nested_meta(|inner| {
804                    let name = inner
805                        .path
806                        .get_ident()
807                        .ok_or_else(|| inner.error("expected parameter name"))?
808                        .to_string();
809                    let _: syn::Token![=] = inner.input.parse()?;
810                    let value: syn::LitStr = inner.input.parse()?;
811                    let choices = crate::r_wrapper_builder::split_choice_list(&value.value());
812                    per_param.entry(name).or_default().choices = Some(choices);
813                    Ok(())
814                })?;
815            } else if meta.path.is_ident("choices_several_ok") {
816                // `choices_several_ok(param = "a, b, c")` — choices + several_ok.
817                meta.parse_nested_meta(|inner| {
818                    let name = inner
819                        .path
820                        .get_ident()
821                        .ok_or_else(|| inner.error("expected parameter name"))?
822                        .to_string();
823                    let _: syn::Token![=] = inner.input.parse()?;
824                    let value: syn::LitStr = inner.input.parse()?;
825                    let choices = crate::r_wrapper_builder::split_choice_list(&value.value());
826                    let entry = per_param.entry(name).or_default();
827                    entry.choices = Some(choices);
828                    entry.several_ok = true;
829                    Ok(())
830                })?;
831            } else {
832                return Err(meta.error(
833                    "unknown #[miniextendr] option on trait impl method; expected one of: \
834                     `env`, `r6`, `s7`, `s3`, `s4`, `worker`, `main_thread`, `coerce`, \
835                     `check_interrupt`, `rng`, `unwrap_in_r`, `skip`, `no_shortcut`, `r_name`, \
836                     `defaults`, `strict`, `lifecycle`, `r_entry`, `r_post_checks`, `r_on_exit`, \
837                     `choices`, `choices_several_ok`",
838                ));
839            }
840            Ok(())
841        })?;
842    }
843
844    Ok(TraitMethodAttrs {
845        worker: worker || cfg!(feature = "worker-default"),
846        unsafe_main_thread,
847        coerce,
848        check_interrupt,
849        rng,
850        unwrap_in_r,
851        skip,
852        strict,
853        defaults,
854        r_name,
855        lifecycle,
856        r_entry,
857        r_post_checks,
858        r_on_exit,
859        no_shortcut,
860        per_param,
861    })
862}
863
864/// Extract associated constant items from a trait impl block.
865///
866/// Each `const NAME: Type = value;` in the impl block becomes a [`TraitConst`]
867/// that will get its own zero-argument C wrapper for R access.
868fn extract_consts(impl_item: &ItemImpl) -> Vec<TraitConst> {
869    impl_item
870        .items
871        .iter()
872        .filter_map(|item| {
873            if let syn::ImplItem::Const(const_item) = item {
874                Some(TraitConst {
875                    ident: const_item.ident.clone(),
876                    ty: const_item.ty.clone(),
877                })
878            } else {
879                None
880            }
881        })
882        .collect()
883}
884
885/// Check if a type is `&Self` or `&mut Self`.
886///
887/// Used to detect trait method parameters that take another instance of the same type
888/// (e.g., `ROrd::cmp(&self, other: &Self)`) so we can generate `ExternalPtr<T>` extraction
889/// and dereference in the C wrapper.
890pub(super) fn is_self_ref_type(ty: &syn::Type) -> bool {
891    if let syn::Type::Reference(r) = ty
892        && let syn::Type::Path(tp) = r.elem.as_ref()
893        && tp.path.is_ident("Self")
894    {
895        return true;
896    }
897    false
898}
899
900/// Generate a C wrapper function and `R_CallMethodDef` for a single trait method.
901///
902/// Uses `CWrapperContext` builder to produce:
903/// - An `extern "C"` function callable from R via `.Call()`
904/// - A `R_CallMethodDef` constant for symbol registration
905///
906/// Instance methods (`has_self`) extract the object via `ErasedExternalPtr::from_sexp`
907/// and call with fully-qualified trait syntax `<Type as Trait>::method(self_ref, ...)`.
908/// Static methods run on the worker thread when the `worker` flag is set.
909///
910/// `&Self` parameters are rewritten to `ExternalPtr<Type>` for the C wrapper,
911/// then dereferenced to `&Type` when calling the actual trait method.
912pub(super) fn generate_trait_method_c_wrapper(
913    method: &TraitMethod,
914    type_ident: &syn::Ident,
915    trait_name: &syn::Ident,
916    trait_path: &syn::Path,
917) -> TokenStream {
918    use crate::c_wrapper_builder::{CWrapperContext, ReturnHandling, ThreadStrategy};
919
920    let method_ident = &method.ident;
921    let c_ident = method.c_wrapper_ident(type_ident, trait_name);
922    let call_method_def_ident = method.call_method_def_ident(type_ident, trait_name);
923
924    // Thread strategy: instance methods stay on main thread (self_ref can't cross threads);
925    // static methods use worker thread only when worker=true (explicit or worker-default feature)
926    let thread_strategy = if method.has_self || method.unsafe_main_thread {
927        ThreadStrategy::MainThread
928    } else if method.worker {
929        ThreadStrategy::WorkerThread
930    } else {
931        ThreadStrategy::MainThread
932    };
933
934    // Build rust argument names from the signature (excluding self receiver)
935    let rust_args: Vec<syn::Ident> = method
936        .sig
937        .inputs
938        .iter()
939        .filter_map(|arg| {
940            if let syn::FnArg::Typed(pt) = arg {
941                if let syn::Pat::Ident(pat_ident) = pt.pat.as_ref() {
942                    Some(pat_ident.ident.clone())
943                } else {
944                    None
945                }
946            } else {
947                None
948            }
949        })
950        .collect();
951
952    // Filter inputs to exclude the receiver (builder handles self separately with has_self())
953    // Also handle &Self params: replace with ExternalPtr<ConcreteType> so the builder
954    // can auto-extract them, and track which params need dereferencing in the call.
955    let mut self_ref_params = std::collections::HashSet::new();
956    let filtered_inputs: syn::punctuated::Punctuated<syn::FnArg, syn::Token![,]> = method
957        .sig
958        .inputs
959        .iter()
960        .filter(|arg| !matches!(arg, syn::FnArg::Receiver(_)))
961        .map(|arg| {
962            if let syn::FnArg::Typed(pt) = arg
963                && is_self_ref_type(&pt.ty)
964            {
965                // Track this param for dereferencing in the call expression
966                if let syn::Pat::Ident(pat_ident) = pt.pat.as_ref() {
967                    self_ref_params.insert(pat_ident.ident.to_string());
968                }
969                // Replace &Self with ExternalPtr<ConcreteType>
970                let pat = &pt.pat;
971                return syn::parse_quote!(#pat: ::miniextendr_api::ExternalPtr<#type_ident>);
972            }
973            arg.clone()
974        })
975        .collect();
976
977    // Build call args: &Self params use `&*param` (deref ExternalPtr), others use `param`
978    let call_args: Vec<proc_macro2::TokenStream> = rust_args
979        .iter()
980        .map(|arg| {
981            if self_ref_params.contains(&arg.to_string()) {
982                quote::quote! { &*#arg }
983            } else {
984                quote::quote! { #arg }
985            }
986        })
987        .collect();
988
989    // Determine return handling
990    let return_handling = if method.unwrap_in_r && output_is_result(&method.sig.output) {
991        ReturnHandling::IntoR
992    } else {
993        crate::c_wrapper_builder::detect_return_handling(&method.sig.output)
994    };
995
996    // Generate R wrapper const name (not actually used but needed by builder)
997    let r_wrappers_const = format_ident!(
998        "R_WRAPPERS_{}_{}_IMPL",
999        type_ident.to_string().to_uppercase(),
1000        trait_name.to_string().to_uppercase()
1001    );
1002
1003    // Build the wrapper using the builder infrastructure
1004    // Use custom call_method_def_ident to avoid collisions with inherent impl methods
1005    let mut builder = CWrapperContext::builder(method_ident.clone(), c_ident)
1006        .r_wrapper_const(r_wrappers_const)
1007        .inputs(filtered_inputs)
1008        .output(method.sig.output.clone())
1009        .thread_strategy(thread_strategy)
1010        .return_handling(return_handling)
1011        .type_context(type_ident.clone())
1012        .call_method_def_ident(call_method_def_ident);
1013
1014    if method.has_self {
1015        // Instance method: generate self extraction and call with self_ref
1016        let trait_method_name = format!("{}::{}()", trait_name, method_ident);
1017        let self_extraction = if method.is_mut {
1018            quote::quote! {
1019                let mut self_ptr = unsafe {
1020                    ::miniextendr_api::externalptr::ErasedExternalPtr::from_sexp(self_sexp)
1021                };
1022                let self_ref = self_ptr.downcast_mut::<#type_ident>()
1023                    .unwrap_or_else(|| panic!(
1024                        "type mismatch in {}: expected ExternalPtr<{}>, got different type. \
1025                         This can happen if you pass an object of a different type to a trait method.",
1026                        #trait_method_name,
1027                        stringify!(#type_ident)
1028                    ));
1029            }
1030        } else {
1031            quote::quote! {
1032                let self_ptr = unsafe {
1033                    ::miniextendr_api::externalptr::ErasedExternalPtr::from_sexp(self_sexp)
1034                };
1035                let self_ref = self_ptr.downcast_ref::<#type_ident>()
1036                    .unwrap_or_else(|| panic!(
1037                        "type mismatch in {}: expected ExternalPtr<{}>, got different type. \
1038                         This can happen if you pass an object of a different type to a trait method.",
1039                        #trait_method_name,
1040                        stringify!(#type_ident)
1041                    ));
1042            }
1043        };
1044
1045        // Call expression with self_ref and dereferenced &Self params
1046        // Use fully-qualified syntax to avoid ambiguity with generic traits
1047        // (e.g., `RExtend<i32>::method()` is ambiguous — `<` parsed as comparison)
1048        let call_expr = quote::quote! {
1049            <#type_ident as #trait_path>::#method_ident(self_ref, #(#call_args),*)
1050        };
1051
1052        builder = builder
1053            .pre_call(vec![self_extraction])
1054            .call_expr(call_expr)
1055            .has_self();
1056    } else {
1057        // Static method: call directly without self
1058        let call_expr = quote::quote! {
1059            <#type_ident as #trait_path>::#method_ident(#(#call_args),*)
1060        };
1061
1062        builder = builder.call_expr(call_expr);
1063    }
1064
1065    // Apply coerce_all if the method has #[miniextendr(coerce)]
1066    if method.coerce {
1067        builder = builder.coerce_all();
1068    }
1069
1070    // Apply check_interrupt if the method has #[miniextendr(check_interrupt)]
1071    if method.check_interrupt {
1072        builder = builder.check_interrupt();
1073    }
1074
1075    // Apply rng if the method has #[miniextendr(rng)]
1076    if method.rng {
1077        builder = builder.rng();
1078    }
1079
1080    // Apply strict mode for lossy type conversions
1081    if method.strict {
1082        builder = builder.strict();
1083    }
1084
1085    // The builder generates both the C wrapper and the R_CallMethodDef
1086    builder.build().generate()
1087}
1088
1089/// Returns true when the return type is syntactically `Result<_, _>`.
1090///
1091/// Used to determine whether `unwrap_in_r` mode should use `ReturnHandling::IntoR`
1092/// (passing the Result through to R) instead of the default return handling.
1093fn output_is_result(output: &syn::ReturnType) -> bool {
1094    match output {
1095        syn::ReturnType::Type(_, ty) => matches!(
1096            ty.as_ref(),
1097            syn::Type::Path(p)
1098                if p.path
1099                    .segments
1100                    .last()
1101                    .map(|s| s.ident == "Result")
1102                    .unwrap_or(false)
1103        ),
1104        syn::ReturnType::Default => false,
1105    }
1106}
1107
1108/// Generate a C wrapper function and `R_CallMethodDef` for a trait associated constant.
1109///
1110/// The generated wrapper takes no arguments and returns the constant value
1111/// converted to SEXP. Uses fully-qualified syntax `<Type as Trait>::CONST`
1112/// to access the value. Always runs on the main thread.
1113pub(super) fn generate_trait_const_c_wrapper(
1114    trait_const: &TraitConst,
1115    type_ident: &syn::Ident,
1116    trait_name: &syn::Ident,
1117    trait_path: &syn::Path,
1118) -> TokenStream {
1119    use crate::c_wrapper_builder::{CWrapperContext, ThreadStrategy};
1120
1121    let const_ident = &trait_const.ident;
1122    let c_ident = trait_const.c_wrapper_ident(type_ident, trait_name);
1123    let call_method_def_ident = trait_const.call_method_def_ident(type_ident, trait_name);
1124    let const_ty = &trait_const.ty;
1125
1126    // Generate R wrapper const name
1127    let r_wrappers_const = format_ident!(
1128        "R_WRAPPERS_{}_{}_IMPL",
1129        type_ident.to_string().to_uppercase(),
1130        trait_name.to_string().to_uppercase()
1131    );
1132
1133    // Build the call expression to access the const
1134    let call_expr = quote::quote! {
1135        <#type_ident as #trait_path>::#const_ident
1136    };
1137
1138    // Determine return type handling - we need to convert the const to SEXP
1139    // The return type is `-> Type` not just `Type`
1140    let return_type: syn::ReturnType = syn::parse_quote!(-> #const_ty);
1141    let return_handling = crate::c_wrapper_builder::detect_return_handling(&return_type);
1142
1143    // Build wrapper - no inputs, just returns the const value
1144    let builder = CWrapperContext::builder(const_ident.clone(), c_ident)
1145        .r_wrapper_const(r_wrappers_const)
1146        .inputs(Default::default()) // no inputs
1147        .output(return_type)
1148        .call_expr(call_expr)
1149        .thread_strategy(ThreadStrategy::MainThread)
1150        .return_handling(return_handling)
1151        .type_context(type_ident.clone())
1152        .call_method_def_ident(call_method_def_ident);
1153
1154    builder.build().generate()
1155}