Skip to main content

miniextendr_macros/miniextendr_impl_trait/
method_context.rs

1//! Shared per-method context for trait-impl R wrapper generation.
2//!
3//! Parallels `r_class_formatter::MethodContext` (used by the 6 inherent-impl
4//! class generators) so the 5 trait generators (env/s3/s4/s7/r6) build
5//! `.Call()` invocations and method preludes the same way instead of each
6//! hand-rolling its own version. See
7//! `audit/2026-07-03-dogfooding-macros-codegen.md` finding #1: the trait path
8//! previously shared nothing above `DotCallBuilder`, which caused a
9//! substring-corruption bug in the receiver-ptr extraction (S4/S7/R6) and a
10//! silent 3-of-6-step prelude omission (missing `precondition_checks` /
11//! `match_arg_prelude`) relative to inherent methods.
12
13use super::{TraitMethod, trait_method_body_lines};
14use crate::miniextendr_impl::ClassSystem;
15
16/// R-visible assignment target for a trait member (method or const) placed in
17/// the per-class trait namespace — the single owner of the namespace-shape
18/// policy the 5 generators previously hand-rolled independently (audit
19/// `2026-07-03-dogfooding-macros-codegen.md` finding #1; #1141). `member` is
20/// the R-facing method/const name.
21///
22/// Policy:
23/// - **Env / S3 / R6 / Vctrs**: `Type$Trait$member` — class-scoped, so it is
24///   collision-free by construction. This is what fixes #1115: two R6 impls of
25///   one trait on *different* types now emit `TypeA$Trait$m` / `TypeB$Trait$m`
26///   rather than a shared, unqualified `r6_trait_Trait_m` that aborted
27///   wrapper-gen via the duplicate-definition guard.
28/// - **S4**: flat `Type_Trait_member` standalone name. S4 objects intercept
29///   `$<-`, so `Type$Trait$member` cannot be assigned onto them; the class
30///   component in the flat name keeps it collision-free.
31/// - **S7**: `env_var$member`, where `env_var` = [`trait_namespace_env_var`]
32///   (`.Type__Trait`) is a local env attached to `Type` via `attr()` at the end
33///   of the generator. S7 objects also intercept `$<-`, and `$` too, so users
34///   reach the member as [`s7_trait_member_access`]'s
35///   `attr(Type, "Trait")$member`.
36pub(super) fn trait_namespace_target(
37    class_system: ClassSystem,
38    type_ident: &syn::Ident,
39    trait_name: &syn::Ident,
40    member: &str,
41) -> String {
42    match class_system {
43        ClassSystem::Env | ClassSystem::S3 | ClassSystem::R6 | ClassSystem::Vctrs => {
44            ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}${1}${2}", type_ident,
                trait_name, member))
    })format!("{type_ident}${trait_name}${member}")
45        }
46        ClassSystem::S4 => ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}_{1}_{2}", type_ident,
                trait_name, member))
    })format!("{type_ident}_{trait_name}_{member}"),
47        ClassSystem::S7 => {
48            ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}${1}",
                trait_namespace_env_var(type_ident, trait_name), member))
    })format!(
49                "{}${member}",
50                trait_namespace_env_var(type_ident, trait_name)
51            )
52        }
53    }
54}
55
56/// [`trait_namespace_target`] in R code position (the left of `<-`, the
57/// argument of `attr()`). A non-syntactic member (an operator `r_name`, #1475)
58/// is backtick-quoted: `Type$Trait$`[[``, or `` `Type_Trait_[[` `` for S4.
59/// Roxygen `@name` keeps the bare [`trait_namespace_target`] spelling.
60pub(super) fn trait_namespace_symbol(
61    class_system: ClassSystem,
62    type_ident: &syn::Ident,
63    trait_name: &syn::Ident,
64    member: &str,
65) -> String {
66    match class_system {
67        ClassSystem::S4 => crate::naming::r_def_name(&trait_namespace_target(
68            class_system,
69            type_ident,
70            trait_name,
71            member,
72        )),
73        _ => trait_namespace_target(
74            class_system,
75            type_ident,
76            trait_name,
77            &crate::naming::r_def_name(member),
78        ),
79    }
80}
81
82/// The local environment variable S7 trait wrappers assign members into before
83/// attaching it to the class object via `attr()`. One owner so
84/// [`trait_namespace_target`]'s S7 arm and the generator's `new.env` / `attr()`
85/// lines can't drift apart.
86pub(super) fn trait_namespace_env_var(type_ident: &syn::Ident, trait_name: &syn::Ident) -> String {
87    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!(".{0}__{1}", type_ident,
                trait_name))
    })format!(".{type_ident}__{trait_name}")
