1//! S7-class R wrapper generator.
2//!
3//! Generates `S7::new_class(...)` with **value semantics, formal property
4//! validation, and parent-based inheritance**. Property attributes
5//! (`s7(getter)` / `s7(setter, prop = "...")`) compile to `S7::new_property`
6//! with `@`-access semantics, including read-only computed properties and
7//! read-write dynamic ones. Similar formal power to S4 with cleaner syntax,
8//! but the S7 ecosystem is younger and the R-package dependency surface is
9//! evolving. Pick S7 for **new** packages wanting modern formal OOP (including
10//! multiple dispatch); use S4 when you need Bioconductor compatibility.
1112use super::{ParsedImpl, ParsedMethod};
13use crate::r_class_formatter::{class_ref_or_verbatim, is_bare_identifier};
1415/// Extract the full property documentation from a getter's `doc_tags` for use in
16/// `@prop <name> <doc>` emission.
17///
18/// A getter's leading prose is promoted to `@description` (see
19/// `roxygen::leading_prose_from_attrs`); a getter's `@title` would only ever be a
20/// hand-written explicit tag. This helper collects any `@title`, `@description`, and
21/// `@details` present and joins them with `\n\n` for multi-line `@prop` continuation
22/// (supported by roxygen2 8.0.0+).
23fn extract_prop_doc_from_tags(doc_tags: &[String]) -> Option<String> {
24let mut parts: Vec<String> = Vec::new();
2526// Look for @title (primary doc — from para 1)
27if let Some(title) = doc_tags.iter().find_map(|t| {
28if !t.starts_with('@') {
29return Some(t.clone()); // plain text before any @tag
30}
31t.strip_prefix("@title ").map(|s| s.to_string())
32 }) {
33parts.push(title);
34 }
3536// Look for @description (para 2)
37if let Some(desc) = doc_tags38 .iter()
39 .find_map(|t| t.strip_prefix("@description ").map(|s| s.to_string()))
40 {
41parts.push(desc);
42 }
4344// Look for @details (para 3+)
45if let Some(details) = doc_tags46 .iter()
47 .find_map(|t| t.strip_prefix("@details ").map(|s| s.to_string()))
48 {
49parts.push(details);
50 }
5152if parts.is_empty() {
53None54 } else {
55Some(parts.join("\n\n"))
56 }
57}
5859/// S7-property variant of [`class_ref_or_verbatim`] that asks the resolver to
60/// fall back silently to `S7::class_any` on miss (unregistered type, or a
61/// registered-but-non-S7 class). Prevents the load-time `object not found`
62/// noise called out in #203.
63fn class_ref_or_any_or_verbatim(name: &str) -> String {
64if is_bare_identifier(name) {
65::alloc::__export::must_use({
::alloc::fmt::format(format_args!(".__MX_CLASS_REF_OR_ANY_{0}__",
name))
})format!(".__MX_CLASS_REF_OR_ANY_{name}__")66 } else {
67name.to_string()
68 }
69}
7071/// Name of the S7 fast-path shortcut `<ClassName>_<method>` (#949), or `None`
72/// when that name is not a syntactic R name.
73///
74/// A method whose R name is an operator (`r_name = "[["`, `"%op%"`) is reached
75/// through R's operator syntax (`obj[[i]]`), which dispatches through the
76/// generic. A shortcut named `` `Foo_[[` `` could only be called with backticks,
77/// so it is not emitted at all. Spell the method `s7(generic = "[[")` on a
78/// Rust method with an ordinary name to keep both the operator and a
79/// `Foo_<name>` shortcut. Shared by the inherent and trait S7 generators and
80/// the shortcut collision check, so all three agree on which names exist.
81pub(crate) fn s7_shortcut_name(class_name: &str, method_name: &str) -> Option<String> {
82let name = ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}_{1}", class_name, method_name))
})format!("{class_name}_{method_name}");
83crate::naming::is_syntactic_r_name(&name).then_some(name)
84}
8586// region: S7 generic targets and dispatch arguments
8788/// R's `Ops` group operators. S7 dispatches them on both operands through its
89/// own `(e1, e2, ...)` generics.
90pub(crate) const S7_OPS_OPERATORS: &[&str] = &[
91"+", "-", "*", "/", "^", "%%", "%/%", "==", "!=", "<", "<=", ">=", ">", "&", "|",
92];
9394/// Operators base R always provides and S7 dispatches on the receiver alone
95/// (through the base primitive as an S3 generic).
96const S7_BASE_S3_OPERATORS: &[&str] = &["[", "[[", "$"];
9798/// The dispatch arguments S7 fixes for a two-operand operator generic:
99/// `(e1, e2)` for the `Ops` group, `(x, y)` for `%*%` (the `matrixOps` group).
100pub(crate) fn s7_operator_dispatch_args(op: &str) -> Option<[&'static str; 2]> {
101if S7_OPS_OPERATORS.contains(&op) {
102Some(["e1", "e2"])
103 } else if op == "%*%" {
104Some(["x", "y"])
105 } else {
106None107 }
108}
109110/// Where an S7 instance method's generic comes from.
111#[derive(#[automatically_derived]
impl ::core::fmt::Debug for S7GenericTarget {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
match self {
Self::BaseOperator(__self_0) =>
::core::fmt::Formatter::debug_tuple_field1_finish(f,
"BaseOperator", &__self_0),
Self::External { package: __self_0, name: __self_1 } =>
::core::fmt::Formatter::debug_struct_field2_finish(f,
"External", "package", __self_0, "name", &__self_1),
Self::Local(__self_0) =>
::core::fmt::Formatter::debug_tuple_field1_finish(f, "Local",
&__self_0),
}
}
}Debug)]
112pub(crate) enum S7GenericTarget {
113/// A base R operator (`[`, `[[`, `$`, the `Ops` group, `%*%`). It always
114 /// exists, so the method attaches to it directly: the package neither
115 /// defines, exports nor documents the generic.
116BaseOperator(String),
117/// `s7(generic = "pkg::name")`: another package's generic. It is bound as
118 /// `S7::new_external_generic()` under its bare name, which S7 resolves
119 /// whether or not `pkg` is loaded (registration waits for its `onLoad`).
120External { package: String, name: String },
121/// The package's own generic (`r_name`, the Rust name, or an unqualified
122 /// `s7(generic = ...)`). Reused when a usable generic already exists,
123 /// otherwise defined here; exported with a standalone doc page.
124Local(String),
125}
126127impl S7GenericTarget {
128/// The generic's bare R name (no `pkg::` prefix).
129pub(crate) fn name(&self) -> &str {
130match self {
131Self::BaseOperator(name) | Self::Local(name) => name,
132Self::External { name, .. } => name,
133 }
134 }
135}
136137/// The line that follows an `S7::method(<op>, ...) <-` registration on a base
138/// operator: `base::rm(list = "<op>")`.
139///
140/// `S7::method(`[[`, Class) <- f` is a replacement call, so R assigns its
141/// result, the base `[[` primitive, to `[[` in the package namespace. A
142/// generic the namespace binds counts as the package's own when R registers
143/// the package's S3 methods (`registerS3methods()`), so every `S3method("[[",
144/// ...)` of the package would land in the namespace's own methods table
145/// instead of base's: those methods would dispatch only from code inside the
146/// namespace, never from a user's `x[[i]]` or from `lapply()`. Removing the
147/// binding right after the registration keeps the S7 call itself unchanged.
148pub(crate) fn s7_base_operator_unbind(op: &str) -> String {
149::alloc::__export::must_use({
::alloc::fmt::format(format_args!("base::rm(list = \"{0}\")", op))
})format!("base::rm(list = \"{op}\")")150}
151152/// Classify a method's generic name (`ctx.generic_name()`).
153pub(crate) fn s7_generic_target(generic_name: &str) -> S7GenericTarget {
154let (package, name) = match generic_name.split_once("::") {
155Some((package, name)) => (Some(package), name),
156None => (None, generic_name),
157 };
158let base_operator =
159S7_BASE_S3_OPERATORS.contains(&name) || s7_operator_dispatch_args(name).is_some();
160match package {
161None | Some("base") if base_operator => S7GenericTarget::BaseOperator(name.to_string()),
162Some(package) => S7GenericTarget::External {
163 package: package.to_string(),
164 name: name.to_string(),
165 },
166None => S7GenericTarget::Local(name.to_string()),
167 }
168}
169170/// The S7 dispatch arguments of an instance method: `s7(dispatch = "a, b")`,
171/// else the pair an operator generic fixes (`e1, e2`), else `x`.
172///
173/// The first argument names the receiver; the others are the method's
174/// leading parameters (validated by `check_s7_dispatch`).
175pub(crate) fn s7_dispatch_args(method: &ParsedMethod, target: &S7GenericTarget) -> Vec<String> {
176if let Some(dispatch) = method.method_attrs.s7.dispatch.as_deref() {
177return dispatch.split(',').map(|a| a.trim().to_string()).collect();
178 }
179match s7_operator_dispatch_args(target.name()) {
180Some(pair) => pair.iter().map(|a| a.to_string()).collect(),
181None => ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
["x".to_string()]))vec!["x".to_string()],
182 }
183}
184185/// The `S7::method()` signature: the class (or `S7::class_any` for a
186/// fallback) for the receiver, and `S7::class_any` for every other dispatch
187/// argument.
188///
189/// The other arguments are not narrowed. The method's own argument checks and
190/// Rust conversions are the type check, and they report the parameter by name.
191/// No single S7 class matches what a parameter accepts in general (an
192/// `ExternalPtr<T>` takes the class object, a bare external pointer or a list
193/// with `.ptr`; `coerce` widens numeric parameters), so a narrower class would
194/// turn accepted values into "Can't find method" errors. A more specific
195/// method registered in R still wins over this one.
196fn s7_method_signature(class_name: &str, n_dispatch: usize, fallback: bool) -> String {
197let receiver = if fallback {
198"S7::class_any"
199} else {
200class_name201 };
202if n_dispatch <= 1 {
203receiver.to_string()
204 } else {
205let mut parts = ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
[receiver]))vec![receiver];
206parts.extend(std::iter::repeat_n("S7::class_any", n_dispatch - 1));
207::alloc::__export::must_use({
::alloc::fmt::format(format_args!("list({0})", parts.join(", ")))
})format!("list({})", parts.join(", "))208 }
209}
210211/// A character vector literal: `"x"` or `c("x", "y")`.
212fn r_chr_vector(values: &[String]) -> String {
213let quoted: Vec<String> = values.iter().map(|v| ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("\"{0}\"", v))
})format!("\"{v}\"")).collect();
214match quoted.as_slice() {
215 [single] => single.clone(),
216_ => ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("c({0})", quoted.join(", ")))
})format!("c({})", quoted.join(", ")),
217 }
218}
219220/// Emit the load-time definition of a package-local S7 generic: define it
221/// when no binding exists, shadow a plain (non-generic) function, otherwise
222/// reuse the existing generic.
223///
224/// The generic's formals are the dispatch arguments (plus `...` unless
225/// `no_dots`); the shadow's `class_any` fallback takes the same formals and
226/// forwards them, so every method S7 registers on it is signature-compatible.
227fn emit_s7_local_generic(
228 lines: &mut Vec<String>,
229 generic_name: &str,
230 dispatch_args: &[String],
231 include_dots: bool,
232) {
233let generic_symbol = crate::naming::r_def_name(generic_name);
234let dispatch_vec = r_chr_vector(dispatch_args);
235let mut generic_formals = dispatch_args.join(", ");
236if include_dots {
237generic_formals.push_str(", ...");
238 }
239let generic_fun = ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("function({0}) S7::S7_dispatch()",
generic_formals))
})format!("function({generic_formals}) S7::S7_dispatch()");
240let class_any_spec = s7_method_signature("S7::class_any", dispatch_args.len(), true);
241242// Classify any existing binding of `generic_name` (#1114). A bare
243 // `exists(...)` check is wrong: if the name resolves to a *plain*
244 // base/stats closure (var, get, row, col, diag, reshape, ...),
245 // `S7::method(<closure>, ...) <-` errors at load ("generic is a
246 // function, but not an S3 generic function") and the package fails
247 // to install. Only create/reuse when the existing binding is
248 // something S7::method<- accepts as a generic: an S7 generic, a
249 // primitive, an S3 standard generic, or an S4 generic. Otherwise
250 // shadow it with a package-local S7 generic.
251 //
252 // Emit an `if (!base::exists(...))` / `else if ({classifier})` chain
253 // rather than a top-level `.mx_gen <- ...` assignment. The leading
254 // statement must NOT be an assignment: roxygen2 documents the first
255 // top-level binding after the doc block, and a `.mx_gen <- ...` there
256 // pollutes every S7 man page with a bogus \alias{.mx_gen}/\usage. The
257 // `.mx_gen` binding used by the classifier lives inside the braced
258 // `else if` condition, which roxygen2 does not descend into.
259 //
260 // `base::exists`/`base::get`: once we define a shadow generic named
261 // e.g. `get`, a bare `get(...)` in a later generic's classifier would
262 // route through our own generic. Qualify to stay robust.
263lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("if (!base::exists(\"{0}\", mode = \"function\")) {{",
generic_name))
})format!(
264"if (!base::exists(\"{generic_name}\", mode = \"function\")) {{"
265));
266lines.push(" # No existing binding; define a plain package-local S7 generic.".to_string());
267lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(" {0} <- S7::new_generic(\"{1}\", {2}, {3})",
generic_symbol, generic_name, dispatch_vec, generic_fun))
})format!(
268" {generic_symbol} <- S7::new_generic(\"{generic_name}\", {dispatch_vec}, {generic_fun})"
269));
270lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("}} else if (local({{ .mx_gen <- base::get(\"{0}\", mode = \"function\"); !(inherits(.mx_gen, \"S7_generic\") || is.primitive(.mx_gen) || isTRUE(utils::isS3stdGeneric(.mx_gen)) || methods::isGeneric(\"{0}\")) }})) {{",
generic_name))
})format!(
271"}} else if (local({{ .mx_gen <- base::get(\"{generic_name}\", mode = \"function\"); !(inherits(.mx_gen, \"S7_generic\") || is.primitive(.mx_gen) || isTRUE(utils::isS3stdGeneric(.mx_gen)) || methods::isGeneric(\"{generic_name}\")) }})) {{"
272));
273lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(" # `{0}` resolves to a plain (non-generic) function S7 cannot",
generic_name))
})format!(
274" # `{generic_name}` resolves to a plain (non-generic) function S7 cannot"
275));
276lines.push(
277" # attach a method to. Define a package-local S7 generic that shadows it,".to_string(),
278 );
279lines.push(
280" # with a class_any fallback delegating to the masked function so ordinary".to_string(),
281 );
282lines.push(
283" # (non-S7) calls (e.g. var(1:10)) keep working. S4 generics also take this".to_string(),
284 );
285lines.push(" # path; the fallback preserves their dispatch.".to_string());
286// `local()` gives the fallback closure its own environment holding an
287 // eagerly-assigned `.mx_masked` (assignment forces the value now: a
288 // function *argument* would stay an unforced promise and later see the
289 // reused `.mx_gen`, which is why we don't pass it as one).
290lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(" {0} <- local({{",
generic_symbol))
})format!(" {generic_symbol} <- local({{"));
291lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(" .mx_masked <- base::get(\"{0}\", mode = \"function\")",
generic_name))
})format!(
292" .mx_masked <- base::get(\"{generic_name}\", mode = \"function\")"
293));
294lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(" .mx_g <- S7::new_generic(\"{0}\", {1}, {2})",
generic_name, dispatch_vec, generic_fun))
})format!(
295" .mx_g <- S7::new_generic(\"{generic_name}\", {dispatch_vec}, {generic_fun})"
296));
297lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(" S7::method(.mx_g, {0}) <- function({1}) .mx_masked({1})",
class_any_spec, generic_formals))
})format!(
298" S7::method(.mx_g, {class_any_spec}) <- function({generic_formals}) .mx_masked({generic_formals})"
299));
300lines.push(" .mx_g".to_string());
301lines.push(" })".to_string());
302lines.push("}".to_string());
303lines.push("# else: existing usable generic (S7/primitive/S3/S4) — reuse as-is.".to_string());
304}
305306// endregion
307308/// Roxygen prose for the fast-path dispatch shortcut advisory block.
309///
310/// Shared between the inherent-impl S7 generator ([`generate_s7_class`])
311/// and the trait-impl S7 generator
312/// (`miniextendr_impl_trait::r_wrappers::generate_trait_s7_r_wrapper`) so both
313/// emit identical guidance. Returns `#'`-prefixed lines (no trailing `@param`
314/// / `@name` scaffolding — the caller appends those).
315pub(crate) fn shortcut_advisory_lines(method_name: &str, class_name: &str) -> Vec<String> {
316::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
[::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' Fast-path shortcut for the `{0}` S7 method on `{1}`.",
method_name, class_name))
}), "#'".to_string(),
"#' Calls the underlying Rust routine directly, bypassing `S7::S7_dispatch()`".to_string(),
"#' (the class walk + method-table lookup). Use in hot loops where the".to_string(),
"#' per-call dispatch overhead matters. **Footgun:** this shortcut does not".to_string(),
"#' perform subclass dispatch: a method override defined on a child class".to_string(),
"#' will *not* be honoured. Use the generic when subclassing is possible.".to_string()]))vec![
317format!(
318"#' Fast-path shortcut for the `{}` S7 method on `{}`.",
319 method_name, class_name
320 ),
321"#'".to_string(),
322"#' Calls the underlying Rust routine directly, bypassing `S7::S7_dispatch()`".to_string(),
323"#' (the class walk + method-table lookup). Use in hot loops where the".to_string(),
324"#' per-call dispatch overhead matters. **Footgun:** this shortcut does not".to_string(),
325// ASCII only: a standalone `tools::checkRd()` flags non-ASCII Rd text.
326"#' perform subclass dispatch: a method override defined on a child class".to_string(),
327"#' will *not* be honoured. Use the generic when subclassing is possible.".to_string(),
328 ]329}
330331/// Map a Rust return type to an S7 class name.
332///
333/// Returns `None` if the type doesn't map to any S7 class — the caller
334/// then omits the `class = …` constraint (so S7 uses `class_any`).
335///
336/// # S7 Class Mapping
337///
338/// | Rust Type | S7 Class |
339/// |-----------|----------|
340/// | `i32`, `i16`, `i8` | `class_integer` |
341/// | `f64`, `f32` | `class_double` |
342/// | `bool` | `class_logical` |
343/// | `u8` | `class_raw` |
344/// | `String`, `&str` | `class_character` |
345/// | `Vec<i32>` | `class_integer` |
346/// | `Vec<f64>` | `class_double` |
347/// | `Vec<bool>` | `class_logical` |
348/// | `Vec<String>` | `class_character` |
349/// | `Option<T>` | `NULL \| class_T` (union) |
350/// | `SomeUserType` (bare ident) | `.__MX_CLASS_REF_SomeUserType__` |
351///
352/// For bare user-defined path types the macro emits the same
353/// `.__MX_CLASS_REF_<Type>__` placeholder used for parent-class and
354/// `convert_from`/`convert_to` references (see [`#154`]). The resolver in
355/// `miniextendr_api::registry::write_r_wrappers_to_file` swaps the
356/// placeholder for the R-visible class name recorded in `MX_CLASS_NAMES`
357/// — so `class = "Override"` on an S7 impl block is honored, and child
358/// class properties tighten from `class_any` to the real class.
359///
360/// Unresolved types fall through the existing CLASS_REF mechanism:
361/// the bare Rust name is emitted with a compile-time warning. If the
362/// user returns a type that isn't a registered class at all, they'll
363/// see that warning + a `object '…' not found` at R load time.
364pub(super) fn rust_type_to_s7_class(ty: &syn::Type) -> Option<String> {
365match ty {
366 syn::Type::Path(type_path) => {
367let seg = type_path.path.segments.last()?;
368let ident = seg.ident.to_string();
369370match ident.as_str() {
371// Scalar types
372"i32" | "i16" | "i8" | "isize" => Some("S7::class_integer".to_string()),
373"f64" | "f32" => Some("S7::class_double".to_string()),
374"bool" => Some("S7::class_logical".to_string()),
375"u8" => Some("S7::class_raw".to_string()),
376"String" => Some("S7::class_character".to_string()),
377378// Vec types - check inner type
379"Vec" => {
380if let syn::PathArguments::AngleBracketed(args) = &seg.arguments
381 && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
382 {
383// Recursively get the inner type's class
384return rust_type_to_s7_class(inner);
385 }
386None387 }
388389// Option types - create union with NULL
390"Option" => {
391if let syn::PathArguments::AngleBracketed(args) = &seg.arguments
392 && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
393 && let Some(inner_class) = rust_type_to_s7_class(inner)
394 {
395return Some(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("NULL | {0}", inner_class))
})format!("NULL | {}", inner_class));
396 }
397None398 }
399400// Result types - use the Ok type
401"Result" => {
402if let syn::PathArguments::AngleBracketed(args) = &seg.arguments
403 && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
404 {
405return rust_type_to_s7_class(inner);
406 }
407None408 }
409410// Bare, un-generic single-segment identifier — reuse the
411 // CLASS_REF write-time placeholder so the resolver swaps
412 // in the registered R-visible class name (honoring any
413 // `class = "Override"` on the referenced impl block).
414 // Paths with `::`, generics, or a lowercase leading char
415 // are rejected to avoid matching crate-local aliases,
416 // primitives, or type parameters — those fall through
417 // to `None` and the caller omits the `class =` entirely.
418_ if type_path.path.segments.len() == 1
419 && #[allow(non_exhaustive_omitted_patterns)] match seg.arguments {
syn::PathArguments::None => true,
_ => false,
}matches!(seg.arguments, syn::PathArguments::None)420 && is_bare_identifier(&ident)
421 && ident.chars().next().is_some_and(|c| c.is_ascii_uppercase()) =>
422 {
423// S7 property class constraint: use the OR_ANY variant so
424 // an unregistered type (or a registered-but-non-S7 class)
425 // falls back silently to `class_any`, restoring pre-#154
426 // behavior for those edge cases (#203).
427Some(class_ref_or_any_or_verbatim(&ident))
428 }
429430_ => None,
431 }
432 }
433 syn::Type::Reference(type_ref) => {
434// Handle &str
435if let syn::Type::Path(type_path) = type_ref.elem.as_ref()
436 && let Some(seg) = type_path.path.segments.last()
437 && seg.ident == "str"
438{
439return Some("S7::class_character".to_string());
440 }
441// Recurse for other reference types
442rust_type_to_s7_class(&type_ref.elem)
443 }
444_ => None,
445 }
446}
447448/// Generates the R wrapper of an S7-style class, in the parts the wrapper
449/// writer merges per type ([`super::ClassWrapper`], #1776).
450///
451/// Produces the following R code:
452/// - Class definition: `ClassName <- S7::new_class("ClassName", ...)` with a `.ptr` property
453/// of `class_any` holding the `ExternalPtr`, plus optional computed properties
454/// - Constructor: inline in `new_class(constructor = function(...) ...)`, supports
455/// `.ptr` shortcut parameter for wrapping pre-built ExternalPtr returns
456/// - Properties: `S7::new_property(...)` for each getter/setter/validator annotated
457/// with `#[miniextendr(s7(getter))]` etc., with support for class constraints,
458/// defaults, required, frozen, and deprecated modifiers
459/// - Instance methods: `S7::new_generic(...)` + `S7::method(generic, class)` pairs
460/// dispatching to Rust `.Call()` wrappers via `x@.ptr` (see
461/// [`S7GenericTarget`] for where the generic comes from)
462/// - External generics: `S7::new_external_generic("pkg", "name", dispatch_args)`
463/// for `s7(generic = "pkg::name")`
464/// - Multiple dispatch: via `#[miniextendr(s7(dispatch = "x, other"))]`, and on
465/// `(e1, e2)` for the `Ops` operators
466/// - Fallback methods: `S7::method(generic, S7::class_any)` with `tryCatch` for
467/// safe slot access on non-S7 objects
468/// - Static methods: regular functions named `ClassName_method(...)`
469/// - Convert methods: `S7::method(convert, list(From, To))` for `convert_from`
470/// and `convert_to` annotations
471/// - S7 parent/abstract: optional `parent` and `abstract = TRUE` in class definition
472///
473/// Roxygen2 documentation and `@importFrom S7 ...` tags are generated automatically.
474///
475/// The class definition, with its properties and constructor, is the head; the
476/// methods, static methods and `convert()` methods are the members.
477pub fn generate_s7_class(parsed_impl: &ParsedImpl) -> super::ClassWrapper {
478use crate::r_class_formatter::{
479ClassDocBuilder, MethodContext, MethodDocBuilder, ParsedImplExt, should_export_from_tags,
480 };
481482let class_name = parsed_impl.class_name();
483let type_ident = &parsed_impl.type_ident;
484let class_doc_tags = &parsed_impl.doc_tags;
485// Check if class has @noRd - if so, skip method documentation and exports. A
486 // plain `noexport` (without `internal`) is folded in too — it must suppress
487 // Rd contribution entirely, matching `ClassDocBuilder::build`'s `suppress_rd`
488 // gate. `should_export` (below) already independently gates @export/@exportMethod.
489let class_has_no_rd = crate::roxygen::has_roxygen_tag(class_doc_tags, "noRd")
490 || (parsed_impl.noexport && !parsed_impl.internal);
491let should_export =
492should_export_from_tags(class_doc_tags, parsed_impl.noexport || parsed_impl.internal);
493494let mut w = super::ClassWrapper::default();
495let mut lines = Vec::new();
496497// Collect S7 property getters, setters, and validators
498 // Property name is: s7_prop if specified, else method name
499 // We store method idents so we can look them up later
500/// Accumulated metadata for a single S7 property, built up from getter,
501 /// setter, and validator method annotations during the first pass over methods.
502struct S7Property {
503/// Property name (from `#[miniextendr(s7(prop = "..."))]` or the method ident).
504name: String,
505/// Ident of the method annotated with `#[miniextendr(s7(getter))]`.
506getter_method_ident: Option<String>,
507/// Ident of the method annotated with `#[miniextendr(s7(setter))]`.
508setter_method_ident: Option<String>,
509/// Ident of the method annotated with `#[miniextendr(s7(validate))]`.
510validator_method_ident: Option<String>,
511/// S7 class type inferred from the getter's return type (e.g., `"S7::class_double"`).
512class_type: Option<String>,
513/// Default value as an R expression string (from `#[miniextendr(s7(default = "..."))]`).
514default_value: Option<String>,
515/// When `true`, the property errors if not provided during construction.
516required: bool,
517/// When `true`, the property can only be set once (subsequent sets error).
518frozen: bool,
519/// If set, a deprecation warning is emitted when the property is accessed or set.
520deprecated: Option<String>,
521/// Documentation extracted from the getter method's first doc line.
522 /// Used to emit `#' @prop name doc` in the class-level roxygen block.
523doc: Option<String>,
524 }
525526let mut properties: std::collections::BTreeMap<String, S7Property> =
527 std::collections::BTreeMap::new();
528let mut property_method_idents: std::collections::HashSet<String> =
529 std::collections::HashSet::new();
530531// First pass: collect all property methods (getters, setters, validators)
532for method in &parsed_impl.methods {
533if !method.should_include() {
534continue;
535 }
536let attrs = &method.method_attrs;
537538if attrs.s7.getter || attrs.s7.setter || attrs.s7.validate {
539let method_ident = crate::naming::ident_name(&method.ident);
540let prop_name = attrs
541 .s7
542 .prop
543 .clone()
544 .unwrap_or_else(|| method_ident.clone());
545546 property_method_idents.insert(method_ident.clone());
547548let entry = properties.entry(prop_name.clone()).or_insert(S7Property {
549 name: prop_name,
550 getter_method_ident: None,
551 setter_method_ident: None,
552 validator_method_ident: None,
553 class_type: None,
554 default_value: None,
555 required: false,
556 frozen: false,
557 deprecated: None,
558 doc: None,
559 });
560561if attrs.s7.getter {
562 entry.getter_method_ident = Some(method_ident.clone());
563// Extract S7 class type from getter's return type
564if let syn::ReturnType::Type(_, ret_type) = &method.sig.output {
565 entry.class_type = rust_type_to_s7_class(ret_type);
566 }
567// Capture property attributes from getter
568if let Some(ref default) = attrs.s7.default {
569 entry.default_value = Some(default.clone());
570 }
571if attrs.s7.required {
572 entry.required = true;
573 }
574if attrs.s7.frozen {
575 entry.frozen = true;
576 }
577if let Some(ref msg) = attrs.s7.deprecated {
578 entry.deprecated = Some(msg.clone());
579 }
580// Extract full doc from getter's doc comment for @prop documentation
581 // in the class-level roxygen block. After #578 the auto_description
582 // path emits @title (para 1), @description (para 2), @details (para 3+).
583 // We collect all three to surface the complete doc under @prop.
584 // Multi-line @prop is supported by roxygen2 8.0.0 (continuation lines
585 // indented with two spaces).
586entry.doc = extract_prop_doc_from_tags(&method.doc_tags);
587 }
588if attrs.s7.setter {
589 entry.setter_method_ident = Some(method_ident.clone());
590 }
591if attrs.s7.validate {
592 entry.validator_method_ident = Some(method_ident);
593 }
594 }
595 }
596597// Helper to find method by ident
598let find_method = |ident: &str| -> Option<&ParsedMethod> {
599parsed_impl.methods.iter().find(|m| m.ident == ident)
600 };
601602// Determine imports based on whether we have properties and what class types are used
603let base_imports = "new_class class_any new_object S7_object new_generic method";
604let mut import_parts: Vec<&str> = ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
[base_imports]))vec![base_imports];
605606if !properties.is_empty() {
607import_parts.push("new_property");
608 }
609610// The `convert()` methods bind S7's `convert` by name: the class docs
611 // import it for whichever impl block of the type has them.
612let has_convert_methods = parsed_impl.methods.iter().any(|m| {
613m.should_include()
614 && (m.method_attrs.s7.convert_from.is_some() || m.method_attrs.s7.convert_to.is_some())
615 });
616if has_convert_methods {
617w.class_doc_lines
618 .push("#' @importFrom S7 convert".to_string());
619 }
620621// Collect unique S7 class types used in properties
622let mut class_imports: std::collections::HashSet<&str> = std::collections::HashSet::new();
623for prop in properties.values() {
624if let Some(ref class_type) = prop.class_type {
625// Extract class name from "S7::class_xxx" or "NULL | S7::class_xxx"
626for part in class_type.split('|') {
627let part = part.trim();
628if let Some(class_name) = part.strip_prefix("S7::") {
629 class_imports.insert(class_name);
630 }
631 }
632 }
633 }
634// Sort for deterministic output
635let mut sorted_imports: Vec<&str> = class_imports.into_iter().collect();
636sorted_imports.sort();
637for class_name in sorted_imports {
638 import_parts.push(class_name);
639 }
640641let imports = ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("@importFrom S7 {0}",
import_parts.join(" ")))
})format!("@importFrom S7 {}", import_parts.join(" "));
642643// Class definition with documentation
644let class_docs = ClassDocBuilder::new(&class_name, type_ident, class_doc_tags, "S7")
645 .with_imports(&imports)
646 .with_export_control(parsed_impl.internal, parsed_impl.noexport);
647lines.extend(class_docs.build());
648w.class_docs = class_docs.docs();
649w.mark_class_doc_lines(&mut lines, parsed_impl);
650651// Document constructor params — include constructor @param tags and auto-generate
652 // for undocumented ones. Class-level @param tags are already emitted by ClassDocBuilder;
653 // constructor-level @param tags must be explicitly pushed here since S7 inlines the
654 // constructor inside new_class().
655 // Skip if class has @noRd
656if !class_has_no_rd {
657if let Some(ctx) = parsed_impl.constructor_context() {
658let mx_doc = ctx.choice_param_docs();
659for param in ctx.params.split(", ").filter(|p| !p.is_empty()) {
660let param_name = param.split('=').next().unwrap_or(param).trim();
661if param_name == ".ptr" || param_name == "..." {
662continue;
663 }
664// Check if already documented at the class level (impl block doc_tags)
665let in_class_docs = crate::roxygen::param_documented(class_doc_tags, param_name);
666if in_class_docs {
667continue; // Already emitted by ClassDocBuilder
668}
669// Check if documented in the constructor method's doc_tags
670let ctor_tag = crate::roxygen::find_param_tag(&ctx.method.doc_tags, param_name);
671if let Some(tag) = ctor_tag {
672// A wrapped `@param` keeps every line inside the block.
673crate::roxygen::push_roxygen_tags_str(&mut lines, &[tag.as_str()]);
674 } else if crate::roxygen::params_documented_elsewhere(
675 class_doc_tags,
676Some(&class_name),
677 ) {
678// The class block joins (`@rdname`) or inherits from a
679 // topic that documents the argument (#1590).
680continue;
681 } else if let Some(doc) = mx_doc.get(param_name) {
682// Choice param: the literal `choices(...)` line, or a
683 // match_arg placeholder rewritten at wrapper write time to
684 // the rendered choice description (#210).
685 lines.push(super::class_default(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @param {0} {1}", param_name,
doc))
})format!(
686"#' @param {} {}",
687 param_name, doc
688 )));
689 } else {
690 lines.push(super::class_default(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @param {0} (undocumented)",
param_name))
})format!(
691"#' @param {} (undocumented)",
692 param_name
693 )));
694 }
695 }
696 }
697// .ptr is always a constructor param. On an author topic (impl-level
698 // `@rdname other`) the topic's own block documents it: the class block
699 // sorts after that block, and roxygen2 keeps the later `@param` (#1590).
700if !crate::roxygen::has_roxygen_tag(class_doc_tags, "param .ptr")
701 && !crate::roxygen::joins_author_topic(class_doc_tags, Some(&class_name))
702 {
703lines.push(super::class_default(
704"#' @param .ptr Internal pointer (used by static methods, not for direct use).",
705 ));
706 }
707708// @prop tags for impl-block S7 properties (getter/setter pairs).
709 // These appear in the class-level @rdname page via roxygen2's @prop tag.
710 //
711 // Properties that ARE constructor formals are already documented above via
712 // @param and must NOT also receive @prop — roxygen2 8.0.0 treats @param /
713 // @prop as disjoint sets (constructor formals → @param; getter/setter-only
714 // props → @prop). Collect the constructor-formal names and skip them here.
715let constructor_param_names: std::collections::HashSet<String> =
716if let Some(ctx) = parsed_impl.constructor_context() {
717ctx.params
718 .split(", ")
719 .filter(|p| !p.is_empty())
720 .map(|p| p.split('=').next().unwrap_or(p).trim().to_string())
721 .filter(|n| n != ".ptr" && n != "...")
722 .collect()
723 } else {
724 std::collections::HashSet::new()
725 };
726for prop in properties.values() {
727if constructor_param_names.contains(&prop.name) {
728continue; // already documented as @param above
729}
730let doc = prop.doc.as_deref().unwrap_or("(undocumented property)");
731// Emit @prop with multi-line continuation support (roxygen2 8.0.0+).
732 // First line: `#' @prop <name> <para1>`.
733 // Additional paragraphs: `#' <continuation>` (two-space indent),
734 // a bare `#'` between them.
735let mut prop_lines = doc.lines();
736if let Some(first_line) = prop_lines.next() {
737 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @prop {0} {1}", prop.name,
first_line))
})format!("#' @prop {} {}", prop.name, first_line));
738for continuation in prop_lines {
739 lines.push(if continuation.trim().is_empty() {
740"#'".to_string()
741 } else {
742::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' {0}", continuation))
})format!("#' {}", continuation)743 });
744 }
745 } else {
746 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @prop {0} {1}", prop.name,
doc))
})format!("#' @prop {} {}", prop.name, doc));
747 }
748 }
749750// @prop tags for sidecar (r_data_accessors) properties.
751 // Emitted via a write-time placeholder that MX_S7_SIDECAR_PROPS resolves.
752 // The placeholder is replaced in `write_r_wrappers_to_file` with one
753 // `#' @prop field doc` line per registered sidecar field.
754if parsed_impl.r_data_accessors.enabled() {
755let type_name = type_ident.to_string();
756lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(".__MX_S7_SIDECAR_PROP_DOCS_{0}__",
type_name))
})format!(".__MX_S7_SIDECAR_PROP_DOCS_{type_name}__"));
757 }
758 }
759760// S7::new_class — optionally include parent and abstract
761if let Some(ref parent) = parsed_impl.s7_parent {
762// Use a placeholder so the resolver can look up the actual R class name
763 // at wrapper write time (handles `class = "Override"` on the parent).
764let parent_ref = class_ref_or_verbatim(parent);
765lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0} <- S7::new_class(\"{1}\", parent = {2},",
class_name, class_name, parent_ref))
})format!(
766"{} <- S7::new_class(\"{}\", parent = {},",
767 class_name, class_name, parent_ref
768 ));
769 } else {
770lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0} <- S7::new_class(\"{1}\",",
class_name, class_name))
})format!(
771"{} <- S7::new_class(\"{}\",",
772 class_name, class_name
773 ));
774 }
775776if parsed_impl.s7_abstract {
777lines.push(" abstract = TRUE,".to_string());
778 }
779780// Properties - .ptr holds the ExternalPtr, plus computed/dynamic properties
781 // When r_data_accessors is set, merge with sidecar properties from #[derive(ExternalPtr)]
782 // Collect property items into a vec, then join with ",\n" to avoid bare commas on standalone lines
783let mut prop_items: Vec<String> = Vec::new();
784prop_items.push(" .ptr = S7::class_any".to_string());
785786// Generate computed/dynamic properties
787for prop in properties.values() {
788// Generate property definition
789let mut prop_parts = Vec::new();
790791// Add class constraint if known (inferred from getter return type)
792if let Some(ref class_type) = prop.class_type {
793 prop_parts.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("class = {0}", class_type))
})format!("class = {}", class_type));
794 }
795796// Handle default value or required pattern
797if prop.required {
798// Required pattern: error if not provided
799 prop_parts.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("default = quote(stop(\"@{0} is required\"))",
prop.name))
})format!(
800"default = quote(stop(\"@{} is required\"))",
801 prop.name
802 ));
803 } else if let Some(ref default) = prop.default_value {
804// Explicit default value (R expression)
805 prop_parts.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("default = {0}", default))
})format!("default = {}", default));
806 }
807808// Add validator if present
809if let Some(ref validator_ident) = prop.validator_method_ident
810 && let Some(validator_method) = find_method(validator_ident)
811 {
812let ctx = MethodContext::new(validator_method, type_ident);
813// Validator is called with just the value, not self.
814 // Use null_call_attribution: this runs inside S7's dispatch lambda, so
815 // sys.call() would name S7 internals, not the user's call site.
816let validator_call = crate::r_wrapper_builder::DotCallBuilder::new(&ctx.c_ident)
817 .null_call_attribution()
818 .with_args(&["value"])
819 .build();
820 prop_parts.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("validator = function(value) {0}",
validator_call))
})format!("validator = function(value) {validator_call}"));
821 }
822823// Generate getter (with optional deprecation warning)
824if let Some(ref getter_ident) = prop.getter_method_ident
825 && let Some(getter_method) = find_method(getter_ident)
826 {
827let ctx = MethodContext::new(getter_method, type_ident);
828// Use null_call_attribution: runs inside S7's property dispatch lambda.
829let getter_call = ctx.instance_call_null_attr("self@.ptr");
830if let Some(ref msg) = prop.deprecated {
831// Deprecated getter: emit warning then return value
832 prop_parts.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("getter = function(self) {{ warning(\"Property @{0} is deprecated: {1}\"); {2} }}",
prop.name, msg, getter_call))
})format!(
833"getter = function(self) {{ warning(\"Property @{} is deprecated: {}\"); {} }}",
834 prop.name, msg, getter_call
835 ));
836 } else {
837 prop_parts.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("getter = function(self) {0}",
getter_call))
})format!("getter = function(self) {}", getter_call));
838 }
839 }
840841// Generate setter (with optional frozen/deprecation handling)
842if let Some(ref setter_ident) = prop.setter_method_ident
843 && let Some(setter_method) = find_method(setter_ident)
844 {
845let ctx = MethodContext::new(setter_method, type_ident);
846// Use null_call_attribution: runs inside S7's property dispatch lambda.
847let setter_call = ctx.instance_call_null_attr("self@.ptr");
848849if prop.frozen {
850// Frozen pattern: error if property was already set (non-NULL)
851 // Note: This is a simplified check; true frozen behavior would need
852 // a separate flag in the object to track if ever set
853if let Some(ref msg) = prop.deprecated {
854 prop_parts.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("setter = function(self, value) {{ warning(\"Property @{0} is deprecated: {1}\"); if (!is.null(self@{2})) stop(\"Property @{3} is frozen and cannot be modified\"); {4}; self }}",
prop.name, msg, prop.name, prop.name, setter_call))
})format!(
855"setter = function(self, value) {{ warning(\"Property @{} is deprecated: {}\"); if (!is.null(self@{})) stop(\"Property @{} is frozen and cannot be modified\"); {}; self }}",
856 prop.name, msg, prop.name, prop.name, setter_call
857 ));
858 } else {
859 prop_parts.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("setter = function(self, value) {{ if (!is.null(self@{0})) stop(\"Property @{1} is frozen and cannot be modified\"); {2}; self }}",
prop.name, prop.name, setter_call))
})format!(
860"setter = function(self, value) {{ if (!is.null(self@{})) stop(\"Property @{} is frozen and cannot be modified\"); {}; self }}",
861 prop.name, prop.name, setter_call
862 ));
863 }
864 } else if let Some(ref msg) = prop.deprecated {
865// Deprecated setter: emit warning then set value
866 prop_parts.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("setter = function(self, value) {{ warning(\"Property @{0} is deprecated: {1}\"); {2}; self }}",
prop.name, msg, setter_call))
})format!(
867"setter = function(self, value) {{ warning(\"Property @{} is deprecated: {}\"); {}; self }}",
868 prop.name, msg, setter_call
869 ));
870 } else {
871// Normal setter
872 prop_parts.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("setter = function(self, value) {{ {0}; self }}",
setter_call))
})format!(
873"setter = function(self, value) {{ {}; self }}",
874 setter_call
875 ));
876 }
877 }
878879if prop_parts.is_empty() {
880// This shouldn't happen, but handle gracefully
881 prop_items.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(" {0} = S7::new_property()",
prop.name))
})format!(" {} = S7::new_property()", prop.name));
882 } else {
883 prop_items.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(" {0} = S7::new_property({1})",
prop.name, prop_parts.join(", ")))
})format!(
884" {} = S7::new_property({})",
885 prop.name,
886 prop_parts.join(", ")
887 ));
888 }
889 }
890891if parsed_impl.r_data_accessors.enabled() {
892lines.push(" properties = c(list(".to_string());
893 } else {
894lines.push(" properties = list(".to_string());
895 }
896lines.push(prop_items.join(",\n"));
897898// Close the properties list (or merge with sidecar properties)
899if parsed_impl.r_data_accessors.enabled() {
900let type_name = type_ident.to_string();
901lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(" ), .rdata_properties_{0}),",
type_name))
})format!(" ), .rdata_properties_{}),", type_name));
902 } else {
903lines.push(" ),".to_string());
904 }
905906if let Some(ctx) = parsed_impl.constructor_context() {
907let ctor_preconditions = ctx.precondition_checks();
908let ctor_match_arg = ctx.match_arg_prelude();
909let params_with_ptr = if ctx.params.is_empty() {
910".ptr = NULL".to_string()
911 } else {
912::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}, .ptr = NULL", ctx.params))
})format!("{}, .ptr = NULL", ctx.params)913 };
914lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(" constructor = function({0}) {{",
params_with_ptr))
})format!(" constructor = function({}) {{", params_with_ptr));
915// Preconditions + match.arg only when not using .ptr shortcut
916if !ctor_preconditions.is_empty() || !ctor_match_arg.is_empty() {
917lines.push(" if (is.null(.ptr)) {".to_string());
918for check in &ctor_preconditions {
919 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(" {0}", check))
})format!(" {}", check));
920 }
921for line in &ctor_match_arg {
922 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(" {0}", line))
})format!(" {}", line));
923 }
924lines.push(" }".to_string());
925 }
926lines.push(" if (!is.null(.ptr)) {".to_string());
927lines.push(" S7::new_object(S7::S7_object(), .ptr = .ptr)".to_string());
928lines.push(" } else {".to_string());
929lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(" .val <- {0}",
ctx.static_call()))
})format!(" .val <- {}", ctx.static_call()));
930lines.extend(crate::method_return_builder::condition_check_lines(
931" ",
932 ));
933lines.push(" S7::new_object(S7::S7_object(), .ptr = .val)".to_string());
934lines.push(" }".to_string());
935lines.push(" }".to_string());
936 } else {
937// No constructor: the factories and cross-class returns wrap their
938 // pointer with `Class(.ptr = val)`, and `Class()` raises an error
939 // rather than build an object without a pointer. An abstract class
940 // keeps S7's own refusal ("Can't construct an object from abstract
941 // class"). S7 requires `new_object()` in the body.
942lines.push(" constructor = function(.ptr = NULL) {".to_string());
943if !parsed_impl.s7_abstract {
944lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(" {0}",
super::no_constructor_guard(&class_name)))
})format!(" {}", super::no_constructor_guard(&class_name)));
945 }
946lines.push(" S7::new_object(S7::S7_object(), .ptr = .ptr)".to_string());
947lines.push(" }".to_string());
948 }
949950lines.push(")".to_string());
951lines.push(String::new());
952w.head = std::mem::take(&mut lines);
953954// Instance methods as S7 generics + methods
955 // Skip methods that are property getters/setters (they're handled as S7 properties)
956for ctx in parsed_impl.instance_method_contexts() {
957let method_ident = crate::naming::ident_name(&ctx.method.ident);
958if property_method_idents.contains(&method_ident) {
959continue;
960 }
961962let method_attrs = &ctx.method.method_attrs;
963let target = s7_generic_target(&ctx.generic_name());
964// The bare generic name (a `pkg::` qualifier only picks the target).
965let generic_name = target.name().to_string();
966// The generic in R symbol position (assignment target, first argument
967 // of `S7::method`); string literals keep the bare name (#1475).
968let generic_symbol = crate::naming::r_def_name(&generic_name);
969970// The first dispatch argument names the receiver; the others are the
971 // method's leading parameters (`check_s7_dispatch` enforces this), so
972 // the method's formals start with exactly the generic's dispatch
973 // arguments, as S7 requires.
974let dispatch_args = s7_dispatch_args(ctx.method, &target);
975let receiver = dispatch_args[0].as_str();
976let include_dots = !method_attrs.s7.no_dots;
977let method_formals = ctx.instance_formals_with_receiver(receiver, include_dots);
978let signature =
979 s7_method_signature(&class_name, dispatch_args.len(), method_attrs.s7.fallback);
980 w.member_names.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("the S7 method of `{0}` for `{1}`",
generic_name, signature))
})format!(
981"the S7 method of `{generic_name}` for `{signature}`"
982));
983984// Emit the generic-doc marker BEFORE the source comment so that the
985 // write-time pass can place the standalone Rd page before the method block.
986 // This ensures the synthesised doc block (ending with NULL) is always
987 // separated from the method's roxygen comment by the source `# comment` line,
988 // preventing roxygen2 from merging the two blocks into one.
989 //
990 // Only package-owned generics get a marker; fallback methods dispatch on
991 // S7::class_any, and base operators / external generics belong elsewhere.
992if #[allow(non_exhaustive_omitted_patterns)] match target {
S7GenericTarget::Local(_) => true,
_ => false,
}matches!(target, S7GenericTarget::Local(_))993 && !method_attrs.s7.fallback
994 && !class_has_no_rd
995 {
996let no_dots_str = if include_dots { "false" } else { "true" };
997 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(".__MX_GENERIC_DOC__(kind=\"S7\", generic=\"{1}\", class=\"{2}\", export={3}, dispatch=\"{0}\", no_dots={4})",
dispatch_args.join(","), generic_name, class_name,
should_export, no_dots_str))
})format!(
998".__MX_GENERIC_DOC__(kind=\"S7\", generic=\"{generic_name}\", class=\"{class_name}\", export={should_export}, dispatch=\"{}\", no_dots={no_dots_str})",
999 dispatch_args.join(",")
1000 ));
1001 }
10021003 lines.push(ctx.source_comment(type_ident));
10041005// For fallback methods (class_any), check class before using @ to extract
1006 // the pointer. Non-S7 objects can't have @.ptr: error in R rather than
1007 // passing a wrong type to Rust (which would segfault). The error is the
1008 // shared argument error on the receiver (#1591), attributed like
1009 // `stop()` was to the method's call.
1010let self_expr = if method_attrs.s7.fallback {
1011::alloc::__export::must_use({
::alloc::fmt::format(format_args!("if (inherits({0}, \"S7_object\")) {0}@.ptr else .miniextendr_arg_error(\"{0}\", paste0(\"must be an S7 object, got \", class({0})[[1L]]))",
receiver))
})format!(
1012"if (inherits({receiver}, \"S7_object\")) {receiver}@.ptr else .miniextendr_arg_error(\"{receiver}\", paste0(\"must be an S7 object, got \", class({receiver})[[1L]]))"
1013)1014 } else {
1015::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}@.ptr", receiver))
})format!("{receiver}@.ptr")1016 };
1017let call = ctx.instance_call(&self_expr);
10181019// Documentation - skip if class has @noRd.
1020 // Use class-qualified @name to avoid duplicate \alias{generic} warnings
1021 // when multiple S7 classes share the same generic (e.g., get_value on both
1022 // S7TraitCounter and CounterTraitS7). The @export is replaced with
1023 // @rawNamespace to explicitly export the bare generic name.
1024if !class_has_no_rd {
1025let qualified_name = ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}-{1}", class_name,
generic_name))
})format!("{}-{}", class_name, generic_name);
1026let method_doc =
1027 MethodDocBuilder::new(&class_name, &generic_name, type_ident, &ctx.method.doc_tags)
1028 .with_source_method(&ctx.method.ident)
1029 .with_suppress_params()
1030 .with_r_name(qualified_name);
1031let mut doc_lines = method_doc.build();
1032 doc_lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @aliases {0}${1}", class_name,
generic_name))
})format!("#' @aliases {}${}", class_name, generic_name));
1033// With no generic definition in between, roxygen2 would document the
1034 // `S7::method(`[[`, Class) <-` right below as an S7 method and write a
1035 // `\usage` (`x[[i, ...]]`) whose formals and operator alias this block
1036 // does not provide. Keep base-operator methods usage-free, like every
1037 // other S7 method block.
1038if #[allow(non_exhaustive_omitted_patterns)] match target {
S7GenericTarget::BaseOperator(_) => true,
_ => false,
}matches!(target, S7GenericTarget::BaseOperator(_)) {
1039 doc_lines.push("#' @usage NULL".to_string());
1040 }
1041 lines.extend(doc_lines);
1042 }
10431044match &target {
1045// `[[`, `+`, ... always exist in base, and S7 resolves them itself
1046 // (the `Ops` operators through its own two-argument generics).
1047S7GenericTarget::BaseOperator(_) => {}
1048 S7GenericTarget::External { package, name } => {
1049// Bind the external generic under the generic's own name: when S7
1050 // registers the method for an S3 generic of a package that is not
1051 // loaded yet, it looks this binding up by name to find the package.
1052 // Calls to `name()` elsewhere in the package are unaffected, since
1053 // R's function lookup skips a non-function binding. The guard keeps
1054 // the assignment out of roxygen2's view and lets a second class's
1055 // method on the same generic reuse the binding.
1056 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("if (!base::inherits(base::get0(\"{0}\", inherits = FALSE), \"S7_external_generic\")) {{",
name))
})format!(
1057"if (!base::inherits(base::get0(\"{name}\", inherits = FALSE), \"S7_external_generic\")) {{"
1058));
1059 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(" {1} <- S7::new_external_generic(\"{2}\", \"{3}\", {0})",
r_chr_vector(&dispatch_args), generic_symbol, package, name))
})format!(
1060" {generic_symbol} <- S7::new_external_generic(\"{package}\", \"{name}\", {})",
1061 r_chr_vector(&dispatch_args)
1062 ));
1063 lines.push("}".to_string());
1064 }
1065 S7GenericTarget::Local(_) => {
1066// Use @rawNamespace to explicitly export the bare generic name.
1067 // Plain @export would export the qualified @name (e.g., "ClassName-method")
1068 // instead of the bare generic. The raw directive is not quoted by
1069 // roxygen2, so an operator generic is written `export("%op%")`.
1070if should_export {
1071 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @rawNamespace export({0})",
crate::naming::r_namespace_name(&generic_name)))
})format!(
1072"#' @rawNamespace export({})",
1073crate::naming::r_namespace_name(&generic_name)
1074 ));
1075 }
1076 emit_s7_local_generic(&mut lines, &generic_name, &dispatch_args, include_dots);
1077 }
1078 }
10791080let strategy = crate::ReturnStrategy::for_method(ctx.method);
1081let body_lines = crate::MethodReturnBuilder::new(call)
1082 .with_strategy(strategy)
1083 .with_invisible(ctx.method.is_invisible())
1084 .with_class_name(class_name.clone())
1085 .with_return_class_from_method(ctx.method, &type_ident.to_string())
1086 .with_chain_var(receiver.to_string())
1087 .build_s7_body();
10881089let what = ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}.{1}", generic_name,
class_name))
})format!("{}.{}", generic_name, class_name);
1090 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("S7::method({0}, {1}) <- function({2}) {{",
generic_symbol, signature, method_formals))
})format!(
1091"S7::method({generic_symbol}, {signature}) <- function({method_formals}) {{"
1092));
1093 ctx.emit_method_prelude(&mut lines, " ", &what, &method_formals);
1094 lines.extend(body_lines);
1095 lines.push("}".to_string());
1096if let S7GenericTarget::BaseOperator(op) = &target {
1097 lines.push(s7_base_operator_unbind(op));
1098 }
1099 lines.push(String::new());
11001101// Per-class fast-path dispatch shortcut (#949).
1102 //
1103 // Alongside the S7 generic, emit a plain non-generic function
1104 // `<ClassName>_<method_name>(self, ...)` whose body is identical to the
1105 // generic's method but calls `.Call` directly — bypassing
1106 // `S7::S7_dispatch()` (class-walk + method-table lookup). On hot loops
1107 // this is several times faster than the generic. The receiver is named
1108 // `self` here (the generic names it `x`) and is wired through `self@.ptr`.
1109 //
1110 // Fallback methods dispatch on `S7::class_any`, so a per-class shortcut
1111 // is meaningless — skip them. `s7(no_shortcut)` opts a method out
1112 // explicitly (e.g. to avoid a name collision with a sidecar accessor).
1113 // An operator R name (`r_name = "[["`) has no syntactic shortcut name,
1114 // so it gets none (see `s7_shortcut_name`).
1115let method_name = ctx.method.r_method_name();
1116let shortcut_name = if method_attrs.s7.fallback || method_attrs.s7.no_shortcut {
1117None
1118} else {
1119 s7_shortcut_name(&class_name, &method_name)
1120 };
1121if let Some(shortcut_name) = shortcut_name {
1122let shortcut_call = ctx.instance_call(".mx_self@.ptr");
1123let shortcut_formals =
1124 ctx.instance_formals_with_receiver(".mx_self", !method_attrs.s7.no_dots);
11251126// Document the shortcut as a standalone function merged onto the
1127 // class @rdname page (same shape as static methods). Prepend a
1128 // fast-path advisory describing the dispatch bypass and the
1129 // subclass-override footgun, then reuse MethodDocBuilder for the
1130 // @name / @rdname / @source / @param scaffolding so roxygen2 emits a
1131 // complete \usage + \arguments block (no "undocumented argument"
1132 // warning). `@param .mx_self` is documented explicitly because
1133 // MethodDocBuilder skips the receiver.
1134if !class_has_no_rd {
1135 lines.extend(shortcut_advisory_lines(&method_name, &class_name));
1136 lines.push(super::member_default(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @param .mx_self A `{0}` object.",
class_name))
})format!(
1137"#' @param .mx_self A `{}` object.",
1138 class_name
1139 )));
1140// `...` appears in the shortcut signature for parity with the S7
1141 // generic but MethodDocBuilder doesn't document it, so emit it
1142 // here to avoid an "Undocumented arguments" R CMD check warning.
1143if !method_attrs.s7.no_dots {
1144 lines.push(super::member_default(
1145"#' @param ... Additional arguments; ignored by the fast-path shortcut.",
1146 ));
1147 }
1148// Pass the method's parameter and page tags: its prose
1149 // (@title/description) is already rendered on the shared @rdname
1150 // page by the generic block above, and re-emitting it here would
1151 // duplicate it. Not its `@noRd`: the shortcut is exported with the
1152 // class (below), and an exported function without a page is an
1153 // undocumented code object. The generic block suppresses `@param`
1154 // (an S7 method assignment has no \usage), so the shortcut's \usage is
1155 // the only place those formals are documented; without them each
1156 // one fell back to `(undocumented)`. Mirrors the trait shortcut,
1157 // which forwards `param_tags`.
1158let mx_doc = ctx.choice_param_docs();
1159let param_tags: Vec<String> = ctx
1160 .method
1161 .doc_tags
1162 .iter()
1163 .filter(|tag| {
1164#[allow(non_exhaustive_omitted_patterns)] match crate::roxygen::roxygen_tag_name(tag)
{
Some("param" | "rdname" | "order" | "inheritParams" | "inherit") => true,
_ => false,
}matches!(
1165crate::roxygen::roxygen_tag_name(tag),
1166Some("param" | "rdname" | "order" | "inheritParams" | "inherit")
1167 )1168 })
1169 .cloned()
1170 .collect();
1171let method_doc =
1172 MethodDocBuilder::new(&class_name, &method_name, type_ident, ¶m_tags)
1173 .with_source_method(&ctx.method.ident)
1174 .with_r_params(&shortcut_formals)
1175 .with_choice_param_docs(&mx_doc)
1176 .with_r_name(shortcut_name.clone());
1177 lines.extend(method_doc.build());
1178 }
1179// Export the shortcut when the class is exported, so users can reach it.
1180if should_export {
1181 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @export {0}", shortcut_name))
})format!("#' @export {}", shortcut_name));
1182 }
11831184let strategy = crate::ReturnStrategy::for_method(ctx.method);
1185let shortcut_body = crate::MethodReturnBuilder::new(shortcut_call)
1186 .with_strategy(strategy)
1187 .with_invisible(ctx.method.is_invisible())
1188 .with_class_name(class_name.clone())
1189 .with_return_class_from_method(ctx.method, &type_ident.to_string())
1190 .with_chain_var(".mx_self".to_string())
1191 .build_s7_body();
11921193let what = ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}.{1}", generic_name,
class_name))
})format!("{}.{}", generic_name, class_name);
1194 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0} <- function({1}) {{",
shortcut_name, shortcut_formals))
})format!(
1195"{shortcut_name} <- function({shortcut_formals}) {{"
1196));
1197 ctx.emit_method_prelude(&mut lines, " ", &what, &shortcut_formals);
1198 lines.extend(shortcut_body);
1199 lines.push("}".to_string());
1200 lines.push(String::new());
1201 }
1202 }
12031204// Static methods as regular functions
1205for ctx in parsed_impl.static_method_contexts() {
1206 lines.push(ctx.source_comment(type_ident));
1207let method_name = ctx.method.r_method_name();
1208let fn_name = ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}_{1}", class_name, method_name))
})format!("{}_{}", class_name, method_name);
12091210// Skip documentation if class has @noRd
1211if !class_has_no_rd {
1212let mx_doc = ctx.choice_param_docs();
1213let doc_tags = parsed_impl.static_doc_tags(
1214 ctx.method,
1215&::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}()", fn_name))
})format!("{fn_name}()"),
1216&::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}()", class_name))
})format!("{class_name}()"),
1217true,
1218 );
1219let method_doc =
1220 MethodDocBuilder::new(&class_name, &method_name, type_ident, &doc_tags)
1221 .with_source_method(&ctx.method.ident)
1222 .with_r_params(&ctx.params)
1223 .with_choice_param_docs(&mx_doc)
1224 .with_r_name(fn_name.clone());
1225 lines.extend(method_doc.build());
1226 }
1227// Export static methods so users can call them (if class should be exported)
1228if should_export {
1229 lines.push("#' @export".to_string());
1230 }
12311232// A static method's R name is its only access path, so an operator
1233 // `r_name` is kept and backtick-quoted rather than dropped (#1475).
1234 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0} <- function({1}) {{",
crate::naming::r_def_name(&fn_name), ctx.params))
})format!(
1235"{} <- function({}) {{",
1236crate::naming::r_def_name(&fn_name),
1237 ctx.params
1238 ));
12391240 ctx.emit_method_prelude(&mut lines, " ", &fn_name, &ctx.params);
12411242let strategy = crate::ReturnStrategy::for_method(ctx.method);
1243let return_expr = crate::MethodReturnBuilder::new(ctx.static_call())
1244 .with_strategy(strategy)
1245 .with_invisible(ctx.method.is_invisible())
1246 .with_class_name(class_name.clone())
1247 .with_return_class_from_method(ctx.method, &type_ident.to_string())
1248 .build_s7_inline();
1249 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(" {0}", return_expr))
})format!(" {}", return_expr));
12501251 lines.push("}".to_string());
1252 lines.push(String::new());
1253 }
12541255// Phase 4: S7 convert() methods from Rust From/TryFrom patterns
1256 // Convert methods enable type coercion between S7 classes using S7::convert()
1257 //
1258 // Two patterns:
1259 // 1. convert_from = "OtherType" on static method: converts FROM OtherType TO this class
1260 // Rust: fn from_other(other: OtherType) -> Self
1261 // R: S7::method(S7::convert, list(OtherType, ThisClass)) <- function(from, to) ...
1262 //
1263 // 2. convert_to = "OtherType" on instance method: converts FROM this class TO OtherType
1264 // Rust: fn to_other(&self) -> OtherType
1265 // R: S7::method(S7::convert, list(ThisClass, OtherType)) <- function(from, to) ...
12661267for method in &parsed_impl.methods {
1268if !method.should_include() {
1269continue;
1270 }
1271let attrs = &method.method_attrs;
12721273// Handle convert_from (static method pattern)
1274 // S7 convert signature is function(from, to) - one parameter for the source object
1275if let Some(ref from_type) = attrs.s7.convert_from {
1276 w.member_names.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("the S7 `convert()` method from `{0}` to `{1}`",
from_type, class_name))
})format!(
1277"the S7 `convert()` method from `{from_type}` to `{class_name}`"
1278));
1279let ctx = MethodContext::new(method, type_ident)
1280 .with_impl_preconditions(parsed_impl.preconditions);
12811282// Documentation for convert method (skip if class has @noRd)
1283if !class_has_no_rd {
1284 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @name convert-{0}-to-{1}",
from_type, class_name))
})format!("#' @name convert-{}-to-{}", from_type, class_name));
1285// Convert methods emit scaffolding only (no method prose), but a
1286 // method-level `@rdname` still decides which page they land on;
1287 // a split page needs its own `@title` (see `MethodDocBuilder`).
1288super::push_member_rdname(&mut lines, &method.doc_tags, &class_name);
1289if crate::roxygen::rdname_value(&method.doc_tags).is_some()
1290 && !crate::roxygen::has_roxygen_tag(&method.doc_tags, "title")
1291 {
1292 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @title convert-{0}-to-{1}",
from_type, class_name))
})format!("#' @title convert-{}-to-{}", from_type, class_name));
1293 }
1294crate::roxygen::push_order_after_topic_blocks(
1295&mut lines,
1296&method.doc_tags,
1297&class_name,
1298 );
1299 lines.extend(crate::roxygen::method_source_tag(type_ident, &method.ident));
1300// The generic belongs to S7. Giving every class page its
1301 // `convert` alias duplicates that alias across the package.
1302 // Describe the registration here; ordinary Rust shortcuts
1303 // retain their own usage and argument documentation.
1304lines.push("#' @usage NULL".to_string());
1305 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @section Conversion from `{0}`:",
class_ref_or_verbatim(from_type)))
})format!(
1306"#' @section Conversion from `{}`:",
1307 class_ref_or_verbatim(from_type)
1308 ));
1309 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' `S7::convert(from, {0})` converts a source object to this class.",
class_name))
})format!(
1310"#' `S7::convert(from, {})` converts a source object to this class.",
1311 class_name
1312 ));
1313 lines.push("#' See [S7::convert()] for the generic's arguments.".to_string());
1314 }
13151316// Generate: S7::method(S7::convert, list(FromType, ThisClass)) <- function(from, to, ...) ...
1317 // The convert_from method takes the source object as its sole parameter
1318 // We pass from@.ptr to extract the ExternalPtr from the S7 object
1319let call_with_from = crate::r_wrapper_builder::DotCallBuilder::new(&ctx.c_ident)
1320 .with_self("from@.ptr")
1321 .build();
13221323let strategy = crate::ReturnStrategy::for_method(method);
1324let return_expr = crate::MethodReturnBuilder::new(call_with_from)
1325 .with_strategy(strategy)
1326 .with_invisible(method.is_invisible())
1327 .with_class_name(class_name.clone())
1328 .with_return_class_from_method(method, &type_ident.to_string())
1329 .build_s7_inline();
13301331// Use imported `convert` - requires `@importFrom S7 convert` in package.
1332 // from_type is a cross-reference → placeholder so the resolver can look it up.
1333 // Method signature includes `...` to match the S7 `convert` generic
1334 // (function(from, to, ...)) and silence R CMD check codoc warnings.
1335let from_type_ref = class_ref_or_verbatim(from_type);
1336 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("S7::method(convert, list({0}, {1})) <- function(from, to, ...) {2}",
from_type_ref, class_name, return_expr))
})format!(
1337"S7::method(convert, list({}, {})) <- function(from, to, ...) {}",
1338 from_type_ref, class_name, return_expr
1339 ));
1340 lines.push(String::new());
1341 }
13421343// Handle convert_to (instance method pattern)
1344 // S7 convert signature is function(from, to) - self becomes from
1345if let Some(ref to_type) = attrs.s7.convert_to {
1346 w.member_names.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("the S7 `convert()` method from `{0}` to `{1}`",
class_name, to_type))
})format!(
1347"the S7 `convert()` method from `{class_name}` to `{to_type}`"
1348));
1349let ctx = MethodContext::new(method, type_ident)
1350 .with_impl_preconditions(parsed_impl.preconditions);
13511352// Documentation for convert method (skip if class has @noRd)
1353if !class_has_no_rd {
1354 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @name convert-{0}-to-{1}",
class_name, to_type))
})format!("#' @name convert-{}-to-{}", class_name, to_type));
1355super::push_member_rdname(&mut lines, &method.doc_tags, &class_name);
1356if crate::roxygen::rdname_value(&method.doc_tags).is_some()
1357 && !crate::roxygen::has_roxygen_tag(&method.doc_tags, "title")
1358 {
1359 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @title convert-{0}-to-{1}",
class_name, to_type))
})format!("#' @title convert-{}-to-{}", class_name, to_type));
1360 }
1361crate::roxygen::push_order_after_topic_blocks(
1362&mut lines,
1363&method.doc_tags,
1364&class_name,
1365 );
1366 lines.extend(crate::roxygen::method_source_tag(type_ident, &method.ident));
1367 lines.push("#' @usage NULL".to_string());
1368 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @section Conversion to `{0}`:",
class_ref_or_verbatim(to_type)))
})format!(
1369"#' @section Conversion to `{}`:",
1370 class_ref_or_verbatim(to_type)
1371 ));
1372 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' `S7::convert(from, {0})` converts an object of this class to the target class.",
class_ref_or_verbatim(to_type)))
})format!(
1373"#' `S7::convert(from, {})` converts an object of this class to the target class.",
1374 class_ref_or_verbatim(to_type)
1375 ));
1376 lines.push("#' See [S7::convert()] for the generic's arguments.".to_string());
1377 }
13781379// Generate: S7::method(convert, list(ThisClass, ToType)) <- function(from, to, ...) ...
1380 // The convert_to method is an instance method where self is mapped to from@.ptr
1381let call = crate::r_wrapper_builder::DotCallBuilder::new(&ctx.c_ident)
1382 .with_self("from@.ptr")
1383 .build();
13841385// to_type is a cross-reference → placeholder for resolver.
1386 // We also pass the placeholder to MethodReturnBuilder so the
1387 // emitted `ToType(.ptr = <result>)` uses the resolved name.
1388let to_type_ref = class_ref_or_verbatim(to_type);
13891390// Force ReturnSelf strategy for convert methods since they return S7 class types
1391 // that need to be wrapped: ToType(.ptr = <result>)
1392let return_expr = crate::MethodReturnBuilder::new(call)
1393 .with_strategy(crate::ReturnStrategy::ReturnSelf)
1394 .with_class_name(to_type_ref.clone())
1395 .with_invisible(method.is_invisible())
1396 .with_return_class_from_method(method, &type_ident.to_string())
1397 .build_s7_inline();
13981399// Use imported `convert` - requires `@importFrom S7 convert` in package.
1400 // Method signature includes `...` to match the S7 `convert` generic
1401 // (function(from, to, ...)) and silence R CMD check codoc warnings.
1402 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("S7::method(convert, list({0}, {1})) <- function(from, to, ...) {2}",
class_name, to_type_ref, return_expr))
})format!(
1403"S7::method(convert, list({}, {})) <- function(from, to, ...) {}",
1404 class_name, to_type_ref, return_expr
1405 ));
1406 lines.push(String::new());
1407 }
1408 }
14091410w.members = lines;
1411w1412}
14131414/// The S7 class as one string: [`generate_s7_class`], rendered.
1415#[cfg(test)]
1416pub fn generate_s7_r_wrapper(parsed_impl: &ParsedImpl) -> String {
1417 generate_s7_class(parsed_impl).render()
1418}