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 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/// [`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 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 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(format!("{indent}{line}"));
357 }
358 }
359 if let Some(ref on_exit) = m.r_on_exit {
360 lines.push(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(format!("{indent}{line}"));
367 }
368 }
369 for check in self.precondition_checks() {
370 lines.push(format!("{indent}{check}"));
371 }
372 for line in self.match_arg_prelude() {
373 lines.push(format!("{indent}{line}"));
374 }
375 if let Some(ref post) = m.r_post_checks {
376 for line in post.lines() {
377 lines.push(format!("{indent}{line}"));
378 }
379 }
380 }
381}