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::{
9    TraitMethodContext, s7_trait_member_access, trait_namespace_env_var, trait_namespace_target,
10};
11use super::{TraitConst, TraitMethod};
12use crate::miniextendr_impl::ClassSystem;
13use crate::r_class_formatter::emit_s3_generic_guard;
14
15/// Options controlling export visibility and documentation for trait R wrapper generation.
16pub(super) struct TraitWrapperOpts {
17    /// Which R class system to generate wrappers for (env, r6, s3, s4, s7, vctrs).
18    pub(super) class_system: ClassSystem,
19    /// Whether the impl block has `@noRd`, suppressing roxygen documentation output.
20    /// For S3/vctrs, method registration tags are preserved even when this is true.
21    pub(super) class_has_no_rd: bool,
22    /// Whether `#[miniextendr(internal)]` is set, adding `@keywords internal` and
23    /// suppressing `@export`/`@exportMethod`.
24    pub(super) internal: bool,
25    /// Whether `#[miniextendr(noexport)]` is set, suppressing `@export`/`@exportMethod`
26    /// without adding `@keywords internal`.
27    pub(super) noexport: bool,
28    /// The impl block's `preconditions` / `no_preconditions`
29    /// (`ImplAttrs::preconditions`), as on an inherent impl.
30    pub(super) impl_preconditions: Option<bool>,
31}
32
33/// Generate R wrapper code for trait methods and consts, dispatching by class system.
34///
35/// Calls the appropriate class-system-specific generator (env, s3, s4, s7, r6),
36/// then applies post-processing for `@noRd`, `internal`, and `noexport` options:
37///
38/// - `class_has_no_rd`: Strips roxygen blocks (for S3/vctrs, keeps `@method`/`@export` tags)
39/// - `internal`: Replaces `@export`/`@exportMethod` with `@keywords internal`
40/// - `noexport`: Removes `@export`/`@exportMethod` entirely
41///
42/// Returns the complete R wrapper code as a string ready for embedding in a `const`.
43pub(super) fn generate_trait_r_wrapper(
44    type_ident: &syn::Ident,
45    trait_name: &syn::Ident,
46    methods: &[TraitMethod],
47    consts: &[TraitConst],
48    opts: TraitWrapperOpts,
49) -> syn::Result<String> {
50    let TraitWrapperOpts {
51        class_system,
52        class_has_no_rd,
53        internal,
54        noexport,
55        impl_preconditions,
56    } = opts;
57    reject_unsupported_describe_in(methods, class_system)?;
58    check_trait_method_formals(methods, class_system)?;
59    let generate = match class_system {
60        ClassSystem::Env => generate_trait_env_r_wrapper,
61        // vctrs uses S3 under the hood, so use the S3 trait wrapper
62        ClassSystem::S3 | ClassSystem::Vctrs => generate_trait_s3_r_wrapper,
63        ClassSystem::S4 => generate_trait_s4_r_wrapper,
64        ClassSystem::S7 => generate_trait_s7_r_wrapper,
65        ClassSystem::R6 => generate_trait_r6_r_wrapper,
66    };
67    let result = generate(type_ident, trait_name, methods, consts, impl_preconditions);
68
69    // When the impl block has @noRd, suppress documentation generation. A plain
70    // `noexport` (without `internal`) is folded into the same gate — it must
71    // produce no Rd contribution at all (no alias, no usage entry, nothing on a
72    // shared page), same as `@noRd`. `internal` wins if both flags are set on
73    // the same impl block (mirrors the standalone-fn precedent, where `internal`
74    // + `noexport` together is a compile error). See #431 for the inherent-impl
75    // S3 generator's analogous `should_register_s3method = !noexport` rule.
76    let suppress_all_rd = class_has_no_rd || (noexport && !internal);
77    if suppress_all_rd {
78        if #[allow(non_exhaustive_omitted_patterns)] match class_system {
    ClassSystem::S3 | ClassSystem::Vctrs => true,
    _ => false,
}matches!(class_system, ClassSystem::S3 | ClassSystem::Vctrs) {
79            // A user-written `@noRd` still preserves S3 dispatch registration
80            // (`@method`/`@export`) so `S3method()` lands in NAMESPACE — the
81            // class stays undocumented but dispatchable. A `noexport`-driven
82            // suppression (no explicit `@noRd`) additionally drops `@export`:
83            // `noexport` means zero observable trace, not "documented nowhere
84            // but still dispatchable".
85            let keep_export = class_has_no_rd;
86            let mut filtered = Vec::new();
87            let mut roxygen_block: Vec<&str> = Vec::new();
88
89            let flush_block = |block: &mut Vec<&str>, out: &mut Vec<String>| {
90                if block.iter().any(|line| line.contains("@method ")) {
91                    out.push("#' @noRd".to_string());
92                    for &line in block.iter() {
93                        if line.contains("@method ")
94                            || line.contains("@param ")
95                            || (keep_export && line.contains("@export"))
96                        {
97                            out.push(line.to_string());
98                        }
99                    }
100                }
101                block.clear();
102            };
103
104            for line in result.lines() {
105                if line.starts_with("#'") {
106                    roxygen_block.push(line);
107                    continue;
108                }
109
110                if !roxygen_block.is_empty() {
111                    flush_block(&mut roxygen_block, &mut filtered);
112                }
113                filtered.push(line.to_string());
114            }
115
116            if !roxygen_block.is_empty() {
117                flush_block(&mut roxygen_block, &mut filtered);
118            }
119
120            Ok(filtered.join("\n"))
121        } else {
122            Ok(result
123                .lines()
124                .filter(|line| !line.starts_with("#'"))
125                .collect::<Vec<_>>()
126                .join("\n"))
127        }
128    } else if internal {
129        // internal → documented, but @export/@exportMethod becomes @keywords
130        // internal. The bare `@export` of an S3 method block (one with
131        // `@method`) stays: it is the `S3method()` registration, which an
132        // inherent `internal` S3 impl keeps too, and without it a method on
133        // another package's generic (`summary`) dispatches only from inside
134        // the namespace.
135        let has_export = result.lines().any(|line| line.contains("@export"));
136        let lines: Vec<&str> = result.lines().collect();
137        let mut processed: Vec<String> = Vec::with_capacity(lines.len());
138        let mut start = 0;
139        while start < lines.len() {
140            let roxygen = lines[start].starts_with("#'");
141            let end = lines[start..]
142                .iter()
143                .position(|line| line.starts_with("#'") != roxygen)
144                .map_or(lines.len(), |len| start + len);
145            let block = &lines[start..end];
146            let registers_s3 = roxygen && block.iter().any(|line| line.contains("@method "));
147            for &line in block {
148                if line.contains("@export") && !(registers_s3 && line.trim_end() == "#' @export") {
149                    processed.push("#' @keywords internal".to_string());
150                } else {
151                    processed.push(line.to_string());
152                }
153            }
154            start = end;
155        }
156        // For class systems without @export (e.g., Env), insert @keywords internal
157        // before the first roxygen tag if no @export line was found to replace.
158        if !has_export && let Some(pos) = processed.iter().position(|l| l.starts_with("#'")) {
159            processed.insert(pos, "#' @keywords internal".to_string());
160        }
161        Ok(processed.join("\n"))
162    } else {
163        Ok(result)
164    }
165}
166
167// region: author page tags (#1438, #1590)
168
169/// The author tags of a trait-impl method that its own wrapper block
170/// forwards besides the `@param` lines: the tags that decide the method's
171/// page and where its arguments are documented. `@describeIn` / `@rdname`
172/// route the block, `@name` names its topic, `@order` sorts it, and
173/// `@inheritParams` / `@inherit` / `@inheritDotParams` fill its arguments.
174/// Env trait methods and the namespace statics of S3, vctrs and S7 forward
175/// every other tag but `@title` and a bare `@export` too
176/// ([`forwarded_body_tag`]); S4 statics get `@param` lines
177/// ([`param_doc_tags`]).
178const PAGE_TAGS: &[&str] = &[
179    "describeIn",
180    "rdname",
181    "name",
182    "order",
183    "inheritParams",
184    "inherit",
185    "inheritDotParams",
186];
187
188/// Whether the body of a block without `\usage` (an env trait method, a
189/// `Type$Trait$method` static of S3 and vctrs, an `attr(Type, "Trait")$method`
190/// static of S7) forwards the author tag
191/// `tag`: every tag but the [`PAGE_TAGS`], which [`own_block_page_lines`]
192/// forwards, `@title`, which the structural split-page title owns, and a bare
193/// `@export`, which would export the member itself
194/// ([`forwarded_member_tag`](crate::roxygen::forwarded_member_tag)). The
195/// `@param` lines become items of the block's `\describe{}` list.
196fn forwarded_body_tag(tag: &str) -> bool {
197    crate::roxygen::forwarded_member_tag(tag)
198        && crate::roxygen::roxygen_tag_name(tag)
199            .is_none_or(|name| name != "title" && !PAGE_TAGS.contains(&name))
200}
201
202/// The documentation body of a block without `\usage` (see
203/// [`forwarded_body_tag`]), documented like an env inherent method: the
204/// forwarded author tags, then one `\describe{}` list with the author's
205/// `@param` items and the choice text of the parameters they leave
206/// undocumented ([`describe_params_lines`](crate::r_class_formatter::describe_params_lines)).
207/// `label` is the block's topic name, used in the list's lead-in. Empty for a
208/// method with no doc comment and no choice parameter.
209fn namespace_member_body_lines(
210    ctx: &TraitMethodContext<'_>,
211    type_str: &str,
212    label: &str,
213) -> Vec<String> {
214    let choice_docs = ctx.choice_param_docs();
215    crate::r_class_formatter::describe_params_lines(
216        &ctx.method.doc_tags,
217        forwarded_body_tag,
218        &ctx.params,
219        type_str,
220        Some(&choice_docs),
221        label,
222    )
223}
224
225/// The `@param` tags of a block that documents the method's arguments in a
226/// `\usage` (an S3 / vctrs instance method, an S4 static): the author's
227/// `@param` lines, then one per formal of `ctx.params` they leave
228/// undocumented, with its choice text for a choice parameter and
229/// `(undocumented)` otherwise. A method whose arguments another block
230/// documents (an author topic, `@inheritParams`) gets no generated tag
231/// (#1590).
232fn param_doc_tags(ctx: &TraitMethodContext<'_>, type_str: &str) -> Vec<String> {
233    let method = ctx.method;
234    let mut tags: Vec<String> = param_tags(method).cloned().collect();
235    if params_documented_elsewhere(method, type_str) {
236        return tags;
237    }
238    let choice_docs = ctx.choice_param_docs();
239    for formal in crate::roxygen::split_r_formals(&ctx.params) {
240        let pname = crate::roxygen::formal_name(formal);
241        if pname == "..." || crate::roxygen::param_documented(&method.doc_tags, pname) {
242            continue;
243        }
244        let body = choice_docs
245            .get(pname)
246            .map_or("(undocumented)", String::as_str);
247        tags.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("@param {0} {1}", pname, body))
    })format!("@param {pname} {body}"));
