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 R wrapper of an environment-based class, in the parts the
14/// wrapper writer merges per type ([`super::ClassWrapper`], #1776).
15///
16/// Produces an R environment object (`new.env(parent = emptyenv())`) that serves as a
17/// class namespace, with methods attached as `ClassName$method_name`. This pattern
18/// supports both inherent methods and trait namespace dispatch via `$`/`[[`.
19///
20/// The generated code includes:
21/// - Class environment: `ClassName <- new.env(parent = emptyenv())`
22/// - Constructor: `ClassName$new(...)` that calls the Rust `new` function, sets
23///   `class(self) <- "ClassName"`, and returns the ExternalPtr as `self`
24/// - Instance methods: `ClassName$method(...)` whose bodies read a free `self`,
25///   which `$` dispatch binds in a fresh environment over the method's own
26/// - Static methods: `ClassName$method(...)` that call Rust directly
27/// - `$.ClassName` S3 method: dispatches `obj$method(...)` by looking up the method
28///   in the class environment, binding `.mx_self` for instance methods, and supporting
29///   trait namespace environments (nested envs with `.__mx_instance__` attributes),
30///   through the package's shared `.miniextendr_env_method` helper
31/// - `[[.ClassName`: the same lookup, with `exact` (ignored) and a `...` that
32///   refuses every argument (#2174)
33/// - With `env(r_data_accessors)`: a field branch at the top of `$.ClassName`
34///   (a `#[r_data]` field wins over a method of the same name),
35///   `.DollarNames.ClassName` (the fields, then the methods; #1885), and,
36///   unless the getters-only `r_data_accessors = "get"`, `$<-.ClassName` with
37///   its `[[<-.ClassName` alias (`field_syntax.rs`, #1848)
38///
39/// Roxygen2 documentation is generated for the class, each method, and the
40/// dispatch methods, with appropriate `@export`/`@keywords internal`/`@noRd` tags.
41///
42/// The class environment and constructor are the head; the instance and
43/// static methods are the members; the dispatch methods are the tail.
44pub fn generate_env_class(parsed_impl: &ParsedImpl) -> super::ClassWrapper {
45    use crate::r_class_formatter::{
46        ClassDocBuilder, MethodDocBuilder, ParsedImplExt, should_export_from_tags,
47    };
48
49    let class_name = parsed_impl.class_name();
50    let type_ident = &parsed_impl.type_ident;
51    // Check if class has @noRd - if so, skip method documentation. A plain
52    // `noexport` (without `internal`) is folded in too — it must suppress Rd
53    // contribution entirely, matching `ClassDocBuilder::build`'s `suppress_rd` gate.
54    let class_has_no_rd = crate::roxygen::has_roxygen_tag(&parsed_impl.doc_tags, "noRd")
55        || (parsed_impl.noexport && !parsed_impl.internal);
56
57    let mut w = super::ClassWrapper::default();
58    let mut lines = Vec::new();
59
60    // Class environment documentation and definition
61    let class_docs = ClassDocBuilder::new(&class_name, type_ident, &parsed_impl.doc_tags, "")
62        .with_export_control(parsed_impl.internal, parsed_impl.noexport);
63    lines.extend(class_docs.build());
64    w.class_docs = class_docs.docs();
65    w.mark_class_doc_lines(&mut lines, parsed_impl);
66    lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} <- new.env(parent = emptyenv())",
                class_name))
    })format!("{} <- new.env(parent = emptyenv())", class_name));
67    lines.push(String::new());
68
69    // Constructor
70    if let Some(ctx) = parsed_impl.constructor_context() {
71        w.member_names.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("`{0}$new`", class_name))
    })format!("`{class_name}$new`"));
72        lines.push(ctx.source_comment(type_ident));
73        // Skip method documentation if class has @noRd
74        if !class_has_no_rd {
75            let choice_docs = ctx.choice_param_docs();
76            let method_doc =
77                MethodDocBuilder::new(&class_name, "new", type_ident, &ctx.method.doc_tags)
78                    .with_source_method(&ctx.method.ident)
79                    .with_name_prefix("$")
80                    .with_params_as_details()
81                    .with_r_params(&ctx.params)
82                    .with_choice_param_docs(&choice_docs);
83            lines.extend(method_doc.build());
84        }
85        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}$new <- function({1}) {{",
                class_name, ctx.params))
    })format!("{}$new <- function({}) {{", class_name, ctx.params));
86        for check in ctx.precondition_checks() {
87            lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("  {0}", check))
    })format!("  {}", check));
88        }
89        // Inject match.arg validation for match_arg/choices params
90        for line in ctx.match_arg_prelude() {
91            lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("  {0}", line))
    })format!("  {}", line));
92        }
93        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("  .val <- {0}", ctx.static_call()))
    })format!("  .val <- {}", ctx.static_call()));
