miniextendr_macros/method_return_builder.rs
1//! Shared utilities for handling method return values in R wrapper generation.
2//!
3//! This module provides helpers for generating consistent return value handling
4//! across all class systems (Env, R6, S7, S3, S4).
5
6use crate::miniextendr_impl::ParsedMethod;
7
8// region: Shared R error-check helpers
9//
10// All wrappers route through the internal helper `.miniextendr_raise_condition`
11// emitted once at the top of the generated wrappers file (see
12// `miniextendr-api/src/registry.rs` `write_r_wrappers_to_file`). The helper
13// performs the `.val$kind` dispatch and `rust_*` class layering; each wrapper
14// only needs a one-line guard at the call site.
15
16/// Generate the R guard that re-raises a tagged Rust error/condition value.
17///
18/// Expects `.val` to already be assigned (e.g., `.val <- .Call(...)`). Emits a
19/// single line indented by `indent`: when `.val` is a tagged `rust_condition_value`,
20/// hand off to the shared helper and return from the enclosing function.
21///
22/// The helper dispatches on `.val$kind` (see
23/// `miniextendr_api::error_value::kind` for canonical kind strings):
24///
25/// - `error` / `panic` / `result_err` / `none_err` / `conversion` (and any
26/// unknown kind) — `stop()` longjmps with the appropriate `rust_*` class
27/// layering.
28/// - `warning` — `warning()` signals; the wrapper's surrounding `return(...)`
29/// propagates `invisible(NULL)` as the wrapper's result.
30/// - `message` — `message()` signals; same propagation.
31/// - `condition` — `signalCondition()` signals; same propagation.
32pub fn condition_check_lines(indent: &str) -> Vec<String> {
33 vec![format!(
34 "{indent}if (inherits(.val, \"rust_condition_value\") && isTRUE(attr(.val, \"__rust_condition__\"))) return(.miniextendr_raise_condition(.val, sys.call()))"
35 )]
36}
37
38/// Generate an inline R error-check block for single-expression contexts (S7, S4).
39///
40/// Returns a multi-line block string: `{ .val <- <call_expr>; if (...) return(...); <inner> }`.
41/// Used where the class system requires a single expression rather than separate lines
42/// (e.g., S7 property definitions, S4 method bodies).
43///
44/// - `call_expr`: The `.Call()` expression to evaluate
45/// - `inner`: The final expression to return after the error check passes
46/// - `indent`: Leading whitespace for the inner lines (e.g., `" "` for 4-space)
47pub fn condition_check_inline_block(call_expr: &str, inner: &str, indent: &str) -> String {
48 format!(
49 "{{\n{indent}.val <- {call_expr}\n\
50 {indent}if (inherits(.val, \"rust_condition_value\") && isTRUE(attr(.val, \"__rust_condition__\"))) return(.miniextendr_raise_condition(.val, sys.call()))\n\
51 {indent}{inner}\n \
52 }}"
53 )
54}
55
56/// Generate a standalone-function R wrapper body.
57///
58/// Returns the full body string: `.val <- <call_expr>; if (...) return(...); <final_return>`.
59/// Used for top-level `#[miniextendr]` functions (not class methods).
60///
61/// - `call_expr`: The `.Call()` expression to evaluate
62/// - `final_return`: The expression to return (typically `".val"` or `"invisible(.val)"`)
63/// - `indent`: Leading whitespace for the body lines (e.g., `" "` for 2-space)
64pub fn standalone_body(call_expr: &str, final_return: &str, indent: &str) -> String {
65 format!(
66 ".val <- {call_expr}\n\
67 {indent}if (inherits(.val, \"rust_condition_value\") && isTRUE(attr(.val, \"__rust_condition__\"))) return(.miniextendr_raise_condition(.val, sys.call()))\n\
68 {indent}{final_return}"
69 )
70}
71// endregion
72
73// region: Return strategy
74
75/// Return handling strategy for class methods.
76///
77/// Determines how the R wrapper function processes and returns the `.Call()` result.
78/// Each class system generator uses this to produce idiomatic R return code.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum ReturnStrategy {
81 /// The method returns `Self`. The wrapper wraps the raw pointer result with
82 /// the appropriate class attribute or creates a new class object (e.g.,
83 /// `R6Class$new(.ptr = result)` or `structure(result, class = "...")`).
84 ReturnSelf,
85 /// The method is a `&mut self` method returning `()`. The wrapper calls the
86 /// `.Call()` for its side effect and returns the receiver (`self`/`x`) for
87 /// method chaining (e.g., `invisible(self)` for R6).
88 ChainableMutation,
89 /// Default strategy: return the `.Call()` result directly without wrapping.
90 Direct,
91}
92
93impl ReturnStrategy {
94 /// Determine the return strategy for a parsed method.
95 ///
96 /// - Methods that return `Self`, `Result<Self, E>`, or `Option<Self>` use
97 /// `ReturnSelf`. For the latter two, the C wrapper already raised on
98 /// `Err` / `None` (see
99 /// [`crate::c_wrapper_builder::ReturnHandling::ResultExternalPtr`] and
100 /// [`crate::c_wrapper_builder::ReturnHandling::OptionExternalPtr`]), so a
101 /// successful `.val` is a bare ExternalPtr — identical in shape to the
102 /// bare-`Self` case — and gets the same class-wrapping tail.
103 /// - In-place builders (`&mut self -> &mut Self` / `&self -> Self`) and
104 /// `&mut self -> ()` methods use `ChainableMutation`. Both return the
105 /// receiver object (`x` / `invisible(self)`) so the call composes under
106 /// the native pipe (`obj |> set_a(1) |> set_b(2)`); the C wrapper hands
107 /// back the same ExternalPtr handle (see
108 /// [`crate::c_wrapper_builder::ReturnHandling::SelfHandle`]).
109 /// - All other methods use `Direct`
110 pub fn for_method(method: &ParsedMethod) -> Self {
111 // In-place builders (`&mut self -> &mut Self` / `&self -> Self`) and
112 // `&mut self -> ()` mutators both return the receiver object.
113 let is_self_ref_builder = method.returns_self_ref() && method.env.is_instance();
114 let is_unit_mutator = method.env.is_mut() && method.returns_unit();
115 if method.returns_self() || method.returns_result_self() || method.returns_option_self() {
116 ReturnStrategy::ReturnSelf
117 } else if is_self_ref_builder || is_unit_mutator {
118 ReturnStrategy::ChainableMutation
119 } else {
120 ReturnStrategy::Direct
121 }
122 }
123}
124
125/// Class-specific tail closures, one per [`ReturnStrategy`] variant.
126///
127/// Each closure receives `(indent, class_name)` and produces the tail lines that
128/// run after `.val <- <call_expr>` and the condition check (both emitted by
129/// [`MethodReturnBuilder::build_with_tails`]). The tail therefore always
130/// references `.val` directly.
131///
132/// `class_name` is `""` for [`ReturnStrategy::ChainableMutation`] and
133/// [`ReturnStrategy::Direct`] — those tails should not use it.
134#[allow(clippy::type_complexity)]
135struct ReturnTails<'a> {
136 /// Tail for [`ReturnStrategy::ReturnSelf`].
137 ///
138 /// Parameters: `(indent, class_name) -> lines`
139 self_tail: Box<dyn Fn(&str, &str) -> Vec<String> + 'a>,
140 /// Tail for [`ReturnStrategy::ChainableMutation`].
141 ///
142 /// Parameters: `(indent) -> lines`
143 chain_tail: Box<dyn Fn(&str) -> Vec<String> + 'a>,
144 /// Tail for [`ReturnStrategy::Direct`].
145 ///
146 /// Parameters: `(indent) -> lines`
147 direct_tail: Box<dyn Fn(&str) -> Vec<String> + 'a>,
148}
149
150/// Builder for generating R method body lines with appropriate return handling.
151///
152/// Produces lines of R code for a method body, combining the `.Call()` expression
153/// with the return strategy and the tagged-condition error guard. Each class
154/// system has specialized builder methods (`build_r6_body`, `build_s3_body`,
155/// etc.) that produce idiomatic R code for that system.
156pub struct MethodReturnBuilder {
157 /// The `.Call()` expression string (e.g., `".Call(C_Counter__inc, .call = match.call(), self)"`).
158 call_expr: String,
159 /// How to handle the return value (direct, chaining, or Self wrapping).
160 strategy: ReturnStrategy,
161 /// R class name, required when `strategy` is `ReturnSelf` to construct
162 /// the class wrapper (e.g., `"Counter"` for `Counter$new(.ptr = result)`).
163 class_name: Option<String>,
164 /// Variable name to return for `ChainableMutation` strategy (e.g., `"self"` for R6,
165 /// `"x"` for S3). Defaults to `"self"` if not set.
166 chain_var: Option<String>,
167 /// Number of leading spaces for each generated line.
168 indent: usize,
169}
170
171impl MethodReturnBuilder {
172 /// Create a new builder with the given .Call expression.
173 pub fn new(call_expr: String) -> Self {
174 Self {
175 call_expr,
176 strategy: ReturnStrategy::Direct,
177 class_name: None,
178 chain_var: None,
179 indent: 2,
180 }
181 }
182
183 /// Set the return strategy.
184 pub fn with_strategy(mut self, strategy: ReturnStrategy) -> Self {
185 self.strategy = strategy;
186 self
187 }
188
189 /// Set the class name (for Self returns).
190 pub fn with_class_name(mut self, class_name: String) -> Self {
191 self.class_name = Some(class_name);
192 self
193 }
194
195 /// Set the variable name to return for chaining (default: "self").
196 pub fn with_chain_var(mut self, var: String) -> Self {
197 self.chain_var = Some(var);
198 self
199 }
200
201 /// Set indentation level (number of spaces).
202 pub fn with_indent(mut self, indent: usize) -> Self {
203 self.indent = indent;
204 self
205 }
206
207 // region: Core shared build path
208
209 /// Emit:
210 /// ```text
211 /// <indent>.val <- <call_expr>
212 /// <indent>if (inherits(.val, ...) ...) return(...)
213 /// <tail lines — .val is live>
214 /// ```
215 ///
216 /// All paths capture the `.Call()` result, dispatch on the tagged
217 /// condition value if present, and then run the class-specific tail.
218 fn build_with_tails(&self, tails: ReturnTails<'_>) -> Vec<String> {
219 let indent = " ".repeat(self.indent);
220 let class_name = self.class_name.as_deref().unwrap_or("");
221 let call_expr = &self.call_expr;
222
223 let mut lines = vec![format!("{}.val <- {}", indent, call_expr)];
224 lines.extend(condition_check_lines(&indent));
225 match self.strategy {
226 ReturnStrategy::ReturnSelf => {
227 lines.extend((tails.self_tail)(&indent, class_name));
228 }
229 ReturnStrategy::ChainableMutation => {
230 lines.extend((tails.chain_tail)(&indent));
231 }
232 ReturnStrategy::Direct => {
233 lines.extend((tails.direct_tail)(&indent));
234 }
235 }
236 lines
237 }
238
239 // endregion
240
241 /// Build R code lines for the method body.
242 ///
243 /// Returns a vector of strings, one per line (without trailing newlines).
244 pub fn build(&self) -> Vec<String> {
245 let chain_var = self.chain_var.as_deref().unwrap_or("self").to_owned();
246 self.build_with_tails(ReturnTails {
247 self_tail: Box::new(|indent, class_name| {
248 assert!(
249 !class_name.is_empty(),
250 "class_name required for ReturnSelf strategy"
251 );
252 vec![
253 format!("{}class(.val) <- \"{}\"", indent, class_name),
254 format!("{}.val", indent),
255 ]
256 }),
257 chain_tail: Box::new(move |indent| vec![format!("{}{}", indent, chain_var)]),
258 direct_tail: Box::new(|indent| vec![format!("{}.val", indent)]),
259 })
260 }
261}
262
263/// Specialized builders for different class systems.
264impl MethodReturnBuilder {
265 /// Build R6-style return (uses invisible(self) for chaining).
266 pub fn build_r6_body(&self) -> Vec<String> {
267 self.build_with_tails(ReturnTails {
268 self_tail: Box::new(|indent, class_name| {
269 assert!(
270 !class_name.is_empty(),
271 "class_name required for ReturnSelf strategy"
272 );
273 vec![format!("{}{}$new(.ptr = .val)", indent, class_name)]
274 }),
275 chain_tail: Box::new(|indent| vec![format!("{}invisible(self)", indent)]),
276 direct_tail: Box::new(|indent| vec![format!("{}.val", indent)]),
277 })
278 }
279
280 /// Build S3-style return (uses structure() for Self returns).
281 pub fn build_s3_body(&self) -> Vec<String> {
282 let chain_var = self.chain_var.as_deref().unwrap_or("x").to_owned();
283 self.build_with_tails(ReturnTails {
284 self_tail: Box::new(|indent, class_name| {
285 assert!(
286 !class_name.is_empty(),
287 "class_name required for ReturnSelf strategy"
288 );
289 vec![format!(
290 "{}structure(.val, class = \"{}\")",
291 indent, class_name
292 )]
293 }),
294 chain_tail: Box::new(move |indent| vec![format!("{}{}", indent, chain_var)]),
295 direct_tail: Box::new(|indent| vec![format!("{}.val", indent)]),
296 })
297 }
298
299 /// Build S7-style method body lines (creates new S7 object with .ptr).
300 ///
301 /// Returns lines suitable for embedding inside an outer `function(...) { ... }`
302 /// block — unlike [`build_s7_inline`](Self::build_s7_inline) which wraps the
303 /// body in its own `{ }` and is intended for callers that emit
304 /// `function(...) <expr>` directly (e.g., S7 `convert` definitions).
305 pub fn build_s7_body(&self) -> Vec<String> {
306 // The chained-mutation tail returns the receiver. The S7 generic method
307 // names its receiver `x`; the per-class fast-path shortcut (#949) names
308 // it `self`. Honour `chain_var` (default `x`) so both reuse this body.
309 let chain_var = self.chain_var.as_deref().unwrap_or("x").to_owned();
310 self.build_with_tails(ReturnTails {
311 self_tail: Box::new(|indent, class_name| {
312 assert!(
313 !class_name.is_empty(),
314 "class_name required for ReturnSelf strategy"
315 );
316 vec![format!("{}{}(.ptr = .val)", indent, class_name)]
317 }),
318 chain_tail: Box::new(move |indent| vec![format!("{}{}", indent, chain_var)]),
319 direct_tail: Box::new(|indent| vec![format!("{}.val", indent)]),
320 })
321 }
322
323 /// Build S4-style method body lines (uses methods::new() to wrap Self returns).
324 ///
325 /// Returns lines suitable for embedding inside an outer
326 /// `function(...) { ... }` block, mirroring [`build_s7_body`](Self::build_s7_body).
327 pub fn build_s4_body(&self) -> Vec<String> {
328 self.build_with_tails(ReturnTails {
329 self_tail: Box::new(|indent, class_name| {
330 assert!(
331 !class_name.is_empty(),
332 "class_name required for ReturnSelf strategy"
333 );
334 vec![format!(
335 "{}methods::new(\"{}\", ptr = .val)",
336 indent, class_name
337 )]
338 }),
339 chain_tail: Box::new(|indent| vec![format!("{}x", indent)]),
340 direct_tail: Box::new(|indent| vec![format!("{}.val", indent)]),
341 })
342 }
343
344 /// Build S7-style return (creates new S7 object with .ptr).
345 ///
346 /// Returns a multi-line block expression that performs the condition check
347 /// inline (suitable for S7 property definitions / convert methods that
348 /// require a single expression).
349 pub fn build_s7_inline(&self) -> String {
350 let inner = match self.strategy {
351 ReturnStrategy::ReturnSelf => {
352 let class_name = self
353 .class_name
354 .as_ref()
355 .expect("class_name required for ReturnSelf strategy");
356 format!("{}(.ptr = .val)", class_name)
357 }
358 ReturnStrategy::ChainableMutation => "x".to_string(),
359 ReturnStrategy::Direct => ".val".to_string(),
360 };
361 condition_check_inline_block(&self.call_expr, &inner, " ")
362 }
363
364 /// Build S4-style return (uses methods::new()).
365 ///
366 /// Returns a multi-line block expression that performs the condition check
367 /// inline.
368 pub fn build_s4_inline(&self) -> String {
369 let inner = match self.strategy {
370 ReturnStrategy::ReturnSelf => {
371 let class_name = self
372 .class_name
373 .as_ref()
374 .expect("class_name required for ReturnSelf strategy");
375 format!("methods::new(\"{}\", ptr = .val)", class_name)
376 }
377 ReturnStrategy::ChainableMutation => "x".to_string(),
378 ReturnStrategy::Direct => ".val".to_string(),
379 };
380 condition_check_inline_block(&self.call_expr, &inner, " ")
381 }
382}
383
384#[cfg(test)]
385mod tests;
386// endregion