248    }
249    tags
250}
251
252/// The `@param` lines of the method's doc comment.
253fn param_tags(method: &TraitMethod) -> impl Iterator<Item = &String> {
254    method
255        .doc_tags
256        .iter()
257        .filter(|tag| crate::roxygen::roxygen_tag_name(tag) == Some("param"))
258}
259
260/// The page a trait method's own block lands on, and with it the companion
261/// block that must share it (the S3 generic block, named after the method):
262/// the `@describeIn` destination, the method-level `@rdname`, else the type's
263/// shared page. Generics of S4 / S7 and consts always use the type page.
264fn method_page<'a>(method: &'a TraitMethod, type_str: &'a str) -> &'a str {
265    crate::roxygen::method_page(&method.doc_tags, type_str)
266}
267
268/// Whether the method's page is an author topic other than the type page
269/// (`@describeIn`, or an `@rdname` naming another page). The topic's own
270/// block documents that page, so the method's blocks sort after it
271/// (`@order NaN`) and leave the structural `@param` lines (`x`, `...`,
272/// `self`) and generated prose to it.
273fn joins_author_topic(method: &TraitMethod, type_str: &str) -> bool {
274    crate::roxygen::joins_author_topic(&method.doc_tags, Some(type_str))
275}
276
277/// Whether the method's arguments are documented by another block: it joins
278/// an author topic, or inherits them (`@inheritParams`, `@inherit`). The
279/// generated `(undocumented)` fillers are left out then.
280fn params_documented_elsewhere(method: &TraitMethod, type_str: &str) -> bool {
281    crate::roxygen::params_documented_elsewhere(&method.doc_tags, Some(type_str))
282}
283
284/// Whether the method-level `@describeIn` lists the method's own block in
285/// its destination's "Functions" section.
286fn describes_in(method: &TraitMethod) -> bool {
287    crate::roxygen::describe_in_topic(&method.doc_tags).is_some()
288}
289
290/// The page lines of a trait method's own wrapper block, as `MethodDocBuilder`
291/// writes an inherent method's (#1590):
292///
293/// - the author's [`PAGE_TAGS`], verbatim;
294/// - without an author `@describeIn`: `@name default_name` unless the author
295///   named the topic (blocks roxygen2 cannot name from their object need
296///   one), `@rdname <Type>` unless the author picked the page, and, on a page
297///   the author's `@rdname` splits off, `@title split_title` (roxygen2 skips
298///   a page without a title). With `@describeIn` roxygen2 takes the page from
299///   the destination and rejects `@name` / `@rdname` next to it, so neither
300///   is generated;
301/// - `@order NaN` on an author topic the author did not order
302///   (`roxygen::orders_after_topic_blocks`).
303fn own_block_page_lines(
304    method: &TraitMethod,
305    type_str: &str,
306    default_name: Option<&str>,
307    split_title: Option<&str>,
308) -> Vec<String> {
309    let tags = &method.doc_tags;
310    let mut lines = Vec::new();
311    if !describes_in(method) {
312        let has = |tag: &str| crate::roxygen::has_roxygen_tag(tags, tag);
313        if has("rdname")
314            && let Some(title) = split_title
315        {
316            lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @title {0}", title))
    })format!("#' @title {title}"));
317        }
318        if !has("name")
319            && let Some(name) = default_name
320        {
321            lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @name {0}", name))
    })format!("#' @name {name}"));
322        }
323        if !has("rdname") {
324            lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @rdname {0}", type_str))
    })format!("#' @rdname {type_str}"));
325        }
326    }
327    let page_tags: Vec<String> = tags
328        .iter()
329        .filter(|tag| {
330            crate::roxygen::roxygen_tag_name(tag).is_some_and(|name| PAGE_TAGS.contains(&name))
331        })
332        .cloned()
333        .collect();
334    crate::roxygen::push_roxygen_tags(&mut lines, &page_tags);
335    crate::roxygen::push_order_after_topic_blocks(&mut lines, tags, type_str);
336    lines
337}
338
339/// Push the method's `@param` lines, each continuation line with its own
340/// `#' ` lead.
341fn push_param_tags(lines: &mut Vec<String>, method: &TraitMethod) {
342    let params: Vec<String> = param_tags(method).cloned().collect();
343    crate::roxygen::push_roxygen_tags(lines, &params);
344}
345
346/// Check the R formals of every trait method
347/// ([`check_r_formals`](crate::r_wrapper_builder::check_r_formals)). Every
348/// dispatching class system defaults an instance method's receiver to `x`.
349/// S3/S4 existing-generic overrides can select another receiver. S7
350/// shortcuts and Env/R6 trait methods use the injected receiver
351/// `.mx_self`, which cannot be spelled by a Rust parameter.
352fn check_trait_method_formals(
353    methods: &[TraitMethod],
354    class_system: ClassSystem,
355) -> syn::Result<()> {
356    let system = match class_system {
357        ClassSystem::Env => "Env-class",
358        ClassSystem::R6 => "R6",
359        ClassSystem::S3 => "S3",
360        ClassSystem::S4 => "S4",
361        ClassSystem::S7 => "S7",
362        ClassSystem::Vctrs => "vctrs",
363    };
364    for method in methods {
365        if (method.generic.is_some() || method.receiver.is_some() || method.no_dispatch_dots)
366            && (!method.has_self || !#[allow(non_exhaustive_omitted_patterns)] match class_system {
    ClassSystem::S3 | ClassSystem::S4 => true,
    _ => false,
}matches!(class_system, ClassSystem::S3 | ClassSystem::S4))
367        {
368            return Err(syn::Error::new_spanned(
369                &method.sig,
370                "`generic`, `receiver` and `no_dots` apply only to S3/S4 instance methods",
371            ));
372        }
373        if (method.receiver.is_some() || method.no_dispatch_dots) && method.generic.is_none() {
374            return Err(syn::Error::new_spanned(
375                &method.sig,
376                "`receiver` and `no_dots` require an explicit S3/S4 `generic = \"...\"`",
377            ));
378        }
379        let mut reserved = Vec::new();
380        if method.has_self && !#[allow(non_exhaustive_omitted_patterns)] match class_system {
    ClassSystem::Env | ClassSystem::R6 => true,
    _ => false,
}matches!(class_system, ClassSystem::Env | ClassSystem::R6) {
381            reserved.push((
382                method.receiver.as_deref().unwrap_or("x").to_string(),
383                ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("is the receiver of the generated {0} trait method",
                system))
    })format!("is the receiver of the generated {system} trait method"),