94        lines.extend(crate::method_return_builder::condition_check_lines("  "));
95        lines.push("  self <- .val".to_string());
96        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("  class(self) <- \"{0}\"",
                class_name))
    })format!("  class(self) <- \"{}\"", class_name));
97        lines.push("  self".to_string());
98        lines.push("}".to_string());
99        lines.push(String::new());
100    }
101    w.head = std::mem::take(&mut lines);
102
103    // Instance methods
104    for ctx in parsed_impl.instance_method_contexts() {
105        let method_name = ctx.method.r_method_name();
106        w.member_names.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("`{0}${1}`", class_name,
                method_name))
    })format!("`{class_name}${method_name}`"));
107        lines.push(ctx.source_comment(type_ident));
108        // Skip method documentation if class has @noRd
109        if !class_has_no_rd {
110            let choice_docs = ctx.choice_param_docs();
111            let method_doc =
112                MethodDocBuilder::new(&class_name, &method_name, type_ident, &ctx.method.doc_tags)
113                    .with_source_method(&ctx.method.ident)
114                    .with_name_prefix("$")
115                    .with_params_as_details()
116                    .with_r_params(&ctx.params)
117                    .with_choice_param_docs(&choice_docs);
118            lines.extend(method_doc.build());
119        }
120
121        // `$` takes an R symbol: quote operator names (#1475).
122        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}${1} <- function({2}) {{",
                class_name, crate::naming::r_def_name(&method_name),
                ctx.params))
    })format!(
123            "{}${} <- function({}) {{",
124            class_name,
125            crate::naming::r_def_name(&method_name),
126            ctx.params
127        ));
128
129        let what = ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}${1}", class_name, method_name))
    })format!("{}${}", class_name, method_name);
130        ctx.emit_method_prelude(&mut lines, "  ", &what, &ctx.params);
131
132        let call = ctx.instance_call(".mx_self");
133        let strategy = crate::ReturnStrategy::for_method(ctx.method);
134        let return_builder = crate::MethodReturnBuilder::new(call)
135            .with_strategy(strategy)
136            .with_invisible(ctx.method.is_invisible())
137            .with_chain_var(".mx_self".to_string())
138            .with_class_name(class_name.clone())
139            .with_return_class_from_method(ctx.method, &type_ident.to_string());
140        lines.extend(return_builder.build());
141
142        lines.push("}".to_string());
143        lines.push(String::new());
144    }
145
146    // Static methods
147    for ctx in parsed_impl.static_method_contexts() {
148        let method_name = ctx.method.r_method_name();
149        w.member_names.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("`{0}${1}`", class_name,
                method_name))
    })format!("`{class_name}${method_name}`"));
150        lines.push(ctx.source_comment(type_ident));
151        // Skip method documentation if class has @noRd
152        if !class_has_no_rd {
153            let choice_docs = ctx.choice_param_docs();
154            // The constructor's arguments are details of `$new`; its alias
155            // doesn't repeat them.
156            let doc_tags = parsed_impl.static_doc_tags(
157                ctx.method,
158                &::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}${1}()", class_name,
                method_name))
    })format!("{class_name}${method_name}()"),
159                &::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}$new()", class_name))
    })format!("{class_name}$new()"),
160                false,
161            );
162            let method_doc =
163                MethodDocBuilder::new(&class_name, &method_name, type_ident, &doc_tags)
164                    .with_source_method(&ctx.method.ident)
165                    .with_name_prefix("$")
166                    .with_params_as_details()
167                    .with_r_params(&ctx.params)
168                    .with_choice_param_docs(&choice_docs);
169            lines.extend(method_doc.build());
170        }
171
172        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}${1} <- function({2}) {{",
                class_name, crate::naming::r_def_name(&method_name),
                ctx.params))
    })format!(
173            "{}${} <- function({}) {{",
174            class_name,
175            crate::naming::r_def_name(&method_name),
176            ctx.params
177        ));
178
179        let what = ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}${1}", class_name, method_name))
    })format!("{}${}", class_name, method_name);
