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