384            ));
385        }
386        crate::r_wrapper_builder::check_r_formals(
387            &method.sig.inputs,
388            &Default::default(),
389            &reserved,
390        )?;
391    }
392    Ok(())
393}
394
395/// Reject a method-level `@describeIn` that roxygen2 cannot honour on the
396/// trait wrapper (#1590), the rule `ParsedImpl::reject_unsupported_describe_in`
397/// applies to inherent methods.
398///
399/// roxygen2 lists a `@describeIn` block in its destination's "Functions"
400/// section through the R object the block documents. The trait wrappers give
401/// the method's own block such an object for S3 and vctrs instance methods
402/// (the S3 method `generic.Type`), S4 instance methods (the block sits on
403/// `methods::setMethod()`, an S4 method object), S4 static methods (the
404/// plain function `Type_Trait_method`) and S7 instance methods (the
405/// fast-path shortcut `Type_method`). Every other method is a
406/// `Type$Trait$method` (or `.Type__Trait$method`) namespace member, whose
407/// block roxygen2 drops ("Block must have a @name"), and an S7 instance
408/// method with `s7(no_shortcut)` has no block of its own. There the tag is a
409/// compile error pointing at `@rdname`, reported for every such method at
410/// once.
411///
412/// The set is wider than the inherent one on S4 instance methods: an
413/// inherent S4 method's block sits on the `if (!exists(...))` generic guard,
414/// a trait method's on its own `setMethod()` call.
415fn reject_unsupported_describe_in(
416    methods: &[TraitMethod],
417    class_system: ClassSystem,
418) -> syn::Result<()> {
419    let system = match class_system {
420        ClassSystem::S3 => "S3",
421        ClassSystem::Vctrs => "vctrs",
422        ClassSystem::S4 => "S4",
423        ClassSystem::S7 => "S7",
424        ClassSystem::Env => "Env",
425        ClassSystem::R6 => "R6",
426    };
427    let mut errors: Option<syn::Error> = None;
428    for method in methods.iter().filter(|m| describes_in(m)) {
429        let supported = match class_system {
430            ClassSystem::S3 | ClassSystem::Vctrs => method.has_self,
431            ClassSystem::S4 => true,
432            ClassSystem::S7 => method.has_self && !method.no_shortcut,
433            ClassSystem::Env | ClassSystem::R6 => false,
434        };
435        if supported {
436            continue;
437        }
438        let (kind, qualifier) = if !method.has_self {
439            ("static method", "")
440        } else if #[allow(non_exhaustive_omitted_patterns)] match class_system {
    ClassSystem::S7 => true,
    _ => false,
}matches!(class_system, ClassSystem::S7) {
441            ("instance method", " without a fast-path shortcut")
442        } else {
443            ("instance method", "")
444        };
445        let error = syn::Error::new_spanned(
446            &method.ident,
447            ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("`@describeIn` is not supported on the {1} trait {2} `{0}`{3}: its R wrapper is not an R function roxygen2 can list in the destination\'s \"Functions\" section, so roxygen2 would drop the block. Use `@rdname <topic>` to document it on that page instead. On trait impls `@describeIn` works on S3, vctrs and S4 instance methods, on S4 static methods, and on S7 instance methods with a fast-path shortcut.",
                method.ident, system, kind, qualifier))
    })format!(
448                "`@describeIn` is not supported on the {system} trait {kind} `{}`{qualifier}: \
449                 its R wrapper is not an R function roxygen2 can list in the destination's \
450                 \"Functions\" section, so roxygen2 would drop the block. Use `@rdname <topic>` \
451                 to document it on that page instead. On trait impls `@describeIn` works on \
452                 S3, vctrs and S4 instance methods, on S4 static methods, and on S7 instance \
453                 methods with a fast-path shortcut.",
454                method.ident,
455            ),
456        );
457        match errors.as_mut() {
458            Some(all) => all.combine(error),
459            None => errors = Some(error),
460        }
461    }
462    errors.map_or(Ok(()), Err)
463}
464
465// endregion
466
467/// Generate Env-style R wrapper code for trait methods.
468///
469/// Env-class trait methods use a namespace hierarchy: `Type$Trait$method(.mx_self, ...)`.
470/// Instance methods take `.mx_self` as the first parameter (the self object) and are
471/// stamped with `.__mx_instance__` attribute for `$` dispatch detection.
472/// Void instance methods return the receiver for pipe-friendly chaining
473/// (invisibly only when marked `Invisible<..>`, #1213).
474///
475/// Static methods and constants also live under `Type$Trait$name`. A method
476/// has no `\usage`, so its block is documented like an env inherent method's
477/// ([`namespace_member_body_lines`]).
478fn generate_trait_env_r_wrapper(
479    type_ident: &syn::Ident,
480    trait_name: &syn::Ident,
481    methods: &[TraitMethod],
482    consts: &[TraitConst],
483    impl_preconditions: Option<bool>,
484) -> String {
485    use crate::r_wrapper_builder::{DotCallBuilder, RoxygenBuilder};
486
487    let mut lines = Vec::new();
488    let type_str = type_ident.to_string();
489
490    // Header comment
491    lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("# Trait methods and consts for {0} implementing {1}",
                type_ident, trait_name))
    })format!(
492        "# Trait methods and consts for {} implementing {}",
493        type_ident, trait_name
494    ));
495    lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("# Generated by #[miniextendr] impl {0} for {1}",
                trait_name, type_ident))
    })format!(
496        "# Generated by #[miniextendr] impl {} for {}",
497        trait_name, type_ident
498    ));
499    lines.push(String::new());
500
501    // Create trait namespace environment
502    lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}${1} <- new.env(parent = emptyenv())",
                type_ident, trait_name))
    })format!(
503        "{}${} <- new.env(parent = emptyenv())",
504        type_ident, trait_name
505    ));
506    lines.push(String::new());
507
508    for method in methods {
509        let r_name = method.r_method_name();
510        let ctx = TraitMethodContext::new(method, type_ident, trait_name)
511            .with_impl_preconditions(impl_preconditions);
512
513        // Trait-namespace assignment target (`Type$Trait$method`), owned by
514        // `trait_namespace_target` — see #1141. `symbol` is its R-code form.
515        let target = ctx.namespace_target(ClassSystem::Env);
516        let symbol = ctx.namespace_symbol(ClassSystem::Env);
517
518        // Roxygen: the body (prose, the `\describe{}` argument list), then
519        // the page lines, the order `MethodDocBuilder` gives an env inherent
520        // method.
521        lines.extend(namespace_member_body_lines(&ctx, &type_str, &target));
522        lines.extend(own_block_page_lines(
523            method,
524            &type_str,
525            Some(&target),
526            Some(&target),
527        ));
528
529        // Build .Call() invocation — C name uses Rust ident, R name uses r_name
530        let (full_params, call) = if method.has_self {
531            let fp = if ctx.params.is_empty() {
532                ".mx_self".to_string()
533            } else {
534                ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!(".mx_self, {0}", ctx.params))
    })format!(".mx_self, {}", ctx.params)
535            };
536            (fp, ctx.instance_call(".mx_self"))
537        } else {
538            (ctx.params.clone(), ctx.static_call())
539        };
540
541        // Generate method wrapper (R-facing name)
542        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} <- function({1}) {{", symbol,
                full_params))
    })format!("{symbol} <- function({full_params}) {{"));
543        ctx.emit_method_prelude(&mut lines, "  ", &r_name);
544        lines.extend(ctx.method_body_lines(&call, ClassSystem::Env));
545        if method.has_self {
546            finish_instance_body(&mut lines, method, ".mx_self");
547        }
548        lines.push("}".to_string());
549
550        // Stamp instance methods with attribute for $ dispatch detection
551        if method.has_self {
552            lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("attr({0}, \".__mx_instance__\") <- TRUE",
                symbol))
    })format!("attr({symbol}, \".__mx_instance__\") <- TRUE"));
553        }
554
555        lines.push(String::new());
556    }
557
558    // Generate const wrappers
559    for trait_const in consts {
560        let const_name = &trait_const.ident;
561        let const_str = const_name.to_string();
562        let target = trait_namespace_target(ClassSystem::Env, type_ident, trait_name, &const_str);
563
564        // Build roxygen tags
565        let roxygen = RoxygenBuilder::new()
566            .name(target.clone())
567            .rdname(&type_str)
568            .build();
569        lines.extend(roxygen);
570
571        // Build .Call() invocation
572        let c_ident = trait_const.c_wrapper_ident_string(type_ident, trait_name);
573        let call = DotCallBuilder::new(&c_ident).build();
574
575        // Generate const wrapper
576        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} <- function() {{", target))
    })format!("{target} <- function() {{"));
577        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("  {0}", call))
    })format!("  {}", call));
