1//! S4-class R wrapper generator.
2//!
3//! Generates `methods::setClass(...)` with an `externalptr` slot, plus
4//! `methods::setGeneric` / `methods::setMethod` for each instance method.
5//! Supports **formal slot validation, multi-dispatch on method signatures,
6//! and contains-based inheritance** — the only system here with native
7//! multi-dispatch. Cost: slowest dispatch path, all helpers live in the
8//! `methods::` namespace (`methods` must be imported, not `base`), and the
9//! ecosystem is increasingly legacy. Pick S4 for Bioconductor interop;
10//! use S7 for new packages wanting similar formal semantics.
1112use super::{ParsedImpl, ParsedMethod};
1314/// The S4 generic an instance method dispatches through: an explicit
15/// `s4(generic = "...")` verbatim, else `s4_<R method name>`, so `r_name`,
16/// `postfix` and raw identifiers follow the usual naming rules. Shared by the
17/// generator and `check_s4_generic_collisions`, so both agree on the names.
18pub(crate) fn s4_generic_name(method: &ParsedMethod) -> String {
19match &method.method_attrs.generic {
20Some(generic) => generic.clone(),
21None => ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("s4_{0}", method.r_method_name()))
})format!("s4_{}", method.r_method_name()),
22 }
23}
2425/// The line that defines an S4 instance method's generic before its
26/// `setMethod()`, shared by the inherent and trait generators.
27///
28/// A generic of the package's own (`explicit` false) is defined unless this
29/// namespace already has it. Unconditional `setGeneric()` would replace the
30/// generic object and clear the methods other types registered on it, and the
31/// check must be a namespace-local `exists()`: a bare `isGeneric()` searches
32/// S4 metadata globally, so with an installed copy of the package attached,
33/// `load_all()` would skip `setGeneric()` while `setMethod()` still can't see
34/// the generic from the namespace being loaded, and `isGeneric(where =)`
35/// resolves the namespace to a package name, which fails mid-install (#1158).
36///
37/// An explicit `generic = "..."` may name a generic that lives elsewhere:
38/// `methods::show` (an S4 generic `exists()` misses when `methods` is not
39/// attached), the primitive `length`, base's S3 `summary`. It is defined only
40/// when no function of that name is visible and `methods::isGeneric()` finds
41/// no generic of another package, with `receiver` as its dispatch formal;
42/// otherwise `setMethod()` attaches to the existing one instead of masking it
43/// with a `function(x, ...)` generic of this package (#1677). A generic whose
44/// `@package` is this namespace's name is an installed copy's, the #1158 case,
45/// so it is defined anyway. `environmentName()` is a plain lookup, unlike
46/// `isGeneric(where =)`. A name nothing defines is created, but gets no
47/// generated page: whether it is the package's own is only known at load time
48/// (#2039).
49pub(crate) fn s4_generic_guard(generic: &str, explicit: bool, receiver: &str) -> String {
50let condition = if explicit {
51::alloc::__export::must_use({
::alloc::fmt::format(format_args!("!exists(\"{0}\", mode = \"function\") && !(methods::isGeneric(\"{0}\") && methods::getGeneric(\"{0}\")@package != environmentName(topenv(environment())))",
generic))
})format!(
52"!exists(\"{generic}\", mode = \"function\") && !(methods::isGeneric(\"{generic}\") && \
53 methods::getGeneric(\"{generic}\")@package != environmentName(topenv(environment())))"
54)55 } else {
56::alloc::__export::must_use({
::alloc::fmt::format(format_args!("!exists(\"{0}\", where = topenv(environment()), inherits = FALSE)",
generic))
})format!("!exists(\"{generic}\", where = topenv(environment()), inherits = FALSE)")57 };
58::alloc::__export::must_use({
::alloc::fmt::format(format_args!("if ({0}) methods::setGeneric(\"{1}\", function({2}, ...) standardGeneric(\"{1}\"))",
condition, generic, receiver))
})format!(
59"if ({condition}) methods::setGeneric(\"{generic}\", function({receiver}, ...) standardGeneric(\"{generic}\"))"
60)61}
6263/// Generates the R wrapper of an S4-style class, in the parts the wrapper
64/// writer merges per type ([`super::ClassWrapper`], #1776).
65///
66/// Produces the following R code:
67/// - Class definition: `methods::setClass("<class>", slots = c(ptr = "externalptr"))`
68/// with a single `ptr` slot holding the `ExternalPtr` to the Rust struct
69/// - Constructor function: `ClassName(...)` that calls the Rust constructor
70/// and wraps the result with `methods::new("<class>", ptr = .val)`. A class
71/// without a constructor has no `ClassName()`: its factories call
72/// `methods::new()`, and a package may define its own (#2069).
73/// - S4 generics: `methods::setGeneric(...)` for each instance method, guarded by
74/// [`s4_generic_guard`]
75/// - S4 methods: `methods::setMethod("<generic>", "<class>", function(x, ...) ...)`
76/// dispatching to the Rust `.Call()` wrapper, extracting the ptr via `x@ptr`
77/// - Static methods: regular functions named `<class>_<method>(...)`
78///
79/// Roxygen2 `@exportMethod`, `@importFrom methods`, and `@slot` tags are generated
80/// as appropriate.
81///
82/// The class, its constructor and the field methods are the head; the S4
83/// methods and the static methods are the members.
84pub fn generate_s4_class(parsed_impl: &ParsedImpl) -> super::ClassWrapper {
85use crate::r_class_formatter::{
86ClassDocBuilder, MethodContext, MethodDocBuilder, ParsedImplExt, should_export_from_tags,
87 };
8889let class_name = parsed_impl.class_name();
90let type_ident = &parsed_impl.type_ident;
91let class_doc_tags = &parsed_impl.doc_tags;
92// Check if class has @noRd - if so, skip method documentation and exports. A
93 // plain `noexport` (without `internal`) is folded in too — it must suppress
94 // Rd contribution entirely, matching `ClassDocBuilder::build`'s `suppress_rd`
95 // gate. `should_export` (below) already independently gates @export/@exportMethod.
96let class_has_no_rd = crate::roxygen::has_roxygen_tag(class_doc_tags, "noRd")
97 || (parsed_impl.noexport && !parsed_impl.internal);
98let should_export =
99should_export_from_tags(class_doc_tags, parsed_impl.noexport || parsed_impl.internal);
100101let mut w = super::ClassWrapper::default();
102let mut lines = Vec::new();
103104// Class definition with documentation (S4 uses setClass, no @export on class definition)
105let has_export = crate::roxygen::has_roxygen_tag(class_doc_tags, "export");
106let class_docs = ClassDocBuilder::new(&class_name, type_ident, class_doc_tags, "S4")
107 .with_imports("@importFrom methods setClass setGeneric setMethod new")
108 .with_export_control(parsed_impl.internal, parsed_impl.noexport);
109lines.extend(class_docs.build());
110w.class_docs = class_docs.docs();
111w.mark_class_doc_lines(&mut lines, parsed_impl);
112// Remove the @export that ClassDocBuilder adds (S4 doesn't export the class
113 // definition). Only pop when the last line actually IS the auto-added
114 // @export — with internal/noexport the builder emits no @export, and a
115 // blind pop would instead drop `@keywords internal` / `@noRd` / a user tag.
116if !has_export117 && lines118 .last()
119 .is_some_and(|l| *l == super::class_default("#' @export"))
120 {
121lines.pop();
122 }
123if !class_has_no_rd {
124lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @slot ptr External pointer to Rust `{0}` struct",
type_ident))
})format!(
125"#' @slot ptr External pointer to Rust `{}` struct",
126 type_ident
127 ));
128 }
129lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("methods::setClass(\"{0}\", slots = c(ptr = \"externalptr\"))",
class_name))
})format!(
130"methods::setClass(\"{}\", slots = c(ptr = \"externalptr\"))",
131 class_name
132 ));
133lines.push(String::new());
134135// Constructor function
136if let Some(ctx) = parsed_impl.constructor_context() {
137lines.push(ctx.source_comment(type_ident));
138// Skip documentation if class has @noRd
139if !class_has_no_rd {
140// Use class name as @name to avoid duplicate "new" alias across S4 classes
141let mx_doc = ctx.choice_param_docs();
142let method_doc =
143MethodDocBuilder::new(&class_name, "new", type_ident, &ctx.method.doc_tags)
144 .with_source_method(&ctx.method.ident)
145 .with_r_params(&ctx.params)
146 .with_choice_param_docs(&mx_doc)
147 .with_r_name(class_name.clone());
148lines.extend(method_doc.build());
149 }
150// Export the constructor function so users can create instances (if class should be exported)
151if should_export {
152lines.push("#' @export".to_string());
153 }
154155lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0} <- function({1}) {{",
class_name, ctx.params))
})format!("{} <- function({}) {{", class_name, ctx.params));
156for check in ctx.precondition_checks() {
157 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(" {0}", check))
})format!(" {}", check));
158 }
159// Inject match.arg validation for match_arg/choices params
160for line in ctx.match_arg_prelude() {
161 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(" {0}", line))
})format!(" {}", line));
162 }
163lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(" .val <- {0}", ctx.static_call()))
})format!(" .val <- {}", ctx.static_call()));
164lines.extend(crate::method_return_builder::condition_check_lines(" "));
165lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(" methods::new(\"{0}\", ptr = .val)",
class_name))
})format!(" methods::new(\"{}\", ptr = .val)", class_name));
166lines.push("}".to_string());
167lines.push(String::new());
168 }
169170// `$` / `$<-` field methods and `.DollarNames` of `s4(r_data_accessors)`
171 // (#1848, #1885), documented on the class page wherever the class has one
172 // (R CMD check wants an `\alias{$,Class-method}`).
173if parsed_impl.r_data_accessors.enabled() {
174lines.extend(super::field_syntax::s4_field_methods(
175parsed_impl,
176 !class_has_no_rd,
177should_export,
178 ));
179 }
180w.head = std::mem::take(&mut lines);
181182// Instance methods as S4 methods
183 // Note: S4 uses empty param_defaults for method signatures (different from other systems)
184for method in parsed_impl.instance_methods() {
185let start = method.ident.span().start();
186let method_name = s4_generic_name(method);
187 w.member_names.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("the S4 method of `{0}` for `{1}`",
method_name, class_name))
})format!(
188"the S4 method of `{method_name}` for `{class_name}`"
189));
190191// Emit the generic-doc marker BEFORE the source comment so the write-time
192 // pass can place the standalone Rd page before the method block. This
193 // ensures the synthesised doc block (ending with NULL) is separated from
194 // the method's own roxygen comment by the `# source` line.
195if !class_has_no_rd && method.method_attrs.generic.is_none() {
196 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(".__MX_GENERIC_DOC__(kind=\"S4\", generic=\"{0}\", class=\"{1}\", export={2})",
method_name, class_name, should_export))
})format!(
197".__MX_GENERIC_DOC__(kind=\"S4\", generic=\"{method_name}\", class=\"{class_name}\", export={should_export})"
198));
199 }
200201 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("# {0}::{1} ({2}:{3})", type_ident,
method.ident, start.line, start.column + 1))
})format!(
202"# {}::{} ({}:{})",
203 type_ident,
204 method.ident,
205 start.line,
206 start.column + 1,
207 ));
208// Build a MethodContext so S4 methods participate in the shared
209 // match_arg prelude + formal-default machinery (#209). The ctx's
210 // `params`/`instance_formals` carry the `c("a", "b")` default for
211 // match_arg'd params, and `match_arg_prelude()` emits the
212 // `base::match.arg()` validation block injected below.
213let ctx = MethodContext::new(method, type_ident)
214 .with_impl_preconditions(parsed_impl.preconditions);
215let receiver = method.method_attrs.receiver.as_deref().unwrap_or("x");
216let call = ctx.instance_call(&::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}@ptr", receiver))
})format!("{receiver}@ptr"));
217let full_params =
218 ctx.instance_formals_with_receiver(receiver, !method.method_attrs.no_dispatch_dots);
219220// Documentation for the generic - skip if class has @noRd
221 // Use class-qualified @name to avoid duplicate \alias{generic} warnings
222 // when multiple S4 classes share the same generic (e.g., s4_get_value on
223 // both S4TraitCounter and CounterTraitS4). The @exportMethod directive
224 // (added separately) correctly exports the bare generic name.
225if !class_has_no_rd {
226let qualified_name = ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}-{1}", class_name, method_name))
})format!("{}-{}", class_name, method_name);
227let method_doc =
228 MethodDocBuilder::new(&class_name, &method_name, type_ident, &method.doc_tags)
229 .with_source_method(&method.ident)
230 .with_suppress_params()
231 .with_r_name(qualified_name);
232let mut doc_lines = method_doc.build();
233// Add S4 method-specific alias so R CMD check finds the documented method
234 doc_lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @aliases {0},{1}-method",
method_name, class_name))
})format!("#' @aliases {},{}-method", method_name, class_name));
235 lines.extend(doc_lines);
236 }
237238// Define the generic unless it exists. Several types can share one
239 // (s4_get_value on both S4TraitCounter and CounterTraitS4), and an
240 // explicit `generic` may be another package's (`show`).
241lines.push(s4_generic_guard(
242&method_name,
243 method.method_attrs.generic.is_some(),
244 receiver,
245 ));
246247// Define method with @exportMethod for proper S4 dispatch (if class should be exported)
248if should_export {
249 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @exportMethod {0}",
method_name))
})format!("#' @exportMethod {}", method_name));
250 }
251252let strategy = crate::ReturnStrategy::for_method(method);
253let body_lines = crate::MethodReturnBuilder::new(call)
254 .with_strategy(strategy)
255 .with_chain_var(receiver.to_string())
256 .with_invisible(method.is_invisible())
257 .with_class_name(class_name.clone())
258 .with_return_class_from_method(method, &type_ident.to_string())
259 .build_s4_body();
260261let what = ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}.{1}", method_name, class_name))
})format!("{}.{}", method_name, class_name);
262let signature = if method.method_attrs.receiver.is_some() {
263::alloc::__export::must_use({
::alloc::fmt::format(format_args!("c({0} = \"{1}\")", receiver,
class_name))
})format!("c({receiver} = \"{class_name}\")")264 } else {
265::alloc::__export::must_use({
::alloc::fmt::format(format_args!("\"{0}\"", class_name))
})format!("\"{class_name}\"")266 };
267 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("methods::setMethod(\"{0}\", {1}, function({2}) {{",
method_name, signature, full_params))
})format!(
268"methods::setMethod(\"{method_name}\", {signature}, function({full_params}) {{"
269));
270 ctx.emit_method_prelude(&mut lines, " ", &what, &full_params);
271 lines.extend(body_lines);
272 lines.push("})".to_string());
273 lines.push(String::new());
274 }
275276// Static methods as regular functions
277for ctx in parsed_impl.static_method_contexts() {
278 lines.push(ctx.source_comment(type_ident));
279let method_name = ctx.method.r_method_name();
280let fn_name = ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}_{1}", class_name, method_name))
})format!("{}_{}", class_name, method_name);
281282// Skip documentation if class has @noRd
283if !class_has_no_rd {
284let mx_doc = ctx.choice_param_docs();
285let doc_tags = parsed_impl.static_doc_tags(
286 ctx.method,
287&::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}()", fn_name))
})format!("{fn_name}()"),
288&::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}()", class_name))
})format!("{class_name}()"),
289true,
290 );
291let method_doc =
292 MethodDocBuilder::new(&class_name, &method_name, type_ident, &doc_tags)
293 .with_source_method(&ctx.method.ident)
294 .with_r_params(&ctx.params)
295 .with_choice_param_docs(&mx_doc)
296 .with_r_name(fn_name.clone());
297 lines.extend(method_doc.build());
298 }
299// Export static methods so users can call them (if class should be exported)
300if should_export {
301 lines.push("#' @export".to_string());
302 }
303304 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0} <- function({1}) {{",
crate::naming::r_def_name(&fn_name), ctx.params))
})format!(
305"{} <- function({}) {{",
306crate::naming::r_def_name(&fn_name),
307 ctx.params
308 ));
309310 ctx.emit_method_prelude(&mut lines, " ", &fn_name, &ctx.params);
311312let strategy = crate::ReturnStrategy::for_method(ctx.method);
313let return_expr = crate::MethodReturnBuilder::new(ctx.static_call())
314 .with_strategy(strategy)
315 .with_invisible(ctx.method.is_invisible())
316 .with_class_name(class_name.clone())
317 .with_return_class_from_method(ctx.method, &type_ident.to_string())
318 .build_s4_inline();
319 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(" {0}", return_expr))
})format!(" {}", return_expr));
320321 lines.push("}".to_string());
322 lines.push(String::new());
323 }
324325w.members = lines;
326w327}
328329/// The S4 class as one string: [`generate_s4_class`], rendered.
330#[cfg(test)]
331pub fn generate_s4_r_wrapper(parsed_impl: &ParsedImpl) -> String {
332 generate_s4_class(parsed_impl).render()
333}