Skip to main content

miniextendr_macros/miniextendr_impl_trait/
r_wrappers.rs

1//! R wrapper generation for trait methods across all class systems.
2//!
3//! Each class system (Env, S3, S4, S7, R6, Vctrs) has its own generator that
4//! produces R code strings for instance methods, static methods, and associated
5//! constants. The top-level [`generate_trait_r_wrapper`] dispatches to the
6//! appropriate generator and applies post-processing for export/documentation control.
7
8use super::method_context::{TraitMethodContext, trait_namespace_env_var, trait_namespace_target};
9use super::{TraitConst, TraitMethod};
10use crate::miniextendr_impl::ClassSystem;
11use crate::r_class_formatter::emit_s3_generic_guard;
12
13/// Options controlling export visibility and documentation for trait R wrapper generation.
14pub(super) struct TraitWrapperOpts {
15    /// Which R class system to generate wrappers for (env, r6, s3, s4, s7, vctrs).
16    pub(super) class_system: ClassSystem,
17    /// Whether the impl block has `@noRd`, suppressing roxygen documentation output.
18    /// For S3/vctrs, method registration tags are preserved even when this is true.
19    pub(super) class_has_no_rd: bool,
20    /// Whether `#[miniextendr(internal)]` is set, adding `@keywords internal` and
21    /// suppressing `@export`/`@exportMethod`.
22    pub(super) internal: bool,
23    /// Whether `#[miniextendr(noexport)]` is set, suppressing `@export`/`@exportMethod`
24    /// without adding `@keywords internal`.
25    pub(super) noexport: bool,
26}
27
28/// Generate R wrapper code for trait methods and consts, dispatching by class system.
29///
30/// Calls the appropriate class-system-specific generator (env, s3, s4, s7, r6),
31/// then applies post-processing for `@noRd`, `internal`, and `noexport` options:
32///
33/// - `class_has_no_rd`: Strips roxygen blocks (for S3/vctrs, keeps `@method`/`@export` tags)
34/// - `internal`: Replaces `@export`/`@exportMethod` with `@keywords internal`
35/// - `noexport`: Removes `@export`/`@exportMethod` entirely
36///
37/// Returns the complete R wrapper code as a string ready for embedding in a `const`.
38pub(super) fn generate_trait_r_wrapper(
39    type_ident: &syn::Ident,
40    trait_name: &syn::Ident,
41    methods: &[TraitMethod],
42    consts: &[TraitConst],
43    opts: TraitWrapperOpts,
44) -> syn::Result<String> {
45    let TraitWrapperOpts {
46        class_system,
47        class_has_no_rd,
48        internal,
49        noexport,
50    } = opts;
51    let result = match class_system {
52        ClassSystem::Env => generate_trait_env_r_wrapper(type_ident, trait_name, methods, consts)?,
53        ClassSystem::S3 => generate_trait_s3_r_wrapper(type_ident, trait_name, methods, consts),
54        ClassSystem::S4 => generate_trait_s4_r_wrapper(type_ident, trait_name, methods, consts),
55        ClassSystem::S7 => generate_trait_s7_r_wrapper(type_ident, trait_name, methods, consts),
56        ClassSystem::R6 => generate_trait_r6_r_wrapper(type_ident, trait_name, methods, consts),
57        // vctrs uses S3 under the hood, so use the S3 trait wrapper
58        ClassSystem::Vctrs => generate_trait_s3_r_wrapper(type_ident, trait_name, methods, consts),
59    };
60
61    // When the impl block has @noRd, suppress documentation generation. A plain
62    // `noexport` (without `internal`) is folded into the same gate — it must
63    // produce no Rd contribution at all (no alias, no usage entry, nothing on a
64    // shared page), same as `@noRd`. `internal` wins if both flags are set on
65    // the same impl block (mirrors the standalone-fn precedent, where `internal`
66    // + `noexport` together is a compile error). See #431 for the inherent-impl
67    // S3 generator's analogous `should_register_s3method = !noexport` rule.
68    let suppress_all_rd = class_has_no_rd || (noexport && !internal);
69    if suppress_all_rd {
70        if matches!(class_system, ClassSystem::S3 | ClassSystem::Vctrs) {
71            // A user-written `@noRd` still preserves S3 dispatch registration
72            // (`@method`/`@export`) so `S3method()` lands in NAMESPACE — the
73            // class stays undocumented but dispatchable. A `noexport`-driven
74            // suppression (no explicit `@noRd`) additionally drops `@export`:
75            // `noexport` means zero observable trace, not "documented nowhere
76            // but still dispatchable".
77            let keep_export = class_has_no_rd;
78            let mut filtered = Vec::new();
79            let mut roxygen_block: Vec<&str> = Vec::new();
80
81            let flush_block = |block: &mut Vec<&str>, out: &mut Vec<String>| {
82                if block.iter().any(|line| line.contains("@method ")) {
83                    out.push("#' @noRd".to_string());
84                    for &line in block.iter() {
85                        if line.contains("@method ")
86                            || line.contains("@param ")
87                            || (keep_export && line.contains("@export"))
88                        {
89                            out.push(line.to_string());
90                        }
91                    }
92                }
93                block.clear();
94            };
95
96            for line in result.lines() {
97                if line.starts_with("#'") {
98                    roxygen_block.push(line);
99                    continue;
100                }
101
102                if !roxygen_block.is_empty() {
103                    flush_block(&mut roxygen_block, &mut filtered);
104                }
105                filtered.push(line.to_string());
106            }
107
108            if !roxygen_block.is_empty() {
109                flush_block(&mut roxygen_block, &mut filtered);
110            }
111
112            Ok(filtered.join("\n"))
113        } else {
114            Ok(result
115                .lines()
116                .filter(|line| !line.starts_with("#'"))
117                .collect::<Vec<_>>()
118                .join("\n"))
119        }
120    } else if internal {
121        // internal → documented, but @export/@exportMethod becomes @keywords internal
122        let has_export = result.lines().any(|line| line.contains("@export"));
123        let mut processed: Vec<String> = result
124            .lines()
125            .flat_map(|line| {
126                if line.contains("@export") {
127                    vec!["#' @keywords internal".to_string()]
128                } else {
129                    vec![line.to_string()]
130                }
131            })
132            .collect();
133        // For class systems without @export (e.g., Env), insert @keywords internal
134        // before the first roxygen tag if no @export line was found to replace.
135        if !has_export && let Some(pos) = processed.iter().position(|l| l.starts_with("#'")) {
136            processed.insert(pos, "#' @keywords internal".to_string());
137        }
138        Ok(processed.join("\n"))
139    } else {
140        Ok(result)
141    }
142}
143
144/// Generate Env-style R wrapper code for trait methods.
145///
146/// Env-class trait methods use a namespace hierarchy: `Type$Trait$method(x, ...)`.
147/// Instance methods take `x` as the first parameter (the self object) and are
148/// stamped with `.__mx_instance__` attribute for `$` dispatch detection.
149/// Void instance methods return `invisible(x)` for pipe-friendly chaining.
150///
151/// Static methods and constants also live under `Type$Trait$name`.
152///
153/// Returns an error if an instance method has a parameter named `x` (collides
154/// with the self parameter in env-class dispatch).
155fn generate_trait_env_r_wrapper(
156    type_ident: &syn::Ident,
157    trait_name: &syn::Ident,
158    methods: &[TraitMethod],
159    consts: &[TraitConst],
160) -> syn::Result<String> {
161    use crate::r_wrapper_builder::{DotCallBuilder, RoxygenBuilder};
162
163    let mut lines = Vec::new();
164    let type_str = type_ident.to_string();
165
166    // Header comment
167    lines.push(format!(
168        "# Trait methods and consts for {} implementing {}",
169        type_ident, trait_name
170    ));
171    lines.push(format!(
172        "# Generated by #[miniextendr] impl {} for {}",
173        trait_name, type_ident
174    ));
175    lines.push(String::new());
176
177    // Create trait namespace environment
178    lines.push(format!(
179        "{}${} <- new.env(parent = emptyenv())",
180        type_ident, trait_name
181    ));
182    lines.push(String::new());
183
184    for method in methods {
185        let r_name = method.r_method_name();
186        let ctx = TraitMethodContext::new(method, type_ident, trait_name);
187
188        // Trait-namespace assignment target (`Type$Trait$method`), owned by
189        // `trait_namespace_target` — see #1141.
190        let target = ctx.namespace_target(ClassSystem::Env);
191
192        // Build roxygen tags
193        let roxygen = RoxygenBuilder::new()
194            .name(target.clone())
195            .rdname(&type_str)
196            .build();
197        lines.extend(roxygen);
198
199        // Check for 'x' parameter collision in instance methods
200        if method.has_self {
201            for input in &method.sig.inputs {
202                if let syn::FnArg::Typed(pt) = input
203                    && let syn::Pat::Ident(pat_ident) = pt.pat.as_ref()
204                    && pat_ident.ident == "x"
205                {
206                    return Err(syn::Error::new_spanned(
207                        &pat_ident.ident,
208                        "trait instance method parameter cannot be named `x` \
209                         (collides with self parameter in env-class dispatch)",
210                    ));
211                }
212            }
213        }
214
215        // Build .Call() invocation — C name uses Rust ident, R name uses r_name
216        let (full_params, call) = if method.has_self {
217            let fp = if ctx.params.is_empty() {
218                "x".to_string()
219            } else {
220                format!("x, {}", ctx.params)
221            };
222            (fp, ctx.instance_call("x"))
223        } else {
224            (ctx.params.clone(), ctx.static_call())
225        };
226
227        // Generate method wrapper (R-facing name)
228        lines.push(format!("{target} <- function({full_params}) {{"));
229        ctx.emit_method_prelude(&mut lines, "  ", &r_name);
230        lines.extend(ctx.method_body_lines(&call, ClassSystem::Env));
231        if method.has_self && method.returns_unit() {
232            lines.push("  invisible(x)".to_string());
233        }
234        lines.push("}".to_string());
235
236        // Stamp instance methods with attribute for $ dispatch detection
237        if method.has_self {
238            lines.push(format!("attr({target}, \".__mx_instance__\") <- TRUE"));
239        }
240
241        lines.push(String::new());
242    }
243
244    // Generate const wrappers
245    for trait_const in consts {
246        let const_name = &trait_const.ident;
247        let const_str = const_name.to_string();
248        let target = trait_namespace_target(ClassSystem::Env, type_ident, trait_name, &const_str);
249
250        // Build roxygen tags
251        let roxygen = RoxygenBuilder::new()
252            .name(target.clone())
253            .rdname(&type_str)
254            .build();
255        lines.extend(roxygen);
256
257        // Build .Call() invocation
258        let c_ident = trait_const.c_wrapper_ident_string(type_ident, trait_name);
259        let call = DotCallBuilder::new(&c_ident).build();
260
261        // Generate const wrapper
262        lines.push(format!("{target} <- function() {{"));
263        lines.push(format!("  {}", call));
264        lines.push("}".to_string());
265        lines.push(String::new());
266    }
267
268    Ok(lines.join("\n"))
269}
270
271/// Generate S3-style R wrapper code (generic + method.Type).
272///
273/// For `impl Counter for SimpleCounter`, generates:
274/// - S3 generic `value(x, ...)` (if not already defined)
275/// - S3 method `value.SimpleCounter <- function(x, ...) { .Call(...) }`
276/// - S7 method registration if the generic is an S7 generic
277///
278/// Static methods and constants use `Type$Trait$name` namespace (env-style).
279/// Void instance methods return `invisible(x)` for pipe-friendly chaining.
280///
281/// Also used for `ClassSystem::Vctrs` since vctrs uses S3 under the hood.
282fn generate_trait_s3_r_wrapper(
283    type_ident: &syn::Ident,
284    trait_name: &syn::Ident,
285    methods: &[TraitMethod],
286    consts: &[TraitConst],
287) -> String {
288    use crate::r_wrapper_builder::{DotCallBuilder, RoxygenBuilder};
289
290    let mut lines = Vec::new();
291    let type_str = type_ident.to_string();
292
293    // Header comment
294    lines.push(format!(
295        "# S3 trait methods for {} implementing {}",
296        type_ident, trait_name
297    ));
298    lines.push(format!(
299        "# Generated by #[miniextendr(s3)] impl {} for {}",
300        trait_name, type_ident
301    ));
302    lines.push(String::new());
303
304    // Separate instance methods (S3 dispatch) from static methods (namespace access)
305    let instance_methods: Vec<_> = methods.iter().filter(|m| m.has_self).collect();
306    let static_methods: Vec<_> = methods.iter().filter(|m| !m.has_self).collect();
307
308    // Generate S3 generics + methods for instance methods
309    for method in &instance_methods {
310        let generic_name = method.r_method_name();
311        let s3_method_name = format!("{}.{}", generic_name, type_str);
312        let ctx = TraitMethodContext::new(method, type_ident, trait_name);
313
314        // S3 generic roxygen (only create if doesn't exist)
315        // Use type-qualified @name to avoid duplicate aliases across types
316        let generic_roxygen = RoxygenBuilder::new()
317            .title(format!("S3 generic for `{}`", generic_name))
318            .custom(format!("S3 generic for `{}`", generic_name))
319            .name(format!("{}.{}", generic_name, type_str))
320            .rdname(&type_str)
321            .custom("@param x An object")
322            .custom("@param ... Additional arguments passed to methods")
323            .source(format!(
324                "Generated by miniextendr from `impl {} for {}`",
325                trait_name, type_ident
326            ))
327            .export()
328            .build();
329        lines.extend(generic_roxygen);
330
331        // S3 generic definition
332        lines.push(emit_s3_generic_guard(generic_name.as_str()));
333        lines.push(String::new());
334
335        // S3 method roxygen (include @param tags from method doc comments)
336        let mut method_roxygen = RoxygenBuilder::new()
337            .rdname(&type_str)
338            .export()
339            .method(&generic_name, &type_str);
340        for tag in &method.param_tags {
341            method_roxygen = method_roxygen.custom(tag.clone());
342        }
343        lines.extend(method_roxygen.build());
344
345        // S3 method: generic.class
346        let full_params = if ctx.params.is_empty() {
347            "x, ...".to_string()
348        } else {
349            format!("x, {}, ...", ctx.params)
350        };
351
352        // Build .Call() invocation
353        let call = ctx.instance_call("x");
354
355        // Always define the S3 method (roxygen expects it for NAMESPACE export)
356        lines.push(format!(
357            "{} <- function({}) {{",
358            s3_method_name, full_params
359        ));
360        ctx.emit_method_prelude(&mut lines, "  ", &generic_name);
361        lines.extend(ctx.method_body_lines(&call, ClassSystem::S3));
362        // Void instance methods return invisible(x) for pipe-friendly chaining
363        if method.returns_unit() {
364            lines.push("  invisible(x)".to_string());
365        }
366        lines.push("}".to_string());
367
368        // Additionally register as S7 method if the generic is S7
369        // This ensures S7 dispatch works when the generic was defined by an S7 class
370        lines.push(format!(
371            "if (inherits(get0(\"{generic_name}\", mode = \"function\"), \"S7_generic\")) {{"
372        ));
373        lines.push(format!(
374            "  S7::method({generic_name}, S7::new_S3_class(\"{type_str}\")) <- {s3_method_name}"
375        ));
376        lines.push("}".to_string());
377        lines.push(String::new());
378    }
379
380    // Create trait namespace for static methods and consts BEFORE assigning to it
381    if !static_methods.is_empty() || !consts.is_empty() {
382        lines.push(format!(
383            "{}${} <- new.env(parent = emptyenv())",
384            type_ident, trait_name
385        ));
386        lines.push(String::new());
387    }
388
389    // Generate static methods in Type$Trait$ namespace
390    for method in &static_methods {
391        let r_name = method.r_method_name();
392        let ctx = TraitMethodContext::new(method, type_ident, trait_name);
393        let target = ctx.namespace_target(ClassSystem::S3);
394
395        // Static method roxygen
396        lines.push(format!(
397            "#' Static trait method {}::{}()",
398            trait_name, r_name
399        ));
400        let roxygen = RoxygenBuilder::new()
401            .name(target.clone())
402            .rdname(&type_str)
403            .build();
404        lines.extend(roxygen);
405
406        let call = ctx.static_call();
407
408        lines.push(format!("{target} <- function({}) {{", ctx.params));
409        ctx.emit_method_prelude(&mut lines, "  ", &r_name);
410        lines.extend(ctx.method_body_lines(&call, ClassSystem::S3));
411        lines.push("}".to_string());
412        lines.push(String::new());
413    }
414
415    // Generate const wrappers in Type$Trait$ namespace
416    for trait_const in consts {
417        let const_name = &trait_const.ident;
418        let const_str = const_name.to_string();
419        let target = trait_namespace_target(ClassSystem::S3, type_ident, trait_name, &const_str);
420
421        let roxygen = RoxygenBuilder::new()
422            .name(target.clone())
423            .rdname(&type_str)
424            .build();
425        lines.extend(roxygen);
426
427        let c_ident = trait_const.c_wrapper_ident_string(type_ident, trait_name);
428        let call = DotCallBuilder::new(&c_ident).build();
429
430        lines.push(format!("{target} <- function() {{"));
431        lines.push(format!("  {}", call));
432        lines.push("}".to_string());
433        lines.push(String::new());
434    }
435
436    lines.join("\n")
437}
438
439/// Generate S4-style R wrapper code.
440///
441/// For `impl Counter for SimpleCounter`, generates:
442/// - `setOldClass("SimpleCounter")` to register the S3 class for S4 dispatch
443/// - S4 generic `s4_trait_Counter_value(x, ...)` via `setGeneric()`
444/// - S4 method via `setMethod("s4_trait_Counter_value", "SimpleCounter", ...)`
445///
446/// Generic names are prefixed with `s4_trait_{Trait}_` to avoid collisions
447/// with user-defined S4 generics. Static methods and constants are generated
448/// as standalone exported functions: `{Type}_{Trait}_{method}()`.
449fn generate_trait_s4_r_wrapper(
450    type_ident: &syn::Ident,
451    trait_name: &syn::Ident,
452    methods: &[TraitMethod],
453    consts: &[TraitConst],
454) -> String {
455    use crate::r_wrapper_builder::{DotCallBuilder, RoxygenBuilder};
456
457    let mut lines = Vec::new();
458    let type_str = type_ident.to_string();
459
460    // Header comment
461    lines.push(format!(
462        "# S4 trait methods for {} implementing {}",
463        type_ident, trait_name
464    ));
465    lines.push(format!(
466        "# Generated by #[miniextendr(s4)] impl {} for {}",
467        trait_name, type_ident
468    ));
469    lines.push(String::new());
470
471    // NOTE: We do NOT call setOldClass here. The inherent impl's class registration
472    // (setClass for S4, or setOldClass for S3/env) takes care of that. Calling
473    // setOldClass here would clobber a proper S4 setClass with slots.
474    lines.push("#' @importFrom methods setGeneric setMethod".to_string());
475    lines.push(String::new());
476
477    // Separate instance methods from static methods
478    let instance_methods: Vec<_> = methods.iter().filter(|m| m.has_self).collect();
479    let static_methods: Vec<_> = methods.iter().filter(|m| !m.has_self).collect();
480
481    // Generate S4 generics + methods for instance methods
482    for method in &instance_methods {
483        let method_name = &method.ident;
484        let generic_name = format!("s4_trait_{}_{}", trait_name, method.r_method_name());
485        let ctx = TraitMethodContext::new(method, type_ident, trait_name);
486
487        // Build full parameter list (x first, then others, then ...)
488        let full_params = if ctx.params.is_empty() {
489            "x, ...".to_string()
490        } else {
491            format!("x, {}, ...", ctx.params)
492        };
493
494        // S4 generic roxygen (include @param tags from method doc comments)
495        // S4 generic names are already type-qualified (s4_trait_TypeName_method)
496        // so @name won't create duplicate aliases across types.
497        let mut generic_roxygen = RoxygenBuilder::new()
498            .custom(format!(
499                "S4 generic for trait method `{}::{}`",
500                trait_name, method_name
501            ))
502            .name(&generic_name)
503            .rdname(&type_str)
504            .source(format!(
505                "Generated by miniextendr from `impl {} for {}`",
506                trait_name, type_ident
507            ))
508            .custom(format!("@param x A `{}` object", type_str))
509            .custom("@param ... Additional arguments passed to methods");
510        for tag in &method.param_tags {
511            generic_roxygen = generic_roxygen.custom(tag.clone());
512        }
513        lines.extend(generic_roxygen.export().build());
514
515        // Define generic only if it doesn't already exist in THIS namespace
516        // (avoid clearing methods). Scoped to topenv(environment()) so an
517        // attached installed copy of the package can't satisfy the check and
518        // starve the setMethod below during load_all() (#1158).
519        lines.push(format!(
520            "if (!exists(\"{generic_name}\", where = topenv(environment()), inherits = FALSE)) methods::setGeneric(\"{generic_name}\", function(x, ...) standardGeneric(\"{generic_name}\"))"
521        ));
522        lines.push(String::new());
523
524        // S4 method roxygen + definition (include @param tags from method doc comments)
525        lines.push(format!("#' @rdname {}", type_str));
526        for tag in &method.param_tags {
527            lines.push(format!("#' {}", tag));
528        }
529        lines.push(format!("#' @exportMethod {}", generic_name));
530
531        lines.push(format!(
532            "methods::setMethod(\"{}\", \"{}\", function({}) {{",
533            generic_name, type_str, full_params
534        ));
535        // S4 objects store the ExternalPtr in x@ptr — extract it for .Call()
536        lines.push("  .ptr <- x@ptr".to_string());
537        let s4_call = ctx.instance_call(".ptr");
538        ctx.emit_method_prelude(&mut lines, "  ", &method.r_method_name());
539        lines.extend(ctx.method_body_lines(&s4_call, ClassSystem::S4));
540        // Void instance methods return invisible(x) for pipe-friendly chaining
541        if method.returns_unit() {
542            lines.push("  invisible(x)".to_string());
543        }
544        lines.push("})".to_string());
545        lines.push(String::new());
546    }
547
548    // Generate static methods as standalone functions. S4 objects intercept
549    // `$<-`, so these use the flat, class-qualified `Type_Trait_method` name
550    // (owned by `trait_namespace_target`) rather than `Type$Trait$method`.
551    for method in &static_methods {
552        let r_name = method.r_method_name();
553        let ctx = TraitMethodContext::new(method, type_ident, trait_name);
554        let fn_name = ctx.namespace_target(ClassSystem::S4);
555
556        // Static method roxygen
557        lines.push(format!(
558            "#' Static trait method {}::{}() for {}",
559            trait_name, r_name, type_str
560        ));
561        let roxygen = RoxygenBuilder::new()
562            .name(&fn_name)
563            .rdname(&type_str)
564            .export()
565            .build();
566        lines.extend(roxygen);
567
568        let call = ctx.static_call();
569
570        lines.push(format!("{} <- function({}) {{", fn_name, ctx.params));
571        ctx.emit_method_prelude(&mut lines, "  ", &r_name);
572        lines.extend(ctx.method_body_lines(&call, ClassSystem::S4));
573        lines.push("}".to_string());
574        lines.push(String::new());
575    }
576
577    // Generate const wrappers as standalone functions (flat name, as above)
578    for trait_const in consts {
579        let const_name = &trait_const.ident;
580        let const_str = const_name.to_string();
581        let fn_name = trait_namespace_target(ClassSystem::S4, type_ident, trait_name, &const_str);
582
583        let roxygen = RoxygenBuilder::new()
584            .name(&fn_name)
585            .rdname(&type_str)
586            .export()
587            .build();
588        lines.extend(roxygen);
589
590        let c_ident = trait_const.c_wrapper_ident_string(type_ident, trait_name);
591        let call = DotCallBuilder::new(&c_ident).build();
592
593        lines.push(format!("{} <- function() {{", fn_name));
594        lines.push(format!("  {}", call));
595        lines.push("}".to_string());
596        lines.push(String::new());
597    }
598
599    lines.join("\n")
600}
601
602/// Generate S7-style R wrapper code.
603///
604/// For `impl Counter for SimpleCounter`, generates:
605/// - S7 S3-class wrapper: `.s7_class_SimpleCounter <- S7::new_S3_class("SimpleCounter")`
606/// - S7 generic: `s7_trait_Counter_value <- S7::new_generic(...)` (if not exists)
607/// - S7 method registration: `S7::method(s7_trait_Counter_value, .s7_class_SimpleCounter) <- ...`
608///
609/// Generic names are prefixed with `s7_trait_{Trait}_` to avoid collisions.
610/// Static methods and constants use `Type$Trait$name` namespace (env-style).
611fn generate_trait_s7_r_wrapper(
612    type_ident: &syn::Ident,
613    trait_name: &syn::Ident,
614    methods: &[TraitMethod],
615    consts: &[TraitConst],
616) -> String {
617    use crate::r_wrapper_builder::{DotCallBuilder, RoxygenBuilder};
618
619    let mut lines = Vec::new();
620    let type_str = type_ident.to_string();
621    let trait_str = trait_name.to_string();
622    let s7_class_var = format!(".s7_class_{}", type_str);
623
624    // Header comment
625    lines.push(format!(
626        "# S7 trait methods for {} implementing {}",
627        type_ident, trait_name
628    ));
629    lines.push(format!(
630        "# Generated by #[miniextendr(s7)] impl {} for {}",
631        trait_name, type_ident
632    ));
633    lines.push(String::new());
634
635    // Use the S7 class object directly for method dispatch.
636    // new_S3_class("Foo") creates a descriptor for "Foo" but S7 new_class
637    // creates instances with the namespaced class "pkg::Foo", so new_S3_class
638    // wouldn't match. Using the class object directly works correctly.
639    lines.push("#' @importFrom S7 new_generic method S7_dispatch".to_string());
640    lines.push(format!("{} <- {}", s7_class_var, type_str));
641    lines.push(String::new());
642
643    // Separate instance methods from static methods
644    let instance_methods: Vec<_> = methods.iter().filter(|m| m.has_self).collect();
645    let static_methods: Vec<_> = methods.iter().filter(|m| !m.has_self).collect();
646
647    // Generate S7 generics + methods for instance methods
648    for method in &instance_methods {
649        let method_name = &method.ident;
650        let generic_name = format!("s7_trait_{}_{}", trait_name, method.r_method_name());
651        let ctx = TraitMethodContext::new(method, type_ident, trait_name);
652
653        // Build full parameter list (x first, then others, then ...)
654        let full_params = if ctx.params.is_empty() {
655            "x, ...".to_string()
656        } else {
657            format!("x, {}, ...", ctx.params)
658        };
659
660        // S7 generic roxygen
661        // Note: Don't include method-specific @param tags here since S7 methods
662        // are assignments and won't appear in \usage, which would cause warnings
663        // S7 generic names are already type-qualified so @name won't duplicate.
664        let generic_roxygen = RoxygenBuilder::new()
665            .custom(format!(
666                "S7 generic for trait method `{}::{}`",
667                trait_name, method_name
668            ))
669            .name(&generic_name)
670            .rdname(&type_str)
671            .source(format!(
672                "Generated by miniextendr from `impl {} for {}`",
673                trait_name, type_ident
674            ))
675            .export()
676            .build();
677        lines.extend(generic_roxygen);
678
679        // S7 generic definition
680        lines.push(format!(
681            "if (!exists(\"{generic_name}\", mode = \"function\")) {{"
682        ));
683        lines.push(format!(
684            "  {generic_name} <- S7::new_generic(\"{generic_name}\", \"x\", function(x, ...) S7::S7_dispatch())"
685        ));
686        lines.push("}".to_string());
687        lines.push(String::new());
688
689        // S7 method definition
690        lines.push(format!(
691            "S7::method({}, {}) <- function({}) {{",
692            generic_name, s7_class_var, full_params
693        ));
694        // S7 objects store the ExternalPtr in x@.ptr — extract it for .Call()
695        lines.push("  .ptr <- x@.ptr".to_string());
696        let s7_call = ctx.instance_call(".ptr");
697        ctx.emit_method_prelude(&mut lines, "  ", &method.r_method_name());
698        lines.extend(ctx.method_body_lines(&s7_call, ClassSystem::S7));
699        // Void instance methods return invisible(x) for pipe-friendly chaining
700        if method.returns_unit() {
701            lines.push("  invisible(x)".to_string());
702        }
703        lines.push("}".to_string());
704        lines.push(String::new());
705
706        // Per-class fast-path dispatch shortcut (#987).
707        //
708        // Mirror the inherent-impl S7 shortcut (#982, see
709        // `miniextendr_impl::s7_class`): alongside the trait generic, emit a
710        // plain `<ClassName>_<method>(self, ...)` function that calls `.Call`
711        // directly, bypassing `S7::S7_dispatch()`. The receiver is named `self`
712        // here (the generic names it `x`) and wired through `self@.ptr`.
713        // `s7(no_shortcut)` opts a method out.
714        if !method.no_shortcut {
715            let shortcut_name = format!("{}_{}", type_str, method.r_method_name());
716            let shortcut_formals = if ctx.params.is_empty() {
717                "self, ...".to_string()
718            } else {
719                format!("self, {}, ...", ctx.params)
720            };
721            let shortcut_call = ctx.instance_call("self@.ptr");
722
723            // Roxygen: shared advisory prose + scaffolding. The shared @rdname
724            // page (type_str) already carries the method's prose via the generic
725            // block above, so only document `self` + each formal here to keep the
726            // shortcut's \usage fully covered (no "undocumented argument" warning).
727            lines.extend(crate::miniextendr_impl::s7_class::shortcut_advisory_lines(
728                &method.r_method_name(),
729                &type_str,
730            ));
731            lines.push(format!("#' @param self A `{}` object.", type_str));
732            for tag in &method.param_tags {
733                lines.push(format!("#' {}", tag));
734            }
735            // Auto-document any formal lacking an explicit @param tag. `...` is
736            // included so roxygen2 covers it (otherwise R CMD check warns about
737            // an undocumented argument). Split on top-level commas only — a
738            // naive `split(", ")` breaks a `mode = c("fast", "slow")` default
739            // into a bogus `"slow")` formal (undocumented-argument warning).
740            for formal in crate::roxygen::split_r_formals(&shortcut_formals) {
741                let pname = crate::roxygen::formal_name(formal);
742                if pname == "self" {
743                    continue;
744                }
745                let documented = crate::roxygen::param_documented(&method.param_tags, pname);
746                if documented {
747                    continue;
748                }
749                if pname == "..." {
750                    lines.push(
751                        "#' @param ... Additional arguments; ignored by the fast-path shortcut."
752                            .to_string(),
753                    );
754                } else {
755                    lines.push(format!("#' @param {} (undocumented)", pname));
756                }
757            }
758            lines.push(format!("#' @name {}", shortcut_name));
759            lines.push(format!("#' @rdname {}", type_str));
760            lines.push(format!(
761                "#' @source Generated by miniextendr from `impl {} for {}` (`{}` shortcut)",
762                trait_name, type_ident, method_name
763            ));
764            lines.push("#' @export".to_string());
765
766            lines.push(format!(
767                "{} <- function({}) {{",
768                shortcut_name, shortcut_formals
769            ));
770            ctx.emit_method_prelude(&mut lines, "  ", &method.r_method_name());
771            lines.extend(ctx.method_body_lines(&shortcut_call, ClassSystem::S7));
772            // Void instance methods return invisible(self) for pipe-friendly chaining
773            if method.returns_unit() {
774                lines.push("  invisible(self)".to_string());
775            }
776            lines.push("}".to_string());
777            lines.push(String::new());
778        }
779    }
780
781    // Create trait namespace for static methods and consts.
782    // For S7 classes, use a local variable + attr() to avoid S7's $<- interception.
783    let trait_env_var = trait_namespace_env_var(type_ident, trait_name);
784    if !static_methods.is_empty() || !consts.is_empty() {
785        lines.push(format!("{} <- new.env(parent = emptyenv())", trait_env_var));
786        lines.push(String::new());
787    }
788
789    // Generate static methods in trait namespace. The wrapper is *assigned*
790    // into the local env (`trait_namespace_target(S7, ..)` = `.Type__Trait$m`),
791    // but its documented `@name` is the call-site form `Type$Trait$m` — S7's
792    // `$` on the class object falls through to the attached attribute, so users
793    // still spell it `Type$Trait$m`.
794    for method in &static_methods {
795        let r_name = method.r_method_name();
796        let ctx = TraitMethodContext::new(method, type_ident, trait_name);
797
798        lines.push(format!(
799            "#' Static trait method {}::{}()",
800            trait_name, r_name
801        ));
802        let roxygen = RoxygenBuilder::new()
803            .name(format!("{}${}${}", type_str, trait_str, r_name))
804            .rdname(&type_str)
805            .build();
806        lines.extend(roxygen);
807
808        let call = ctx.static_call();
809
810        lines.push(format!(
811            "{} <- function({}) {{",
812            ctx.namespace_target(ClassSystem::S7),
813            ctx.params
814        ));
815        ctx.emit_method_prelude(&mut lines, "  ", &r_name);
816        lines.extend(ctx.method_body_lines(&call, ClassSystem::S7));
817        lines.push("}".to_string());
818        lines.push(String::new());
819    }
820
821    // Generate const wrappers in trait namespace (assigned into `.Type__Trait`,
822    // documented as `Type$Trait$const` — see the static-method note above).
823    for trait_const in consts {
824        let const_name = &trait_const.ident;
825        let const_str = const_name.to_string();
826
827        let roxygen = RoxygenBuilder::new()
828            .name(format!("{}${}${}", type_str, trait_str, const_str))
829            .rdname(&type_str)
830            .build();
831        lines.extend(roxygen);
832
833        let c_ident = trait_const.c_wrapper_ident_string(type_ident, trait_name);
834        let call = DotCallBuilder::new(&c_ident).build();
835
836        lines.push(format!(
837            "{} <- function() {{",
838            trait_namespace_target(ClassSystem::S7, type_ident, trait_name, &const_str)
839        ));
840        lines.push(format!("  {}", call));
841        lines.push("}".to_string());
842        lines.push(String::new());
843    }
844
845    // Attach the trait env to the S7 class via attr() to bypass S7's $<- interception.
846    // R's $ accessor on S7 objects falls through to attributes, so Type$Trait$method still works.
847    if !static_methods.is_empty() || !consts.is_empty() {
848        lines.push(format!(
849            "attr({}, \"{}\") <- {}",
850            type_ident, trait_name, trait_env_var
851        ));
852        lines.push(String::new());
853    }
854
855    lines.join("\n")
856}
857
858/// Generate R6-style R wrapper code.
859///
860/// R6 classes are defined monolithically (all methods in `R6Class()`), so trait
861/// methods cannot be injected into the class definition. Instead, both instance
862/// and static trait methods live in the class-scoped `Type$Trait$name`
863/// namespace (env-style) — the R6 generator object is an environment, so
864/// `Type$Trait <- new.env()` attaches cleanly and `Type$Trait$method(x)`
865/// resolves at the call site.
866///
867/// For `impl Counter for SimpleCounter`, generates:
868/// - `SimpleCounter$Counter$value(x)`      -- instance method (takes the object)
869/// - `SimpleCounter$Counter$increment(x)`  -- instance method
870///
871/// This class-qualified shape is collision-free by construction: two R6 impls
872/// of one trait on different types no longer share an unqualified
873/// `r6_trait_<Trait>_<method>` name (#1115). It also unifies R6 with the Env/S3
874/// namespace shape (#1141) — R6 instance and static methods previously
875/// disagreed on shape for no functional reason.
876fn generate_trait_r6_r_wrapper(
877    type_ident: &syn::Ident,
878    trait_name: &syn::Ident,
879    methods: &[TraitMethod],
880    consts: &[TraitConst],
881) -> String {
882    use crate::r_wrapper_builder::{DotCallBuilder, RoxygenBuilder};
883
884    let mut lines = Vec::new();
885    let type_str = type_ident.to_string();
886
887    // Header comment
888    lines.push(format!(
889        "# R6 trait methods for {} implementing {}",
890        type_ident, trait_name
891    ));
892    lines.push(format!(
893        "# Generated by #[miniextendr(r6)] impl {} for {}",
894        trait_name, type_ident
895    ));
896    lines.push("# Note: R6 trait methods live in the Type$Trait$method namespace".to_string());
897    lines.push(String::new());
898
899    // Separate instance methods from static methods
900    let instance_methods: Vec<_> = methods.iter().filter(|m| m.has_self).collect();
901    let static_methods: Vec<_> = methods.iter().filter(|m| !m.has_self).collect();
902
903    // Create the trait namespace env up front — instance methods now live in it
904    // too (not just static methods / consts).
905    if !methods.is_empty() || !consts.is_empty() {
906        lines.push(format!(
907            "{}${} <- new.env(parent = emptyenv())",
908            type_ident, trait_name
909        ));
910        lines.push(String::new());
911    }
912
913    // Generate instance methods in the Type$Trait$ namespace
914    for method in &instance_methods {
915        let ctx = TraitMethodContext::new(method, type_ident, trait_name);
916        let target = ctx.namespace_target(ClassSystem::R6);
917
918        // Build parameter list (x first, then others)
919        let full_params = if ctx.params.is_empty() {
920            "x".to_string()
921        } else {
922            format!("x, {}", ctx.params)
923        };
924
925        // Namespace-member roxygen — a `$<-` assignment target, so roxygen emits
926        // no `\usage` and needs no per-formal `@param` docs (matches Env; #1141).
927        let roxygen = RoxygenBuilder::new()
928            .name(target.clone())
929            .rdname(&type_str)
930            .build();
931        lines.extend(roxygen);
932
933        let call = ctx.instance_call(".ptr");
934
935        lines.push(format!("{target} <- function({full_params}) {{"));
936        // R6 objects store the ExternalPtr in private$.ptr — extract it for .Call()
937        lines.push("  .ptr <- x$.__enclos_env__$private$.ptr".to_string());
938        ctx.emit_method_prelude(&mut lines, "  ", &method.r_method_name());
939        lines.extend(ctx.method_body_lines(&call, ClassSystem::R6));
940        // Void instance methods return invisible(x) for pipe-friendly chaining
941        if method.returns_unit() {
942            lines.push("  invisible(x)".to_string());
943        }
944        lines.push("}".to_string());
945        lines.push(String::new());
946    }
947
948    // Generate static methods in Type$Trait$ namespace
949    for method in &static_methods {
950        let r_name = method.r_method_name();
951        let ctx = TraitMethodContext::new(method, type_ident, trait_name);
952        let target = ctx.namespace_target(ClassSystem::R6);
953
954        lines.push(format!(
955            "#' Static trait method {}::{}()",
956            trait_name, r_name
957        ));
958        let roxygen = RoxygenBuilder::new()
959            .name(target.clone())
960            .rdname(&type_str)
961            .build();
962        lines.extend(roxygen);
963
964        let call = ctx.static_call();
965
966        lines.push(format!("{target} <- function({}) {{", ctx.params));
967        ctx.emit_method_prelude(&mut lines, "  ", &r_name);
968        lines.extend(ctx.method_body_lines(&call, ClassSystem::R6));
969        lines.push("}".to_string());
970        lines.push(String::new());
971    }
972
973    // Generate const wrappers in Type$Trait$ namespace
974    for trait_const in consts {
975        let const_name = &trait_const.ident;
976        let const_str = const_name.to_string();
977        let target = trait_namespace_target(ClassSystem::R6, type_ident, trait_name, &const_str);
978
979        let roxygen = RoxygenBuilder::new()
980            .name(target.clone())
981            .rdname(&type_str)
982            .build();
983        lines.extend(roxygen);
984
985        let c_ident = trait_const.c_wrapper_ident_string(type_ident, trait_name);
986        let call = DotCallBuilder::new(&c_ident).build();
987
988        lines.push(format!("{target} <- function() {{"));
989        lines.push(format!("  {}", call));
990        lines.push("}".to_string());
991        lines.push(String::new());
992    }
993
994    lines.join("\n")
995}