578        lines.push("}".to_string());
579        lines.push(String::new());
580    }
581
582    lines.join("\n")
583}
584
585/// Generate S3-style R wrapper code (generic + method.Type).
586///
587/// For `impl Counter for SimpleCounter`, generates:
588/// - S3 generic `value(x, ...)` (if not already defined)
589/// - S3 method `value.SimpleCounter <- function(x, ...) { .Call(...) }`
590/// - S7 method registration if the generic is an S7 generic
591///
592/// Static methods and constants use `Type$Trait$name` namespace (env-style).
593/// Void instance methods return the receiver `x` for pipe-friendly chaining
594/// (invisibly only when marked `Invisible<..>`, #1213).
595///
596/// Also used for `ClassSystem::Vctrs` since vctrs uses S3 under the hood.
597fn generate_trait_s3_r_wrapper(
598    type_ident: &syn::Ident,
599    trait_name: &syn::Ident,
600    methods: &[TraitMethod],
601    consts: &[TraitConst],
602    impl_preconditions: Option<bool>,
603) -> String {
604    use crate::r_wrapper_builder::{DotCallBuilder, RoxygenBuilder};
605
606    let mut lines = Vec::new();
607    let type_str = type_ident.to_string();
608
609    // Header comment
610    lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("# S3 trait methods for {0} implementing {1}",
                type_ident, trait_name))
    })format!(
611        "# S3 trait methods for {} implementing {}",
612        type_ident, trait_name
613    ));
614    lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("# Generated by #[miniextendr(s3)] impl {0} for {1}",
                trait_name, type_ident))
    })format!(
615        "# Generated by #[miniextendr(s3)] impl {} for {}",
616        trait_name, type_ident
617    ));
618    lines.push(String::new());
619
620    // Separate instance methods (S3 dispatch) from static methods (namespace access)
621    let instance_methods: Vec<_> = methods.iter().filter(|m| m.has_self).collect();
622    let static_methods: Vec<_> = methods.iter().filter(|m| !m.has_self).collect();
623
624    // Generate S3 generics + methods for instance methods. Their conditions
625    // report the generic's call, resolved from the method frame on a raise
626    // (#1851).
627    for method in &instance_methods {
628        let qualified_generic = method
629            .generic
630            .clone()
631            .unwrap_or_else(|| method.r_method_name());
632        let (generic_package, generic_name) = crate::naming::split_generic(&qualified_generic);
633        let receiver = method.receiver.as_deref().unwrap_or("x");
634        let s3_method_name = ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}.{1}", generic_name, type_str))
    })format!("{}.{}", generic_name, type_str);
635        let ctx = TraitMethodContext::new(method, type_ident, trait_name)
636            .with_impl_preconditions(impl_preconditions)
637            .with_call_attribution(crate::r_wrapper_builder::CallAttribution::Generic);
638
639        if method.generic.is_none() {
640            // S3 generic roxygen (only create if doesn't exist). The type-qualified
641            // @name avoids duplicate aliases across types, but it is also the S3
642            // method's own name, so the generic block must follow a method-level
643            // `@rdname` / `@describeIn` onto the method's page or R CMD check
644            // reports the alias as duplicated across two pages. The `@title` is
645            // inert on the type page (the type block's title comes first) and
646            // would beat the method's structural title on a split page, so it is
647            // only emitted when the block stays on the type page. The prose is a
648            // `@description`: a bare line here continues the tag before it
649            // (`@rdname` is single-line and roxygen2 skips the page; #1552
650            // surfaced this once `@source` stopped absorbing it).
651            let tags = &method.doc_tags;
652            let generic_roxygen = RoxygenBuilder::new();
653            let generic_roxygen =
654                if crate::roxygen::has_roxygen_tag(tags, "rdname") || describes_in(method) {
655                    generic_roxygen
656                } else {
657                    generic_roxygen.title(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("S3 generic for `{0}`",
                generic_name))
    })format!("S3 generic for `{}`", generic_name))
658                };
659            let generic_roxygen = generic_roxygen
660                .name(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}.{1}", generic_name, type_str))
    })format!("{}.{}", generic_name, type_str))
661                .rdname(method_page(method, &type_str));
662            // On an author topic its own block describes the method and documents
663            // `x` and `...` (#1590); the structural lines would only compete.
664            let generic_roxygen = if joins_author_topic(method, &type_str) {
665                generic_roxygen
666            } else {
667                generic_roxygen
668                    .description(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("S3 generic for `{0}`",
                generic_name))
    })format!("S3 generic for `{}`", generic_name))
669                    .custom("@param x An object")
670                    .custom("@param ... Additional arguments passed to methods")
671            };
672            let generic_roxygen = if crate::roxygen::orders_after_topic_blocks(tags, &type_str) {
673                generic_roxygen.custom(crate::roxygen::ORDER_AFTER_TOPIC_BLOCKS)
674            } else {
675                generic_roxygen
676            };
677            let generic_roxygen = generic_roxygen
678                .source(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("Generated by miniextendr from `impl {0} for {1}`",
                trait_name, type_ident))
    })format!(
679                    "Generated by miniextendr from `impl {} for {}`",
680                    trait_name, type_ident
681                ))
682                // Explicit target: roxygen2 can't see the generic inside the
683                // `if (!exists(...))` guard and would attach a bare `@export` to
684                // the next function, exporting the method `generic.Type` as a plain
685                // function and leaving the generic unexported (see s3_class.rs).
686                .custom(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("@export {0}", generic_name))
    })format!("@export {}", generic_name))
687                .build();
688            lines.extend(generic_roxygen);
689
690            // S3 generic definition
691            lines.push(emit_s3_generic_guard(generic_name));
692            lines.push(String::new());
693        }
694
695        // S3 method: generic.class
696        let full_params = crate::r_class_formatter::instance_formals(
697            &ctx.params,
698            receiver,
699            !method.no_dispatch_dots,
700        );
701
702        // S3 method roxygen: the method's own @param tags, then a filler for
703        // every other formal in its `\usage` (`x` and `...` are documented by
704        // the generic block above), like an inherent S3 method's
705        // `MethodDocBuilder`: choice text for a `choices(...)` param,
706        // `(undocumented)` otherwise. A method whose arguments another block
707        // documents (an author topic, `@inheritParams`) gets no filler
708        // (#1590). roxygen2 names the block from the S3 method object, so it
709        // needs no generated `@name`.
710        lines.extend(own_block_page_lines(
711            method,
712            &type_str,
713            None,
714            Some(&s3_method_name),
715        ));
716        // A method on `pkg::generic` registers through `@exportS3Method`, so
717        // `pkg` may stay in Suggests (#2154).
718        let mut method_roxygen = match generic_package {
719            Some(package) => RoxygenBuilder::new().custom(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("@exportS3Method {0}::{1} {2}",
                package, generic_name, type_str))
    })format!(
720                "@exportS3Method {package}::{generic_name} {type_str}"
721            )),
722            None => RoxygenBuilder::new().export(),
723        }
724        .method(generic_name, &type_str);
725        if method.generic.is_some() && !joins_author_topic(method, &type_str) {
726            method_roxygen = method_roxygen.custom(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("@param {0} An object", receiver))
    })format!("@param {receiver} An object"));
