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 `$<-`; routing through an
34///   attribute-attached env lets `Type$Trait$member` still resolve at the call
35///   site (R's `$` on an S7 object falls through to attributes).
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            format!("{type_ident}${trait_name}${member}")
45        }
46        ClassSystem::S4 => format!("{type_ident}_{trait_name}_{member}"),
47        ClassSystem::S7 => {
48            format!(
49                "{}${member}",
50                trait_namespace_env_var(type_ident, trait_name)
51            )
52        }
53    }
54}
55
56/// The local environment variable S7 trait wrappers assign members into before
57/// attaching it to the class object via `attr()`. One owner so
58/// [`trait_namespace_target`]'s S7 arm and the generator's `new.env` / `attr()`
59/// lines can't drift apart.
60pub(super) fn trait_namespace_env_var(type_ident: &syn::Ident, trait_name: &syn::Ident) -> String {
61    format!(".{type_ident}__{trait_name}")
62}
63
64/// Pre-computed context for a trait method, mirroring `MethodContext`
65/// (`r_class_formatter.rs`) for inherent-impl methods. Holds the C wrapper
66/// name, R formals (with defaults), and `.Call()` argument string so all 5
67/// trait generators build calls identically.
68pub(super) struct TraitMethodContext<'a> {
69    /// Reference to the parsed trait method metadata.
70    pub(super) method: &'a TraitMethod,
71    /// The implementing type ident (e.g. `Foo`) — used to build the per-class
72    /// trait-namespace assignment target and the Self-return wrapper class.
73    pub(super) type_ident: &'a syn::Ident,
74    /// The trait ident (e.g. `Bar`) — used to build the per-class trait
75    /// namespace assignment target.
76    pub(super) trait_name: &'a syn::Ident,
77    /// The C wrapper identifier string (e.g., `"C_Foo__Bar__value"`), used in `.Call()`.
78    pub(super) c_ident: String,
79    /// R formals string with defaults, used in `function(...)` signatures.
80    pub(super) params: String,
81    /// R call arguments string (without defaults), used inside `.Call()`
82    /// expressions. `Missing<T>` parameters are forwarded as
83    /// `if (missing(p)) quote(expr=) else p` — see
84    /// `r_wrapper_builder::RArgumentBuilder::build_call_args_vec`. Before this
85    /// context existed, trait methods built call args via
86    /// `collect_param_idents` instead, which skipped that forwarding entirely.
87    pub(super) args: String,
88}
89
90impl<'a> TraitMethodContext<'a> {
91    /// Build a context for `method`, which implements `trait_name` for `type_ident`.
92    pub(super) fn new(
93        method: &'a TraitMethod,
94        type_ident: &'a syn::Ident,
95        trait_name: &'a syn::Ident,
96    ) -> Self {
97        let c_ident = method.c_wrapper_ident_string(type_ident, trait_name);
98        // match_arg/choices formal defaults are load-bearing for match.arg()
99        // (see `effective_r_defaults` docs) — not just cosmetic.
100        let effective_defaults = crate::r_class_formatter::effective_r_defaults(
101            &method.param_defaults,
102            &method.per_param,
103            &c_ident,
104        );
105        let params =
106            crate::r_wrapper_builder::build_r_formals_from_sig(&method.sig, &effective_defaults);
107        let args = crate::r_wrapper_builder::build_r_call_args_from_sig(&method.sig);
108        Self {
109            method,
110            type_ident,
111            trait_name,
112            c_ident,
113            params,
114            args,
115        }
116    }
117
118    /// The R-visible assignment target for this method within `class_system`'s
119    /// per-class trait namespace — a thin wrapper over [`trait_namespace_target`]
120    /// using this method's R-facing name. See that function for the policy.
121    pub(super) fn namespace_target(&self, class_system: ClassSystem) -> String {
122        trait_namespace_target(
123            class_system,
124            self.type_ident,
125            self.trait_name,
126            &self.method.r_method_name(),
127        )
128    }
129
130    /// Whether this method re-wraps its return into a classed object — i.e. it
131    /// returns `Self` / `Result<Self, E>` / `Option<Self>`. Mirrors the
132    /// `ReturnStrategy::ReturnSelf` arm of `ReturnStrategy::for_method` on the
133    /// inherent-impl path.
134    pub(super) fn returns_self(&self) -> bool {
135        self.method.returns_self()
136            || self.method.returns_result_self()
137            || self.method.returns_option_self()
138    }
139
140    /// Emit the R body lines for a trait method (2-space indent): capture
141    /// `.Call()` into `.val`, run the tagged-condition guard, then return
142    /// `.val` — or, for `-> Self` factory methods, re-wrap `.val` into a
143    /// classed object via the shared [`crate::MethodReturnBuilder`], exactly
144    /// as the inherent-impl generators do (audit finding #4 / #1141). The
145    /// wrapper class is the implementing type; the wrapping idiom is selected
146    /// per class system (`class(.val) <-` / `structure()` / `methods::new()` /
147    /// `Class(.ptr=)` / `Class$new(.ptr=)`).
148    pub(super) fn method_body_lines(&self, call: &str, class_system: ClassSystem) -> Vec<String> {
149        if !self.returns_self() {
150            return trait_method_body_lines(call, "  ");
151        }
152        let builder = crate::MethodReturnBuilder::new(call.to_string())
153            .with_strategy(crate::ReturnStrategy::ReturnSelf)
154            .with_class_name(self.type_ident.to_string())
155            .with_indent(2);
156        match class_system {
157            ClassSystem::Env => builder.build(),
158            ClassSystem::S3 | ClassSystem::Vctrs => builder.build_s3_body(),
159            ClassSystem::S4 => builder.build_s4_body(),
160            ClassSystem::S7 => builder.build_s7_body(),
161            ClassSystem::R6 => builder.build_r6_body(),
162        }
163    }
164
165    /// Build the `.Call()` expression for a static (non-receiver) trait method.
166    pub(super) fn static_call(&self) -> String {
167        crate::r_wrapper_builder::DotCallBuilder::new(&self.c_ident)
168            .with_args_str(&self.args)
169            .build()
170    }
171
172    /// Build the `.Call()` expression for an instance trait method, with
173    /// `self_expr` passed directly as the receiver argument (e.g. `".ptr"`,
174    /// `"x"`, `"self@.ptr"`).
175    ///
176    /// This is the structured equivalent of `MethodContext::instance_call` —
177    /// no string surgery. It fixes the substring-corruption bug where S4/S7/R6
178    /// built the call with `self = "x"` and then ran
179    /// `call.replace(", x", ", .ptr")`: `str::replace` rewrites *every* match
180    /// of the substring `", x"`, so a parameter whose R name started with `x`
181    /// (e.g. `x_factor`) was corrupted into `.ptr_factor`, producing a runtime
182    /// "object '.ptr_factor' not found" error. Passing the receiver expression
183    /// directly to `with_self` never touches the other arguments.
184    pub(super) fn instance_call(&self, self_expr: &str) -> String {
185        crate::r_wrapper_builder::DotCallBuilder::new(&self.c_ident)
186            .with_self(self_expr)
187            .with_args_str(&self.args)
188            .build()
189    }
190
191    /// Build R prelude lines validating `match_arg`/`choices` params. See
192    /// `r_class_formatter::build_match_arg_prelude`.
193    pub(super) fn match_arg_prelude(&self) -> Vec<String> {
194        crate::r_class_formatter::build_match_arg_prelude(&self.method.per_param)
195    }
196
197    /// R-side `stopifnot()` precondition checks for this method's parameters.
198    /// See `MethodContext::precondition_checks` for the inherent-impl twin.
199    pub(super) fn precondition_checks(&self) -> Vec<String> {
200        crate::r_class_formatter::build_method_precondition_checks(
201            &self.method.sig.inputs,
202            &self.method.per_param,
203            self.method.coerce,
204        )
205    }
206
207    /// Emit the shared method prelude into `lines`, each line prefixed with
208    /// `indent` — the trait-impl twin of `MethodContext::emit_method_prelude`.
209    /// In order: `r_entry`, `r_on_exit`, `lifecycle_prelude`,
210    /// `precondition_checks`, `match_arg_prelude`, `r_post_checks`.
211    ///
212    /// `Missing<T>` forwarding is not a prelude step here either — it's inline
213    /// in `self.args` (built in `new`), matching the inherent path.
214    ///
215    /// `what` is the human-readable label passed to the lifecycle prelude.
216    /// This mirrors the pre-refactor `trait_method_preamble_lines`, which
217    /// always used the R-facing method name as `what` regardless of class
218    /// system (inherent methods instead use a class-qualified label like
219    /// `"Type.method"` — trait methods keep the simpler unqualified label to
220    /// avoid changing existing lifecycle-warning wording).
221    pub(super) fn emit_method_prelude(&self, lines: &mut Vec<String>, indent: &str, what: &str) {
222        let m = self.method;
223        if let Some(ref entry) = m.r_entry {
224            for line in entry.lines() {
225                lines.push(format!("{indent}{line}"));
226            }
227        }
228        if let Some(ref on_exit) = m.r_on_exit {
229            lines.push(format!("{indent}{}", on_exit.to_r_code()));
230        }
231        if let Some(ref spec) = m.lifecycle
232            && let Some(prelude) = spec.r_prelude(what)
233        {
234            for line in prelude.lines() {
235                lines.push(format!("{indent}{line}"));
236            }
237        }
238        for check in self.precondition_checks() {
239            lines.push(format!("{indent}{check}"));
240        }
241        for line in self.match_arg_prelude() {
242            lines.push(format!("{indent}{line}"));
243        }
244        if let Some(ref post) = m.r_post_checks {
245            for line in post.lines() {
246                lines.push(format!("{indent}{line}"));
247            }
248        }
249    }
250}