Skip to main content

miniextendr_macros/miniextendr_impl/
vctrs_class.rs

1//! Vctrs class R wrapper generator.
2//!
3//! Generates an S3 class that **integrates with the tidyverse `vctrs`
4//! package**: emits a `new_<class>` wrapper around `vctrs::new_vctr` /
5//! `new_rcrd` / `new_list_of` (selected via `VctrsKind`), plus the standard
6//! `vec_ptype2` / `vec_cast` self-coercion methods so the type composes
7//! cleanly inside `tibble`, `dplyr`, and `tidyr`. Pick vctrs when your Rust
8//! type is "a vector of X" and you want first-class tibble columns; use
9//! plain S3/S7 for scalar-like objects.
10
11use super::{ParsedImpl, VctrsKind};
12
13/// The roxygen imports of a vctrs class's roxygen block.
14const VCTRS_IMPORTS: &str =
15    "@importFrom vctrs new_vctr new_rcrd new_list_of vec_ptype2 vec_cast vec_ptype_abbr";
16
17/// Generates the R wrapper of a vctrs-compatible S3 class, in the parts the
18/// wrapper writer merges per type ([`super::ClassWrapper`], #1776).
19///
20/// This is used when an `impl` block is annotated with `#[miniextendr(vctrs)]`.
21/// Unlike the `#[derive(Vctrs)]` macro (which generates standalone S3 methods from
22/// struct attributes), this generator produces class wrappers from `impl` block methods.
23///
24/// Produces the following R code:
25/// - Constructor: `new_<class>(...)` that calls the Rust `new` constructor, then wraps
26///   the result with `vctrs::new_vctr()`, `vctrs::new_rcrd()`, or `vctrs::new_list_of()`
27///   depending on the `VctrsKind`
28/// - `vec_ptype_abbr.<class>`: compact abbreviation for printing (if `abbr` is specified)
29/// - `vec_ptype2.<class>.<class>`: self-coercion prototype (returns empty typed vector)
30/// - `vec_cast.<class>.<class>`: identity cast (returns `x` unchanged)
31/// - Static methods: regular functions named `<class>_<method>(...)`, or
32///   `<protocol>.<class>` S3 methods with `#[miniextendr(vctrs(<protocol>))]`
33///   (instance methods are refused, MXL120)
34///
35/// Roxygen2 documentation and `@importFrom vctrs ...` tags are generated automatically.
36/// A class-level `@noRd` (or a plain `noexport` without `internal`) suppresses the Rd
37/// contribution of every block — self-coercion methods and static method docs
38/// all collapse to `@noRd` (#1180), like the other five
39/// class-system generators. S3 `S3method()` dispatch registration (`@method` +
40/// `@export`) is kept unconditionally: vctrs generics dispatch from the vctrs
41/// namespace, so an unregistered method would leave even a gated class non-functional
42/// (and roxygen2 warns on any recognized-but-unregistered S3 method).
43///
44/// The constructor, `vec_ptype_abbr` and the self-coercion methods are the
45/// head; the static methods and protocol methods are the members.
46pub fn generate_vctrs_class(parsed_impl: &ParsedImpl) -> super::ClassWrapper {
47    use crate::r_class_formatter::{
48        ClassDocBuilder, MethodDocBuilder, ParsedImplExt, should_export_from_tags,
49    };
50
51    let class_name = parsed_impl.class_name();
52    let type_ident = &parsed_impl.type_ident;
53    let class_doc_tags = &parsed_impl.doc_tags;
54    let vctrs_attrs = &parsed_impl.vctrs_attrs;
55    let should_export =
56        should_export_from_tags(class_doc_tags, parsed_impl.noexport || parsed_impl.internal);
57    // Check if the class has @noRd — if so, skip method documentation (#1180). A
58    // plain `noexport` (without `internal`) is folded into the gate too: it must
59    // suppress Rd contribution entirely (audit A10 semantics), matching the other
60    // five class generators. `internal` keeps full docs (the constructor's
61    // ClassDocBuilder adds `@keywords internal`); `should_export` (above) gates
62    // the export()-shaped surface (static helpers) and is always false when
63    // this gate is true.
64    //
65    // S3-method-shaped emissions (`vec_ptype_abbr`/`vec_ptype2`/`vec_cast`,
66    // protocol overrides) keep their `@method` + `@export`
67    // pair unconditionally: with `@method`, `@export` produces an `S3method()`
68    // dispatch registration in NAMESPACE — not an `export()` — and both
69    // roxygen2 (`warn_missing_s3_exports`) and vctrs itself (generics dispatch
70    // from the vctrs namespace) require registered methods. A gated class stays
71    // hidden (no Rd page, no `export()` entries) but remains a functioning vctr.
72    let class_has_no_rd = crate::roxygen::has_roxygen_tag(class_doc_tags, "noRd")
73        || (parsed_impl.noexport && !parsed_impl.internal);
74
75    // Constructor name follows vctrs convention: new_<class>
76    let ctor_name = ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("new_{0}",
                class_name.to_lowercase()))
    })format!("new_{}", class_name.to_lowercase());