727            if !method.no_dispatch_dots {
728                method_roxygen =
729                    method_roxygen.custom("@param ... Additional arguments passed to methods");
730            }
731        }
732        for tag in param_doc_tags(&ctx, &type_str) {
733            method_roxygen = method_roxygen.custom(tag);
734        }
735        lines.extend(method_roxygen.build());
736
737        // Build .Call() invocation
738        let call = ctx.instance_call(receiver);
739
740        // Always define the S3 method (roxygen expects it for NAMESPACE export)
741        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} <- function({1}) {{",
                crate::naming::r_def_name(&s3_method_name), full_params))
    })format!(
742            "{} <- function({}) {{",
743            crate::naming::r_def_name(&s3_method_name),
744            full_params
745        ));
746        ctx.emit_method_prelude(&mut lines, "  ", generic_name);
747        lines.extend(ctx.method_body_lines(&call, ClassSystem::S3));
748        finish_instance_body(&mut lines, method, receiver);
749        lines.push("}".to_string());
750
751        // Additionally register as S7 method if the generic is S7
752        // This ensures S7 dispatch works when the generic was defined by an S7 class.
753        // A `pkg::generic` belongs to another package, which may not be
754        // installed at build time, so it is registered as an S3 method only.
755        if generic_package.is_none() {
756            lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("if (inherits(get0(\"{0}\", mode = \"function\"), \"S7_generic\")) {{",
                generic_name))
    })format!(
757                "if (inherits(get0(\"{generic_name}\", mode = \"function\"), \"S7_generic\")) {{"
758            ));
759            // Both sides of the registration are R symbols: quote operator names
760            // such as `[` so the emitted line parses (#1475).
761            let generic_symbol = crate::naming::r_def_name(generic_name);
762            let method_symbol = crate::naming::r_def_name(&s3_method_name);
763            lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("  S7::method({0}, S7::new_S3_class(\"{1}\")) <- {2}",
                generic_symbol, type_str, method_symbol))
    })format!(
764                "  S7::method({generic_symbol}, S7::new_S3_class(\"{type_str}\")) <- {method_symbol}"
765            ));
766            lines.push("}".to_string());
767        }
768        lines.push(String::new());
769    }
770
771    // Create trait namespace for static methods and consts BEFORE assigning to it
772    if !static_methods.is_empty() || !consts.is_empty() {
773        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}${1} <- new.env(parent = emptyenv())",
                type_ident, trait_name))
    })format!(
774            "{}${} <- new.env(parent = emptyenv())",
775            type_ident, trait_name
776        ));
777        lines.push(String::new());
778    }
779
780    // Generate static methods in Type$Trait$ namespace
781    for method in &static_methods {
782        let r_name = method.r_method_name();
783        let ctx = TraitMethodContext::new(method, type_ident, trait_name)
784            .with_impl_preconditions(impl_preconditions);
785        let target = ctx.namespace_target(ClassSystem::S3);
786
787        // Static method roxygen: the intro line (the block's title), the
788        // body of a block without `\usage`, the page lines.
789        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' Static trait method {0}::{1}()",
                trait_name, r_name))
    })format!(
790            "#' Static trait method {}::{}()",
791            trait_name, r_name
792        ));
793        lines.extend(namespace_member_body_lines(&ctx, &type_str, &target));
794        lines.extend(own_block_page_lines(method, &type_str, Some(&target), None));
795
796        let call = ctx.static_call();
797
798        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} <- function({1}) {{",
                ctx.namespace_symbol(ClassSystem::S3), ctx.params))
    })format!(
799            "{} <- function({}) {{",
800            ctx.namespace_symbol(ClassSystem::S3),
801            ctx.params
802        ));
803        ctx.emit_method_prelude(&mut lines, "  ", &r_name);
804        lines.extend(ctx.method_body_lines(&call, ClassSystem::S3));
805        lines.push("}".to_string());
806        lines.push(String::new());
807    }
808
809    // Generate const wrappers in Type$Trait$ namespace
810    for trait_const in consts {
811        let const_name = &trait_const.ident;
812        let const_str = const_name.to_string();
813        let target = trait_namespace_target(ClassSystem::S3, type_ident, trait_name, &const_str);
814
815        let roxygen = RoxygenBuilder::new()
816            .name(target.clone())
817            .rdname(&type_str)
818            .build();
819        lines.extend(roxygen);
820
821        let c_ident = trait_const.c_wrapper_ident_string(type_ident, trait_name);
822        let call = DotCallBuilder::new(&c_ident).build();
823
824        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} <- function() {{", target))
    })format!("{target} <- function() {{"));
825        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("  {0}", call))
    })format!("  {}", call));
826        lines.push("}".to_string());
827        lines.push(String::new());
828    }
829
830    lines.join("\n")
831}
832
833/// Generate S4-style R wrapper code.
834///
835/// For `impl Counter for SimpleCounter`, generates:
836/// - `setOldClass("SimpleCounter")` to register the S3 class for S4 dispatch
837/// - S4 generic `s4_trait_Counter_value(x, ...)` via `setGeneric()`
838/// - S4 method via `setMethod("s4_trait_Counter_value", "SimpleCounter", ...)`
839///
840/// Generic names are prefixed with `s4_trait_{Trait}_` to avoid collisions
841/// with user-defined S4 generics. Static methods and constants are generated
842/// as standalone exported functions: `{Type}_{Trait}_{method}()`.
843fn generate_trait_s4_r_wrapper(
844    type_ident: &syn::Ident,
845    trait_name: &syn::Ident,
846    methods: &[TraitMethod],
847    consts: &[TraitConst],
848    impl_preconditions: Option<bool>,
849) -> String {
850    use crate::r_wrapper_builder::{DotCallBuilder, RoxygenBuilder};
851
852    let mut lines = Vec::new();
853    let type_str = type_ident.to_string();
854
855    // Header comment
856    lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("# S4 trait methods for {0} implementing {1}",
                type_ident, trait_name))
    })format!(
857        "# S4 trait methods for {} implementing {}",
858        type_ident, trait_name
859    ));
860    lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("# Generated by #[miniextendr(s4)] impl {0} for {1}",
                trait_name, type_ident))
    })format!(
861        "# Generated by #[miniextendr(s4)] impl {} for {}",
862        trait_name, type_ident
863    ));
864    lines.push(String::new());
865
866    // Separate instance methods from static methods
867    let instance_methods: Vec<_> = methods.iter().filter(|m| m.has_self).collect();
868    let static_methods: Vec<_> = methods.iter().filter(|m| !m.has_self).collect();
869
870    // NOTE: We do NOT call setOldClass here. The inherent impl's class registration
871    // (setClass for S4, or setOldClass for S3/env) takes care of that. Calling
872    // setOldClass here would clobber a proper S4 setClass with slots.
873    //
874    // The import joins the next roxygen block (a blank line does not end
875    // one), the first generic's, which opens with a tag. Only the instance
876    // methods need it: before a static's untagged intro line it would read
877    // that line as a continuation of `@importFrom`.
878    if !instance_methods.is_empty() {
879        lines.push("#' @importFrom methods setGeneric setMethod".to_string());
880        lines.push(String::new());
881    }
882
883    // Generate S4 generics + methods for instance methods
884    for method in &instance_methods {
885        let generic_name = method
886            .generic
887            .clone()
888            .unwrap_or_else(|| ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("s4_trait_{0}_{1}", trait_name,
                method.r_method_name()))
    })format!("s4_trait_{}_{}", trait_name, method.r_method_name()));
889        let receiver = method.receiver.as_deref().unwrap_or("x");
890        let ctx = TraitMethodContext::new(method, type_ident, trait_name)
891            .with_impl_preconditions(impl_preconditions);
892
893        // Build full parameter list (x first, then others, then ...)
894        let full_params = crate::r_class_formatter::instance_formals(
895            &ctx.params,
896            receiver,
897            !method.no_dispatch_dots,
898        );
899
900        if method.generic.is_none() {
901            // S4 generic roxygen (include @param tags from method doc comments)
902            // S4 generic names are already type-qualified (s4_trait_TypeName_method)
903            // so @name won't create duplicate aliases across types. The prose is a
904            // `@description`, not a bare line (see the S3 generic above).
905            let mut generic_roxygen = RoxygenBuilder::new()
906                .description(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("S4 generic for trait method `{0}::{1}`",
                trait_name, method.r_method_name()))
    })format!(
907                    "S4 generic for trait method `{}::{}`",
908                    trait_name,
909                    method.r_method_name()
910                ))
911                .name(&generic_name)
912                .rdname(&type_str)
913                .source(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("Generated by miniextendr from `impl {0} for {1}`",
                trait_name, type_ident))
    })format!(
914                    "Generated by miniextendr from `impl {} for {}`",
915                    trait_name, type_ident
916                ))
917                .custom(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("@param x A `{0}` object",
                type_str))
    })format!("@param x A `{}` object", type_str))
918                .custom("@param ... Additional arguments passed to methods");
919            for tag in param_tags(method) {
920                generic_roxygen = generic_roxygen.custom(tag.clone());
921            }
922            lines.extend(generic_roxygen.export().build());
923        }
924        // Define the generic unless it exists; an explicit `generic` may be
925        // another package's. Also ends the `@importFrom` block above.
926        lines.push(crate::miniextendr_impl::s4_class::s4_generic_guard(
927            &generic_name,
928            method.generic.is_some(),
929            receiver,
930        ));
931        lines.push(String::new());
932
933        // S4 method roxygen + definition (include @param tags from method doc
934        // comments). The block sits on `methods::setMethod()`: roxygen2 names
935        // it from the S4 method object, and a split page (#1438) still needs
936        // a structural title or roxygen2 skips it.
937        lines.extend(own_block_page_lines(
938            method,
939            &type_str,
940            None,
941            Some(&generic_name),
942        ));
943        if method.generic.is_some() && !joins_author_topic(method, &type_str) {
944            lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @param {0} A `{1}` object",
                receiver, type_str))
    })format!("#' @param {receiver} A `{type_str}` object"));
945            if !method.no_dispatch_dots {
946                lines.push("#' @param ... Additional arguments passed to methods".to_string());
947            }
948        }
949        push_param_tags(&mut lines, method);
950        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @exportMethod {0}",
                generic_name))
    })format!("#' @exportMethod {}", generic_name));
951
952        let signature = if method.receiver.is_some() {
953            ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("c({0} = \"{1}\")", receiver,
                type_str))
    })format!("c({receiver} = \"{type_str}\")")
954        } else {
955            ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("\"{0}\"", type_str))
    })format!("\"{type_str}\"")
956        };
957        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("methods::setMethod(\"{0}\", {1}, function({2}) {{",
                generic_name, signature, full_params))
    })format!(
958            "methods::setMethod(\"{generic_name}\", {signature}, function({full_params}) {{"
959        ));
960        // S4 objects store the ExternalPtr in x@ptr — extract it for .Call()
961        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("  .ptr <- {0}@ptr", receiver))
    })format!("  .ptr <- {receiver}@ptr"));