180        ctx.emit_method_prelude(&mut lines, "  ", &what, &ctx.params);
181
182        let strategy = crate::ReturnStrategy::for_method(ctx.method);
183        let return_builder = crate::MethodReturnBuilder::new(ctx.static_call())
184            .with_strategy(strategy)
185            .with_invisible(ctx.method.is_invisible())
186            .with_class_name(class_name.clone())
187            .with_return_class_from_method(ctx.method, &type_ident.to_string());
188        lines.extend(return_builder.build());
189
190        lines.push("}".to_string());
191        lines.push(String::new());
192    }
193    w.members = std::mem::take(&mut lines);
194
195    // $ dispatch - export as S3 methods
196    // Handles both functions (inherent methods) and environments (trait namespaces)
197    let should_export = should_export_from_tags(
198        &parsed_impl.doc_tags,
199        parsed_impl.noexport || parsed_impl.internal,
200    );
201
202    // Generate roxygen tags for dispatch methods.
203    // roxygen2 8.0.0+ enforces that any `generic.class`-named function carry
204    // @export or @exportS3Method (@noRd alone doesn't satisfy the check). We
205    // always emit @export so roxygen2 emits a properly-quoted
206    // `S3method("$", Class)` / `S3method("[[", Class)` (bare @exportS3Method
207    // skips the operator-name quoting and produces invalid NAMESPACE entries).
208    // For internal/noexport classes the @rdname target is dropped so the
209    // helpers don't bleed into the user-visible Rd page.
210    // `env(r_data_accessors)` (#1848): `$` / `[[` read a field before the
211    // method lookup, and `$<-` / `[[<-` write one.
212    let fields = parsed_impl.r_data_accessors.enabled();
213    if fields {
214        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("# Field syntax for the `#[r_data]` fields of {0} (`env(r_data_accessors)`).",
                type_ident))
    })format!(
215            "# Field syntax for the `#[r_data]` fields of {type_ident} (`env(r_data_accessors)`)."
216        ));
217        lines.push(super::field_syntax::env_load_check(parsed_impl));
218        lines.push(String::new());
219    }
220    if class_has_no_rd {
221        lines.push("#' @noRd".to_string());
222        lines.push("#' @export".to_string());
223    } else if !should_export {
224        lines.push("#' @export".to_string());
225    } else {
226        // The class's topic: the impl's own `@rdname`, else the class page.
227        lines.extend(super::field_syntax::page_lines(
228            parsed_impl,
229            &[
230                ("self", "The object instance."),
231                (
232                    "name",
233                    if fields {
234                        super::field_syntax::env_name_param(parsed_impl)
235                    } else {
236                        "Method name for dispatch."
237                    },
238                ),
239            ],
240        ));
241        lines.push("#' @export".to_string());
242    }
243    // The lookup `$` and `[[` share. `[[` is its own function, not an alias
244    // of `$`, so that each method's frame names its own generic for
245    // `.miniextendr_frame_call`.
246    let mut lookup = Vec::new();
247    if fields {
248        lookup.extend(super::field_syntax::env_field_branch(parsed_impl));
249    }
250    let method_lookup = ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!(".miniextendr_env_method(self, name, {0})",
                class_name))
    })format!(".miniextendr_env_method(self, name, {class_name})");
251    if fields {
252        lookup.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("  obj <- {0}", method_lookup))
    })format!("  obj <- {method_lookup}"));
253        lookup.push("  if (is.null(obj)) {".to_string());
254        lookup.extend(super::field_syntax::env_unknown_name_check(parsed_impl));
255        lookup.push("  }".to_string());
256        lookup.push("  obj".to_string());
257    } else {
258        lookup.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("  {0}", method_lookup))
    })format!("  {method_lookup}"));
259    }
260    lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("`$.{0}` <- function(self, name) {{",
                class_name))
    })format!("`$.{}` <- function(self, name) {{", class_name));
261    lines.extend(lookup.iter().cloned());
262    lines.push("}".to_string());
263    // `[[` takes `exact`, as a list's does, and ignores it: names match
264    // exactly. Nothing reads its `...`, so every argument there is refused
265    // (#2174).
266    if class_has_no_rd {
267        lines.push("#' @noRd".to_string());
268        lines.push("#' @export".to_string());
269    } else if !should_export {
270        lines.push("#' @export".to_string());
271    } else {
272        lines.extend(super::field_syntax::page_lines(
273            parsed_impl,
274            &[
275                ("...", "Refused: `[[` takes one name."),
276                ("exact", "Ignored: names match exactly."),
277            ],
278        ));
279        lines.push("#' @export".to_string());
280    }
281    lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("`[[.{1}` <- function({0}) {{",
                super::field_syntax::ENV_SUBSET_FORMALS, class_name))
    })format!(
282        "`[[.{class_name}` <- function({}) {{",
283        super::field_syntax::ENV_SUBSET_FORMALS
284    ));
285    lines.push(super::field_syntax::env_subset_dots_check(parsed_impl));
286    lines.extend(lookup);
287    lines.push("}".to_string());
288    if fields {
289        lines.extend(super::field_syntax::env_dollar_names(
290            parsed_impl,
291            !class_has_no_rd && should_export,
292        ));
293    }
294    if fields && parsed_impl.r_data_accessors.setters() {
295        lines.extend(super::field_syntax::env_replacement_methods(
296            parsed_impl,
297            !class_has_no_rd && should_export,
298        ));
299    }
300
301    w.tail = lines;
302    w
303}
304
305/// The env class as one string: [`generate_env_class`], rendered.
306#[cfg(test)]
307pub fn generate_env_r_wrapper(parsed_impl: &ParsedImpl) -> String {
308    generate_env_class(parsed_impl).render()
309}