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 returns a bare type name that may be another registered
86    /// ExternalPtr-backed class. The final decision is deferred to
87    /// `write_r_wrappers_to_file`, where the complete class registry is known.
88    ReturnOtherClass,
89    /// The method returns a *list* of values (`Vec<Class>`, plus the
90    /// `Option<Vec<Class>>` / `Result<Vec<Class>, E>` wrappers the C wrapper
91    /// already unwraps) whose element type may be another registered
92    /// ExternalPtr-backed class (#1284). Deferred to `write_r_wrappers_to_file`
93    /// like [`ReturnOtherClass`](Self::ReturnOtherClass), but through a
94    /// distinct `.__MX_WRAP_LIST_RETURN_*` marker that resolves to a per-element
95    /// `lapply(...)` wrap. The marker prefix must never overlap the scalar
96    /// `.__MX_WRAP_RETURN_` family: the scalar resolver rewrites unrecognized
97    /// markers of its own prefix to the bare expression, so a shared prefix
98    /// would consume list markers before the list resolver ran.
99    ReturnOtherClassList,
100    /// The method is a `&mut self` method returning `()`. The wrapper calls the
101    /// `.Call()` for its side effect and returns the receiver (`self`/`x`) for
102    /// method chaining (e.g., `invisible(self)` for R6).
103    ChainableMutation,
104    /// Default strategy: return the `.Call()` result directly without wrapping.
105    Direct,
106}
107
108impl ReturnStrategy {
109    /// Determine the return strategy for a parsed method.
110    ///
111    /// - Methods that return `Self`, `Result<Self, E>`, or `Option<Self>` use
112    ///   `ReturnSelf`. For the latter two, the C wrapper already raised on
113    ///   `Err` / `None` (see
114    ///   [`crate::c_wrapper_builder::ReturnHandling::ResultExternalPtr`] and
115    ///   [`crate::c_wrapper_builder::ReturnHandling::OptionExternalPtr`]), so a
116    ///   successful `.val` is a bare ExternalPtr — identical in shape to the
117    ///   bare-`Self` case — and gets the same class-wrapping tail.
118    /// - In-place builders (`&mut self -> &mut Self` / `&self -> Self`) and
119    ///   `&mut self -> ()` methods use `ChainableMutation`. Both return the
120    ///   receiver object (`x` / `invisible(self)`) so the call composes under
121    ///   the native pipe (`obj |> set_a(1) |> set_b(2)`); the C wrapper hands
122    ///   back the same ExternalPtr handle (see
123    ///   [`crate::c_wrapper_builder::ReturnHandling::SelfHandle`]).
124    /// - Bare capitalized return types that are not known primitives/containers
125    ///   use `ReturnOtherClass`; write-time registry lookup wraps registered
126    ///   classes and leaves false positives unchanged.
127    /// - `Vec<Class>` (and the `Option`/`Result` wrappers of it the C wrapper
128    ///   unwraps) use `ReturnOtherClassList`; write-time registry lookup wraps
129    ///   registered element classes via `lapply` and leaves false positives
130    ///   unchanged (#1284).
131    /// - All other methods use `Direct`.
132    pub fn for_method(method: &ParsedMethod) -> Self {
133        // In-place builders (`&mut self -> &mut Self` / `&self -> Self`) and
134        // `&mut self -> ()` mutators both return the receiver object.
135        let is_self_ref_builder = method.returns_self_ref() && method.env.is_instance();
136        let is_unit_mutator = method.env.is_mut() && method.returns_unit();
137        if method.returns_self() || method.returns_result_self() || method.returns_option_self() {
138            ReturnStrategy::ReturnSelf
139        } else if is_self_ref_builder || is_unit_mutator {
140            ReturnStrategy::ChainableMutation
141        } else if method.returns_other_class().is_some() {
142            ReturnStrategy::ReturnOtherClass
143        } else if method.returns_other_class_list().is_some() {
144            ReturnStrategy::ReturnOtherClassList
145        } else {
146            ReturnStrategy::Direct
147        }
148    }
149}
150
151/// Class-specific tail closures, one per [`ReturnStrategy`] variant.
152///
153/// Each closure receives `(indent, class_name)` and produces the tail lines that
154/// run after `.val <- <call_expr>` and the condition check (both emitted by
155/// [`MethodReturnBuilder::build_with_tails`]). The tail therefore always
156/// references `.val` directly.
157///
158/// `class_name` is `""` for [`ReturnStrategy::ChainableMutation`],
159/// [`ReturnStrategy::ReturnOtherClass`],
160/// [`ReturnStrategy::ReturnOtherClassList`], and [`ReturnStrategy::Direct`] —
161/// those tails should not use it.
162#[allow(clippy::type_complexity)]
163struct ReturnTails<'a> {
164    /// Tail for [`ReturnStrategy::ReturnSelf`].
165    ///
166    /// Parameters: `(indent, class_name) -> lines`
167    self_tail: Box<dyn Fn(&str, &str) -> Vec<String> + 'a>,
168    /// Tail for [`ReturnStrategy::ChainableMutation`].
169    ///
170    /// Parameters: `(indent) -> lines`
171    chain_tail: Box<dyn Fn(&str) -> Vec<String> + 'a>,
172    /// Tail for [`ReturnStrategy::Direct`].
173    ///
174    /// Parameters: `(indent) -> lines`
175    direct_tail: Box<dyn Fn(&str) -> Vec<String> + 'a>,
176}
177
178/// Builder for generating R method body lines with appropriate return handling.
179///
180/// Produces lines of R code for a method body, combining the `.Call()` expression
181/// with the return strategy and the tagged-condition error guard. Each class
182/// system has specialized builder methods (`build_r6_body`, `build_s3_body`,
183/// etc.) that produce idiomatic R code for that system.
184pub struct MethodReturnBuilder {
185    /// The `.Call()` expression string (e.g., `".Call(C_Counter__inc, .call = match.call(), self)"`).
186    call_expr: String,
187    /// How to handle the return value (direct, chaining, or Self wrapping).
188    strategy: ReturnStrategy,
189    /// R class name, required when `strategy` is `ReturnSelf` to construct
190    /// the class wrapper (e.g., `"Counter"` for `Counter$new(.ptr = result)`).
191    class_name: Option<String>,
192    /// Rust return type name, required when `strategy` is `ReturnOtherClass`
193    /// or `ReturnOtherClassList` (the element type in the list case). The
194    /// write-time resolver maps this Rust name to the registered target class
195    /// and constructor syntax, or falls back to the bare value on miss.
196    return_class: Option<String>,
197    /// Variable name to return for `ChainableMutation` strategy (e.g., `"self"` for R6,
198    /// `"x"` for S3). Defaults to `"self"` if not set.
199    chain_var: Option<String>,
200    /// Number of leading spaces for each generated line.
201    indent: usize,
202}
203
204impl MethodReturnBuilder {
205    /// Create a new builder with the given .Call expression.
206    pub fn new(call_expr: String) -> Self {
207        Self {
208            call_expr,
209            strategy: ReturnStrategy::Direct,
210            class_name: None,
211            return_class: None,
212            chain_var: None,
213            indent: 2,
214        }
215    }
216
217    /// Set the return strategy.
218    pub fn with_strategy(mut self, strategy: ReturnStrategy) -> Self {
219        self.strategy = strategy;
220        self
221    }
222
223    /// Set the class name (for Self returns).
224    pub fn with_class_name(mut self, class_name: String) -> Self {
225        self.class_name = Some(class_name);
226        self
227    }
228
229    /// Set the return class name (for cross-class ExternalPtr returns).
230    pub fn with_return_class(mut self, return_class: String) -> Self {
231        self.return_class = Some(return_class);
232        self
233    }
234
235    /// Attach the method's cross-class return name when this builder uses
236    /// [`ReturnStrategy::ReturnOtherClass`] or
237    /// [`ReturnStrategy::ReturnOtherClassList`].
238    pub fn with_return_class_from_method(mut self, method: &ParsedMethod) -> Self {
239        match self.strategy {
240            ReturnStrategy::ReturnOtherClass => {
241                let return_class = method
242                    .returns_other_class()
243                    .expect("return_class required for ReturnOtherClass strategy");
244                self = self.with_return_class(return_class.to_string());
245            }
246            ReturnStrategy::ReturnOtherClassList => {
247                let return_class = method
248                    .returns_other_class_list()
249                    .expect("return_class required for ReturnOtherClassList strategy");
250                self = self.with_return_class(return_class.to_string());
251            }
252            _ => {}
253        }
254        self
255    }
256
257    /// Set the variable name to return for chaining (default: "self").
258    pub fn with_chain_var(mut self, var: String) -> Self {
259        self.chain_var = Some(var);
260        self
261    }
262
263    /// Set indentation level (number of spaces).
264    pub fn with_indent(mut self, indent: usize) -> Self {
265        self.indent = indent;
266        self
267    }
268
269    fn return_other_class_expr(&self) -> String {
270        let return_class = self
271            .return_class
272            .as_ref()
273            .expect("return_class required for ReturnOtherClass strategy");
274        format!(".__MX_WRAP_RETURN_{return_class}__(.val)")
275    }
276
277    /// List-shaped sibling of
278    /// [`return_other_class_expr`](Self::return_other_class_expr) (#1284).
279    ///
280    /// The `LIST_` infix keeps the marker family disjoint from the scalar
281    /// `.__MX_WRAP_RETURN_` prefix: the scalar resolver rewrites unrecognized
282    /// markers of its own prefix to the bare expression, so a shared prefix
283    /// would destroy list markers before the list resolver saw them.
284    fn return_other_class_list_expr(&self) -> String {
285        let return_class = self
286            .return_class
287            .as_ref()
288            .expect("return_class required for ReturnOtherClassList strategy");
289        format!(".__MX_WRAP_LIST_RETURN_{return_class}__(.val)")
290    }
291
292    // region: Core shared build path
293
294    /// Emit:
295    /// ```text
296    /// <indent>.val <- <call_expr>
297    /// <indent>if (inherits(.val, ...) ...) return(...)
298    /// <tail lines — .val is live>
299    /// ```
300    ///
301    /// All paths capture the `.Call()` result, dispatch on the tagged
302    /// condition value if present, and then run the class-specific tail.
303    fn build_with_tails(&self, tails: ReturnTails<'_>) -> Vec<String> {
304        let indent = " ".repeat(self.indent);
305        let class_name = self.class_name.as_deref().unwrap_or("");
306        let call_expr = &self.call_expr;
307
308        let mut lines = vec![format!("{}.val <- {}", indent, call_expr)];
309        lines.extend(condition_check_lines(&indent));
310        match self.strategy {
311            ReturnStrategy::ReturnSelf => {
312                lines.extend((tails.self_tail)(&indent, class_name));
313            }
314            ReturnStrategy::ChainableMutation => {
315                lines.extend((tails.chain_tail)(&indent));
316            }
317            ReturnStrategy::ReturnOtherClass => {
318                lines.push(format!("{}{}", indent, self.return_other_class_expr()));
319            }
320            ReturnStrategy::ReturnOtherClassList => {
321                lines.push(format!("{}{}", indent, self.return_other_class_list_expr()));
322            }
323            ReturnStrategy::Direct => {
324                lines.extend((tails.direct_tail)(&indent));
325            }
326        }
327        lines
328    }
329
330    // endregion
331
332    /// Build R code lines for the method body.
333    ///
334    /// Returns a vector of strings, one per line (without trailing newlines).
335    pub fn build(&self) -> Vec<String> {
336        let chain_var = self.chain_var.as_deref().unwrap_or("self").to_owned();
337        self.build_with_tails(ReturnTails {
338            self_tail: Box::new(|indent, class_name| {
339                assert!(
340                    !class_name.is_empty(),
341                    "class_name required for ReturnSelf strategy"
342                );
343                vec![
344                    format!("{}class(.val) <- \"{}\"", indent, class_name),
345                    format!("{}.val", indent),
346                ]
347            }),
348            chain_tail: Box::new(move |indent| vec![format!("{}{}", indent, chain_var)]),
349            direct_tail: Box::new(|indent| vec![format!("{}.val", indent)]),
350        })
351    }
352}
353
354/// Specialized builders for different class systems.
355impl MethodReturnBuilder {
356    /// Build R6-style return (uses invisible(self) for chaining).
357    pub fn build_r6_body(&self) -> Vec<String> {
358        self.build_with_tails(ReturnTails {
359            self_tail: Box::new(|indent, class_name| {
360                assert!(
361                    !class_name.is_empty(),
362                    "class_name required for ReturnSelf strategy"
363                );
364                vec![format!("{}{}$new(.ptr = .val)", indent, class_name)]
365            }),
366            chain_tail: Box::new(|indent| vec![format!("{}invisible(self)", indent)]),
367            direct_tail: Box::new(|indent| vec![format!("{}.val", indent)]),
368        })
369    }
370
371    /// Build S3-style return (uses structure() for Self returns).
372    pub fn build_s3_body(&self) -> Vec<String> {
373        let chain_var = self.chain_var.as_deref().unwrap_or("x").to_owned();
374        self.build_with_tails(ReturnTails {
375            self_tail: Box::new(|indent, class_name| {
376                assert!(
377                    !class_name.is_empty(),
378                    "class_name required for ReturnSelf strategy"
379                );
380                vec![format!(
381                    "{}structure(.val, class = \"{}\")",
382                    indent, class_name
383                )]
384            }),
385            chain_tail: Box::new(move |indent| vec![format!("{}{}", indent, chain_var)]),
386            direct_tail: Box::new(|indent| vec![format!("{}.val", indent)]),
387        })
388    }
389
390    /// Build S7-style method body lines (creates new S7 object with .ptr).
391    ///
392    /// Returns lines suitable for embedding inside an outer `function(...) { ... }`
393    /// block — unlike [`build_s7_inline`](Self::build_s7_inline) which wraps the
394    /// body in its own `{ }` and is intended for callers that emit
395    /// `function(...) <expr>` directly (e.g., S7 `convert` definitions).
396    pub fn build_s7_body(&self) -> Vec<String> {
397        // The chained-mutation tail returns the receiver. The S7 generic method
398        // names its receiver `x`; the per-class fast-path shortcut (#949) names
399        // it `self`. Honour `chain_var` (default `x`) so both reuse this body.
400        let chain_var = self.chain_var.as_deref().unwrap_or("x").to_owned();
401        self.build_with_tails(ReturnTails {
402            self_tail: Box::new(|indent, class_name| {
403                assert!(
404                    !class_name.is_empty(),
405                    "class_name required for ReturnSelf strategy"
406                );
407                vec![format!("{}{}(.ptr = .val)", indent, class_name)]
408            }),
409            chain_tail: Box::new(move |indent| vec![format!("{}{}", indent, chain_var)]),
410            direct_tail: Box::new(|indent| vec![format!("{}.val", indent)]),
411        })
412    }
413
414    /// Build S4-style method body lines (uses methods::new() to wrap Self returns).
415    ///
416    /// Returns lines suitable for embedding inside an outer
417    /// `function(...) { ... }` block, mirroring [`build_s7_body`](Self::build_s7_body).
418    pub fn build_s4_body(&self) -> Vec<String> {
419        self.build_with_tails(ReturnTails {
420            self_tail: Box::new(|indent, class_name| {
421                assert!(
422                    !class_name.is_empty(),
423                    "class_name required for ReturnSelf strategy"
424                );
425                vec![format!(
426                    "{}methods::new(\"{}\", ptr = .val)",
427                    indent, class_name
428                )]
429            }),
430            chain_tail: Box::new(|indent| vec![format!("{}x", indent)]),
431            direct_tail: Box::new(|indent| vec![format!("{}.val", indent)]),
432        })
433    }
434
435    /// Build S7-style return (creates new S7 object with .ptr).
436    ///
437    /// Returns a multi-line block expression that performs the condition check
438    /// inline (suitable for S7 property definitions / convert methods that
439    /// require a single expression).
440    pub fn build_s7_inline(&self) -> String {
441        let inner = match self.strategy {
442            ReturnStrategy::ReturnSelf => {
443                let class_name = self
444                    .class_name
445                    .as_ref()
446                    .expect("class_name required for ReturnSelf strategy");
447                format!("{}(.ptr = .val)", class_name)
448            }
449            ReturnStrategy::ChainableMutation => "x".to_string(),
450            ReturnStrategy::ReturnOtherClass => self.return_other_class_expr(),
451            ReturnStrategy::ReturnOtherClassList => self.return_other_class_list_expr(),
452            ReturnStrategy::Direct => ".val".to_string(),
453        };
454        condition_check_inline_block(&self.call_expr, &inner, "    ")
455    }
456
457    /// Build S4-style return (uses methods::new()).
458    ///
459    /// Returns a multi-line block expression that performs the condition check
460    /// inline.
461    pub fn build_s4_inline(&self) -> String {
462        let inner = match self.strategy {
463            ReturnStrategy::ReturnSelf => {
464                let class_name = self
465                    .class_name
466                    .as_ref()
467                    .expect("class_name required for ReturnSelf strategy");
468                format!("methods::new(\"{}\", ptr = .val)", class_name)
469            }
470            ReturnStrategy::ChainableMutation => "x".to_string(),
471            ReturnStrategy::ReturnOtherClass => self.return_other_class_expr(),
472            ReturnStrategy::ReturnOtherClassList => self.return_other_class_list_expr(),
473            ReturnStrategy::Direct => ".val".to_string(),
474        };
475        condition_check_inline_block(&self.call_expr, &inner, "    ")
476    }
477}
478
479#[cfg(test)]
480mod tests;
481// endregion