962        let s4_call = ctx.instance_call(".ptr");
963        ctx.emit_method_prelude(&mut lines, "  ", &method.r_method_name());
964        lines.extend(ctx.method_body_lines(&s4_call, ClassSystem::S4));
965        finish_instance_body(&mut lines, method, receiver);
966        lines.push("})".to_string());
967        lines.push(String::new());
968    }
969
970    // Generate static methods as standalone functions. S4 objects intercept
971    // `$<-`, so these use the flat, class-qualified `Type_Trait_method` name
972    // (owned by `trait_namespace_target`) rather than `Type$Trait$method`.
973    for method in &static_methods {
974        let r_name = method.r_method_name();
975        let ctx = TraitMethodContext::new(method, type_ident, trait_name)
976            .with_impl_preconditions(impl_preconditions);
977        let fn_name = ctx.namespace_target(ClassSystem::S4);
978
979        // Static method roxygen. A `@describeIn` block is listed in its
980        // destination's "Functions" section, whose own block titles the page,
981        // so it carries no intro line that could title it instead.
982        if !describes_in(method) {
983            lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' Static trait method {0}::{1}() for {2}",
                trait_name, r_name, type_str))
    })format!(
984                "#' Static trait method {}::{}() for {}",
985                trait_name, r_name, type_str
986            ));
987        }
988        lines.extend(own_block_page_lines(
989            method,
990            &type_str,
991            Some(&fn_name),
992            None,
993        ));
994        // The block documents the plain function `Type_Trait_method`, whose
995        // `\usage` lists every formal: `R CMD check` codoc needs each one
996        // documented.
997        crate::roxygen::push_roxygen_tags(&mut lines, &param_doc_tags(&ctx, &type_str));
998        lines.push("#' @export".to_string());
999
1000        let call = ctx.static_call();
1001
1002        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} <- function({1}) {{",
                ctx.namespace_symbol(ClassSystem::S4), ctx.params))
    })format!(
1003            "{} <- function({}) {{",
1004            ctx.namespace_symbol(ClassSystem::S4),
1005            ctx.params
1006        ));
1007        ctx.emit_method_prelude(&mut lines, "  ", &r_name);
1008        lines.extend(ctx.method_body_lines(&call, ClassSystem::S4));
1009        lines.push("}".to_string());
1010        lines.push(String::new());
1011    }
1012
1013    // Generate const wrappers as standalone functions (flat name, as above)
1014    for trait_const in consts {
1015        let const_name = &trait_const.ident;
1016        let const_str = const_name.to_string();
1017        let fn_name = trait_namespace_target(ClassSystem::S4, type_ident, trait_name, &const_str);
1018
1019        let roxygen = RoxygenBuilder::new()
1020            .name(&fn_name)
1021            .rdname(&type_str)
1022            .export()
1023            .build();
1024        lines.extend(roxygen);
1025
1026        let c_ident = trait_const.c_wrapper_ident_string(type_ident, trait_name);
1027        let call = DotCallBuilder::new(&c_ident).build();
1028
1029        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} <- function() {{", fn_name))
    })format!("{} <- function() {{", fn_name));
1030        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("  {0}", call))
    })format!("  {}", call));
1031        lines.push("}".to_string());
1032        lines.push(String::new());
1033    }
1034
1035    lines.join("\n")
1036}
1037
1038/// Generate S7-style R wrapper code.
1039///
1040/// For `impl Counter for SimpleCounter`, generates:
1041/// - S7 S3-class wrapper: `.s7_class_SimpleCounter <- S7::new_S3_class("SimpleCounter")`
1042/// - S7 generic: `s7_trait_Counter_value <- S7::new_generic(...)` (if not exists)
1043/// - S7 method registration: `S7::method(s7_trait_Counter_value, .s7_class_SimpleCounter) <- ...`
1044///
1045/// Generic names are prefixed with `s7_trait_{Trait}_` to avoid collisions.
1046/// Static methods and constants live in an env attached to the class object,
1047/// read as `attr(Type, "Trait")$name` ([`s7_trait_member_access`]).
1048fn generate_trait_s7_r_wrapper(
1049    type_ident: &syn::Ident,
1050    trait_name: &syn::Ident,
1051    methods: &[TraitMethod],
1052    consts: &[TraitConst],
1053    impl_preconditions: Option<bool>,
1054) -> String {
1055    use crate::r_wrapper_builder::{DotCallBuilder, RoxygenBuilder};
1056
1057    let mut lines = Vec::new();
1058    let type_str = type_ident.to_string();
1059    let s7_class_var = ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!(".s7_class_{0}", type_str))
    })format!(".s7_class_{}", type_str);
1060
1061    // Header comment
1062    lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("# S7 trait methods for {0} implementing {1}",
                type_ident, trait_name))
    })format!(
1063        "# S7 trait methods for {} implementing {}",
1064        type_ident, trait_name
1065    ));
1066    lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("# Generated by #[miniextendr(s7)] impl {0} for {1}",
                trait_name, type_ident))
    })format!(
1067        "# Generated by #[miniextendr(s7)] impl {} for {}",
1068        trait_name, type_ident
1069    ));
1070    lines.push(String::new());
1071
1072    // Use the S7 class object directly for method dispatch.
1073    // new_S3_class("Foo") creates a descriptor for "Foo" but S7 new_class
1074    // creates instances with the namespaced class "pkg::Foo", so new_S3_class
1075    // wouldn't match. Using the class object directly works correctly.
1076    lines.push("#' @importFrom S7 new_generic method S7_dispatch".to_string());
1077    lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} <- {1}", s7_class_var,
                type_str))
    })format!("{} <- {}", s7_class_var, type_str));
1078    lines.push(String::new());
1079
1080    // Separate instance methods from static methods
1081    let instance_methods: Vec<_> = methods.iter().filter(|m| m.has_self).collect();
1082    let static_methods: Vec<_> = methods.iter().filter(|m| !m.has_self).collect();
1083
1084    // Generate S7 generics + methods for instance methods
1085    for method in &instance_methods {
1086        let generic_name = ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("s7_trait_{0}_{1}", trait_name,
                method.r_method_name()))
    })format!("s7_trait_{}_{}", trait_name, method.r_method_name());
1087        let ctx = TraitMethodContext::new(method, type_ident, trait_name)
1088            .with_impl_preconditions(impl_preconditions);
1089
1090        // Build full parameter list (x first, then others, then ...)
1091        let full_params = if ctx.params.is_empty() {
1092            "x, ...".to_string()
1093        } else {
1094            ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("x, {0}, ...", ctx.params))
    })format!("x, {}, ...", ctx.params)
1095        };
1096
1097        // S7 generic roxygen
1098        // Note: Don't include method-specific @param tags here since S7 methods
1099        // are assignments and won't appear in \usage, which would cause warnings
1100        // S7 generic names are already type-qualified so @name won't duplicate.
1101        // The prose is a `@description`, not a bare line (see the S3 generic).
1102        let generic_roxygen = RoxygenBuilder::new()
1103            .description(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("S7 generic for trait method `{0}::{1}`",
                trait_name, method.r_method_name()))
    })format!(
1104                "S7 generic for trait method `{}::{}`",
1105                trait_name,
1106                method.r_method_name()
1107            ))
1108            .name(&generic_name)
1109            .rdname(&type_str)
1110            .source(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("Generated by miniextendr from `impl {0} for {1}`",
                trait_name, type_ident))
    })format!(
1111                "Generated by miniextendr from `impl {} for {}`",
1112                trait_name, type_ident
1113            ))
1114            .export()
1115            .build();
1116        lines.extend(generic_roxygen);
1117
1118        // S7 generic definition. An operator `r_name` makes the prefixed
1119        // generic non-syntactic (`s7_trait_Ops_[[`), so symbol positions are
1120        // backtick-quoted (#1475); string literals keep the bare name.
1121        let generic_symbol = crate::naming::r_def_name(&generic_name);
1122        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("if (!exists(\"{0}\", mode = \"function\")) {{",
                generic_name))
    })format!(
1123            "if (!exists(\"{generic_name}\", mode = \"function\")) {{"
1124        ));
1125        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("  {0} <- S7::new_generic(\"{1}\", \"x\", function(x, ...) S7::S7_dispatch())",
                generic_symbol, generic_name))
    })format!(
1126            "  {generic_symbol} <- S7::new_generic(\"{generic_name}\", \"x\", function(x, ...) S7::S7_dispatch())"
1127        ));
1128        lines.push("}".to_string());
1129        lines.push(String::new());
1130
1131        // S7 method definition
1132        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("S7::method({0}, {1}) <- function({2}) {{",
                generic_symbol, s7_class_var, full_params))
    })format!(
1133            "S7::method({}, {}) <- function({}) {{",
1134            generic_symbol, s7_class_var, full_params
1135        ));
1136        // S7 objects store the ExternalPtr in x@.ptr — extract it for .Call()
1137        lines.push("  .ptr <- x@.ptr".to_string());
1138        let s7_call = ctx.instance_call(".ptr");
1139        ctx.emit_method_prelude(&mut lines, "  ", &method.r_method_name());
1140        lines.extend(ctx.method_body_lines(&s7_call, ClassSystem::S7));
1141        finish_instance_body(&mut lines, method, "x");
1142        lines.push("}".to_string());
1143        lines.push(String::new());
1144
1145        // Per-class fast-path dispatch shortcut (#987).
1146        //
1147        // Mirror the inherent-impl S7 shortcut (#982, see
1148        // `miniextendr_impl::s7_class`): alongside the trait generic, emit a
1149        // plain `<ClassName>_<method>(self, ...)` function that calls `.Call`
1150        // directly, bypassing `S7::S7_dispatch()`. The receiver is `.mx_self`
1151        // here (the generic names it `x`) and wired through `self@.ptr`.
1152        // `s7(no_shortcut)` opts a method out, and an operator `r_name` gets no
1153        // shortcut (see `s7_class::s7_shortcut_name`).
1154        let shortcut_name = if method.no_shortcut {
1155            None
1156        } else {
1157            crate::miniextendr_impl::s7_class::s7_shortcut_name(&type_str, &method.r_method_name())
1158        };
1159        if let Some(shortcut_name) = shortcut_name {
1160            let shortcut_formals = if ctx.params.is_empty() {
1161                ".mx_self, ...".to_string()
1162            } else {
1163                ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!(".mx_self, {0}, ...", ctx.params))
    })format!(".mx_self, {}, ...", ctx.params)