77
78    let mut w = super::ClassWrapper::default();
79    let mut lines = Vec::new();
80    // The class block is the constructor's; a block without one still
81    // documents the class with its impl docs.
82    crate::roxygen::push_roxygen_tags(&mut w.class_docs, class_doc_tags);
83
84    // Constructor with combined class and constructor documentation
85    if let Some(ctx) = parsed_impl.constructor_context() {
86        lines.push(ctx.source_comment(type_ident));
87        let mut ctor_doc_tags = Vec::new();
88        ctor_doc_tags.extend(class_doc_tags.iter().cloned());
89        ctor_doc_tags.extend(ctx.method.doc_tags.iter().cloned());
90        let ctor_choice_docs = ctx.choice_param_docs();
91
92        let class_docs = ClassDocBuilder::new(&class_name, type_ident, &ctor_doc_tags, "vctrs S3")
93            .with_ctor_params(&ctx.params, &ctor_choice_docs)
94            .with_imports(VCTRS_IMPORTS)
95            .with_export_control(parsed_impl.internal, parsed_impl.noexport);
96        lines.extend(class_docs.build());
97        w.class_docs = class_docs.docs();
98        w.mark_class_doc_lines(&mut lines, parsed_impl);
99
100        // Generate constructor body based on vctrs kind
101        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} <- function({1}) {{",
                ctor_name, ctx.params))
    })format!("{} <- function({}) {{", ctor_name, ctx.params));
102        for check in ctx.precondition_checks() {
103            lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("  {0}", check))
    })format!("  {}", check));
104        }
105        // Inject match.arg validation for match_arg/choices params
106        for line in ctx.match_arg_prelude() {
107            lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("  {0}", line))
    })format!("  {}", line));
108        }
109        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("  .val <- {0}", ctx.static_call()))
    })format!("  .val <- {}", ctx.static_call()));
110        lines.extend(crate::method_return_builder::condition_check_lines("  "));
111        lines.push("  data <- .val".to_string());
112
113        match vctrs_attrs.kind {
114            VctrsKind::Vctr => {
115                // Build new_vctr call with optional inherit_base_type
116                let inherit_arg = match vctrs_attrs.inherit_base_type {
117                    Some(true) => ", inherit_base_type = TRUE",
118                    Some(false) => ", inherit_base_type = FALSE",
119                    None => "",
120                };
121                lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("  vctrs::new_vctr(data, class = \"{0}\"{1})",
                class_name, inherit_arg))
    })format!(
122                    "  vctrs::new_vctr(data, class = \"{}\"{})",
123                    class_name, inherit_arg
124                ));
125            }
126            VctrsKind::Rcrd => {
127                // Record type - data should be a list
128                lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("  vctrs::new_rcrd(data, class = \"{0}\")",
                class_name))
    })format!(
129                    "  vctrs::new_rcrd(data, class = \"{}\")",
130                    class_name
131                ));
132            }
133            VctrsKind::ListOf => {
134                // list_of - needs ptype
135                let ptype_arg = vctrs_attrs
136                    .ptype
137                    .as_ref()
138                    .map(|p| ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!(", ptype = {0}", p))
    })format!(", ptype = {}", p))
139                    .unwrap_or_default();
140                lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("  vctrs::new_list_of(data, class = \"{0}\"{1})",
                class_name, ptype_arg))
    })format!(
141                    "  vctrs::new_list_of(data, class = \"{}\"{})",
142                    class_name, ptype_arg
143                ));
144            }
145        }
146        lines.push("}".to_string());
147        lines.push(String::new());
148    } else {
149        super::s3_class::no_constructor_head(
150            parsed_impl,
151            &mut w,
152            &mut lines,
153            "vctrs S3",
154            Some(VCTRS_IMPORTS),
155        );
156    }
157
158    // vec_ptype_abbr for compact printing (if abbr is specified)
159    if let Some(abbr) = &vctrs_attrs.abbr {
160        if class_has_no_rd {
161            // Gated class (@noRd / plain noexport): no Rd contribution — keep
162            // only the @method + @export S3method() registration pair.
163            lines.push("#' @noRd".to_string());
164        } else {
165            lines.push(super::member_default(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @rdname {0}", class_name))
    })format!("#' @rdname {}", class_name)));
