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;
9use super::{TraitConst, TraitMethod, trait_method_body_lines};
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    let trait_str = trait_name.to_string();
166
167    // Header comment
168    lines.push(format!(
169        "# Trait methods and consts for {} implementing {}",
170        type_ident, trait_name
171    ));
172    lines.push(format!(
173        "# Generated by #[miniextendr] impl {} for {}",
174        trait_name, type_ident
175    ));
176    lines.push(String::new());
177
178    // Create trait namespace environment
179    lines.push(format!(
180        "{}${} <- new.env(parent = emptyenv())",
181        type_ident, trait_name
182    ));
183    lines.push(String::new());
184
185    for method in methods {
186        let r_name = method.r_method_name();
187        let ctx = TraitMethodContext::new(method, type_ident, trait_name);
188
189        // Build roxygen tags
190        let roxygen = RoxygenBuilder::new()
191            .name(format!("{}${}${}", type_str, trait_str, r_name))
192            .rdname(&type_str)
193            .build();
194        lines.extend(roxygen);
195
196        // Check for 'x' parameter collision in instance methods
197        if method.has_self {
198            for input in &method.sig.inputs {
199                if let syn::FnArg::Typed(pt) = input
200                    && let syn::Pat::Ident(pat_ident) = pt.pat.as_ref()
201                    && pat_ident.ident == "x"
202                {
203                    return Err(syn::Error::new_spanned(
204                        &pat_ident.ident,
205                        "trait instance method parameter cannot be named `x` \
206                         (collides with self parameter in env-class dispatch)",
207                    ));
208                }
209            }
210        }
211
212        // Build .Call() invocation — C name uses Rust ident, R name uses r_name
213        let (full_params, call) = if method.has_self {
214            let fp = if ctx.params.is_empty() {
215                "x".to_string()
216            } else {
217                format!("x, {}", ctx.params)
218            };
219            (fp, ctx.instance_call("x"))
220        } else {
221            (ctx.params.clone(), ctx.static_call())
222        };
223
224        // Generate method wrapper (R-facing name)
225        lines.push(format!(
226            "{}${}${} <- function({}) {{",
227            type_ident, trait_name, r_name, full_params
228        ));
229        ctx.emit_method_prelude(&mut lines, "  ", &r_name);
230        lines.extend(trait_method_body_lines(&call, "  "));
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!(
239                "attr({}${}${}, \".__mx_instance__\") <- TRUE",
240                type_ident, trait_name, r_name
241            ));
242        }
243
244        lines.push(String::new());
245    }
246
247    // Generate const wrappers
248    for trait_const in consts {
249        let const_name = &trait_const.ident;
250        let const_str = const_name.to_string();
251
252        // Build roxygen tags
253        let roxygen = RoxygenBuilder::new()
254            .name(format!("{}${}${}", type_str, trait_str, const_str))
255            .rdname(&type_str)
256            .build();
257        lines.extend(roxygen);
258
259        // Build .Call() invocation
260        let c_ident = trait_const.c_wrapper_ident_string(type_ident, trait_name);
261        let call = DotCallBuilder::new(&c_ident).build();
262
263        // Generate const wrapper
264        lines.push(format!(
265            "{}${}${} <- function() {{",
266            type_ident, trait_name, const_name
267        ));
268        lines.push(format!("  {}", call));
269        lines.push("}".to_string());
270        lines.push(String::new());
271    }
272
273    Ok(lines.join("\n"))
274}
275
276/// Generate S3-style R wrapper code (generic + method.Type).
277///
278/// For `impl Counter for SimpleCounter`, generates:
279/// - S3 generic `value(x, ...)` (if not already defined)
280/// - S3 method `value.SimpleCounter <- function(x, ...) { .Call(...) }`
281/// - S7 method registration if the generic is an S7 generic
282///
283/// Static methods and constants use `Type$Trait$name` namespace (env-style).
284/// Void instance methods return `invisible(x)` for pipe-friendly chaining.
285///
286/// Also used for `ClassSystem::Vctrs` since vctrs uses S3 under the hood.
287fn generate_trait_s3_r_wrapper(
288    type_ident: &syn::Ident,
289    trait_name: &syn::Ident,
290    methods: &[TraitMethod],
291    consts: &[TraitConst],
292) -> String {
293    use crate::r_wrapper_builder::{DotCallBuilder, RoxygenBuilder};
294
295    let mut lines = Vec::new();
296    let type_str = type_ident.to_string();
297    let trait_str = trait_name.to_string();
298
299    // Header comment
300    lines.push(format!(
301        "# S3 trait methods for {} implementing {}",
302        type_ident, trait_name
303    ));
304    lines.push(format!(
305        "# Generated by #[miniextendr(s3)] impl {} for {}",
306        trait_name, type_ident
307    ));
308    lines.push(String::new());
309
310    // Separate instance methods (S3 dispatch) from static methods (namespace access)
311    let instance_methods: Vec<_> = methods.iter().filter(|m| m.has_self).collect();
312    let static_methods: Vec<_> = methods.iter().filter(|m| !m.has_self).collect();
313
314    // Generate S3 generics + methods for instance methods
315    for method in &instance_methods {
316        let generic_name = method.r_method_name();
317        let s3_method_name = format!("{}.{}", generic_name, type_str);
318        let ctx = TraitMethodContext::new(method, type_ident, trait_name);
319
320        // S3 generic roxygen (only create if doesn't exist)
321        // Use type-qualified @name to avoid duplicate aliases across types
322        let generic_roxygen = RoxygenBuilder::new()
323            .title(format!("S3 generic for `{}`", generic_name))
324            .custom(format!("S3 generic for `{}`", generic_name))
325            .name(format!("{}.{}", generic_name, type_str))
326            .rdname(&type_str)
327            .custom("@param x An object")
328            .custom("@param ... Additional arguments passed to methods")
329            .source(format!(
330                "Generated by miniextendr from `impl {} for {}`",
331                trait_name, type_ident
332            ))
333            .export()
334            .build();
335        lines.extend(generic_roxygen);
336
337        // S3 generic definition
338        lines.push(emit_s3_generic_guard(generic_name.as_str()));
339        lines.push(String::new());
340
341        // S3 method roxygen (include @param tags from method doc comments)
342        let mut method_roxygen = RoxygenBuilder::new()
343            .rdname(&type_str)
344            .export()
345            .method(&generic_name, &type_str);
346        for tag in &method.param_tags {
347            method_roxygen = method_roxygen.custom(tag.clone());
348        }
349        lines.extend(method_roxygen.build());
350
351        // S3 method: generic.class
352        let full_params = if ctx.params.is_empty() {
353            "x, ...".to_string()
354        } else {
355            format!("x, {}, ...", ctx.params)
356        };
357
358        // Build .Call() invocation
359        let call = ctx.instance_call("x");
360
361        // Always define the S3 method (roxygen expects it for NAMESPACE export)
362        lines.push(format!(
363            "{} <- function({}) {{",
364            s3_method_name, full_params
365        ));
366        ctx.emit_method_prelude(&mut lines, "  ", &generic_name);
367        lines.extend(trait_method_body_lines(&call, "  "));
368        // Void instance methods return invisible(x) for pipe-friendly chaining
369        if method.returns_unit() {
370            lines.push("  invisible(x)".to_string());
371        }
372        lines.push("}".to_string());
373
374        // Additionally register as S7 method if the generic is S7
375        // This ensures S7 dispatch works when the generic was defined by an S7 class
376        lines.push(format!(
377            "if (inherits(get0(\"{generic_name}\", mode = \"function\"), \"S7_generic\")) {{"
378        ));
379        lines.push(format!(
380            "  S7::method({generic_name}, S7::new_S3_class(\"{type_str}\")) <- {s3_method_name}"
381        ));
382        lines.push("}".to_string());
383        lines.push(String::new());
384    }
385
386    // Create trait namespace for static methods and consts BEFORE assigning to it
387    if !static_methods.is_empty() || !consts.is_empty() {
388        lines.push(format!(
389            "{}${} <- new.env(parent = emptyenv())",
390            type_ident, trait_name
391        ));
392        lines.push(String::new());
393    }
394
395    // Generate static methods in Type$Trait$ namespace
396    for method in &static_methods {
397        let r_name = method.r_method_name();
398        let ctx = TraitMethodContext::new(method, type_ident, trait_name);
399
400        // Static method roxygen
401        lines.push(format!(
402            "#' Static trait method {}::{}()",
403            trait_name, r_name
404        ));
405        let roxygen = RoxygenBuilder::new()
406            .name(format!("{}${}${}", type_str, trait_str, r_name))
407            .rdname(&type_str)
408            .build();
409        lines.extend(roxygen);
410
411        let call = ctx.static_call();
412
413        lines.push(format!(
414            "{}${}${} <- function({}) {{",
415            type_ident, trait_name, r_name, ctx.params
416        ));
417        ctx.emit_method_prelude(&mut lines, "  ", &r_name);
418        lines.extend(trait_method_body_lines(&call, "  "));
419        lines.push("}".to_string());
420        lines.push(String::new());
421    }
422
423    // Generate const wrappers in Type$Trait$ namespace
424    for trait_const in consts {
425        let const_name = &trait_const.ident;
426        let const_str = const_name.to_string();
427
428        let roxygen = RoxygenBuilder::new()
429            .name(format!("{}${}${}", type_str, trait_str, const_str))
430            .rdname(&type_str)
431            .build();
432        lines.extend(roxygen);
433
434        let c_ident = trait_const.c_wrapper_ident_string(type_ident, trait_name);
435        let call = DotCallBuilder::new(&c_ident).build();
436
437        lines.push(format!(
438            "{}${}${} <- function() {{",
439            type_ident, trait_name, const_name
440        ));
441        lines.push(format!("  {}", call));
442        lines.push("}".to_string());
443        lines.push(String::new());
444    }
445
446    lines.join("\n")
447}
448
449/// Generate S4-style R wrapper code.
450///
451/// For `impl Counter for SimpleCounter`, generates:
452/// - `setOldClass("SimpleCounter")` to register the S3 class for S4 dispatch
453/// - S4 generic `s4_trait_Counter_value(x, ...)` via `setGeneric()`
454/// - S4 method via `setMethod("s4_trait_Counter_value", "SimpleCounter", ...)`
455///
456/// Generic names are prefixed with `s4_trait_{Trait}_` to avoid collisions
457/// with user-defined S4 generics. Static methods and constants are generated
458/// as standalone exported functions: `{Type}_{Trait}_{method}()`.
459fn generate_trait_s4_r_wrapper(
460    type_ident: &syn::Ident,
461    trait_name: &syn::Ident,
462    methods: &[TraitMethod],
463    consts: &[TraitConst],
464) -> String {
465    use crate::r_wrapper_builder::{DotCallBuilder, RoxygenBuilder};
466
467    let mut lines = Vec::new();
468    let type_str = type_ident.to_string();
469    let trait_str = trait_name.to_string();
470
471    // Header comment
472    lines.push(format!(
473        "# S4 trait methods for {} implementing {}",
474        type_ident, trait_name
475    ));
476    lines.push(format!(
477        "# Generated by #[miniextendr(s4)] impl {} for {}",
478        trait_name, type_ident
479    ));
480    lines.push(String::new());
481
482    // NOTE: We do NOT call setOldClass here. The inherent impl's class registration
483    // (setClass for S4, or setOldClass for S3/env) takes care of that. Calling
484    // setOldClass here would clobber a proper S4 setClass with slots.
485    lines.push("#' @importFrom methods setGeneric setMethod".to_string());
486    lines.push(String::new());
487
488    // Separate instance methods from static methods
489    let instance_methods: Vec<_> = methods.iter().filter(|m| m.has_self).collect();
490    let static_methods: Vec<_> = methods.iter().filter(|m| !m.has_self).collect();
491
492    // Generate S4 generics + methods for instance methods
493    for method in &instance_methods {
494        let method_name = &method.ident;
495        let generic_name = format!("s4_trait_{}_{}", trait_name, method.r_method_name());
496        let ctx = TraitMethodContext::new(method, type_ident, trait_name);
497
498        // Build full parameter list (x first, then others, then ...)
499        let full_params = if ctx.params.is_empty() {
500            "x, ...".to_string()
501        } else {
502            format!("x, {}, ...", ctx.params)
503        };
504
505        // S4 generic roxygen (include @param tags from method doc comments)
506        // S4 generic names are already type-qualified (s4_trait_TypeName_method)
507        // so @name won't create duplicate aliases across types.
508        let mut generic_roxygen = RoxygenBuilder::new()
509            .custom(format!(
510                "S4 generic for trait method `{}::{}`",
511                trait_name, method_name
512            ))
513            .name(&generic_name)
514            .rdname(&type_str)
515            .source(format!(
516                "Generated by miniextendr from `impl {} for {}`",
517                trait_name, type_ident
518            ))
519            .custom(format!("@param x A `{}` object", type_str))
520            .custom("@param ... Additional arguments passed to methods");
521        for tag in &method.param_tags {
522            generic_roxygen = generic_roxygen.custom(tag.clone());
523        }
524        lines.extend(generic_roxygen.export().build());
525
526        // Define generic only if it doesn't already exist in THIS namespace
527        // (avoid clearing methods). Scoped to topenv(environment()) so an
528        // attached installed copy of the package can't satisfy the check and
529        // starve the setMethod below during load_all() (#1158).
530        lines.push(format!(
531            "if (!exists(\"{generic_name}\", where = topenv(environment()), inherits = FALSE)) methods::setGeneric(\"{generic_name}\", function(x, ...) standardGeneric(\"{generic_name}\"))"
532        ));
533        lines.push(String::new());
534
535        // S4 method roxygen + definition (include @param tags from method doc comments)
536        lines.push(format!("#' @rdname {}", type_str));
537        for tag in &method.param_tags {
538            lines.push(format!("#' {}", tag));
539        }
540        lines.push(format!("#' @exportMethod {}", generic_name));
541
542        lines.push(format!(
543            "methods::setMethod(\"{}\", \"{}\", function({}) {{",
544            generic_name, type_str, full_params
545        ));
546        // S4 objects store the ExternalPtr in x@ptr — extract it for .Call()
547        lines.push("  .ptr <- x@ptr".to_string());
548        let s4_call = ctx.instance_call(".ptr");
549        ctx.emit_method_prelude(&mut lines, "  ", &method.r_method_name());
550        lines.extend(trait_method_body_lines(&s4_call, "  "));
551        // Void instance methods return invisible(x) for pipe-friendly chaining
552        if method.returns_unit() {
553            lines.push("  invisible(x)".to_string());
554        }
555        lines.push("})".to_string());
556        lines.push(String::new());
557    }
558
559    // Generate static methods as standalone functions
560    for method in &static_methods {
561        let r_name = method.r_method_name();
562        let fn_name = format!("{}_{}_{}", type_str, trait_str, r_name);
563        let ctx = TraitMethodContext::new(method, type_ident, trait_name);
564
565        // Static method roxygen
566        lines.push(format!(
567            "#' Static trait method {}::{}() for {}",
568            trait_name, r_name, type_str
569        ));
570        let roxygen = RoxygenBuilder::new()
571            .name(&fn_name)
572            .rdname(&type_str)
573            .export()
574            .build();
575        lines.extend(roxygen);
576
577        let call = ctx.static_call();
578
579        lines.push(format!("{} <- function({}) {{", fn_name, ctx.params));
580        ctx.emit_method_prelude(&mut lines, "  ", &r_name);
581        lines.extend(trait_method_body_lines(&call, "  "));
582        lines.push("}".to_string());
583        lines.push(String::new());
584    }
585
586    // Generate const wrappers as standalone functions
587    for trait_const in consts {
588        let const_name = &trait_const.ident;
589        let fn_name = format!("{}_{}_{}", type_str, trait_str, const_name);
590
591        let roxygen = RoxygenBuilder::new()
592            .name(&fn_name)
593            .rdname(&type_str)
594            .export()
595            .build();
596        lines.extend(roxygen);
597
598        let c_ident = trait_const.c_wrapper_ident_string(type_ident, trait_name);
599        let call = DotCallBuilder::new(&c_ident).build();
600
601        lines.push(format!("{} <- function() {{", fn_name));
602        lines.push(format!("  {}", call));
603        lines.push("}".to_string());
604        lines.push(String::new());
605    }
606
607    lines.join("\n")
608}
609
610/// Generate S7-style R wrapper code.
611///
612/// For `impl Counter for SimpleCounter`, generates:
613/// - S7 S3-class wrapper: `.s7_class_SimpleCounter <- S7::new_S3_class("SimpleCounter")`
614/// - S7 generic: `s7_trait_Counter_value <- S7::new_generic(...)` (if not exists)
615/// - S7 method registration: `S7::method(s7_trait_Counter_value, .s7_class_SimpleCounter) <- ...`
616///
617/// Generic names are prefixed with `s7_trait_{Trait}_` to avoid collisions.
618/// Static methods and constants use `Type$Trait$name` namespace (env-style).
619fn generate_trait_s7_r_wrapper(
620    type_ident: &syn::Ident,
621    trait_name: &syn::Ident,
622    methods: &[TraitMethod],
623    consts: &[TraitConst],
624) -> String {
625    use crate::r_wrapper_builder::{DotCallBuilder, RoxygenBuilder};
626
627    let mut lines = Vec::new();
628    let type_str = type_ident.to_string();
629    let trait_str = trait_name.to_string();
630    let s7_class_var = format!(".s7_class_{}", type_str);
631
632    // Header comment
633    lines.push(format!(
634        "# S7 trait methods for {} implementing {}",
635        type_ident, trait_name
636    ));
637    lines.push(format!(
638        "# Generated by #[miniextendr(s7)] impl {} for {}",
639        trait_name, type_ident
640    ));
641    lines.push(String::new());
642
643    // Use the S7 class object directly for method dispatch.
644    // new_S3_class("Foo") creates a descriptor for "Foo" but S7 new_class
645    // creates instances with the namespaced class "pkg::Foo", so new_S3_class
646    // wouldn't match. Using the class object directly works correctly.
647    lines.push("#' @importFrom S7 new_generic method S7_dispatch".to_string());
648    lines.push(format!("{} <- {}", s7_class_var, type_str));
649    lines.push(String::new());
650
651    // Separate instance methods from static methods
652    let instance_methods: Vec<_> = methods.iter().filter(|m| m.has_self).collect();
653    let static_methods: Vec<_> = methods.iter().filter(|m| !m.has_self).collect();
654
655    // Generate S7 generics + methods for instance methods
656    for method in &instance_methods {
657        let method_name = &method.ident;
658        let generic_name = format!("s7_trait_{}_{}", trait_name, method.r_method_name());
659        let ctx = TraitMethodContext::new(method, type_ident, trait_name);
660
661        // Build full parameter list (x first, then others, then ...)
662        let full_params = if ctx.params.is_empty() {
663            "x, ...".to_string()
664        } else {
665            format!("x, {}, ...", ctx.params)
666        };
667
668        // S7 generic roxygen
669        // Note: Don't include method-specific @param tags here since S7 methods
670        // are assignments and won't appear in \usage, which would cause warnings
671        // S7 generic names are already type-qualified so @name won't duplicate.
672        let generic_roxygen = RoxygenBuilder::new()
673            .custom(format!(
674                "S7 generic for trait method `{}::{}`",
675                trait_name, method_name
676            ))
677            .name(&generic_name)
678            .rdname(&type_str)
679            .source(format!(
680                "Generated by miniextendr from `impl {} for {}`",
681                trait_name, type_ident
682            ))
683            .export()
684            .build();
685        lines.extend(generic_roxygen);
686
687        // S7 generic definition
688        lines.push(format!(
689            "if (!exists(\"{generic_name}\", mode = \"function\")) {{"
690        ));
691        lines.push(format!(
692            "  {generic_name} <- S7::new_generic(\"{generic_name}\", \"x\", function(x, ...) S7::S7_dispatch())"
693        ));
694        lines.push("}".to_string());
695        lines.push(String::new());
696
697        // S7 method definition
698        lines.push(format!(
699            "S7::method({}, {}) <- function({}) {{",
700            generic_name, s7_class_var, full_params
701        ));
702        // S7 objects store the ExternalPtr in x@.ptr — extract it for .Call()
703        lines.push("  .ptr <- x@.ptr".to_string());
704        let s7_call = ctx.instance_call(".ptr");
705        ctx.emit_method_prelude(&mut lines, "  ", &method.r_method_name());
706        lines.extend(trait_method_body_lines(&s7_call, "  "));
707        // Void instance methods return invisible(x) for pipe-friendly chaining
708        if method.returns_unit() {
709            lines.push("  invisible(x)".to_string());
710        }
711        lines.push("}".to_string());
712        lines.push(String::new());
713
714        // Per-class fast-path dispatch shortcut (#987).
715        //
716        // Mirror the inherent-impl S7 shortcut (#982, see
717        // `miniextendr_impl::s7_class`): alongside the trait generic, emit a
718        // plain `<ClassName>_<method>(self, ...)` function that calls `.Call`
719        // directly, bypassing `S7::S7_dispatch()`. The receiver is named `self`
720        // here (the generic names it `x`) and wired through `self@.ptr`.
721        // `s7(no_shortcut)` opts a method out.
722        if !method.no_shortcut {
723            let shortcut_name = format!("{}_{}", type_str, method.r_method_name());
724            let shortcut_formals = if ctx.params.is_empty() {
725                "self, ...".to_string()
726            } else {
727                format!("self, {}, ...", ctx.params)
728            };
729            let shortcut_call = ctx.instance_call("self@.ptr");
730
731            // Roxygen: shared advisory prose + scaffolding. The shared @rdname
732            // page (type_str) already carries the method's prose via the generic
733            // block above, so only document `self` + each formal here to keep the
734            // shortcut's \usage fully covered (no "undocumented argument" warning).
735            lines.extend(crate::miniextendr_impl::s7_class::shortcut_advisory_lines(
736                &method.r_method_name(),
737                &type_str,
738            ));
739            lines.push(format!("#' @param self A `{}` object.", type_str));
740            for tag in &method.param_tags {
741                lines.push(format!("#' {}", tag));
742            }
743            // Auto-document any formal lacking an explicit @param tag. `...` is
744            // included so roxygen2 covers it (otherwise R CMD check warns about
745            // an undocumented argument). Split on top-level commas only — a
746            // naive `split(", ")` breaks a `mode = c("fast", "slow")` default
747            // into a bogus `"slow")` formal (undocumented-argument warning).
748            for formal in crate::roxygen::split_r_formals(&shortcut_formals) {
749                let pname = crate::roxygen::formal_name(formal);
750                if pname == "self" {
751                    continue;
752                }
753                let documented = method
754                    .param_tags
755                    .iter()
756                    .any(|t| t.starts_with(&format!("@param {}", pname)));
757                if documented {
758                    continue;
759                }
760                if pname == "..." {
761                    lines.push(
762                        "#' @param ... Additional arguments; ignored by the fast-path shortcut."
763                            .to_string(),
764                    );
765                } else {
766                    lines.push(format!("#' @param {} (undocumented)", pname));
767                }
768            }
769            lines.push(format!("#' @name {}", shortcut_name));
770            lines.push(format!("#' @rdname {}", type_str));
771            lines.push(format!(
772                "#' @source Generated by miniextendr from `impl {} for {}` (`{}` shortcut)",
773                trait_name, type_ident, method_name
774            ));
775            lines.push("#' @export".to_string());
776
777            lines.push(format!(
778                "{} <- function({}) {{",
779                shortcut_name, shortcut_formals
780            ));
781            ctx.emit_method_prelude(&mut lines, "  ", &method.r_method_name());
782            lines.extend(trait_method_body_lines(&shortcut_call, "  "));
783            // Void instance methods return invisible(self) for pipe-friendly chaining
784            if method.returns_unit() {
785                lines.push("  invisible(self)".to_string());
786            }
787            lines.push("}".to_string());
788            lines.push(String::new());
789        }
790    }
791
792    // Create trait namespace for static methods and consts.
793    // For S7 classes, use a local variable + attr() to avoid S7's $<- interception.
794    let trait_env_var = format!(".{}__{}", type_ident, trait_name);
795    if !static_methods.is_empty() || !consts.is_empty() {
796        lines.push(format!("{} <- new.env(parent = emptyenv())", trait_env_var));
797        lines.push(String::new());
798    }
799
800    // Generate static methods in trait namespace
801    for method in &static_methods {
802        let r_name = method.r_method_name();
803        let ctx = TraitMethodContext::new(method, type_ident, trait_name);
804
805        lines.push(format!(
806            "#' Static trait method {}::{}()",
807            trait_name, r_name
808        ));
809        let roxygen = RoxygenBuilder::new()
810            .name(format!("{}${}${}", type_str, trait_str, r_name))
811            .rdname(&type_str)
812            .build();
813        lines.extend(roxygen);
814
815        let call = ctx.static_call();
816
817        lines.push(format!(
818            "{}${} <- function({}) {{",
819            trait_env_var, r_name, ctx.params
820        ));
821        ctx.emit_method_prelude(&mut lines, "  ", &r_name);
822        lines.extend(trait_method_body_lines(&call, "  "));
823        lines.push("}".to_string());
824        lines.push(String::new());
825    }
826
827    // Generate const wrappers in trait namespace
828    for trait_const in consts {
829        let const_name = &trait_const.ident;
830        let const_str = const_name.to_string();
831
832        let roxygen = RoxygenBuilder::new()
833            .name(format!("{}${}${}", type_str, trait_str, const_str))
834            .rdname(&type_str)
835            .build();
836        lines.extend(roxygen);
837
838        let c_ident = trait_const.c_wrapper_ident_string(type_ident, trait_name);
839        let call = DotCallBuilder::new(&c_ident).build();
840
841        lines.push(format!("{}${} <- function() {{", trait_env_var, const_name));
842        lines.push(format!("  {}", call));
843        lines.push("}".to_string());
844        lines.push(String::new());
845    }
846
847    // Attach the trait env to the S7 class via attr() to bypass S7's $<- interception.
848    // R's $ accessor on S7 objects falls through to attributes, so Type$Trait$method still works.
849    if !static_methods.is_empty() || !consts.is_empty() {
850        lines.push(format!(
851            "attr({}, \"{}\") <- {}",
852            type_ident, trait_name, trait_env_var
853        ));
854        lines.push(String::new());
855    }
856
857    lines.join("\n")
858}
859
860/// Generate R6-style R wrapper code.
861///
862/// R6 classes are defined monolithically (all methods in `R6Class()`), so trait
863/// methods cannot be injected into the class definition. Instead, they are
864/// generated as standalone exported functions that accept the R6 object.
865///
866/// For `impl Counter for SimpleCounter`, generates:
867/// - `r6_trait_Counter_value(x)` -- exported standalone function
868/// - `r6_trait_Counter_increment(x)` -- exported standalone function
869///
870/// Instance method names are prefixed with `r6_trait_{Trait}_` to avoid collisions.
871/// Static methods and constants use `Type$Trait$name` namespace (env-style).
872fn generate_trait_r6_r_wrapper(
873    type_ident: &syn::Ident,
874    trait_name: &syn::Ident,
875    methods: &[TraitMethod],
876    consts: &[TraitConst],
877) -> String {
878    use crate::r_wrapper_builder::{DotCallBuilder, RoxygenBuilder};
879
880    let mut lines = Vec::new();
881    let type_str = type_ident.to_string();
882    let trait_str = trait_name.to_string();
883
884    // Header comment
885    lines.push(format!(
886        "# R6 trait methods for {} implementing {}",
887        type_ident, trait_name
888    ));
889    lines.push(format!(
890        "# Generated by #[miniextendr(r6)] impl {} for {}",
891        trait_name, type_ident
892    ));
893    lines.push("# Note: R6 trait methods are standalone functions".to_string());
894    lines.push(String::new());
895
896    // Separate instance methods from static methods
897    let instance_methods: Vec<_> = methods.iter().filter(|m| m.has_self).collect();
898    let static_methods: Vec<_> = methods.iter().filter(|m| !m.has_self).collect();
899
900    // Generate standalone functions for instance methods
901    for method in &instance_methods {
902        let method_name = &method.ident;
903        let fn_name = format!("r6_trait_{}_{}", trait_name, method.r_method_name());
904        let ctx = TraitMethodContext::new(method, type_ident, trait_name);
905
906        // Build parameter list (x first, then others)
907        let full_params = if ctx.params.is_empty() {
908            "x".to_string()
909        } else {
910            format!("x, {}", ctx.params)
911        };
912
913        // R6 trait method roxygen (include @param tags from method doc comments)
914        let mut roxygen = RoxygenBuilder::new()
915            .custom(format!(
916                "R6 trait method `{}::{}` for {}",
917                trait_name, method_name, type_str
918            ))
919            .name(&fn_name)
920            .rdname(&type_str)
921            .source(format!(
922                "Generated by miniextendr from `impl {} for {}`",
923                trait_name, type_ident
924            ))
925            .custom(format!("@param x A `{}` object", type_str));
926        for tag in &method.param_tags {
927            roxygen = roxygen.custom(tag.clone());
928        }
929        // Auto-document any method formal lacking an explicit @param tag —
930        // `x` is covered above; the rest are in `\usage` and would otherwise
931        // trip R CMD check's "Undocumented arguments" warning. Split on
932        // top-level commas only so a `mode = c("fast", "slow")` default isn't
933        // shredded into a bogus `"slow")` formal.
934        for formal in crate::roxygen::split_r_formals(&full_params) {
935            let pname = crate::roxygen::formal_name(formal);
936            if pname == "x" {
937                continue;
938            }
939            let documented = method
940                .param_tags
941                .iter()
942                .any(|t| t.trim_start().starts_with(&format!("@param {pname} ")));
943            if documented {
944                continue;
945            }
946            roxygen = if pname == "..." {
947                roxygen.custom("@param ... Additional arguments passed to the method.")
948            } else {
949                roxygen.custom(format!("@param {pname} (undocumented)"))
950            };
951        }
952        lines.extend(roxygen.export().build());
953
954        let call = ctx.instance_call(".ptr");
955
956        lines.push(format!("{} <- function({}) {{", fn_name, full_params));
957        // R6 objects store the ExternalPtr in private$.ptr — extract it for .Call()
958        lines.push("  .ptr <- x$.__enclos_env__$private$.ptr".to_string());
959        ctx.emit_method_prelude(&mut lines, "  ", &method.r_method_name());
960        lines.extend(trait_method_body_lines(&call, "  "));
961        // Void instance methods return invisible(x) for pipe-friendly chaining
962        if method.returns_unit() {
963            lines.push("  invisible(x)".to_string());
964        }
965        lines.push("}".to_string());
966        lines.push(String::new());
967    }
968
969    // Create trait namespace for static methods and consts
970    if !static_methods.is_empty() || !consts.is_empty() {
971        lines.push(format!(
972            "{}${} <- new.env(parent = emptyenv())",
973            type_ident, trait_name
974        ));
975        lines.push(String::new());
976    }
977
978    // Generate static methods in Type$Trait$ namespace
979    for method in &static_methods {
980        let r_name = method.r_method_name();
981        let ctx = TraitMethodContext::new(method, type_ident, trait_name);
982
983        lines.push(format!(
984            "#' Static trait method {}::{}()",
985            trait_name, r_name
986        ));
987        let roxygen = RoxygenBuilder::new()
988            .name(format!("{}${}${}", type_str, trait_str, r_name))
989            .rdname(&type_str)
990            .build();
991        lines.extend(roxygen);
992
993        let call = ctx.static_call();
994
995        lines.push(format!(
996            "{}${}${} <- function({}) {{",
997            type_ident, trait_name, r_name, ctx.params
998        ));
999        ctx.emit_method_prelude(&mut lines, "  ", &r_name);
1000        lines.extend(trait_method_body_lines(&call, "  "));
1001        lines.push("}".to_string());
1002        lines.push(String::new());
1003    }
1004
1005    // Generate const wrappers in Type$Trait$ namespace
1006    for trait_const in consts {
1007        let const_name = &trait_const.ident;
1008        let const_str = const_name.to_string();
1009
1010        let roxygen = RoxygenBuilder::new()
1011            .name(format!("{}${}${}", type_str, trait_str, const_str))
1012            .rdname(&type_str)
1013            .build();
1014        lines.extend(roxygen);
1015
1016        let c_ident = trait_const.c_wrapper_ident_string(type_ident, trait_name);
1017        let call = DotCallBuilder::new(&c_ident).build();
1018
1019        lines.push(format!(
1020            "{}${}${} <- function() {{",
1021            type_ident, trait_name, const_name
1022        ));
1023        lines.push(format!("  {}", call));
1024        lines.push("}".to_string());
1025        lines.push(String::new());
1026    }
1027
1028    lines.join("\n")
1029}