88}
89
90/// How R code reaches an S7 trait static or const: `attr(Type, "Trait")$member`,
91/// also the topic alias of its documentation block. The trait namespace is an
92/// attribute of the class object, and `Type$Trait$member` does not reach it:
93/// an S7 class object is a function, and S7's `$` method stops on any S7
94/// object that is not a list or an environment ("Can't get S7 properties with
95/// `$`").
96pub(super) fn s7_trait_member_access(
97    type_ident: &syn::Ident,
98    trait_name: &syn::Ident,
99    member: &str,
100) -> String {
101    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("attr({0}, \"{1}\")${2}",
                type_ident, trait_name, member))
    })format!("attr({type_ident}, \"{trait_name}\")${member}")
102}
103
104/// Pre-computed context for a trait method, mirroring `MethodContext`
105/// (`r_class_formatter.rs`) for inherent-impl methods. Holds the C wrapper
106/// name, R formals (with defaults), and `.Call()` argument string so all 5
107/// trait generators build calls identically.
108pub(super) struct TraitMethodContext<'a> {
109    /// Reference to the parsed trait method metadata.
110    pub(super) method: &'a TraitMethod,
111    /// The implementing type ident (e.g. `Foo`) — used to build the per-class
112    /// trait-namespace assignment target and the Self-return wrapper class.
113    pub(super) type_ident: &'a syn::Ident,
114    /// The trait ident (e.g. `Bar`) — used to build the per-class trait
115    /// namespace assignment target.
116    pub(super) trait_name: &'a syn::Ident,
117    /// The C wrapper identifier string (e.g., `"C_Foo__Bar__value"`), used in `.Call()`.
118    pub(super) c_ident: String,
119    /// R formals string with defaults, used in `function(...)` signatures.
120    pub(super) params: String,
121    /// R call arguments string (without defaults), used inside `.Call()`
122    /// expressions. `Missing<T>` parameters are forwarded as
123    /// `if (missing(p)) quote(expr=) else p` (a single-choice one also
124    /// forwards the sentinel for a value `identical()` to its formal, #2152)
125    /// — see `r_wrapper_builder::RArgumentBuilder::build_call_args_vec`. Before this
126    /// context existed, trait methods built call args via
127    /// `collect_param_idents` instead, which skipped that forwarding entirely.
128    pub(super) args: String,
129    /// The impl block's `preconditions` / `no_preconditions`
130    /// (`ImplAttrs::preconditions`; for an empty-body impl, its decision with
131    /// the crate default and the feature folded in), below the method's own
132    /// and each parameter's.
133    pub(super) impl_preconditions: Option<bool>,
134    /// The call the method's conditions report, as on
135    /// `MethodContext::call_attribution`: the method's own `sys.call()` by
136    /// default, `CallAttribution::Generic` for the S3 generator (#1851).
137    pub(super) call_attribution: crate::r_wrapper_builder::CallAttribution,
138}
139
140impl<'a> TraitMethodContext<'a> {
141    /// Build a context for `method`, which implements `trait_name` for `type_ident`.
142    pub(super) fn new(
143        method: &'a TraitMethod,
144        type_ident: &'a syn::Ident,
145        trait_name: &'a syn::Ident,
146    ) -> Self {
147        let c_ident = method.c_wrapper_ident_string(type_ident, trait_name);
148        // The choice formals (the choice vector, `NULL` for `Option<T>`, none
149        // under `no_default(p)`), as on inherent methods; see
150        // `effective_r_defaults`.
151        let effective_defaults = crate::r_class_formatter::effective_r_defaults(
152            &method.param_defaults,
153            &method.per_param,
154            &c_ident,
155        );
156        // Trait methods declare no formals (#1842): each is the parameter's
157        // Rust name without leading underscores.
158        let formals = crate::r_wrapper_builder::Formals::default();
159        let params = crate::r_wrapper_builder::build_r_formals_from_sig(
160            &method.sig,
161            &formals,
162            &effective_defaults,
163        );
164        let args = crate::r_wrapper_builder::build_r_call_args_from_sig(
165            &method.sig,
166            &formals,
167            crate::miniextendr_fn::absent_formals_by_r_name(&method.per_param, &c_ident),
168        );
169        Self {
170            method,
171            type_ident,
172            trait_name,
173            c_ident,
174            params,
175            args,
176            impl_preconditions: None,
177            call_attribution: crate::r_wrapper_builder::CallAttribution::Wrapper,
178        }
179    }
180
181    /// Set the `preconditions` decision inherited from the impl block; the
182    /// twin of `MethodContext::with_impl_preconditions`.
183    pub(super) fn with_impl_preconditions(mut self, impl_preconditions: Option<bool>) -> Self {
184        self.impl_preconditions = impl_preconditions;
185        self
186    }
187
188    /// Set the call the method's conditions report; the twin of
189    /// `MethodContext::with_call_attribution`.
190    pub(super) fn with_call_attribution(
191        mut self,
192        call_attribution: crate::r_wrapper_builder::CallAttribution,
193    ) -> Self {
194        self.call_attribution = call_attribution;
195        self
196    }
197
198    /// The R-visible assignment target for this method within `class_system`'s
199    /// per-class trait namespace — a thin wrapper over [`trait_namespace_target`]
200    /// using this method's R-facing name. See that function for the policy.
201    pub(super) fn namespace_target(&self, class_system: ClassSystem) -> String {
202        trait_namespace_target(
203            class_system,
204            self.type_ident,
205            self.trait_name,
206            &self.method.r_method_name(),
207        )
208    }
209
210    /// [`Self::namespace_target`] in R code position, via
211    /// [`trait_namespace_symbol`].
212    pub(super) fn namespace_symbol(&self, class_system: ClassSystem) -> String {
213        trait_namespace_symbol(
214            class_system,
215            self.type_ident,
216            self.trait_name,
217            &self.method.r_method_name(),
218        )
219    }
220
221    /// Whether this method re-wraps its return into a classed object — i.e. it
222    /// returns `Self` / `Result<Self, E>` / `Option<Self>`. Mirrors the
223    /// `ReturnStrategy::ReturnSelf` arm of `ReturnStrategy::for_method` on the
224    /// inherent-impl path.
225    pub(super) fn returns_self(&self) -> bool {
226        self.method.returns_self()
227            || self.method.returns_result_self()
228            || self.method.returns_option_self()
229    }
230
231    /// Emit the R body lines for a trait method (2-space indent): capture
232    /// `.Call()` into `.val`, run the tagged-condition guard, then return
233    /// `.val` — or, for `-> Self` factory methods, re-wrap `.val` into a
234    /// classed object via the shared [`crate::MethodReturnBuilder`], exactly
235    /// as the inherent-impl generators do (audit finding #4 / #1141). The
236    /// wrapper class is the implementing type; the wrapping idiom is selected
237    /// per class system (`class(.val) <-` / `structure()` / `methods::new()` /
238    /// `Class(.ptr=)` / `Class$new(.ptr=)`).
239    pub(super) fn method_body_lines(&self, call: &str, class_system: ClassSystem) -> Vec<String> {
240        let raise_default = self.call_attribution.raise_default();
241        if let Some(wrap) = &self.method.return_wrap {
242            return crate::MethodReturnBuilder::new(call.to_owned())
243                .with_raise_default(raise_default)
244                .with_class_name(self.type_ident.to_string())
245                .with_explicit_wrap(wrap.clone())
246                .build();
247        }
248        if !self.returns_self() {
249            return trait_method_body_lines(call, "  ", raise_default);
250        }
251        let builder = crate::MethodReturnBuilder::new(call.to_string())
252            .with_raise_default(raise_default)
253            .with_strategy(crate::ReturnStrategy::ReturnSelf)
254            .with_class_name(self.type_ident.to_string())
255            .with_nullable(self.method.returns_option_self())
256            .with_indent(2);
257        match class_system {
258            ClassSystem::Env => builder.build(),
259            ClassSystem::S3 | ClassSystem::Vctrs => builder.build_s3_body(),
260            ClassSystem::S4 => builder.build_s4_body(),
261            ClassSystem::S7 => builder.build_s7_body(),
262            ClassSystem::R6 => builder.build_r6_body(),
263        }
264    }
265
266    /// Build the `.Call()` expression for a static (non-receiver) trait method.
267    pub(super) fn static_call(&self) -> String {
268        crate::r_wrapper_builder::DotCallBuilder::new(&self.c_ident)
269            .with_args_str(&self.args)
270            .build()
271    }
272
273    /// Build the `.Call()` expression for an instance trait method, with
274    /// `self_expr` passed directly as the receiver argument (e.g. `".ptr"`,
275    /// `"x"`, `"self@.ptr"`).
276    ///
277    /// This is the structured equivalent of `MethodContext::instance_call` —
278    /// no string surgery. It fixes the substring-corruption bug where S4/S7/R6
279    /// built the call with `self = "x"` and then ran
280    /// `call.replace(", x", ", .ptr")`: `str::replace` rewrites *every* match
281    /// of the substring `", x"`, so a parameter whose R name started with `x`
282    /// (e.g. `x_factor`) was corrupted into `.ptr_factor`, producing a runtime
283    /// "object '.ptr_factor' not found" error. Passing the receiver expression
284    /// directly to `with_self` never touches the other arguments.
285    pub(super) fn instance_call(&self, self_expr: &str) -> String {
286        crate::r_wrapper_builder::DotCallBuilder::new(&self.c_ident)
287            .with_call_attribution(self.call_attribution)
288            .with_self(self_expr)
289            .with_args_str(&self.args)
290            .build()
291    }
292
293    /// R-param-name → auto-generated `@param` text of this method's choice
294    /// params (`One of "a", "b".` for `choices(...)`) and of its `Missing<..>`
295    /// params with a default (#2161). See
296    /// `r_class_formatter::choice_param_doc_map` and
297    /// `miniextendr_fn::missing_default_param_docs`.
298    pub(super) fn choice_param_docs(&self) -> std::collections::HashMap<String, String> {
299        let mut docs =
300            crate::r_class_formatter::choice_param_doc_map(&self.c_ident, &self.method.per_param);
301        docs.extend(crate::miniextendr_fn::missing_default_param_docs(
302            &self.method.sig.inputs,
303            &self.method.per_param,
304            &self.method.param_defaults,
305            &crate::r_wrapper_builder::Formals::default(),
306        ));
307        docs
308    }
309
310    /// Build R prelude lines validating `match_arg`/`choices` params. See
311    /// `r_class_formatter::build_match_arg_prelude`.
312    pub(super) fn match_arg_prelude(&self) -> Vec<String> {
313        crate::r_class_formatter::build_match_arg_prelude(
314            &self.method.sig.inputs,
315            &self.method.per_param,
316            &self.c_ident,
317            self.call_attribution,
318        )
319    }
320
321    /// R-side precondition guards for this method's parameters.
322    /// See `MethodContext::precondition_checks` for the inherent-impl twin.
323    /// Each parameter keeps or drops its type-derived checks as its own
324    /// `preconditions(p)` / `no_preconditions(p)`, the method's, the impl
325    /// block's, the crate default and the feature decide; the per-parameter
326    /// `inherits(...)` / `not_inherits(...)` / `no_na(...)` checks stay.
327    pub(super) fn precondition_checks(&self) -> Vec<String> {
328        crate::r_class_formatter::build_method_precondition_checks(
329            &self.method.sig.inputs,
330            &self.method.per_param,
331            self.method.coerce,
332            self.method.preconditions,
333            self.impl_preconditions,
334            self.call_attribution.r_check_call(),
335        )
336    }
337
338    /// Emit the shared method prelude into `lines`, each line prefixed with
339    /// `indent` — the trait-impl twin of `MethodContext::emit_method_prelude`.
340    /// In order: `r_entry`, `r_on_exit`, `lifecycle_prelude`,
341    /// `precondition_checks`, `match_arg_prelude`, `r_post_checks`.
342    ///
343    /// `Missing<T>` forwarding is not a prelude step here either — it's inline
344    /// in `self.args` (built in `new`), matching the inherent path.
345    ///
346    /// `what` is the human-readable label passed to the lifecycle prelude.
347    /// This mirrors the pre-refactor `trait_method_preamble_lines`, which
348    /// always used the R-facing method name as `what` regardless of class
349    /// system (inherent methods instead use a class-qualified label like
350    /// `"Type.method"` — trait methods keep the simpler unqualified label to
351    /// avoid changing existing lifecycle-warning wording).
352    pub(super) fn emit_method_prelude(&self, lines: &mut Vec<String>, indent: &str, what: &str) {
353        let m = self.method;
354        if let Some(ref entry) = m.r_entry {
355            for line in entry.lines() {
356                lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}{1}", indent, line))
    })format!("{indent}{line}"));
357            }
358        }
359        if let Some(ref on_exit) = m.r_on_exit {
360            lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{1}{0}", on_exit.to_r_code(),
                indent))
    })format!("{indent}{}", on_exit.to_r_code()));
361        }
362        if let Some(ref spec) = m.lifecycle
363            && let Some(prelude) = spec.r_prelude(what)
364        {
365            for line in prelude.lines() {
366                lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}{1}", indent, line))
    })format!("{indent}{line}"));
367            }
368        }
369        for check in self.precondition_checks() {
370            lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}{1}", indent, check))
    })format!("{indent}{check}"));
371        }
372        for line in self.match_arg_prelude() {
373            lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}{1}", indent, line))
    })format!("{indent}{line}"));
374        }
375        if let Some(ref post) = m.r_post_checks {
376            for line in post.lines() {
377                lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}{1}", indent, line))
    })format!("{indent}{line}"));
378            }
379        }
380    }
381}