166        }
167        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @method vec_ptype_abbr {0}",
                class_name))
    })format!("#' @method vec_ptype_abbr {}", class_name));
168        lines.push("#' @export".to_string());
169        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("vec_ptype_abbr.{0} <- function(x, ...) \"{1}\"",
                class_name, abbr))
    })format!(
170            "vec_ptype_abbr.{} <- function(x, ...) \"{}\"",
171            class_name, abbr
172        ));
173        lines.push(String::new());
174    }
175
176    // Self-coercion methods (required for vctrs to work properly)
177    // vec_ptype2.<class>.<class> - returns prototype for combining same types
178    if class_has_no_rd {
179        lines.push("#' @noRd".to_string());
180        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @method vec_ptype2 {0}.{1}",
                class_name, class_name))
    })format!(
181            "#' @method vec_ptype2 {}.{}",
182            class_name, class_name
183        ));
184    } else {
185        lines.push(super::member_default(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @rdname {0}", class_name))
    })format!("#' @rdname {}", class_name)));
186        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @method vec_ptype2 {0}.{1}",
                class_name, class_name))
    })format!(
187            "#' @method vec_ptype2 {}.{}",
188            class_name, class_name
189        ));
190        lines.push(super::member_default(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @param x A {0} vector.",
                class_name))
    })format!(
191            "#' @param x A {} vector.",
192            class_name
193        )));
194        lines.push(super::member_default(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @param y A {0} vector.",
                class_name))
    })format!(
195            "#' @param y A {} vector.",
196            class_name
197        )));
198        lines.push(super::member_default(
199            "#' @param ... Additional arguments (unused).".to_string(),
200        ));
201    }
202    lines.push("#' @export".to_string());
203    match vctrs_attrs.kind {
204        VctrsKind::Vctr => {
205            let base_type = vctrs_attrs
206                .base
207                .as_ref()
208                .map(|b| ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}()", b))
    })format!("{}()", b))
209                .unwrap_or_else(|| "double()".to_string());
210            let inherit_arg = match vctrs_attrs.inherit_base_type {
211                Some(true) => ", inherit_base_type = TRUE",
212                Some(false) => ", inherit_base_type = FALSE",
213                None => "",
214            };
215            lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("vec_ptype2.{0}.{0} <- function(x, y, ...) vctrs::new_vctr({1}, class = \"{0}\"{2})",
                class_name, base_type, inherit_arg))
    })format!(
216                "vec_ptype2.{c}.{c} <- function(x, y, ...) vctrs::new_vctr({base}, class = \"{c}\"{inherit})",
217                c = class_name,
218                base = base_type,
219                inherit = inherit_arg
220            ));
221        }
222        VctrsKind::Rcrd => {
223            // For records, return empty record with same field structure
224            lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("vec_ptype2.{0}.{0} <- function(x, y, ...) x[0]",
                class_name))
    })format!(
225                "vec_ptype2.{c}.{c} <- function(x, y, ...) x[0]",
226                c = class_name
227            ));
228        }
229        VctrsKind::ListOf => {
230            let ptype_arg = vctrs_attrs
231                .ptype
232                .as_ref()
233                .map(|p| ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!(", ptype = {0}", p))
    })format!(", ptype = {}", p))
234                .unwrap_or_default();
235            lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("vec_ptype2.{0}.{0} <- function(x, y, ...) vctrs::new_list_of(list(), class = \"{0}\"{1})",
                class_name, ptype_arg))
    })format!(
236                "vec_ptype2.{c}.{c} <- function(x, y, ...) vctrs::new_list_of(list(), class = \"{c}\"{ptype})",
237                c = class_name,
238                ptype = ptype_arg
239            ));
240        }
241    }
242    lines.push(String::new());
243
244    // vec_cast.<class>.<class> - identity cast (no-op for same type)
245    if class_has_no_rd {
246        lines.push("#' @noRd".to_string());
247        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @method vec_cast {0}.{1}",
                class_name, class_name))
    })format!("#' @method vec_cast {}.{}", class_name, class_name));
248    } else {
249        lines.push(super::member_default(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @rdname {0}", class_name))
    })format!("#' @rdname {}", class_name)));
250        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @method vec_cast {0}.{1}",
                class_name, class_name))
    })format!("#' @method vec_cast {}.{}", class_name, class_name));
