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}