Skip to main content

miniextendr_macros/miniextendr_impl/
env_class.rs

1//! Env-class R wrapper generator.
2//!
3//! Generates an R environment (`new.env(parent = emptyenv())`) that serves as
4//! the class namespace, with `obj$method()` dispatched through an `$.ClassName`
5//! S3 method. This is the **fastest** of the six class systems and has **no R
6//! package dependencies**, but provides no formal class machinery: no
7//! inheritance, no multi-dispatch, no slot validation. Pick env for simple
8//! ExternalPtr-backed APIs; reach for R6/S3/S4/S7 when you need dispatch or
9//! formal class semantics.
10
11use super::ParsedImpl;
12
13/// Generates the complete R wrapper string for an environment-based class.
14///
15/// Produces an R environment object (`new.env(parent = emptyenv())`) that serves as a
16/// class namespace, with methods attached as `ClassName$method_name`. This pattern
17/// supports both inherent methods and trait namespace dispatch via `$`/`[[`.
18///
19/// The generated code includes:
20/// - Class environment: `ClassName <- new.env(parent = emptyenv())`
21/// - Constructor: `ClassName$new(...)` that calls the Rust `new` function, sets
22///   `class(self) <- "ClassName"`, and returns the ExternalPtr as `self`
23/// - Instance methods: `ClassName$method(x = self, ...)` using default-arg binding
24///   so that `$` dispatch re-parents the environment to make `self` visible
25/// - Static methods: `ClassName$method(...)` that call Rust directly
26/// - `$.ClassName` S3 method: dispatches `obj$method(...)` by looking up the method
27///   in the class environment, binding `self` for instance methods, and supporting
28///   trait namespace environments (nested envs with `.__mx_instance__` attributes)
29/// - `[[.ClassName` alias: delegates to `$.ClassName`
30///
31/// Roxygen2 documentation is generated for the class, each method, and the
32/// dispatch methods, with appropriate `@export`/`@keywords internal`/`@noRd` tags.
33pub fn generate_env_r_wrapper(parsed_impl: &ParsedImpl) -> String {
34    use crate::r_class_formatter::{
35        ClassDocBuilder, MethodDocBuilder, ParsedImplExt, should_export_from_tags,
36    };
37
38    let class_name = parsed_impl.class_name();
39    let type_ident = &parsed_impl.type_ident;
40    // Check if class has @noRd - if so, skip method documentation. A plain
41    // `noexport` (without `internal`) is folded in too — it must suppress Rd
42    // contribution entirely, matching `ClassDocBuilder::build`'s `suppress_rd` gate.
43    let class_has_no_rd = crate::roxygen::has_roxygen_tag(&parsed_impl.doc_tags, "noRd")
44        || (parsed_impl.noexport && !parsed_impl.internal);
45
46    let mut lines = Vec::new();
47
48    // Class environment documentation and definition
49    lines.extend(
50        ClassDocBuilder::new(&class_name, type_ident, &parsed_impl.doc_tags, "")
51            .with_export_control(parsed_impl.internal, parsed_impl.noexport)
52            .build(),
53    );
54    // Inject lifecycle imports from methods into class-level roxygen block
55    if let Some(lc_import) = crate::lifecycle::collect_lifecycle_imports(
56        parsed_impl
57            .methods
58            .iter()
59            .filter_map(|m| m.method_attrs.lifecycle.as_ref()),
60    ) {
61        let insert_pos = lines.len().saturating_sub(1);
62        lines.insert(insert_pos, format!("#' {}", lc_import));
63    }
64    lines.push(format!("{} <- new.env(parent = emptyenv())", class_name));
65    lines.push(String::new());
66
67    // Constructor
68    if let Some(ctx) = parsed_impl.constructor_context() {
69        lines.push(ctx.source_comment(type_ident));
70        // Skip method documentation if class has @noRd
71        if !class_has_no_rd {
72            let method_doc =
73                MethodDocBuilder::new(&class_name, "new", type_ident, &ctx.method.doc_tags)
74                    .with_name_prefix("$")
75                    .with_params_as_details();
76            lines.extend(method_doc.build());
77        }
78        lines.push(format!("{}$new <- function({}) {{", class_name, ctx.params));
79        for check in ctx.precondition_checks() {
80            lines.push(format!("  {}", check));
81        }
82        // Inject match.arg validation for match_arg/choices params
83        for line in ctx.match_arg_prelude() {
84            lines.push(format!("  {}", line));
85        }
86        lines.push(format!("  .val <- {}", ctx.static_call()));
87        lines.extend(crate::method_return_builder::condition_check_lines("  "));
88        lines.push("  self <- .val".to_string());
89        lines.push(format!("  class(self) <- \"{}\"", class_name));
90        lines.push("  self".to_string());
91        lines.push("}".to_string());
92        lines.push(String::new());
93    }
94
95    // Instance methods
96    for ctx in parsed_impl.instance_method_contexts() {
97        let method_name = ctx.method.r_method_name();
98        lines.push(ctx.source_comment(type_ident));
99        // Skip method documentation if class has @noRd
100        if !class_has_no_rd {
101            let method_doc =
102                MethodDocBuilder::new(&class_name, &method_name, type_ident, &ctx.method.doc_tags)
103                    .with_name_prefix("$")
104                    .with_params_as_details();
105            lines.extend(method_doc.build());
106        }
107
108        lines.push(format!(
109            "{}${} <- function({}) {{",
110            class_name, method_name, ctx.params
111        ));
112
113        let what = format!("{}${}", class_name, method_name);
114        ctx.emit_method_prelude(&mut lines, "  ", &what);
115
116        let call = ctx.instance_call("self");
117        let strategy = crate::ReturnStrategy::for_method(ctx.method);
118        let return_builder = crate::MethodReturnBuilder::new(call)
119            .with_strategy(strategy)
120            .with_class_name(class_name.clone());
121        lines.extend(return_builder.build());
122
123        lines.push("}".to_string());
124        lines.push(String::new());
125    }
126
127    // Static methods
128    for ctx in parsed_impl.static_method_contexts() {
129        let method_name = ctx.method.r_method_name();
130        lines.push(ctx.source_comment(type_ident));
131        // Skip method documentation if class has @noRd
132        if !class_has_no_rd {
133            let method_doc =
134                MethodDocBuilder::new(&class_name, &method_name, type_ident, &ctx.method.doc_tags)
135                    .with_name_prefix("$")
136                    .with_params_as_details();
137            lines.extend(method_doc.build());
138        }
139
140        lines.push(format!(
141            "{}${} <- function({}) {{",
142            class_name, method_name, ctx.params
143        ));
144
145        let what = format!("{}${}", class_name, method_name);
146        ctx.emit_method_prelude(&mut lines, "  ", &what);
147
148        let strategy = crate::ReturnStrategy::for_method(ctx.method);
149        let return_builder = crate::MethodReturnBuilder::new(ctx.static_call())
150            .with_strategy(strategy)
151            .with_class_name(class_name.clone());
152        lines.extend(return_builder.build());
153
154        lines.push("}".to_string());
155        lines.push(String::new());
156    }
157
158    // $ dispatch - export as S3 methods
159    // Handles both functions (inherent methods) and environments (trait namespaces)
160    let should_export = should_export_from_tags(
161        &parsed_impl.doc_tags,
162        parsed_impl.noexport || parsed_impl.internal,
163    );
164
165    // Generate roxygen tags for dispatch methods.
166    // roxygen2 8.0.0+ enforces that any `generic.class`-named function carry
167    // @export or @exportS3Method (@noRd alone doesn't satisfy the check). We
168    // always emit @export so roxygen2 emits a properly-quoted
169    // `S3method("$", Class)` / `S3method("[[", Class)` (bare @exportS3Method
170    // skips the operator-name quoting and produces invalid NAMESPACE entries).
171    // For internal/noexport classes the @rdname target is dropped so the
172    // helpers don't bleed into the user-visible Rd page.
173    if class_has_no_rd {
174        lines.push("#' @noRd".to_string());
175        lines.push("#' @export".to_string());
176    } else if !should_export {
177        lines.push("#' @export".to_string());
178    } else {
179        lines.push(format!("#' @rdname {}", class_name));
180        lines.push("#' @param self The object instance.".to_string());
181        lines.push("#' @param name Method name for dispatch.".to_string());
182        lines.push("#' @export".to_string());
183    }
184    lines.push(format!("`$.{}` <- function(self, name) {{", class_name));
185    lines.push(format!("  obj <- {}[[name]]", class_name));
186    lines.push("  if (is.environment(obj)) {".to_string());
187    lines.push("    # Trait namespace - wrap instance methods to prepend self".to_string());
188    lines.push("    bound <- new.env(parent = emptyenv())".to_string());
189    lines.push("    for (method_name in names(obj)) {".to_string());
190    lines.push("      method <- obj[[method_name]]".to_string());
191    lines.push("      if (is.function(method)) {".to_string());
192    lines.push("        if (isTRUE(attr(method, \".__mx_instance__\"))) {".to_string());
193    lines.push("          local({".to_string());
194    lines.push("            m <- method".to_string());
195    lines.push("            bound[[method_name]] <<- function(...) m(self, ...)".to_string());
196    lines.push("          })".to_string());
197    lines.push("        } else {".to_string());
198    lines.push("          bound[[method_name]] <- method".to_string());
199    lines.push("        }".to_string());
200    lines.push("      }".to_string());
201    lines.push("    }".to_string());
202    lines.push("    bound".to_string());
203    lines.push("  } else if (is.null(obj)) {".to_string());
204    lines.push("    # Not found at top level -- search trait namespace environments".to_string());
205    lines.push(format!("    for (ns_name in names({})) {{", class_name));
206    lines.push(format!("      ns <- {}[[ns_name]]", class_name));
207    lines.push(
208        "      if (is.environment(ns) && exists(name, envir = ns, inherits = FALSE)) {".to_string(),
209    );
210    lines.push("        method <- ns[[name]]".to_string());
211    lines.push(
212        "        if (is.function(method) && isTRUE(attr(method, \".__mx_instance__\"))) {"
213            .to_string(),
214    );
215    lines.push("          # Instance method -- bind self as first arg".to_string());
216    lines.push("          m <- method".to_string());
217    lines.push("          s <- self".to_string());
218    lines.push("          return(function(...) m(s, ...))".to_string());
219    lines.push("        } else if (is.function(method)) {".to_string());
220    lines.push("          return(method)".to_string());
221    lines.push("        }".to_string());
222    lines.push("      }".to_string());
223    lines.push("    }".to_string());
224    lines.push("    NULL".to_string());
225    lines.push("  } else {".to_string());
226    lines.push("    environment(obj) <- environment()".to_string());
227    lines.push("    obj".to_string());
228    lines.push("  }".to_string());
229    lines.push("}".to_string());
230    if class_has_no_rd {
231        lines.push("#' @noRd".to_string());
232        lines.push("#' @export".to_string());
233    } else if !should_export {
234        lines.push("#' @export".to_string());
235    } else {
236        lines.push(format!("#' @rdname {}", class_name));
237        lines.push("#' @export".to_string());
238    }
239    lines.push(format!("`[[.{}` <- `$.{}`", class_name, class_name));
240
241    lines.join("\n")
242}