Skip to main content

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` use `ReturnSelf`
97    /// - `&mut self` methods returning `()` use `ChainableMutation`
98    /// - All other methods use `Direct`
99    pub fn for_method(method: &ParsedMethod) -> Self {
100        if method.returns_self() {
101            ReturnStrategy::ReturnSelf
102        } else if method.env.is_mut() && method.returns_unit() {
103            ReturnStrategy::ChainableMutation
104        } else {
105            ReturnStrategy::Direct
106        }
107    }
108}
109
110/// Class-specific tail closures, one per [`ReturnStrategy`] variant.
111///
112/// Each closure receives `(indent, class_name)` and produces the tail lines that
113/// run after `.val <- <call_expr>` and the condition check (both emitted by
114/// [`MethodReturnBuilder::build_with_tails`]). The tail therefore always
115/// references `.val` directly.
116///
117/// `class_name` is `""` for [`ReturnStrategy::ChainableMutation`] and
118/// [`ReturnStrategy::Direct`] — those tails should not use it.
119#[allow(clippy::type_complexity)]
120struct ReturnTails<'a> {
121    /// Tail for [`ReturnStrategy::ReturnSelf`].
122    ///
123    /// Parameters: `(indent, class_name) -> lines`
124    self_tail: Box<dyn Fn(&str, &str) -> Vec<String> + 'a>,
125    /// Tail for [`ReturnStrategy::ChainableMutation`].
126    ///
127    /// Parameters: `(indent) -> lines`
128    chain_tail: Box<dyn Fn(&str) -> Vec<String> + 'a>,
129    /// Tail for [`ReturnStrategy::Direct`].
130    ///
131    /// Parameters: `(indent) -> lines`
132    direct_tail: Box<dyn Fn(&str) -> Vec<String> + 'a>,
133}
134
135/// Builder for generating R method body lines with appropriate return handling.
136///
137/// Produces lines of R code for a method body, combining the `.Call()` expression
138/// with the return strategy and the tagged-condition error guard. Each class
139/// system has specialized builder methods (`build_r6_body`, `build_s3_body`,
140/// etc.) that produce idiomatic R code for that system.
141pub struct MethodReturnBuilder {
142    /// The `.Call()` expression string (e.g., `".Call(C_Counter__inc, .call = match.call(), self)"`).
143    call_expr: String,
144    /// How to handle the return value (direct, chaining, or Self wrapping).
145    strategy: ReturnStrategy,
146    /// R class name, required when `strategy` is `ReturnSelf` to construct
147    /// the class wrapper (e.g., `"Counter"` for `Counter$new(.ptr = result)`).
148    class_name: Option<String>,
149    /// Variable name to return for `ChainableMutation` strategy (e.g., `"self"` for R6,
150    /// `"x"` for S3). Defaults to `"self"` if not set.
151    chain_var: Option<String>,
152    /// Number of leading spaces for each generated line.
153    indent: usize,
154}
155
156impl MethodReturnBuilder {
157    /// Create a new builder with the given .Call expression.
158    pub fn new(call_expr: String) -> Self {
159        Self {
160            call_expr,
161            strategy: ReturnStrategy::Direct,
162            class_name: None,
163            chain_var: None,
164            indent: 2,
165        }
166    }
167
168    /// Set the return strategy.
169    pub fn with_strategy(mut self, strategy: ReturnStrategy) -> Self {
170        self.strategy = strategy;
171        self
172    }
173
174    /// Set the class name (for Self returns).
175    pub fn with_class_name(mut self, class_name: String) -> Self {
176        self.class_name = Some(class_name);
177        self
178    }
179
180    /// Set the variable name to return for chaining (default: "self").
181    pub fn with_chain_var(mut self, var: String) -> Self {
182        self.chain_var = Some(var);
183        self
184    }
185
186    /// Set indentation level (number of spaces).
187    pub fn with_indent(mut self, indent: usize) -> Self {
188        self.indent = indent;
189        self
190    }
191
192    // region: Core shared build path
193
194    /// Emit:
195    /// ```text
196    /// <indent>.val <- <call_expr>
197    /// <indent>if (inherits(.val, ...) ...) return(...)
198    /// <tail lines — .val is live>
199    /// ```
200    ///
201    /// All paths capture the `.Call()` result, dispatch on the tagged
202    /// condition value if present, and then run the class-specific tail.
203    fn build_with_tails(&self, tails: ReturnTails<'_>) -> Vec<String> {
204        let indent = " ".repeat(self.indent);
205        let class_name = self.class_name.as_deref().unwrap_or("");
206        let call_expr = &self.call_expr;
207
208        let mut lines = vec![format!("{}.val <- {}", indent, call_expr)];
209        lines.extend(condition_check_lines(&indent));
210        match self.strategy {
211            ReturnStrategy::ReturnSelf => {
212                lines.extend((tails.self_tail)(&indent, class_name));
213            }
214            ReturnStrategy::ChainableMutation => {
215                lines.extend((tails.chain_tail)(&indent));
216            }
217            ReturnStrategy::Direct => {
218                lines.extend((tails.direct_tail)(&indent));
219            }
220        }
221        lines
222    }
223
224    // endregion
225
226    /// Build R code lines for the method body.
227    ///
228    /// Returns a vector of strings, one per line (without trailing newlines).
229    pub fn build(&self) -> Vec<String> {
230        let chain_var = self.chain_var.as_deref().unwrap_or("self").to_owned();
231        self.build_with_tails(ReturnTails {
232            self_tail: Box::new(|indent, class_name| {
233                assert!(
234                    !class_name.is_empty(),
235                    "class_name required for ReturnSelf strategy"
236                );
237                vec![
238                    format!("{}class(.val) <- \"{}\"", indent, class_name),
239                    format!("{}.val", indent),
240                ]
241            }),
242            chain_tail: Box::new(move |indent| vec![format!("{}{}", indent, chain_var)]),
243            direct_tail: Box::new(|indent| vec![format!("{}.val", indent)]),
244        })
245    }
246}
247
248/// Specialized builders for different class systems.
249impl MethodReturnBuilder {
250    /// Build R6-style return (uses invisible(self) for chaining).
251    pub fn build_r6_body(&self) -> Vec<String> {
252        self.build_with_tails(ReturnTails {
253            self_tail: Box::new(|indent, class_name| {
254                assert!(
255                    !class_name.is_empty(),
256                    "class_name required for ReturnSelf strategy"
257                );
258                vec![format!("{}{}$new(.ptr = .val)", indent, class_name)]
259            }),
260            chain_tail: Box::new(|indent| vec![format!("{}invisible(self)", indent)]),
261            direct_tail: Box::new(|indent| vec![format!("{}.val", indent)]),
262        })
263    }
264
265    /// Build S3-style return (uses structure() for Self returns).
266    pub fn build_s3_body(&self) -> Vec<String> {
267        let chain_var = self.chain_var.as_deref().unwrap_or("x").to_owned();
268        self.build_with_tails(ReturnTails {
269            self_tail: Box::new(|indent, class_name| {
270                assert!(
271                    !class_name.is_empty(),
272                    "class_name required for ReturnSelf strategy"
273                );
274                vec![format!(
275                    "{}structure(.val, class = \"{}\")",
276                    indent, class_name
277                )]
278            }),
279            chain_tail: Box::new(move |indent| vec![format!("{}{}", indent, chain_var)]),
280            direct_tail: Box::new(|indent| vec![format!("{}.val", indent)]),
281        })
282    }
283
284    /// Build S7-style method body lines (creates new S7 object with .ptr).
285    ///
286    /// Returns lines suitable for embedding inside an outer `function(...) { ... }`
287    /// block — unlike [`build_s7_inline`](Self::build_s7_inline) which wraps the
288    /// body in its own `{ }` and is intended for callers that emit
289    /// `function(...) <expr>` directly (e.g., S7 `convert` definitions).
290    pub fn build_s7_body(&self) -> Vec<String> {
291        self.build_with_tails(ReturnTails {
292            self_tail: Box::new(|indent, class_name| {
293                assert!(
294                    !class_name.is_empty(),
295                    "class_name required for ReturnSelf strategy"
296                );
297                vec![format!("{}{}(.ptr = .val)", indent, class_name)]
298            }),
299            chain_tail: Box::new(|indent| vec![format!("{}x", indent)]),
300            direct_tail: Box::new(|indent| vec![format!("{}.val", indent)]),
301        })
302    }
303
304    /// Build S4-style method body lines (uses methods::new() to wrap Self returns).
305    ///
306    /// Returns lines suitable for embedding inside an outer
307    /// `function(...) { ... }` block, mirroring [`build_s7_body`](Self::build_s7_body).
308    pub fn build_s4_body(&self) -> Vec<String> {
309        self.build_with_tails(ReturnTails {
310            self_tail: Box::new(|indent, class_name| {
311                assert!(
312                    !class_name.is_empty(),
313                    "class_name required for ReturnSelf strategy"
314                );
315                vec![format!(
316                    "{}methods::new(\"{}\", ptr = .val)",
317                    indent, class_name
318                )]
319            }),
320            chain_tail: Box::new(|indent| vec![format!("{}x", indent)]),
321            direct_tail: Box::new(|indent| vec![format!("{}.val", indent)]),
322        })
323    }
324
325    /// Build S7-style return (creates new S7 object with .ptr).
326    ///
327    /// Returns a multi-line block expression that performs the condition check
328    /// inline (suitable for S7 property definitions / convert methods that
329    /// require a single expression).
330    pub fn build_s7_inline(&self) -> String {
331        let inner = match self.strategy {
332            ReturnStrategy::ReturnSelf => {
333                let class_name = self
334                    .class_name
335                    .as_ref()
336                    .expect("class_name required for ReturnSelf strategy");
337                format!("{}(.ptr = .val)", class_name)
338            }
339            ReturnStrategy::ChainableMutation => "x".to_string(),
340            ReturnStrategy::Direct => ".val".to_string(),
341        };
342        condition_check_inline_block(&self.call_expr, &inner, "    ")
343    }
344
345    /// Build S4-style return (uses methods::new()).
346    ///
347    /// Returns a multi-line block expression that performs the condition check
348    /// inline.
349    pub fn build_s4_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!("methods::new(\"{}\", 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
365#[cfg(test)]
366mod tests;
367// endregion