miniextendr_macros/r_wrapper_builder.rs
1//! Shared utilities for building R wrapper code.
2//!
3//! This module provides builders for constructing R function signatures and call arguments
4//! consistently across both standalone functions and impl methods.
5//!
6//! ## Key Components
7//!
8//! - [`RArgumentBuilder`]: Builds R formals and `.Call()` arguments from Rust signatures
9//! - [`DotCallBuilder`]: Formats `.Call()` invocations with proper argument handling
10//! - [`RoxygenBuilder`]: Generates roxygen2 documentation tags
11//!
12//! ## Usage
13//!
14//! ```ignore
15//! // Build R function signature
16//! let formals = build_r_formals_from_sig(&method.sig, &r_formals, &defaults);
17//! let call_args = build_r_call_args_from_sig(&method.sig, &r_formals, absent_formals);
18//!
19//! // Build .Call() invocation
20//! let call = DotCallBuilder::new("C_MyType__method")
21//! .with_self("self")
22//! .with_args(&["x", "y"])
23//! .build();
24//!
25//! // Build roxygen tags
26//! let tags = RoxygenBuilder::new("MyType")
27//! .name("method")
28//! .rdname("MyType")
29//! .export()
30//! .build();
31//! ```
32
33/// Normalizes a Rust argument name (its [`crate::naming::ident_name`], no
34/// `r#`) for R.
35///
36/// - Leading `_` → stripped (Rust convention for unused params)
37/// - Leading `__` → stripped
38/// - Otherwise → unchanged
39///
40/// # Examples
41/// - `_x` → `x`
42/// - `_to` → `to`
43/// - `__field` → `field`
44/// - `value` → `value`
45///
46/// Note: We strip underscores rather than prefixing "unused" because R callers
47/// (like vctrs) may use named arguments that must match the original name.
48///
49/// This is the R formal of a parameter without a declared `formal = ".."`
50/// only: R-facing names come from [`Formals::of`], which applies it.
51pub fn normalize_r_arg_string(name: &str) -> String {
52 let normalized = name.trim_start_matches('_');
53 if normalized.is_empty() {
54 "arg".to_string()
55 } else {
56 normalized.to_string()
57 }
58}
59
60/// The R formal of each parameter of one signature (#1842).
61///
62/// A parameter declared with `formal = "conf.int"` (a standalone function's
63/// per-parameter option, a method's `formal(conf_int = "conf.int")`) has that
64/// formal; every other one has [`normalize_r_arg_string`] of its Rust name.
65/// [`Formals::of`] is the one place that rule lives: every site that writes a
66/// parameter's name into R text (the formals, the `.Call()` arguments, the
67/// argument checks and their errors, `e$param` of a conversion error, the
68/// `@param` lines) asks it. Maps keyed by Rust name (the per-parameter
69/// options) stay keyed by Rust name, and so do the `match_arg` placeholders,
70/// which the wrapper writer replaces before R reads them.
71///
72/// A declared formal may hold a `.`, which no Rust identifier can, so it is
73/// only ever a string: never an identifier, and never part of one.
74#[derive(Clone, Debug, Default)]
75pub(crate) struct Formals {
76 /// The declared formals, keyed by Rust parameter name
77 /// ([`crate::naming::ident_name`]), each with the literal that declared
78 /// it (where its errors point).
79 declared: std::collections::HashMap<String, syn::LitStr>,
80}
81
82impl Formals {
83 /// The formals the per-parameter options of one signature declare.
84 pub(crate) fn from_param_attrs(
85 per_param: &std::collections::HashMap<String, crate::miniextendr_fn::ParamAttrs>,
86 ) -> Self {
87 Self {
88 declared: per_param
89 .iter()
90 .filter_map(|(name, attrs)| Some((name.clone(), attrs.formal.clone()?)))
91 .collect(),
92 }
93 }
94
95 /// The R formal of the parameter whose Rust name is `rust_name`: its
96 /// declared formal, else the name without leading underscores.
97 pub(crate) fn of(&self, rust_name: &str) -> String {
98 match self.declared.get(rust_name) {
99 Some(formal) => formal.value(),
100 None => normalize_r_arg_string(rust_name),
101 }
102 }
103
104 /// [`Self::of`] the parameter `ident` (`r#` dropped).
105 pub(crate) fn of_ident(&self, ident: &syn::Ident) -> String {
106 self.of(&crate::naming::ident_name(ident))
107 }
108
109 /// The literal that declared the formal of `rust_name`, if one did.
110 pub(crate) fn declared(&self, rust_name: &str) -> Option<&syn::LitStr> {
111 self.declared.get(rust_name)
112 }
113
114 /// `map` with each key, a Rust parameter name, replaced by that
115 /// parameter's R formal: for maps keyed by Rust name (a function's
116 /// `default = ".."`, a method's `defaults(p = "..")`) that feed a builder
117 /// keyed by R formal.
118 pub(crate) fn rekey<V>(
119 &self,
120 map: impl IntoIterator<Item = (String, V)>,
121 ) -> std::collections::HashMap<String, V> {
122 map.into_iter()
123 .map(|(rust_name, value)| (self.of(&rust_name), value))
124 .collect()
125 }
126}
127
128/// The R names a declared formal must not take, beyond R's own syntax rules:
129/// the names the generated wrapper binds or reads in the function's frame
130/// (#1842). Another dotted name is the framework's only when it starts with
131/// one of [`FRAMEWORK_FORMAL_PREFIXES`].
132const FRAMEWORK_FORMALS: &[(&str, &str)] = &[
133 (
134 ".call",
135 "is the call the generated wrapper passes to `.Call()`",
136 ),
137 (
138 ".val",
139 "is where the generated wrapper keeps the result of `.Call()`",
140 ),
141 (
142 ".ptr",
143 "is the pointer argument of a generated class constructor",
144 ),
145 (
146 ".Generic",
147 "is bound by R's S3 dispatch in a method's frame",
148 ),
149 (".Class", "is bound by R's S3 dispatch in a method's frame"),
150 (".Method", "is bound by R's S3 dispatch in a method's frame"),
151 (".Group", "is bound by R's S3 dispatch in a method's frame"),
152 (
153 ".GenericCallEnv",
154 "is bound by R's S3 dispatch in a method's frame",
155 ),
156 (
157 ".GenericDefEnv",
158 "is bound by R's S3 dispatch in a method's frame",
159 ),
160];
161
162/// Prefixes of the R names the framework generates (locals such as
163/// `.mx_call` and `.mx_self`, the wrappers preamble's `.miniextendr_*`
164/// helpers, the `.rdata_*` helpers `#[derive(ExternalPtr)]` writes for a
165/// sidecar, the write-time `.__MX_*` placeholders): a declared formal starting
166/// with one is refused.
167const FRAMEWORK_FORMAL_PREFIXES: &[&str] = &[".mx_", ".miniextendr_", ".rdata_", ".__MX_"];
168
169/// Why `formal` can't be a declared formal, or `None` when it can: it must be
170/// a syntactic R name ([`crate::naming::is_syntactic_r_name`]: letters,
171/// digits, `.` and `_`, not starting with a digit, `_` or `.` and a digit, and
172/// not a reserved word), not `...` or `..1` (R's dots), and none of the names
173/// the generated wrapper binds itself ([`FRAMEWORK_FORMALS`],
174/// [`FRAMEWORK_FORMAL_PREFIXES`]).
175pub(crate) fn declared_formal_problem(formal: &str) -> Option<String> {
176 if formal.is_empty() {
177 return Some("is empty".to_string());
178 }
179 if formal.starts_with("..") {
180 return Some(
181 "is R's `...` or one of its elements (`..1`, `..2`, ...), not an argument name"
182 .to_string(),
183 );
184 }
185 if crate::naming::is_r_reserved_word(formal) {
186 return Some("is an R reserved word".to_string());
187 }
188 if !crate::naming::is_syntactic_r_name(formal) {
189 return Some(
190 "is not a syntactic R name: it must start with a letter, or with a `.` that is not \
191 followed by a digit, and hold only ASCII letters, digits, `.` and `_`"
192 .to_string(),
193 );
194 }
195 if let Some((_, role)) = FRAMEWORK_FORMALS.iter().find(|(name, _)| *name == formal) {
196 return Some(format!("{role}, so the generated wrapper can't take it"));
197 }
198 FRAMEWORK_FORMAL_PREFIXES
199 .iter()
200 .find(|prefix| formal.starts_with(*prefix))
201 .map(|prefix| {
202 format!(
203 "starts with `{prefix}`, a prefix of the names the framework generates, so \
204 the generated wrapper can't take it"
205 )
206 })
207}
208
209/// Check the R formals a signature produces, before any wrapper is generated.
210///
211/// Each parameter becomes its formal ([`Formals::of`]: the declared
212/// `formal = ".."`, else [`normalize_r_arg_string`] of its name); the `&Dots`
213/// parameter becomes `...`, wherever it sits, and is skipped. A formal must be
214/// an R name (not a reserved word such as `if` or `in`, not starting with a
215/// digit; a declared one must pass [`declared_formal_problem`]), distinct
216/// from the others (`x` and `_x` both become `x`), and not one of the
217/// `reserved` names the generated wrapper binds itself (a method's receiver,
218/// R6's `self` and `private`), each paired with what it is. Any of these would
219/// break the wrapper: a parse error in the wrappers file, or a parameter that
220/// hides the object the method works on.
221///
222/// Every case is a compile error rather than a rename: callers pass arguments
223/// by name, so the R formal has to stay the parameter's own name unless the
224/// author declares another. An error points at the declaring literal for a
225/// declared formal, else at the parameter. Every problem of the signature is
226/// reported, in one combined error.
227pub(crate) fn check_r_formals(
228 inputs: &syn::punctuated::Punctuated<syn::FnArg, syn::token::Comma>,
229 formals: &Formals,
230 reserved: &[(String, String)],
231) -> syn::Result<()> {
232 let mut errors: Option<syn::Error> = None;
233 let mut push = |error: syn::Error| match errors.as_mut() {
234 Some(all) => all.combine(error),
235 None => errors = Some(error),
236 };
237 let mut seen: std::collections::HashMap<String, &syn::Ident> = std::collections::HashMap::new();
238 for (formal, ident) in r_formal_names(inputs, formals) {
239 let declared = formals.declared(&crate::naming::ident_name(ident));
240 let problem = if let Some(lit) = declared {
241 declared_formal_problem(&formal).map(|problem| (lit.span(), problem))
242 } else if crate::naming::is_r_reserved_word(&formal) {
243 Some((
244 ident.span(),
245 format!(
246 "is an R reserved word, so the generated R wrapper would not parse. \
247 Rename the parameter (for example `{formal}_`)"
248 ),
249 ))
250 } else if formal.starts_with(|c: char| c.is_ascii_digit()) {
251 Some((
252 ident.span(),
253 "starts with a digit, so the generated R wrapper would not parse. \
254 Rename the parameter so that, without its leading underscores, it \
255 starts with a letter"
256 .to_string(),
257 ))
258 } else {
259 None
260 };
261 let problem = problem.or_else(|| {
262 reserved
263 .iter()
264 .find(|(name, _)| *name == formal)
265 .map(|(_, role)| {
266 let fix = if declared.is_some() {
267 "Declare another formal"
268 } else {
269 "Rename the parameter"
270 };
271 (
272 declared.map_or(ident.span(), syn::LitStr::span),
273 format!("{role}. {fix}"),
274 )
275 })
276 });
277 if let Some((span, problem)) = problem {
278 let becomes = if declared.is_some() {
279 "is declared as"
280 } else {
281 "becomes"
282 };
283 push(syn::Error::new(
284 span,
285 format!(
286 "parameter `{ident}` {becomes} the R argument `{formal}`, which {problem}."
287 ),
288 ));
289 continue;
290 }
291 if let Some(first) = seen.insert(formal.clone(), ident) {
292 let span = declared.map_or(ident.span(), syn::LitStr::span);
293 let how = if declared.is_some()
294 || formals
295 .declared(&crate::naming::ident_name(first))
296 .is_some()
297 {
298 "a declared `formal` names the R argument"
299 } else {
300 "the R name drops leading underscores"
301 };
302 push(syn::Error::new(
303 span,
304 format!(
305 "parameters `{first}` and `{ident}` both become the R argument `{formal}` \
306 ({how}), and R rejects a repeated argument name. Rename one of them."
307 ),
308 ));
309 }
310 }
311 errors.map_or(Ok(()), Err)
312}
313
314/// The R formal each parameter of a signature becomes, with the parameter's
315/// ident: [`Formals::of`] its name. The `&Dots` / `LazyDots` parameter becomes
316/// `...` wherever it sits and is skipped, as are receivers, non-ident patterns
317/// and an `NArgs` parameter, which is no formal (#1860).
318///
319/// [`check_r_formals`] checks these names, and the shadowing pass
320/// ([`formal_names`](crate::r_shadowing::formal_names)) qualifies the calls
321/// they would shadow, so the two agree on what each formal is named.
322pub(crate) fn r_formal_names<'a>(
323 inputs: &'a syn::punctuated::Punctuated<syn::FnArg, syn::token::Comma>,
324 formals: &'a Formals,
325) -> impl Iterator<Item = (String, &'a syn::Ident)> {
326 let dots_index = crate::miniextendr_fn::dots_index(inputs);
327 inputs.iter().enumerate().filter_map(move |(idx, input)| {
328 let syn::FnArg::Typed(pat_type) = input else {
329 return None;
330 };
331 if Some(idx) == dots_index || crate::type_inspect::is_nargs_marker(&pat_type.ty) {
332 return None;
333 }
334 let syn::Pat::Ident(pat_ident) = pat_type.pat.as_ref() else {
335 return None;
336 };
337 let ident = &pat_ident.ident;
338 Some((formals.of_ident(ident), ident))
339 })
340}
341
342/// Split a comma-separated choices list (as given to `choices(param = "a, b, c")`)
343/// into individual trimmed entries. Surrounding double-quotes are tolerated so
344/// users can spell the list either way: `"a, b"` or `"\"a\", \"b\""`.
345///
346/// Shared by the inherent-impl (`miniextendr_impl.rs`) and trait-impl
347/// (`miniextendr_impl_trait/vtable.rs`) `choices(...)` attribute parsers so the
348/// two independently-maintained parsers can't drift on quoting/whitespace rules.
349pub(crate) fn split_choice_list(raw: &str) -> Vec<String> {
350 raw.split(',')
351 .map(|s| s.trim().trim_matches('"').to_string())
352 .filter(|s| !s.is_empty())
353 .collect()
354}
355
356/// Builder for R function formal parameters and call arguments.
357///
358/// Handles:
359/// - R formals: the declared `formal = ".."` (#1842), else the Rust name
360/// without leading underscores (`_x` → `x`)
361/// - Unit type defaults (`()` → `= NULL`)
362/// - Dots: the `&Dots` parameter is `...` in the formals and `list(...)` in
363/// the call arguments, at its own position in the signature; a `LazyDots`
364/// parameter is `...` too, and `environment()` in the call arguments, which
365/// passes the dots unforced (#1892)
366/// - The argument count: an `NArgs` parameter is no formal and `nargs()` in
367/// the call arguments, at its own position (#1860)
368/// - Consistent formatting across function and method wrappers
369pub struct RArgumentBuilder<'a> {
370 /// The function's input parameters from the parsed Rust signature.
371 inputs: &'a syn::punctuated::Punctuated<syn::FnArg, syn::token::Comma>,
372 /// Position of the `&Dots` / `LazyDots` parameter in `inputs` (receiver
373 /// included, the way both build loops count). Its Rust name never reaches
374 /// R: the formal is always plain `...`.
375 dots_index: Option<usize>,
376 /// Which `...` type it is: what the call arguments pass at its position.
377 dots_kind: Option<crate::miniextendr_fn::DotsKind>,
378 /// If true, skip the first parameter (used for `self`/`&self` in method wrappers,
379 /// since the self argument is handled separately by [`DotCallBuilder::with_self`]).
380 skip_first: bool,
381 /// Parameter default values from `#[miniextendr(default = "...")]` attributes.
382 /// Keys are R formals ([`Formals::of`]), values are R expressions emitted
383 /// verbatim (e.g., `"1L"`, `"c(1, 2, 3)"`, `"NULL"`).
384 defaults: std::collections::HashMap<String, String>,
385 /// The single-choice `Missing<..>` parameters that read a value
386 /// `identical()` to their formal as omitted (#2152), R formal → the
387 /// formal's default (see
388 /// [`crate::miniextendr_fn::ParamAttrs::absent_formal`]).
389 absent_formals: std::collections::HashMap<String, String>,
390 /// The R formal of each parameter (#1842).
391 formals: Formals,
392}
393
394impl<'a> RArgumentBuilder<'a> {
395 /// Create a new builder for the given function inputs.
396 pub fn new(inputs: &'a syn::punctuated::Punctuated<syn::FnArg, syn::token::Comma>) -> Self {
397 let dots_index = crate::miniextendr_fn::dots_index(inputs);
398 let dots_kind = dots_index.and_then(|idx| match &inputs[idx] {
399 syn::FnArg::Typed(pt) => crate::miniextendr_fn::dots_kind(pt.ty.as_ref()),
400 syn::FnArg::Receiver(_) => None,
401 });
402 Self {
403 inputs,
404 dots_index,
405 dots_kind,
406 skip_first: false,
407 defaults: std::collections::HashMap::new(),
408 absent_formals: std::collections::HashMap::new(),
409 formals: Formals::default(),
410 }
411 }
412
413 /// Name each parameter by its R formal (#1842): the declared
414 /// `formal = ".."`, else the normalized Rust name (the default). The
415 /// keys of [`Self::with_defaults`] and [`Self::with_absent_formals`] are
416 /// these formals.
417 pub fn with_formals(mut self, formals: Formals) -> Self {
418 self.formals = formals;
419 self
420 }
421
422 /// Add the `Missing<..>` choice parameters that read a value `identical()`
423 /// to their formal as omitted (#2152), keyed by R formal, valued by that
424 /// formal's default (see
425 /// [`crate::miniextendr_fn::absent_formals_by_r_name`]).
426 pub fn with_absent_formals(
427 mut self,
428 absent_formals: std::collections::HashMap<String, String>,
429 ) -> Self {
430 self.absent_formals = absent_formals;
431 self
432 }
433
434 /// Add parameter defaults from `#[miniextendr(default = "...")]` attributes.
435 ///
436 /// Keys are R formals (see [`Self::with_formals`]; without a declared
437 /// formal, the Rust name without leading underscores), values are R
438 /// expression strings emitted verbatim into formals.
439 pub fn with_defaults(mut self, defaults: std::collections::HashMap<String, String>) -> Self {
440 self.defaults = defaults;
441 self
442 }
443
444 /// Skip the first parameter (for instance methods with `self`).
445 pub fn skip_first(mut self) -> Self {
446 self.skip_first = true;
447 self
448 }
449
450 /// Build R formal parameters string (for function signature).
451 ///
452 /// # Returns
453 /// Comma-separated parameter list, e.g., `"x, y = NULL, ..."`
454 ///
455 /// This method handles R-style defaults (like `1L`, `c(1,2,3)`) that aren't
456 /// valid Rust syntax by outputting them directly as strings.
457 pub fn build_formals(&self) -> String {
458 let mut formals = Vec::new();
459
460 for (idx, input) in self.inputs.iter().enumerate() {
461 // Skip first if requested (for self in methods)
462 if self.skip_first && idx == 0 {
463 continue;
464 }
465
466 let pat_type = match input {
467 syn::FnArg::Typed(pt) => pt,
468 syn::FnArg::Receiver(_) => continue, // Skip self receivers
469 };
470
471 // The dots, at their own position. In R, `...` takes no name or
472 // default, so the Rust binding never shows: the formal is `...`.
473 if Some(idx) == self.dots_index {
474 formals.push("...".to_string());
475 continue;
476 }
477
478 // The argument count (#1860) is no formal: the call arguments pass
479 // `nargs()` in its place.
480 if crate::type_inspect::is_nargs_marker(&pat_type.ty) {
481 continue;
482 }
483
484 // The parameter's R formal (#1842)
485 let formal = match pat_type.pat.as_ref() {
486 syn::Pat::Ident(pat_ident) => self.formals.of_ident(&pat_ident.ident),
487 _ => continue,
488 };
489
490 // Check for user-specified default value
491 if let Some(default_val) = self.defaults.get(&formal) {
492 // User provided default via #[miniextendr(default = "...")]
493 // Output directly as string - supports R-style defaults like "1L", "c(1,2,3)"
494 formals.push(format!("{} = {}", formal, default_val));
495 continue;
496 }
497
498 // Add default for unit types
499 match pat_type.ty.as_ref() {
500 syn::Type::Tuple(t) if t.elems.is_empty() => {
501 formals.push(format!("{} = NULL", formal));
502 }
503 _ => {
504 formals.push(formal);
505 }
506 }
507 }
508
509 formals.join(", ")
510 }
511
512 /// Build R call arguments string (for `.Call()` invocation).
513 ///
514 /// # Returns
515 /// Comma-separated argument list, e.g., `"x, y, list(...)"` (or
516 /// `"x, y, environment()"` for a `LazyDots` parameter)
517 pub fn build_call_args(&self) -> String {
518 self.build_call_args_vec().join(", ")
519 }
520
521 /// Build R call arguments as a `Vec<String>`.
522 ///
523 /// Each element is a single argument expression. A `&Dots` parameter
524 /// becomes `"list(...)"`, which captures the variadic args as an R list for
525 /// the `.Call()` interface; a `LazyDots` parameter becomes
526 /// `"environment()"`, the wrapper's own frame, whose `...` binding holds
527 /// the dots unforced (#1892).
528 pub fn build_call_args_vec(&self) -> Vec<String> {
529 let mut call_args = Vec::new();
530
531 for (idx, input) in self.inputs.iter().enumerate() {
532 // Skip first if requested (for self in methods)
533 if self.skip_first && idx == 0 {
534 continue;
535 }
536
537 let syn::FnArg::Typed(pat_type) = input else {
538 continue;
539 };
540
541 // The dots, at their own position: the formal is plain `...`, so
542 // the argument is `list(...)` for `&Dots` and `environment()` for
543 // `LazyDots` (#1892). `.Call()` evaluates its arguments in the
544 // wrapper's frame, so `environment()` is that frame, and nothing in
545 // `...` is forced on the way in.
546 if Some(idx) == self.dots_index {
547 let kind = self
548 .dots_kind
549 .unwrap_or(crate::miniextendr_fn::DotsKind::Forced);
550 call_args.push(kind.r_call_arg().to_string());
551 continue;
552 }
553
554 // The argument count (#1860): the wrapper's own `nargs()`, which
555 // the C wrapper converts like any argument. `.Call()` evaluates
556 // its arguments in the wrapper's frame, so `nargs()` counts the
557 // wrapper's call as typed.
558 if crate::type_inspect::is_nargs_marker(&pat_type.ty) {
559 call_args.push("nargs()".to_string());
560 continue;
561 }
562
563 // The parameter's R formal (#1842)
564 let formal = match pat_type.pat.as_ref() {
565 syn::Pat::Ident(pat_ident) => self.formals.of_ident(&pat_ident.ident),
566 _ => continue,
567 };
568
569 // `Quoted` / `Quosure` (also under `Missing<..>`, #1835): the
570 // argument unevaluated, behind the same missing-argument sentinel
571 // as `Missing<T>`. Neither branch forces it. A `default` on
572 // `Missing<..>` only writes the formal: `missing()` is `TRUE` for
573 // an omitted argument with a default, so the default is never
574 // evaluated.
575 if let Some(param) = crate::type_inspect::unevaluated_param(pat_type.ty.as_ref()) {
576 call_args.push(param.kind.r_call_arg(&formal));
577 continue;
578 }
579
580 // `Missing<T>`: forward true missingness as the `R_MissingArg`
581 // sentinel, produced *at the argument position*. A binding holding
582 // the sentinel errors on symbol lookup ("argument is missing, with
583 // no default"), so the former `if (missing(x)) x <- quote(expr=)`
584 // prelude broke every truly-missing call. A `default` on it
585 // (#2161) only writes the formal: `missing()` is `TRUE` for an
586 // omitted argument with a default, so the default is never
587 // evaluated, and a supplied value, the default's own included, is
588 // `Present`. A single-choice parameter also forwards the
589 // sentinel for a value `identical()` to its formal (#2152), which
590 // its prelude check skipped: the default shown in the usage line
591 // means the same as leaving it out.
592 if is_missing_type(pat_type.ty.as_ref()) {
593 let p = formal;
594 call_args.push(match self.absent_formals.get(&p) {
595 Some(absent) => {
596 format!(
597 "if (missing({p}) || identical({p}, {absent})) quote(expr=) else {p}"
598 )
599 }
600 None => format!("if (missing({p})) quote(expr=) else {p}"),
601 });
602 continue;
603 }
604
605 call_args.push(formal);
606 }
607
608 call_args
609 }
610}
611
612/// Build R formal parameters from a Rust function signature, with optional defaults.
613///
614/// Automatically skips `self`/`&self` receivers. `Missing<T>` parameters without
615/// user-provided defaults appear as bare formals (no default value); the
616/// `R_MissingArg` sentinel forwarding is emitted inline in the `.Call()` args
617/// (see [`RArgumentBuilder::build_call_args_vec`]).
618///
619/// Returns a comma-separated string of R formals, e.g., `"x, y = NULL, ..."`.
620/// `formals` names each parameter (see [`RArgumentBuilder::with_formals`]),
621/// and `defaults` is keyed by those formals.
622pub(crate) fn build_r_formals_from_sig(
623 sig: &syn::Signature,
624 formals: &Formals,
625 defaults: &std::collections::HashMap<String, String>,
626) -> String {
627 let mut builder = RArgumentBuilder::new(&sig.inputs).with_formals(formals.clone());
628 if matches!(sig.inputs.first(), Some(syn::FnArg::Receiver(_))) {
629 builder = builder.skip_first();
630 }
631 builder = builder.with_defaults(defaults.clone());
632 builder.build_formals()
633}
634
635/// Build R `.Call()` arguments from a Rust function signature.
636///
637/// Automatically skips `self`/`&self` receivers (those are passed separately
638/// via [`DotCallBuilder::with_self`]). `&Dots` becomes `list(...)`, `LazyDots`
639/// `environment()`. `absent_formals` are the choice parameters that read a
640/// value `identical()` to their formal as omitted (see
641/// [`RArgumentBuilder::with_absent_formals`]).
642///
643/// Returns a comma-separated string of R call arguments, e.g., `"x, y, list(...)"`.
644/// `formals` names each parameter, as in [`build_r_formals_from_sig`].
645pub(crate) fn build_r_call_args_from_sig(
646 sig: &syn::Signature,
647 formals: &Formals,
648 absent_formals: std::collections::HashMap<String, String>,
649) -> String {
650 let mut builder = RArgumentBuilder::new(&sig.inputs)
651 .with_formals(formals.clone())
652 .with_absent_formals(absent_formals);
653 if matches!(sig.inputs.first(), Some(syn::FnArg::Receiver(_))) {
654 builder = builder.skip_first();
655 }
656 builder.build_call_args()
657}
658
659// region: Missing<T> detection for automatic defaults
660
661/// Check if a type is `Missing<T>` by examining the last path segment.
662///
663/// `Missing<T>` is the miniextendr wrapper for R's "missing argument" concept,
664/// allowing Rust functions to accept optional arguments that R callers can omit.
665pub(crate) fn is_missing_type(ty: &syn::Type) -> bool {
666 match ty {
667 syn::Type::Path(tp) => tp
668 .path
669 .segments
670 .last()
671 .map(|s| s.ident == "Missing")
672 .unwrap_or(false),
673 _ => false,
674 }
675}
676
677// endregion
678
679// region: DotCallBuilder - .Call() invocation formatting
680
681/// Which frame a generated wrapper hands to `.Call(.., .call = ..)` and uses as
682/// the raise fallback (`.miniextendr_raise_condition(.val, <default>)`).
683#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
684pub enum CallAttribution {
685 /// `.call = sys.call()`: the wrapper's own call, as written (default).
686 #[default]
687 Wrapper,
688 /// `.call = .mx_call`, where the wrapper body first binds
689 /// `.mx_call <- .miniextendr_caller_call()`: the caller's call as written,
690 /// resolved by the preamble helper in `miniextendr-api/src/registry.rs`
691 /// (one line per wrapper since #1552, four before). The helper looks two
692 /// frames up the calling chain for the wrapper's caller. It falls back to
693 /// the wrapper's own call when there is no parent frame (top level) or the
694 /// parent frame is not a closure: `eval()`'d code such as a testthat block
695 /// or `source()` has the `eval` primitive as its frame function, whose call
696 /// names `eval`. For a `noexport` entry point behind a hand-written R
697 /// function (`#[miniextendr(noexport, call = caller)]`).
698 ///
699 /// A standalone wrapper with this attribution also takes a trailing
700 /// `.call = NULL` formal ([`CallAttribution::formal`], #1613), which the
701 /// prelude hands to the helper: a hand-written helper sitting between the
702 /// public function and the entry point passes its own caller's frame
703 /// (`call = parent.frame()` in the helper's formals, `.call = call` in its
704 /// body), and the condition names the public function. `.call` also takes a
705 /// call object, used as is. S3 methods keep the zero-argument prelude: a
706 /// trailing formal would break generic/method consistency.
707 Caller,
708 /// `.call = .mx_call`, where the wrapper body first binds
709 /// `.mx_call <- if (is.null(.call)) sys.call() else
710 /// .miniextendr_caller_call(.call, own = TRUE)`: the wrapper's own call,
711 /// as for [`CallAttribution::Wrapper`], unless the caller passes another
712 /// as the trailing `.call = NULL` formal (#1834). Selected by
713 /// `#[miniextendr(call_arg)]` on a standalone function, exported or not:
714 /// an R function composing exported functions (applying them through
715 /// `do.call()`, say) passes `.call = environment()` and every condition
716 /// names its own call. The helper resolves a frame or a call as under
717 /// [`CallAttribution::Caller`]; a frame no closure owns gives the
718 /// wrapper's own call. The success path without `.call` costs one
719 /// `is.null()`. Never parsed from `call = ...`: it is `wrapper`
720 /// attribution plus the formal, so a `Call` marker reads the resolved
721 /// call.
722 Argument,
723 /// `.call = FALSE`: no call at all (#1851). Selected by
724 /// `#[miniextendr(call = none)]` on a standalone function (an S3 method
725 /// included) or by the crate default `call_attribution = "none"`, which
726 /// applies to every standalone function. Every condition the wrapper
727 /// raises then has a `NULL` `conditionCall()`, like R's `call. = FALSE`:
728 /// the R-side checks and the `match_arg` helpers are passed `NULL`, the
729 /// slot carries `FALSE`, the marker the raise helper already reads as "no
730 /// call" (what `call = none` on a condition macro writes, see
731 /// `ConditionCall::None`), and the raise fallback is `NULL`. There is no
732 /// marker spelling: a `Call` / `CallerCall` parameter receives the call,
733 /// and this attribution has none, so either one is a compile error with
734 /// it, as is `call_arg` (whose `.call` would have nothing to replace).
735 NoCall,
736 /// `.call = environment()`: the wrapper's own frame, turned into a call
737 /// only when a condition is raised (#1851). The attribution of every
738 /// generated S3 method that would otherwise be `wrapper`: a standalone
739 /// `s3(...)` function, an impl-block or trait `s3` / `vctrs` method
740 /// ([`CallAttribution::for_s3_method`]). The raise helper and
741 /// `.miniextendr_arg_error` resolve the frame with
742 /// `.miniextendr_frame_call` (`miniextendr-api/src/registry.rs`), which
743 /// names the generic the user called when R dispatched the method
744 /// (`.Generic` is bound in the frame: `summary(x, a = 1)` for
745 /// `summary.cls(x, a = 1)`, `x[i]` for `` `[.cls`(x, i) ``) and writes a
746 /// replacement generic's call as the assignment, `x$name <- value`,
747 /// without the value R passed. A direct call of the method keeps the
748 /// method's name. The success path evaluates `environment()` instead of
749 /// `sys.call()`, the same cost; the rewrite runs only on a raise.
750 Generic,
751 /// `.call = .mx_call`, where the wrapper body first binds
752 /// `.mx_call <- .miniextendr_frame_call(environment())`: the
753 /// [`CallAttribution::Generic`] call resolved before the `.Call()`, for an
754 /// S3 method with a `Call` parameter, whose C wrapper binds it from the
755 /// slot and so needs a call object there (#1851). Costs the frame scan on
756 /// every call, so only the marker selects it.
757 GenericEager,
758}
759
760impl CallAttribution {
761 /// The spelling shared by the attribute (`call = wrapper | caller | none`)
762 /// and the crate default (`call_attribution = "wrapper" | "caller" |
763 /// "none"`).
764 pub fn parse_name(name: &str) -> Option<Self> {
765 match name {
766 "wrapper" => Some(CallAttribution::Wrapper),
767 "caller" => Some(CallAttribution::Caller),
768 "none" => Some(CallAttribution::NoCall),
769 _ => None,
770 }
771 }
772
773 /// The attribute spelling of this attribution. [`CallAttribution::Argument`]
774 /// is `wrapper` attribution with the `call_arg` option, and the two S3
775 /// forms are `wrapper` attribution on a dispatched method, so all three
776 /// share `wrapper`'s spelling.
777 pub fn name(self) -> &'static str {
778 match self {
779 CallAttribution::Wrapper
780 | CallAttribution::Argument
781 | CallAttribution::Generic
782 | CallAttribution::GenericEager => "wrapper",
783 CallAttribution::Caller => "caller",
784 CallAttribution::NoCall => "none",
785 }
786 }
787
788 /// The parameter marker type that selects this attribution (#1566): `Call`
789 /// for `wrapper`, `CallerCall` for `caller`. `none` has no marker: a
790 /// marker receives the call, and `none` passes none.
791 pub fn marker_name(self) -> Option<&'static str> {
792 match self {
793 CallAttribution::Wrapper
794 | CallAttribution::Argument
795 | CallAttribution::Generic
796 | CallAttribution::GenericEager => Some("Call"),
797 CallAttribution::Caller => Some("CallerCall"),
798 CallAttribution::NoCall => None,
799 }
800 }
801
802 /// The attribution of a generated S3 method (#1851): `wrapper` becomes
803 /// [`CallAttribution::Generic`], or [`CallAttribution::GenericEager`] when
804 /// the method takes a `Call` parameter (`has_call_marker`), which needs
805 /// the call resolved before the `.Call()`. `caller` and `none` are
806 /// explicit choices and stay; `Argument` never reaches an S3 method
807 /// (`call_arg` is refused on them).
808 pub fn for_s3_method(self, has_call_marker: bool) -> Self {
809 match self {
810 CallAttribution::Wrapper if has_call_marker => CallAttribution::GenericEager,
811 CallAttribution::Wrapper => CallAttribution::Generic,
812 other => other,
813 }
814 }
815
816 /// Resolve a standalone function's attribution from its three spellings
817 /// (#1566), most specific first: the `Call` / `CallerCall` parameter
818 /// marker, the `call = wrapper | caller | none` attribute, the crate's
819 /// `[package.metadata.miniextendr] call_attribution` default and finally
820 /// the framework default, `wrapper`. A crate default of `caller` applies
821 /// to internal entry points (`noexport` / `internal`) only; an exported
822 /// function's caller is arbitrary user code, so it keeps `wrapper`. A
823 /// crate default of `none` applies to every standalone function, like
824 /// `wrapper` (#1851). The explicit spellings are validated before this
825 /// runs (a `caller` marker or attribute on an exported function is a
826 /// compile error, not a fallback). Class and trait methods always use
827 /// `wrapper`, except the R6 / S7 lambda frames
828 /// ([`DotCallBuilder::null_call_attribution`]) and the S3 methods
829 /// ([`CallAttribution::for_s3_method`]).
830 pub fn resolve(
831 marker: Option<Self>,
832 attribute: Option<Self>,
833 crate_default: Option<Self>,
834 internal_entry: bool,
835 ) -> Self {
836 marker.or(attribute).unwrap_or(match crate_default {
837 Some(CallAttribution::Caller) if !internal_entry => CallAttribution::Wrapper,
838 Some(default) => default,
839 None => CallAttribution::Wrapper,
840 })
841 }
842
843 /// Apply the `call_arg` option (#1834) to a resolved attribution:
844 /// `wrapper` becomes [`CallAttribution::Argument`]. The option is
845 /// validated before this runs: it is a compile error with `call = caller`
846 /// or a `CallerCall` parameter (whose wrapper already takes `.call`), on
847 /// an S3 method and on an `extern "C-unwind"` function, and the caller
848 /// leaves the crate default out of [`CallAttribution::resolve`] when it
849 /// is set, so `caller` never reaches here with it.
850 pub fn with_call_arg(self, call_arg: bool) -> Self {
851 match self {
852 CallAttribution::Wrapper if call_arg => CallAttribution::Argument,
853 other => other,
854 }
855 }
856
857 /// The expression the `.Call()` line passes as `.call`: the call as
858 /// written, the frame it is read from on a raise, or `FALSE` for none.
859 pub fn dot_call_expr(self) -> &'static str {
860 match self {
861 CallAttribution::Wrapper => "sys.call()",
862 CallAttribution::Caller | CallAttribution::Argument | CallAttribution::GenericEager => {
863 ".mx_call"
864 }
865 CallAttribution::NoCall => "FALSE",
866 CallAttribution::Generic => "environment()",
867 }
868 }
869
870 /// The `.call = ...` argument for the `.Call()` line.
871 pub fn dot_call_arg(self) -> &'static str {
872 match self {
873 CallAttribution::Wrapper => ".call = sys.call()",
874 CallAttribution::Caller | CallAttribution::Argument | CallAttribution::GenericEager => {
875 ".call = .mx_call"
876 }
877 CallAttribution::NoCall => ".call = FALSE",
878 CallAttribution::Generic => ".call = environment()",
879 }
880 }
881
882 /// The fallback call handed to `.miniextendr_raise_condition`: the same
883 /// value as the slot, except that `none` passes `NULL` (the helper reads
884 /// `FALSE` in the slot as "no call" and would read it here too; `NULL` is
885 /// the plainer spelling).
886 pub fn raise_default(self) -> &'static str {
887 match self {
888 CallAttribution::Caller | CallAttribution::Argument | CallAttribution::GenericEager => {
889 ".mx_call"
890 }
891 CallAttribution::Wrapper => "sys.call()",
892 CallAttribution::NoCall => "NULL",
893 CallAttribution::Generic => "environment()",
894 }
895 }
896
897 /// The statement the wrapper body needs before anything else: empty for
898 /// [`CallAttribution::Wrapper`], [`CallAttribution::NoCall`] and
899 /// [`CallAttribution::Generic`]; the others bind `.mx_call`. It is the
900 /// first part of the wrapper prelude, ahead of the R-side checks
901 /// (preconditions, `match.arg`), so that those checks can attribute their
902 /// failures to the caller too (#1548). `call_formal` says whether the
903 /// wrapper has the `.call` formal ([`CallAttribution::formal`]); the
904 /// helper then resolves whatever the caller passed there (#1613). Under
905 /// [`CallAttribution::Argument`] a `NULL` `.call` is the wrapper's own
906 /// call, read inline so that a call without `.call` costs one `is.null()`
907 /// (#1834); the wrapper always has the formal there, since the option is
908 /// refused on S3 methods. [`CallAttribution::GenericEager`] resolves the
909 /// method frame's call up front (#1851).
910 pub fn prelude(self, call_formal: bool) -> String {
911 match self {
912 CallAttribution::Caller if call_formal => {
913 ".mx_call <- .miniextendr_caller_call(.call)".to_string()
914 }
915 CallAttribution::Caller => ".mx_call <- .miniextendr_caller_call()".to_string(),
916 CallAttribution::Argument => ".mx_call <- if (is.null(.call)) sys.call() else \
917 .miniextendr_caller_call(.call, own = TRUE)"
918 .to_string(),
919 CallAttribution::GenericEager => {
920 ".mx_call <- .miniextendr_frame_call(environment())".to_string()
921 }
922 CallAttribution::Wrapper | CallAttribution::NoCall | CallAttribution::Generic => {
923 String::new()
924 }
925 }
926 }
927
928 /// The trailing formal a standalone wrapper with this attribution takes:
929 /// `.call = NULL` for [`CallAttribution::Caller`] (#1613), so a
930 /// hand-written helper between the public function and the entry point
931 /// can pass on the call to report, and for [`CallAttribution::Argument`]
932 /// (#1834), so an R function composing exported functions can. `NULL`
933 /// keeps the wrapper's default (the caller's call, or its own); an
934 /// environment names the closure owning that frame (`parent.frame()` from
935 /// a helper, `environment()` from the composing function); a call object
936 /// is used as is. Rust identifiers cannot start with `.`, so the name
937 /// never collides with a parameter. The caller leaves S3 methods out.
938 pub fn formal(self) -> Option<&'static str> {
939 match self {
940 CallAttribution::Caller | CallAttribution::Argument => Some(".call = NULL"),
941 CallAttribution::Wrapper
942 | CallAttribution::NoCall
943 | CallAttribution::Generic
944 | CallAttribution::GenericEager => None,
945 }
946 }
947
948 /// The generated `@param` text for the [`CallAttribution::formal`]. It is
949 /// written as the author's own line would be, not as a filler, so a block
950 /// on a page or an inheritance source no author block documents `.call`
951 /// for still documents it (see `crate::roxygen::push_fn_param_tags`).
952 pub fn param_doc(self) -> Option<&'static str> {
953 match self {
954 CallAttribution::Caller => Some(
955 "The call conditions from this function report: NULL (the default) for \
956 the calling function's call, a frame such as parent.frame() for the call \
957 of the function owning that frame, or a call object. Pass it by name.",
958 ),
959 CallAttribution::Argument => Some(
960 "The call conditions from this function report: NULL (the default) for \
961 this call, a frame such as environment() for the call of the function \
962 owning that frame, or a call object. Pass it by name.",
963 ),
964 CallAttribution::Wrapper
965 | CallAttribution::NoCall
966 | CallAttribution::Generic
967 | CallAttribution::GenericEager => None,
968 }
969 }
970
971 /// The call an R-side check raised in the wrapper body should carry:
972 /// `.mx_call` when the prelude binds it, `NULL` for `none`, the frame
973 /// (`environment()`, which `.miniextendr_arg_error` resolves) for an S3
974 /// method, otherwise `None` (the check keeps its own attribution, which
975 /// is the wrapper's frame).
976 pub fn r_check_call(self) -> Option<&'static str> {
977 match self {
978 CallAttribution::Caller | CallAttribution::Argument | CallAttribution::GenericEager => {
979 Some(".mx_call")
980 }
981 CallAttribution::NoCall => Some("NULL"),
982 CallAttribution::Generic => Some("environment()"),
983 CallAttribution::Wrapper => None,
984 }
985 }
986
987 /// The R statement validating a choice parameter (`match_arg` / `choices`).
988 /// `choices` is the R expression for the choice list: a literal
989 /// `c("a", "b")`, or the write-time placeholder for an enum. `attrs` are
990 /// the parameter's own attributes, which carry `several_ok` and the
991 /// type's layers.
992 ///
993 /// Every form is one call to a preamble helper (`.miniextendr_match_arg`
994 /// for a scalar, `.miniextendr_match_arg_several` for `several_ok`,
995 /// #1472), guarded by the layers of the parameter type: `!missing(..)`
996 /// for `Missing<..>` (#1551), with `!identical(.., <choices>)` for a
997 /// single choice, whose formal reads as omitted (#2152; the `.Call()`
998 /// argument forwards the sentinel for it, see
999 /// [`RArgumentBuilder::with_absent_formals`]), then `!is.null(..)` for `Option<..>`
1000 /// (#1473), or `is.character(..) || is.factor(..)` for `Either<T, R>` and
1001 /// `Either<Vec<T>, R>` (#1612), which leaves `NULL` out too: Rust reads it
1002 /// as `None` under `Option`, and otherwise converts it to the `R` arm, even
1003 /// for `several_ok`, whose plain form reads `NULL` as every choice. The
1004 /// helpers name the argument in their messages, read a factor as its
1005 /// labels, and attribute the error to the wrapper's own call by default;
1006 /// under [`CallAttribution::Caller`] the statement passes `.mx_call` so the
1007 /// caller is named instead (#1548), and under
1008 /// [`CallAttribution::Argument`] so that the call passed as `.call` is.
1009 /// The list is spelled out because the helpers, unlike
1010 /// `base::match.arg(param)`, do not read it off the formal.
1011 ///
1012 /// `aliases` is the `match_arg` parameter's write-time
1013 /// `match_arg_keys::aliases_placeholder`, written last as `, <placeholder>`:
1014 /// the wrapper writer turns it into the type's `aliases =` argument, or
1015 /// removes it for a type without aliases (#1843). `None` for a literal
1016 /// `choices(...)` list, which has no aliases.
1017 pub fn match_arg_statement(
1018 self,
1019 param: &str,
1020 choices: &str,
1021 aliases: Option<&str>,
1022 attrs: &crate::miniextendr_fn::ParamAttrs,
1023 ) -> String {
1024 let helper = if attrs.several_ok {
1025 ".miniextendr_match_arg_several"
1026 } else {
1027 ".miniextendr_match_arg"
1028 };
1029 let call = match self.r_check_call() {
1030 Some(call) => format!(", {call}"),
1031 None => String::new(),
1032 };
1033 let aliases = match aliases {
1034 Some(placeholder) => format!(", {placeholder}"),
1035 None => String::new(),
1036 };
1037 let statement =
1038 format!("{param} <- {helper}({param}, {choices}, \"{param}\"{call}{aliases})");
1039 let mut guards = Vec::new();
1040 if attrs.omittable {
1041 guards.push(format!("!missing({param})"));
1042 // The formal itself reads as omitted too (#2152): the `.Call()`
1043 // argument forwards the sentinel for it.
1044 if let Some(formal) = attrs.absent_formal(choices) {
1045 guards.push(format!("!identical({param}, {formal})"));
1046 }
1047 }
1048 if attrs.either_noun.is_some() {
1049 // Only the forms `match.arg()` reads are choices; everything else
1050 // (NULL included) goes to the `Either`'s `R` arm unchanged.
1051 let reads = format!("is.character({param}) || is.factor({param})");
1052 guards.push(if guards.is_empty() {
1053 reads
1054 } else {
1055 format!("({reads})")
1056 });
1057 } else if attrs.optional {
1058 guards.push(format!("!is.null({param})"));
1059 }
1060 if guards.is_empty() {
1061 statement
1062 } else {
1063 format!("if ({}) {statement}", guards.join(" && "))
1064 }
1065 }
1066}
1067
1068/// Append a standalone wrapper's [`CallAttribution::formal`] to its joined
1069/// `formals` (#1613). The formal goes last: after `...` and after any formal
1070/// that follows the dots, so positional extras land in the dots and `.call`
1071/// is matched by name only; a wrapper without other formals takes it alone.
1072pub(crate) fn with_call_formal(formals: &str, call_formal: Option<&str>) -> String {
1073 match call_formal {
1074 Some(formal) if formals.is_empty() => formal.to_string(),
1075 Some(formal) => format!("{formals}, {formal}"),
1076 None => formals.to_string(),
1077 }
1078}
1079
1080/// Builder for formatting `.Call()` invocations in R wrapper code.
1081///
1082/// Handles the common pattern of `.Call(C_ident, .call = sys.call(), args...)`.
1083///
1084/// # Example
1085///
1086/// ```ignore
1087/// let call = DotCallBuilder::new("C_Counter__increment")
1088/// .with_self("self")
1089/// .build();
1090/// // => ".Call(C_Counter__increment, .call = sys.call(), self)"
1091///
1092/// let call = DotCallBuilder::new("C_Counter__add")
1093/// .with_self("x")
1094/// .with_args(&["n"])
1095/// .build();
1096/// // => ".Call(C_Counter__add, .call = sys.call(), x, n)"
1097/// ```
1098pub struct DotCallBuilder {
1099 /// The C entry point symbol name (e.g., `"C_Counter__increment"`).
1100 /// This is the first argument to `.Call()`.
1101 c_ident: String,
1102 /// Optional self/receiver variable name (e.g., `"self"`, `"x"`).
1103 /// When present, prepended before other arguments in the `.Call()` invocation.
1104 self_var: Option<String>,
1105 /// Additional argument names passed after self (if any) in the `.Call()` invocation.
1106 args: Vec<String>,
1107 /// Expression for the `.call` named argument. `None` means `sys.call()` (the default).
1108 /// Set via [`DotCallBuilder::null_call_attribution`] to emit `.call = NULL` instead.
1109 call_expr: Option<String>,
1110}
1111
1112impl DotCallBuilder {
1113 /// Create a new builder with the C function identifier.
1114 pub fn new(c_ident: impl Into<String>) -> Self {
1115 Self {
1116 c_ident: c_ident.into(),
1117 self_var: None,
1118 args: Vec::new(),
1119 call_expr: None,
1120 }
1121 }
1122
1123 /// Add a self/x parameter (prepended to args).
1124 pub fn with_self(mut self, var: impl Into<String>) -> Self {
1125 self.self_var = Some(var.into());
1126 self
1127 }
1128
1129 /// Add arguments after self (if any).
1130 pub fn with_args(mut self, args: &[impl AsRef<str>]) -> Self {
1131 self.args = args.iter().map(|s| s.as_ref().to_string()).collect();
1132 self
1133 }
1134
1135 /// Add a pre-joined argument string (e.g., `"x, y"`) as a single emit unit.
1136 ///
1137 /// Empty strings are ignored, so callers can pass the result of
1138 /// `build_r_call_args_from_sig` directly without a length check.
1139 pub fn with_args_str(mut self, args: &str) -> Self {
1140 if !args.is_empty() {
1141 self.args.push(args.to_string());
1142 }
1143 self
1144 }
1145
1146 /// Pass `.call = NULL` instead of `.call = sys.call()`: for the R6 / S7
1147 /// lambda frames, which R6 / S7 dispatch calls (R6 finalizer / `deep_clone`, S7
1148 /// property getter / setter / validator), where `sys.call()` names an
1149 /// internal dispatch frame instead of the user's call. Not reachable from
1150 /// any attribute. With `NULL`, the raise helper's fallback (a `NULL`
1151 /// `.val$call` takes `.call_default`, the `sys.call()` in
1152 /// `condition_check_lines`; only a call-less condition's `FALSE` marker
1153 /// gives `NULL`) surfaces the nearest meaningful frame instead.
1154 pub fn null_call_attribution(mut self) -> Self {
1155 self.call_expr = Some("NULL".to_string());
1156 self
1157 }
1158
1159 /// Pass the `.call` of `attribution` ([`CallAttribution::dot_call_expr`]):
1160 /// `environment()` for a generated S3 method
1161 /// ([`CallAttribution::Generic`], #1851), which the raise helper turns into
1162 /// the generic's call when a condition is raised.
1163 pub fn with_call_attribution(mut self, attribution: CallAttribution) -> Self {
1164 self.call_expr = Some(attribution.dot_call_expr().to_string());
1165 self
1166 }
1167
1168 /// Build the `.Call()` string.
1169 pub fn build(&self) -> String {
1170 let call_arg = self.call_expr.as_deref().unwrap_or("sys.call()");
1171
1172 let mut all_args = Vec::new();
1173
1174 if let Some(ref self_var) = self.self_var {
1175 all_args.push(self_var.clone());
1176 }
1177 all_args.extend(self.args.clone());
1178
1179 if all_args.is_empty() {
1180 format!(".Call({}, .call = {})", self.c_ident, call_arg)
1181 } else {
1182 format!(
1183 ".Call({}, .call = {}, {})",
1184 self.c_ident,
1185 call_arg,
1186 all_args.join(", ")
1187 )
1188 }
1189 }
1190}
1191// endregion
1192
1193// region: RoxygenBuilder - roxygen2 documentation tag generation
1194
1195/// Builder for generating roxygen2 documentation tags.
1196///
1197/// Provides a fluent API for building common roxygen tag patterns used
1198/// across all class systems.
1199///
1200/// # Example
1201///
1202/// ```ignore
1203/// let tags = RoxygenBuilder::new()
1204/// .name("Counter$increment")
1205/// .rdname("Counter")
1206/// .export()
1207/// .build();
1208/// // => vec!["#' @name Counter$increment", "#' @rdname Counter", "#' @export"]
1209/// ```
1210pub struct RoxygenBuilder {
1211 /// Value for `@name` tag. Identifies the documented topic (e.g., `"Counter$increment"`).
1212 name: Option<String>,
1213 /// Value for `@rdname` tag. Groups multiple entries onto a single help page
1214 /// (e.g., all methods of `"Counter"` share one Rd file).
1215 rdname: Option<String>,
1216 /// Value for `@title` tag. The one-line title shown in help page headers.
1217 title: Option<String>,
1218 /// Value for `@description` tag. Longer description text below the title.
1219 description: Option<String>,
1220 /// Value for `@source` tag. Typically `"Generated by miniextendr"` provenance info.
1221 source: Option<String>,
1222 /// Whether to emit `@export`. When true, the item is exported from the package NAMESPACE.
1223 export: bool,
1224 /// Value for `@exportMethod` tag. Used for S4 method exports (e.g., `"show"`).
1225 export_method: Option<String>,
1226 /// Values for `@method` tag as `(generic, class)`. Used for S3 method dispatch
1227 /// (e.g., `("print", "Counter")` emits `@method print Counter`).
1228 method: Option<(String, String)>,
1229 /// Additional custom tag lines emitted verbatim (without the `#' ` prefix,
1230 /// which is added during [`build`](Self::build)). Used for tags like
1231 /// `@keywords internal` or `@param` entries.
1232 custom_tags: Vec<String>,
1233}
1234
1235impl RoxygenBuilder {
1236 /// Create a new empty builder.
1237 pub fn new() -> Self {
1238 Self {
1239 name: None,
1240 rdname: None,
1241 title: None,
1242 description: None,
1243 source: None,
1244 export: false,
1245 export_method: None,
1246 method: None,
1247 custom_tags: Vec::new(),
1248 }
1249 }
1250
1251 /// Set the `@name` tag.
1252 pub fn name(mut self, name: impl Into<String>) -> Self {
1253 self.name = Some(name.into());
1254 self
1255 }
1256
1257 /// Set the `@rdname` tag (groups docs into one page).
1258 pub fn rdname(mut self, rdname: impl Into<String>) -> Self {
1259 self.rdname = Some(rdname.into());
1260 self
1261 }
1262
1263 /// Set the `@title` tag.
1264 pub fn title(mut self, title: impl Into<String>) -> Self {
1265 self.title = Some(title.into());
1266 self
1267 }
1268
1269 /// Set the `@description` tag.
1270 #[allow(dead_code)] // Exercised by tests
1271 pub fn description(mut self, desc: impl Into<String>) -> Self {
1272 self.description = Some(desc.into());
1273 self
1274 }
1275
1276 /// Set the `@source` tag (typically "Generated by miniextendr...").
1277 pub fn source(mut self, source: impl Into<String>) -> Self {
1278 self.source = Some(source.into());
1279 self
1280 }
1281
1282 /// Add `@export` tag.
1283 pub fn export(mut self) -> Self {
1284 self.export = true;
1285 self
1286 }
1287
1288 /// Add `@exportMethod` tag (for S4).
1289 #[allow(dead_code)] // Exercised by tests
1290 pub fn export_method(mut self, method: impl Into<String>) -> Self {
1291 self.export_method = Some(method.into());
1292 self
1293 }
1294
1295 /// Add `@method` tag (for S3).
1296 pub fn method(mut self, generic: impl Into<String>, class: impl Into<String>) -> Self {
1297 self.method = Some((generic.into(), class.into()));
1298 self
1299 }
1300
1301 /// Add a custom tag line (without the `#' ` prefix).
1302 pub fn custom(mut self, tag: impl Into<String>) -> Self {
1303 self.custom_tags.push(tag.into());
1304 self
1305 }
1306
1307 /// Build the roxygen tag lines (each prefixed with `#' `).
1308 pub fn build(&self) -> Vec<String> {
1309 let mut lines = Vec::new();
1310
1311 if let Some(ref title) = self.title {
1312 lines.push(format!("#' @title {}", title));
1313 }
1314 if let Some(ref desc) = self.description {
1315 lines.push(format!("#' @description {}", desc));
1316 }
1317 if let Some(ref name) = self.name {
1318 lines.push(format!("#' @name {}", name));
1319 }
1320 if let Some(ref rdname) = self.rdname {
1321 lines.push(format!("#' @rdname {}", rdname));
1322 }
1323 if let Some(ref source) = self.source {
1324 lines.extend(crate::roxygen::source_tag(source));
1325 }
1326 if let Some((ref generic, ref class)) = self.method {
1327 lines.push(format!("#' @method {} {}", generic, class));
1328 }
1329 // A tag the author wrapped (`@param`, `@describeIn`, ...) keeps its
1330 // continuation lines, each with its own `#' ` lead.
1331 crate::roxygen::push_roxygen_tags(&mut lines, &self.custom_tags);
1332 if self.export {
1333 lines.push("#' @export".to_string());
1334 }
1335 if let Some(ref method) = self.export_method {
1336 lines.push(format!("#' @exportMethod {}", method));
1337 }
1338
1339 lines
1340 }
1341}
1342
1343/// Creates an empty builder with no tags set.
1344impl Default for RoxygenBuilder {
1345 fn default() -> Self {
1346 Self::new()
1347 }
1348}
1349// endregion
1350
1351// region: RngSeedContract - the stats::simulate() seed contract
1352
1353/// The seed contract of `stats::simulate()`, which
1354/// `#[miniextendr(rng(seed = <param>))]` writes into a standalone wrapper in
1355/// the order `stats:::simulate.lm` uses (#2198).
1356///
1357/// The [`prelude`](Self::prelude) comes after the argument checks, so a
1358/// refused argument changes no RNG state. It creates `.Random.seed` when it is
1359/// missing. With a `NULL` seed it then remembers the state; otherwise it
1360/// registers the restore of the session's state with `on.exit()` and calls
1361/// `set.seed()` with the value as passed, which validates it. The C wrapper's
1362/// `GetRNGstate()` then loads the seeded state, and its `PutRNGstate()` saves
1363/// the state inside `.Call()`, before `on.exit()` puts the session's state
1364/// back, also when the body returns an error or panics.
1365/// [`ATTRIBUTE`](Self::ATTRIBUTE) records the seed in the result, after the
1366/// result's condition check.
1367pub(crate) struct RngSeedContract<'a> {
1368 /// The seed's R formal.
1369 pub formal: &'a str,
1370}
1371
1372impl RngSeedContract<'_> {
1373 /// The statements before `.Call()`, each line after the first prefixed by
1374 /// `indent`.
1375 pub(crate) fn prelude(&self, indent: &str) -> String {
1376 let seed = self.formal;
1377 let lines = [
1378 "if (!exists(\".Random.seed\", envir = globalenv(), inherits = FALSE)) stats::runif(1)"
1379 .to_owned(),
1380 format!("if (is.null({seed})) {{"),
1381 " .mx_rng_state <- get(\".Random.seed\", envir = globalenv())".to_owned(),
1382 "} else {".to_owned(),
1383 " .mx_r_seed <- get(\".Random.seed\", envir = globalenv())".to_owned(),
1384 " on.exit(assign(\".Random.seed\", .mx_r_seed, envir = globalenv()), add = TRUE)"
1385 .to_owned(),
1386 format!(" set.seed({seed})"),
1387 format!(" .mx_rng_state <- structure({seed}, kind = as.list(RNGkind()))"),
1388 "}".to_owned(),
1389 ];
1390 lines.join(&format!("\n{indent}"))
1391 }
1392
1393 /// The statement that gives the result `.val` its `seed` attribute. A
1394 /// value that can't carry attributes (`NULL`, an environment, an external
1395 /// pointer, a function) is left as it is; `NULL` is tested on its own,
1396 /// since `is.atomic(NULL)` is `TRUE` before R 4.4.
1397 pub(crate) const ATTRIBUTE: &'static str = "if (!is.null(.val) && (is.atomic(.val) || \
1398 is.list(.val))) attr(.val, \"seed\") <- .mx_rng_state";
1399}
1400// endregion
1401
1402// region: Tests
1403
1404#[cfg(test)]
1405mod tests;
1406// endregion