1164            };
1165            let shortcut_call = ctx.instance_call(".mx_self@.ptr");
1166
1167            // Roxygen: shared advisory prose + scaffolding. The shared @rdname
1168            // page (type_str) already carries the method's prose via the generic
1169            // block above, so only document `.mx_self` + each formal here to keep the
1170            // shortcut's \usage fully covered (no "undocumented argument" warning).
1171            //
1172            // A method-level `@rdname` naming another page or a `@describeIn`
1173            // sends the shortcut to a topic whose own block documents its
1174            // arguments (#1590): only the type page needs the structural `.mx_self` /
1175            // `...` lines, and the advisory's description would be a paragraph of
1176            // the topic's description that names no function. Its first line
1177            // stays as the block's title, which a page of the shortcut's own
1178            // (`@rdname` naming a new page) needs.
1179            let joined = joins_author_topic(method, &type_str);
1180            let advisory = crate::miniextendr_impl::s7_class::shortcut_advisory_lines(
1181                &method.r_method_name(),
1182                &type_str,
1183            );
1184            let advisory_len = if joined { 1 } else { advisory.len() };
1185            lines.extend(advisory.into_iter().take(advisory_len));
1186            if !joined {
1187                lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @param .mx_self A `{0}` object.",
                type_str))
    })format!("#' @param .mx_self A `{}` object.", type_str));
1188            }
1189            push_param_tags(&mut lines, method);
1190            // Auto-document any formal lacking an explicit @param tag. `...` is
1191            // included so roxygen2 covers it (otherwise R CMD check warns about
1192            // an undocumented argument). Split on top-level commas only — a
1193            // naive `split(", ")` breaks a `mode = c("fast", "slow")` default
1194            // into a bogus `"slow")` formal (undocumented-argument warning). A
1195            // method that inherits its arguments (`@inheritParams`) gets no
1196            // filler, which would block the inheritance. A `choices(...)`
1197            // param's filler is its choice text.
1198            let fillers = !params_documented_elsewhere(method, &type_str);
1199            let choice_docs = ctx.choice_param_docs();
1200            for formal in crate::roxygen::split_r_formals(&shortcut_formals) {
1201                let pname = crate::roxygen::formal_name(formal);
1202                if joined || pname == ".mx_self" {
1203                    continue;
1204                }
1205                let documented = crate::roxygen::param_documented(&method.doc_tags, pname);
1206                if documented {
1207                    continue;
1208                }
1209                if pname == "..." {
1210                    lines.push(
1211                        "#' @param ... Additional arguments; ignored by the fast-path shortcut."
1212                            .to_string(),
1213                    );
1214                } else if fillers {
1215                    let body = choice_docs
1216                        .get(pname)
1217                        .map_or("(undocumented)", String::as_str);
1218                    lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @param {0} {1}", pname, body))
    })format!("#' @param {pname} {body}"));
1219                }
1220            }
1221            lines.extend(own_block_page_lines(
1222                method,
1223                &type_str,
1224                Some(&shortcut_name),
1225                None,
1226            ));
1227            lines.extend(crate::roxygen::source_tag(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("Generated by miniextendr from `impl {0} for {1}` (`{2}` shortcut)",
                trait_name, type_ident, method.r_method_name()))
    })format!(
1228                "Generated by miniextendr from `impl {} for {}` (`{}` shortcut)",
1229                trait_name,
1230                type_ident,
1231                method.r_method_name()
1232            )));
1233            lines.push("#' @export".to_string());
1234
1235            lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} <- function({1}) {{",
                shortcut_name, shortcut_formals))
    })format!(
1236                "{} <- function({}) {{",
1237                shortcut_name, shortcut_formals
1238            ));
1239            ctx.emit_method_prelude(&mut lines, "  ", &method.r_method_name());
1240            lines.extend(ctx.method_body_lines(&shortcut_call, ClassSystem::S7));
1241            finish_instance_body(&mut lines, method, ".mx_self");
1242            lines.push("}".to_string());
1243            lines.push(String::new());
1244        }
1245    }
1246
1247    // Create trait namespace for static methods and consts.
1248    // For S7 classes, use a local variable + attr() to avoid S7's $<- interception.
1249    let trait_env_var = trait_namespace_env_var(type_ident, trait_name);
1250    if !static_methods.is_empty() || !consts.is_empty() {
1251        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} <- new.env(parent = emptyenv())",
                trait_env_var))
    })format!("{} <- new.env(parent = emptyenv())", trait_env_var));
1252        lines.push(String::new());
1253    }
1254
1255    // Generate static methods in trait namespace. The wrapper is *assigned*
1256    // into the local env (`trait_namespace_target(S7, ..)` = `.Type__Trait$m`),
1257    // but its documented `@name` is the call-site form `attr(Type, "Trait")$m`
1258    // (`s7_trait_member_access`): S7's `$` stops on the class object.
1259    for method in &static_methods {
1260        let r_name = method.r_method_name();
1261        let ctx = TraitMethodContext::new(method, type_ident, trait_name)
1262            .with_impl_preconditions(impl_preconditions);
1263
1264        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' Static trait method {0}::{1}()",
                trait_name, r_name))
    })format!(
1265            "#' Static trait method {}::{}()",
1266            trait_name, r_name
1267        ));
1268        let name = s7_trait_member_access(type_ident, trait_name, &r_name);
1269        lines.extend(namespace_member_body_lines(&ctx, &type_str, &name));
1270        lines.extend(own_block_page_lines(method, &type_str, Some(&name), None));
1271
1272        let call = ctx.static_call();
1273
1274        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} <- function({1}) {{",
                ctx.namespace_symbol(ClassSystem::S7), ctx.params))
    })format!(
1275            "{} <- function({}) {{",
1276            ctx.namespace_symbol(ClassSystem::S7),
1277            ctx.params
1278        ));
1279        ctx.emit_method_prelude(&mut lines, "  ", &r_name);
1280        lines.extend(ctx.method_body_lines(&call, ClassSystem::S7));
1281        lines.push("}".to_string());
1282        lines.push(String::new());
1283    }
1284
1285    // Generate const wrappers in trait namespace (assigned into `.Type__Trait`,
1286    // documented as `attr(Type, "Trait")$const`, see the static-method note above).
1287    for trait_const in consts {
1288        let const_name = &trait_const.ident;
1289        let const_str = const_name.to_string();
1290
1291        let roxygen = RoxygenBuilder::new()
1292            .name(s7_trait_member_access(type_ident, trait_name, &const_str))
1293            .rdname(&type_str)
1294            .build();
1295        lines.extend(roxygen);
1296
1297        let c_ident = trait_const.c_wrapper_ident_string(type_ident, trait_name);
1298        let call = DotCallBuilder::new(&c_ident).build();
1299
1300        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} <- function() {{",
                trait_namespace_target(ClassSystem::S7, type_ident,
                    trait_name, &const_str)))
    })format!(
1301            "{} <- function() {{",
1302            trait_namespace_target(ClassSystem::S7, type_ident, trait_name, &const_str)
1303        ));
1304        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("  {0}", call))
    })format!("  {}", call));