251        lines.push(super::member_default(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @param x A {0} vector to cast.",
                class_name))
    })format!(
252            "#' @param x A {} vector to cast.",
253            class_name
254        )));
255        lines.push(super::member_default(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @param to A {0} prototype.",
                class_name))
    })format!(
256            "#' @param to A {} prototype.",
257            class_name
258        )));
259        lines.push(super::member_default(
260            "#' @param ... Additional arguments (unused).".to_string(),
261        ));
262    }
263    lines.push("#' @export".to_string());
264    lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("vec_cast.{0}.{0} <- function(x, to, ...) x",
                class_name))
    })format!(
265        "vec_cast.{c}.{c} <- function(x, to, ...) x",
266        c = class_name
267    ));
268    lines.push(String::new());
269    w.head = std::mem::take(&mut lines);
270
271    // No instance methods: `ParsedImpl::parse` refuses every receiver on a
272    // vctrs impl (MXL120), since the R object is the vector, not a pointer.
273
274    // Static methods as regular functions, or as vctrs protocol S3 methods when
275    // `#[miniextendr(vctrs(protocol))]` is set (e.g. `vctrs(format)` → `format.<Class>`).
276    for ctx in parsed_impl.static_method_contexts() {
277        lines.push(ctx.source_comment(type_ident));
278
279        let is_protocol = ctx.method.method_attrs.vctrs_protocol.is_some();
280        let fn_name = if let Some(ref proto) = ctx.method.method_attrs.vctrs_protocol {
281            // vctrs protocol override: emit as `<protocol>.<Class>` S3 method
282            ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}.{1}", proto, class_name))
    })format!("{}.{}", proto, class_name)
283        } else {
284            let method_name = ctx.method.r_method_name();
285            ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}_{1}",
                class_name.to_lowercase(), method_name))
    })format!("{}_{}", class_name.to_lowercase(), method_name)
286        };
287        let r_name = fn_name.clone();
288
289        let mx_doc = ctx.choice_param_docs();
290        let method_name = ctx.method.r_method_name();
291        if is_protocol {
292            let proto = ctx.method.method_attrs.vctrs_protocol.as_ref().unwrap();
293            let method_doc =
294                MethodDocBuilder::new(&class_name, &method_name, type_ident, &ctx.method.doc_tags)
295                    .with_source_method(&ctx.method.ident)
296                    .with_r_params(&ctx.params)
297                    .with_choice_param_docs(&mx_doc)
298                    .with_r_name(r_name.clone())
299                    .with_class_no_rd(class_has_no_rd);
300            lines.extend(method_doc.build());
301            lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @method {0} {1}", proto,
                class_name))
    })format!("#' @method {} {}", proto, class_name));
302            lines.push("#' @export".to_string());
303        } else {
304            let method_doc =
305                MethodDocBuilder::new(&class_name, &method_name, type_ident, &ctx.method.doc_tags)
306                    .with_source_method(&ctx.method.ident)
307                    .with_r_params(&ctx.params)
308                    .with_choice_param_docs(&mx_doc)
309                    .with_r_name(r_name.clone())
310                    .with_class_no_rd(class_has_no_rd);
311            lines.extend(method_doc.build());
312            // Export the helper so users can call it, as the S3 / S4 / S7
313            // generators do for their static methods; without it the
314            // function was documented on the class page but unreachable.
315            if should_export {
316                lines.push("#' @export".to_string());
317            }
318        }
319
320        // Protocol methods accept `...` so `format(x, nsmall = 2)` and similar
321        // S3 dispatch calls with extra arguments don't error with "unused argument".
322        // The `...` is silently dropped; the underlying Rust function has a fixed signature.
323        let formals = if is_protocol && !ctx.method.has_dots {
324            if ctx.params.is_empty() {
325                "...".to_string()
326            } else {
327                ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}, ...", ctx.params))
    })format!("{}, ...", ctx.params)
328            }
329        } else {
330            ctx.params.to_string()
331        };
332        lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} <- function({1}) {{",
                crate::naming::r_def_name(&fn_name), formals))
    })format!(
333            "{} <- function({}) {{",
334            crate::naming::r_def_name(&fn_name),
335            formals
336        ));
337
338        ctx.emit_method_prelude(&mut lines, "  ", &fn_name, &formals);
339
340        let strategy = crate::ReturnStrategy::for_method(ctx.method);
341        let return_builder = crate::MethodReturnBuilder::new(ctx.static_call())
342            .with_strategy(strategy)
343            .with_invisible(ctx.method.is_invisible())
344            .with_class_name(class_name.clone())
345            .with_return_class_from_method(ctx.method, &type_ident.to_string());
346        lines.extend(return_builder.build_s3_body());
347
348        lines.push("}".to_string());
349        lines.push(String::new());
350    }
351
352    w.members = lines;
353    w
354}
355
356/// The vctrs class as one string: [`generate_vctrs_class`], rendered.
357#[cfg(test)]
358pub fn generate_vctrs_r_wrapper(parsed_impl: &ParsedImpl) -> String {
359    generate_vctrs_class(parsed_impl).render()
360}