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;
14
15/// Pre-computed context for a trait method, mirroring `MethodContext`
16/// (`r_class_formatter.rs`) for inherent-impl methods. Holds the C wrapper
17/// name, R formals (with defaults), and `.Call()` argument string so all 5
18/// trait generators build calls identically.
19pub(super) struct TraitMethodContext<'a> {
20    /// Reference to the parsed trait method metadata.
21    pub(super) method: &'a TraitMethod,
22    /// The C wrapper identifier string (e.g., `"C_Foo__Bar__value"`), used in `.Call()`.
23    pub(super) c_ident: String,
24    /// R formals string with defaults, used in `function(...)` signatures.
25    pub(super) params: String,
26    /// R call arguments string (without defaults), used inside `.Call()`
27    /// expressions. `Missing<T>` parameters are forwarded as
28    /// `if (missing(p)) quote(expr=) else p` — see
29    /// `r_wrapper_builder::RArgumentBuilder::build_call_args_vec`. Before this
30    /// context existed, trait methods built call args via
31    /// `collect_param_idents` instead, which skipped that forwarding entirely.
32    pub(super) args: String,
33}
34
35impl<'a> TraitMethodContext<'a> {
36    /// Build a context for `method`, which implements `trait_name` for `type_ident`.
37    pub(super) fn new(
38        method: &'a TraitMethod,
39        type_ident: &syn::Ident,
40        trait_name: &syn::Ident,
41    ) -> Self {
42        let c_ident = method.c_wrapper_ident_string(type_ident, trait_name);
43        // match_arg/choices formal defaults are load-bearing for match.arg()
44        // (see `effective_r_defaults` docs) — not just cosmetic.
45        let effective_defaults = crate::r_class_formatter::effective_r_defaults(
46            &method.param_defaults,
47            &method.per_param,
48            &c_ident,
49        );
50        let params =
51            crate::r_wrapper_builder::build_r_formals_from_sig(&method.sig, &effective_defaults);
52        let args = crate::r_wrapper_builder::build_r_call_args_from_sig(&method.sig);
53        Self {
54            method,
55            c_ident,
56            params,
57            args,
58        }
59    }
60
61    /// Build the `.Call()` expression for a static (non-receiver) trait method.
62    pub(super) fn static_call(&self) -> String {
63        crate::r_wrapper_builder::DotCallBuilder::new(&self.c_ident)
64            .with_args_str(&self.args)
65            .build()
66    }
67
68    /// Build the `.Call()` expression for an instance trait method, with
69    /// `self_expr` passed directly as the receiver argument (e.g. `".ptr"`,
70    /// `"x"`, `"self@.ptr"`).
71    ///
72    /// This is the structured equivalent of `MethodContext::instance_call` —
73    /// no string surgery. It fixes the substring-corruption bug where S4/S7/R6
74    /// built the call with `self = "x"` and then ran
75    /// `call.replace(", x", ", .ptr")`: `str::replace` rewrites *every* match
76    /// of the substring `", x"`, so a parameter whose R name started with `x`
77    /// (e.g. `x_factor`) was corrupted into `.ptr_factor`, producing a runtime
78    /// "object '.ptr_factor' not found" error. Passing the receiver expression
79    /// directly to `with_self` never touches the other arguments.
80    pub(super) fn instance_call(&self, self_expr: &str) -> String {
81        crate::r_wrapper_builder::DotCallBuilder::new(&self.c_ident)
82            .with_self(self_expr)
83            .with_args_str(&self.args)
84            .build()
85    }
86
87    /// Build R prelude lines validating `match_arg`/`choices` params. See
88    /// `r_class_formatter::build_match_arg_prelude`.
89    pub(super) fn match_arg_prelude(&self) -> Vec<String> {
90        crate::r_class_formatter::build_match_arg_prelude(&self.method.per_param)
91    }
92
93    /// R-side `stopifnot()` precondition checks for this method's parameters.
94    /// See `MethodContext::precondition_checks` for the inherent-impl twin.
95    pub(super) fn precondition_checks(&self) -> Vec<String> {
96        crate::r_class_formatter::build_method_precondition_checks(
97            &self.method.sig.inputs,
98            &self.method.per_param,
99            self.method.coerce,
100        )
101    }
102
103    /// Emit the shared method prelude into `lines`, each line prefixed with
104    /// `indent` — the trait-impl twin of `MethodContext::emit_method_prelude`.
105    /// In order: `r_entry`, `r_on_exit`, `lifecycle_prelude`,
106    /// `precondition_checks`, `match_arg_prelude`, `r_post_checks`.
107    ///
108    /// `Missing<T>` forwarding is not a prelude step here either — it's inline
109    /// in `self.args` (built in `new`), matching the inherent path.
110    ///
111    /// `what` is the human-readable label passed to the lifecycle prelude.
112    /// This mirrors the pre-refactor `trait_method_preamble_lines`, which
113    /// always used the R-facing method name as `what` regardless of class
114    /// system (inherent methods instead use a class-qualified label like
115    /// `"Type.method"` — trait methods keep the simpler unqualified label to
116    /// avoid changing existing lifecycle-warning wording).
117    pub(super) fn emit_method_prelude(&self, lines: &mut Vec<String>, indent: &str, what: &str) {
118        let m = self.method;
119        if let Some(ref entry) = m.r_entry {
120            for line in entry.lines() {
121                lines.push(format!("{indent}{line}"));
122            }
123        }
124        if let Some(ref on_exit) = m.r_on_exit {
125            lines.push(format!("{indent}{}", on_exit.to_r_code()));
126        }
127        if let Some(ref spec) = m.lifecycle
128            && let Some(prelude) = spec.r_prelude(what)
129        {
130            for line in prelude.lines() {
131                lines.push(format!("{indent}{line}"));
132            }
133        }
134        for check in self.precondition_checks() {
135            lines.push(format!("{indent}{check}"));
136        }
137        for line in self.match_arg_prelude() {
138            lines.push(format!("{indent}{line}"));
139        }
140        if let Some(ref post) = m.r_post_checks {
141            for line in post.lines() {
142                lines.push(format!("{indent}{line}"));
143            }
144        }
145    }
146}