1305        lines.push("}".to_string());
1306        lines.push(String::new());
1307    }
1308
1309    // Attach the trait env to the S7 class via attr() to bypass S7's $<- interception.
1310    // S7's `$` stops on the class object too, so callers read it back the same way:
1311    // attr(Type, "Trait")$method.
1312    if !static_methods.is_empty() || !consts.is_empty() {
1313        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("attr({0}, \"{1}\") <- {2}",
                type_ident, trait_name, trait_env_var))
    })format!(
1314            "attr({}, \"{}\") <- {}",
1315            type_ident, trait_name, trait_env_var
1316        ));
1317        lines.push(String::new());
1318    }
1319
1320    lines.join("\n")
1321}
1322
1323/// Generate R6-style R wrapper code.
1324///
1325/// R6 classes are defined monolithically (all methods in `R6Class()`), so trait
1326/// methods cannot be injected into the class definition. Instead, both instance
1327/// and static trait methods live in the class-scoped `Type$Trait$name`
1328/// namespace (env-style) — the R6 generator object is an environment, so
1329/// `Type$Trait <- new.env()` attaches cleanly and `Type$Trait$method(x)`
1330/// resolves at the call site.
1331///
1332/// For `impl Counter for SimpleCounter`, generates:
1333/// - `SimpleCounter$Counter$value(x)`      -- instance method (takes the object)
1334/// - `SimpleCounter$Counter$increment(x)`  -- instance method
1335///
1336/// This class-qualified shape is collision-free by construction: two R6 impls
1337/// of one trait on different types no longer share an unqualified
1338/// `r6_trait_<Trait>_<method>` name (#1115). It also unifies R6 with the Env/S3
1339/// namespace shape (#1141) — R6 instance and static methods previously
1340/// disagreed on shape for no functional reason.
1341fn generate_trait_r6_r_wrapper(
1342    type_ident: &syn::Ident,
1343    trait_name: &syn::Ident,
1344    methods: &[TraitMethod],
1345    consts: &[TraitConst],
1346    impl_preconditions: Option<bool>,
1347) -> String {
1348    use crate::r_wrapper_builder::{DotCallBuilder, RoxygenBuilder};
1349
1350    let mut lines = Vec::new();
1351    let type_str = type_ident.to_string();
1352
1353    // Header comment
1354    lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("# R6 trait methods for {0} implementing {1}",
                type_ident, trait_name))
    })format!(
1355        "# R6 trait methods for {} implementing {}",
1356        type_ident, trait_name
1357    ));
1358    lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("# Generated by #[miniextendr(r6)] impl {0} for {1}",
                trait_name, type_ident))
    })format!(
1359        "# Generated by #[miniextendr(r6)] impl {} for {}",
1360        trait_name, type_ident
1361    ));
1362    lines.push("# Note: R6 trait methods live in the Type$Trait$method namespace".to_string());
1363    lines.push(String::new());
1364
1365    // Separate instance methods from static methods
1366    let instance_methods: Vec<_> = methods.iter().filter(|m| m.has_self).collect();
1367    let static_methods: Vec<_> = methods.iter().filter(|m| !m.has_self).collect();
1368
1369    // Create the trait namespace env up front — instance methods now live in it
1370    // too (not just static methods / consts).
1371    if !methods.is_empty() || !consts.is_empty() {
1372        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}${1} <- new.env(parent = emptyenv())",
                type_ident, trait_name))
    })format!(
1373            "{}${} <- new.env(parent = emptyenv())",
1374            type_ident, trait_name
1375        ));
1376        lines.push(String::new());
1377    }
1378
1379    // Generate instance methods in the Type$Trait$ namespace
1380    for method in &instance_methods {
1381        let ctx = TraitMethodContext::new(method, type_ident, trait_name)
1382            .with_impl_preconditions(impl_preconditions);
1383        let target = ctx.namespace_target(ClassSystem::R6);
1384
1385        // The injected receiver cannot collide with a Rust parameter.
1386        let full_params = if ctx.params.is_empty() {
1387            ".mx_self".to_string()
1388        } else {
1389            ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!(".mx_self, {0}", ctx.params))
    })format!(".mx_self, {}", ctx.params)
1390        };
1391
1392        // Namespace-member roxygen — a `$<-` assignment target, so roxygen emits
1393        // no `\usage` and needs no per-formal `@param` docs (#1141; Env puts
1394        // them in a `\describe{}` list of the page's description instead).
1395        // No `@param` lines on purpose, generated or the method's own: roxygen2
1396        // renders a per-method Arguments list on an R6 page only for a member of
1397        // the generator's `public_methods` (`r6_extract_methods`; an explicit
1398        // `@R6method Type$method` tag warns "can't find matching R6 method" for
1399        // anything else), and a trait method is not one. A `@param` here lands
1400        // in a top-level `\arguments` of the class page, detached from any
1401        // usage (so `R CMD check` never flags it) and merged by name with every
1402        // other trait method's parameters, the last block winning.
1403        lines.extend(own_block_page_lines(
1404            method,
1405            &type_str,
1406            Some(&target),
1407            Some(&target),
1408        ));
1409
1410        let call = ctx.instance_call(".ptr");
1411
1412        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} <- function({1}) {{",
                ctx.namespace_symbol(ClassSystem::R6), full_params))
    })format!(
1413            "{} <- function({full_params}) {{",
1414            ctx.namespace_symbol(ClassSystem::R6)
1415        ));
1416        // R6 objects store the ExternalPtr in private$.ptr — extract it for .Call()
1417        lines.push("  .ptr <- .mx_self$.__enclos_env__$private$.ptr".to_string());
1418        ctx.emit_method_prelude(&mut lines, "  ", &method.r_method_name());
1419        lines.extend(ctx.method_body_lines(&call, ClassSystem::R6));
1420        finish_instance_body(&mut lines, method, ".mx_self");
1421        lines.push("}".to_string());
1422        lines.push(String::new());
1423    }
1424
1425    // Generate static methods in Type$Trait$ namespace
1426    for method in &static_methods {
1427        let r_name = method.r_method_name();
1428        let ctx = TraitMethodContext::new(method, type_ident, trait_name)
1429            .with_impl_preconditions(impl_preconditions);
1430        let target = ctx.namespace_target(ClassSystem::R6);
1431
1432        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' Static trait method {0}::{1}()",
                trait_name, r_name))
    })format!(
1433            "#' Static trait method {}::{}()",
1434            trait_name, r_name
1435        ));
1436        lines.extend(own_block_page_lines(method, &type_str, Some(&target), None));
1437
1438        let call = ctx.static_call();
1439
1440        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} <- function({1}) {{",
                ctx.namespace_symbol(ClassSystem::R6), ctx.params))
    })format!(
1441            "{} <- function({}) {{",
1442            ctx.namespace_symbol(ClassSystem::R6),
1443            ctx.params
1444        ));
1445        ctx.emit_method_prelude(&mut lines, "  ", &r_name);
1446        lines.extend(ctx.method_body_lines(&call, ClassSystem::R6));
1447        lines.push("}".to_string());
1448        lines.push(String::new());
1449    }
1450
1451    // Generate const wrappers in Type$Trait$ namespace
1452    for trait_const in consts {
1453        let const_name = &trait_const.ident;
1454        let const_str = const_name.to_string();
1455        let target = trait_namespace_target(ClassSystem::R6, type_ident, trait_name, &const_str);
1456
1457        let roxygen = RoxygenBuilder::new()
1458            .name(target.clone())
1459            .rdname(&type_str)
1460            .build();
1461        lines.extend(roxygen);
1462
1463        let c_ident = trait_const.c_wrapper_ident_string(type_ident, trait_name);
1464        let call = DotCallBuilder::new(&c_ident).build();
1465
1466        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} <- function() {{", target))
    })format!("{target} <- function() {{"));
1467        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("  {0}", call))
    })format!("  {}", call));
1468        lines.push("}".to_string());
1469        lines.push(String::new());
1470    }
1471
1472    lines.join("\n")
1473}
1474
1475/// Close an instance-method body (#1213).
1476///
1477/// A void method hands back its receiver (`x` / `self`) so calls chain under
1478/// the pipe; the receiver is returned visibly, like every other return. A
1479/// method whose declared return is `Invisible<..>` wraps its final expression
1480/// (the receiver, or the converted `.val` / classed value) in `invisible()`.
1481fn finish_instance_body(lines: &mut Vec<String>, method: &super::TraitMethod, receiver: &str) {
1482    if method.returns_unit() {
1483        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("  {0}", receiver))
    })format!("  {receiver}"));
1484    }
1485    if method.is_invisible()
1486        && let Some(last) = lines.last_mut()
1487    {
1488        let expr = last.trim_start();
1489        let indent = &last[..last.len() - expr.len()];
1490        *last = ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}invisible({1})", indent, expr))
    })format!("{indent}invisible({expr})");
1491    }
1492}