1//! Automatic registration for miniextendr.
2//!
3//! Every `#[miniextendr]` item self-registers at link time. `package_init()`
4//! (generated by `miniextendr_init!`) calls [`miniextendr_register_routines`]
5//! during `R_init_*` to finalize registration with R. Users never interact
6//! with this module.
78use crate::abi::{mx_erased, mx_tag};
9use crate::sys::{DllInfo, R_CallMethodDef};
10#[cfg(not(target_arch = "wasm32"))]
11use linkme::distributed_slice;
12use std::os::raw::c_void;
1314// region: Distributed Slices
15//
16// `linkme::distributed_slice` does not compile for `wasm32-*` targets — the
17// proc macro hits a `compile_error!` arm in `linkme-impl/src/declaration.rs`.
18// On wasm32 we keep the same public surface for the three runtime-critical
19// slices (MX_CALL_DEFS / MX_ALTREP_REGISTRATIONS / MX_TRAIT_DISPATCH) but
20// back them with a `OnceLock` populated from the user crate's
21// `wasm_registry.rs` snapshot at `R_init_*` time (see
22// `install_wasm_runtime_slices` below). The host-only slices (MX_R_WRAPPERS,
23// MX_MATCH_ARG_*, MX_CLASS_NAMES, MX_CLASS_BLOCKS, MX_S7_SIDECAR_PROPS, ...) are
24// only consumed by the wrapper-gen pass — that pass is itself
25// native-only, so the slices, their consumers, and the `wasm_registry_writer`
26// module are all `cfg(not(target_arch = "wasm32"))`-gated.
2728/// R `.Call` method registrations (function + method C wrappers).
29///
30/// Each `#[miniextendr]` function or method emits an entry here.
31#[cfg(not(target_arch = "wasm32"))]
32#[doc = " R `.Call` method registrations (function + method C wrappers)."]
#[doc = ""]
#[doc = " Each `#[miniextendr]` function or method emits an entry here."]
pub static MX_CALL_DEFS: ::linkme::DistributedSlice<[R_CallMethodDef]> =
{
unsafe extern "Rust" {
#[link_name = "__start_linkme_MX_CALL_DEFS"]
static LINKME_START:
[<[R_CallMethodDef] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__stop_linkme_MX_CALL_DEFS"]
static LINKME_STOP:
[<[R_CallMethodDef] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__start_linkm2_MX_CALL_DEFS"]
static DUPCHECK_START: ();
#[link_name = "__stop_linkm2_MX_CALL_DEFS"]
static DUPCHECK_STOP: ();
}
#[used]
#[unsafe(link_section = "linkme_MX_CALL_DEFS")]
static mut LINKME_PLEASE:
[<[R_CallMethodDef] as ::linkme::__private36::Slice>::Element; 0]
=
[];
#[used]
#[unsafe(link_section = "linkm2_MX_CALL_DEFS")]
static DUPCHECK: ::linkme::__private36::isize = 1;
unsafe {
::linkme::DistributedSlice::private_new("MX_CALL_DEFS",
(&raw const LINKME_START).cast::<<[R_CallMethodDef] as
::linkme::__private36::Slice>::Element>(),
(&raw const LINKME_STOP).cast::<<[R_CallMethodDef] as
::linkme::__private36::Slice>::Element>(),
(&raw const DUPCHECK_START).cast::<::linkme::__private36::isize>(),
(&raw const DUPCHECK_STOP).cast::<::linkme::__private36::isize>())
}
};
#[doc(hidden)]
#[macro_export]
macro_rules! _linkme_macro_MX_CALL_DEFS {
(# ! [linkme_macro = $macro : path] # ! [linkme_sort_key = $key : tt]
$item : item) =>
{
$macro !
{
# ! [linkme_linux_section = concat! ("linkme_MX_CALL_DEFS", $key)]
# !
[linkme_macho_section = concat!
("__DATA,__linkmeAFud52Jy,regular,no_dead_strip", $key)] # !
[linkme_windows_section = concat!
(".linkme_MX_CALL_DEFS$b", $key)] # !
[linkme_illumos_section = concat!
("set_linkme_MX_CALL_DEFS", $key)] # !
[linkme_bsd_section = concat! ("linkme_MX_CALL_DEFS", $key)] $item
}
}; (# ! [linkme_macro = $macro : path] $item : item) =>
{
$macro !
{
# ! [linkme_linux_section = "linkme_MX_CALL_DEFS"] # !
[linkme_macho_section =
"__DATA,__linkmeAFud52Jy,regular,no_dead_strip"] # !
[linkme_windows_section = ".linkme_MX_CALL_DEFS$b"] # !
[linkme_illumos_section = "set_linkme_MX_CALL_DEFS"] # !
[linkme_bsd_section = "linkme_MX_CALL_DEFS"] $item
}
};
(# ! [linkme_linux_section = $linux_section : expr] # !
[linkme_macho_section = $macho_section : expr] # !
[linkme_windows_section = $windows_section : expr] # !
[linkme_illumos_section = $illumos_section : expr] # !
[linkme_bsd_section = $bsd_section : expr] $item : item) =>
{
#[used]
#[cfg_attr(any(target_os = "none", target_os = "linux", target_os =
"android", target_os = "fuchsia", target_os = "psp"), unsafe
(link_section = $linux_section))]
#[cfg_attr(any(target_os = "macos", target_os = "ios", target_os =
"tvos"), unsafe (link_section = $macho_section))]
#[cfg_attr(any(target_os = "uefi", target_os = "windows"), unsafe
(link_section = $windows_section))]
#[cfg_attr(target_os = "illumos", unsafe
(link_section = $illumos_section))]
#[cfg_attr(any(target_os = "freebsd", target_os = "openbsd"), unsafe
(link_section = $bsd_section))] $item
};
}
#[doc(hidden)]
pub use _linkme_macro_MX_CALL_DEFS as MX_CALL_DEFS;#[distributed_slice]33pub static MX_CALL_DEFS: [R_CallMethodDef];
3435/// R wrapper code fragments with priority for ordering. **Host-only.**
36///
37/// Each `#[miniextendr]` function, impl block, or trait impl emits an entry.
38/// Priorities ensure correct evaluation order when R sources the wrapper file
39/// (sidecar helpers must be defined before class definitions that reference them).
40#[cfg(not(target_arch = "wasm32"))]
41#[doc =
" R wrapper code fragments with priority for ordering. **Host-only.**"]
#[doc = ""]
#[doc =
" Each `#[miniextendr]` function, impl block, or trait impl emits an entry."]
#[doc =
" Priorities ensure correct evaluation order when R sources the wrapper file"]
#[doc =
" (sidecar helpers must be defined before class definitions that reference them)."]
pub static MX_R_WRAPPERS: ::linkme::DistributedSlice<[RWrapperEntry]> =
{
unsafe extern "Rust" {
#[link_name = "__start_linkme_MX_R_WRAPPERS"]
static LINKME_START:
[<[RWrapperEntry] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__stop_linkme_MX_R_WRAPPERS"]
static LINKME_STOP:
[<[RWrapperEntry] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__start_linkm2_MX_R_WRAPPERS"]
static DUPCHECK_START: ();
#[link_name = "__stop_linkm2_MX_R_WRAPPERS"]
static DUPCHECK_STOP: ();
}
#[used]
#[unsafe(link_section = "linkme_MX_R_WRAPPERS")]
static mut LINKME_PLEASE:
[<[RWrapperEntry] as ::linkme::__private36::Slice>::Element; 0] =
[];
#[used]
#[unsafe(link_section = "linkm2_MX_R_WRAPPERS")]
static DUPCHECK: ::linkme::__private36::isize = 1;
unsafe {
::linkme::DistributedSlice::private_new("MX_R_WRAPPERS",
(&raw const LINKME_START).cast::<<[RWrapperEntry] as
::linkme::__private36::Slice>::Element>(),
(&raw const LINKME_STOP).cast::<<[RWrapperEntry] as
::linkme::__private36::Slice>::Element>(),
(&raw const DUPCHECK_START).cast::<::linkme::__private36::isize>(),
(&raw const DUPCHECK_STOP).cast::<::linkme::__private36::isize>())
}
};
#[doc(hidden)]
#[macro_export]
macro_rules! _linkme_macro_MX_R_WRAPPERS {
(# ! [linkme_macro = $macro : path] # ! [linkme_sort_key = $key : tt]
$item : item) =>
{
$macro !
{
# !
[linkme_linux_section = concat! ("linkme_MX_R_WRAPPERS", $key)] #
!
[linkme_macho_section = concat!
("__DATA,__linkmeA6xXjXOK,regular,no_dead_strip", $key)] # !
[linkme_windows_section = concat!
(".linkme_MX_R_WRAPPERS$b", $key)] # !
[linkme_illumos_section = concat!
("set_linkme_MX_R_WRAPPERS", $key)] # !
[linkme_bsd_section = concat! ("linkme_MX_R_WRAPPERS", $key)]
$item
}
}; (# ! [linkme_macro = $macro : path] $item : item) =>
{
$macro !
{
# ! [linkme_linux_section = "linkme_MX_R_WRAPPERS"] # !
[linkme_macho_section =
"__DATA,__linkmeA6xXjXOK,regular,no_dead_strip"] # !
[linkme_windows_section = ".linkme_MX_R_WRAPPERS$b"] # !
[linkme_illumos_section = "set_linkme_MX_R_WRAPPERS"] # !
[linkme_bsd_section = "linkme_MX_R_WRAPPERS"] $item
}
};
(# ! [linkme_linux_section = $linux_section : expr] # !
[linkme_macho_section = $macho_section : expr] # !
[linkme_windows_section = $windows_section : expr] # !
[linkme_illumos_section = $illumos_section : expr] # !
[linkme_bsd_section = $bsd_section : expr] $item : item) =>
{
#[used]
#[cfg_attr(any(target_os = "none", target_os = "linux", target_os =
"android", target_os = "fuchsia", target_os = "psp"), unsafe
(link_section = $linux_section))]
#[cfg_attr(any(target_os = "macos", target_os = "ios", target_os =
"tvos"), unsafe (link_section = $macho_section))]
#[cfg_attr(any(target_os = "uefi", target_os = "windows"), unsafe
(link_section = $windows_section))]
#[cfg_attr(target_os = "illumos", unsafe
(link_section = $illumos_section))]
#[cfg_attr(any(target_os = "freebsd", target_os = "openbsd"), unsafe
(link_section = $bsd_section))] $item
};
}
#[doc(hidden)]
pub use _linkme_macro_MX_R_WRAPPERS as MX_R_WRAPPERS;#[distributed_slice]42pub static MX_R_WRAPPERS: [RWrapperEntry];
4344/// ALTREP class registration entries, called once at package init.
45///
46/// Each ALTREP struct or trait impl emits an entry pairing the registration
47/// function pointer with its `#[no_mangle]` symbol name. The fn is declared
48/// `pub extern "C"` with `#[unsafe(no_mangle)]`, making it externally
49/// addressable from a separate compilation unit (e.g. the WASM snapshot
50/// codegen path). The fn pointer is not `unsafe` — R-thread invariants are a
51/// module-level contract, not encoded in the type (same convention as the
52/// `extern "C-unwind" fn` entries in `MX_CALL_DEFS`). The `symbol` string is
53/// consumed by the host-time WASM snapshot writer to emit
54/// `extern "C-unwind" { fn <symbol>(); }` declarations in `wasm_registry.rs`.
55#[cfg(not(target_arch = "wasm32"))]
56#[doc = " ALTREP class registration entries, called once at package init."]
#[doc = ""]
#[doc =
" Each ALTREP struct or trait impl emits an entry pairing the registration"]
#[doc =
" function pointer with its `#[no_mangle]` symbol name. The fn is declared"]
#[doc =
" `pub extern \"C\"` with `#[unsafe(no_mangle)]`, making it externally"]
#[doc =
" addressable from a separate compilation unit (e.g. the WASM snapshot"]
#[doc =
" codegen path). The fn pointer is not `unsafe` — R-thread invariants are a"]
#[doc =
" module-level contract, not encoded in the type (same convention as the"]
#[doc =
" `extern \"C-unwind\" fn` entries in `MX_CALL_DEFS`). The `symbol` string is"]
#[doc = " consumed by the host-time WASM snapshot writer to emit"]
#[doc =
" `extern \"C-unwind\" { fn <symbol>(); }` declarations in `wasm_registry.rs`."]
pub static MX_ALTREP_REGISTRATIONS:
::linkme::DistributedSlice<[AltrepRegistration]> =
{
unsafe extern "Rust" {
#[link_name = "__start_linkme_MX_ALTREP_REGISTRATIONS"]
static LINKME_START:
[<[AltrepRegistration] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__stop_linkme_MX_ALTREP_REGISTRATIONS"]
static LINKME_STOP:
[<[AltrepRegistration] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__start_linkm2_MX_ALTREP_REGISTRATIONS"]
static DUPCHECK_START: ();
#[link_name = "__stop_linkm2_MX_ALTREP_REGISTRATIONS"]
static DUPCHECK_STOP: ();
}
#[used]
#[unsafe(link_section = "linkme_MX_ALTREP_REGISTRATIONS")]
static mut LINKME_PLEASE:
[<[AltrepRegistration] as
::linkme::__private36::Slice>::Element; 0] =
[];
#[used]
#[unsafe(link_section = "linkm2_MX_ALTREP_REGISTRATIONS")]
static DUPCHECK: ::linkme::__private36::isize = 1;
unsafe {
::linkme::DistributedSlice::private_new("MX_ALTREP_REGISTRATIONS",
(&raw const LINKME_START).cast::<<[AltrepRegistration] as
::linkme::__private36::Slice>::Element>(),
(&raw const LINKME_STOP).cast::<<[AltrepRegistration] as
::linkme::__private36::Slice>::Element>(),
(&raw const DUPCHECK_START).cast::<::linkme::__private36::isize>(),
(&raw const DUPCHECK_STOP).cast::<::linkme::__private36::isize>())
}
};
#[doc(hidden)]
#[macro_export]
macro_rules! _linkme_macro_MX_ALTREP_REGISTRATIONS {
(# ! [linkme_macro = $macro : path] # ! [linkme_sort_key = $key : tt]
$item : item) =>
{
$macro !
{
# !
[linkme_linux_section = concat!
("linkme_MX_ALTREP_REGISTRATIONS", $key)] # !
[linkme_macho_section = concat!
("__DATA,__linkmeJFQYUnae,regular,no_dead_strip", $key)] # !
[linkme_windows_section = concat!
(".linkme_MX_ALTREP_REGISTRATIONS$b", $key)] # !
[linkme_illumos_section = concat!
("set_linkme_MX_ALTREP_REGISTRATIONS", $key)] # !
[linkme_bsd_section = concat!
("linkme_MX_ALTREP_REGISTRATIONS", $key)] $item
}
}; (# ! [linkme_macro = $macro : path] $item : item) =>
{
$macro !
{
# ! [linkme_linux_section = "linkme_MX_ALTREP_REGISTRATIONS"] # !
[linkme_macho_section =
"__DATA,__linkmeJFQYUnae,regular,no_dead_strip"] # !
[linkme_windows_section = ".linkme_MX_ALTREP_REGISTRATIONS$b"] # !
[linkme_illumos_section = "set_linkme_MX_ALTREP_REGISTRATIONS"] #
! [linkme_bsd_section = "linkme_MX_ALTREP_REGISTRATIONS"] $item
}
};
(# ! [linkme_linux_section = $linux_section : expr] # !
[linkme_macho_section = $macho_section : expr] # !
[linkme_windows_section = $windows_section : expr] # !
[linkme_illumos_section = $illumos_section : expr] # !
[linkme_bsd_section = $bsd_section : expr] $item : item) =>
{
#[used]
#[cfg_attr(any(target_os = "none", target_os = "linux", target_os =
"android", target_os = "fuchsia", target_os = "psp"), unsafe
(link_section = $linux_section))]
#[cfg_attr(any(target_os = "macos", target_os = "ios", target_os =
"tvos"), unsafe (link_section = $macho_section))]
#[cfg_attr(any(target_os = "uefi", target_os = "windows"), unsafe
(link_section = $windows_section))]
#[cfg_attr(target_os = "illumos", unsafe
(link_section = $illumos_section))]
#[cfg_attr(any(target_os = "freebsd", target_os = "openbsd"), unsafe
(link_section = $bsd_section))] $item
};
}
#[doc(hidden)]
pub use _linkme_macro_MX_ALTREP_REGISTRATIONS as MX_ALTREP_REGISTRATIONS;#[distributed_slice]57pub static MX_ALTREP_REGISTRATIONS: [AltrepRegistration];
5859/// Trait dispatch entries for [`universal_query`].
60///
61/// Each `#[miniextendr] impl Trait for Type` emits an entry mapping
62/// `(concrete_tag, trait_tag)` to the trait's vtable pointer.
63#[cfg(not(target_arch = "wasm32"))]
64#[doc = " Trait dispatch entries for [`universal_query`]."]
#[doc = ""]
#[doc = " Each `#[miniextendr] impl Trait for Type` emits an entry mapping"]
#[doc = " `(concrete_tag, trait_tag)` to the trait\'s vtable pointer."]
pub static MX_TRAIT_DISPATCH: ::linkme::DistributedSlice<[TraitDispatchEntry]>
=
{
unsafe extern "Rust" {
#[link_name = "__start_linkme_MX_TRAIT_DISPATCH"]
static LINKME_START:
[<[TraitDispatchEntry] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__stop_linkme_MX_TRAIT_DISPATCH"]
static LINKME_STOP:
[<[TraitDispatchEntry] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__start_linkm2_MX_TRAIT_DISPATCH"]
static DUPCHECK_START: ();
#[link_name = "__stop_linkm2_MX_TRAIT_DISPATCH"]
static DUPCHECK_STOP: ();
}
#[used]
#[unsafe(link_section = "linkme_MX_TRAIT_DISPATCH")]
static mut LINKME_PLEASE:
[<[TraitDispatchEntry] as
::linkme::__private36::Slice>::Element; 0] =
[];
#[used]
#[unsafe(link_section = "linkm2_MX_TRAIT_DISPATCH")]
static DUPCHECK: ::linkme::__private36::isize = 1;
unsafe {
::linkme::DistributedSlice::private_new("MX_TRAIT_DISPATCH",
(&raw const LINKME_START).cast::<<[TraitDispatchEntry] as
::linkme::__private36::Slice>::Element>(),
(&raw const LINKME_STOP).cast::<<[TraitDispatchEntry] as
::linkme::__private36::Slice>::Element>(),
(&raw const DUPCHECK_START).cast::<::linkme::__private36::isize>(),
(&raw const DUPCHECK_STOP).cast::<::linkme::__private36::isize>())
}
};
#[doc(hidden)]
#[macro_export]
macro_rules! _linkme_macro_MX_TRAIT_DISPATCH {
(# ! [linkme_macro = $macro : path] # ! [linkme_sort_key = $key : tt]
$item : item) =>
{
$macro !
{
# !
[linkme_linux_section = concat!
("linkme_MX_TRAIT_DISPATCH", $key)] # !
[linkme_macho_section = concat!
("__DATA,__linkmehjtiMuFn,regular,no_dead_strip", $key)] # !
[linkme_windows_section = concat!
(".linkme_MX_TRAIT_DISPATCH$b", $key)] # !
[linkme_illumos_section = concat!
("set_linkme_MX_TRAIT_DISPATCH", $key)] # !
[linkme_bsd_section = concat! ("linkme_MX_TRAIT_DISPATCH", $key)]
$item
}
}; (# ! [linkme_macro = $macro : path] $item : item) =>
{
$macro !
{
# ! [linkme_linux_section = "linkme_MX_TRAIT_DISPATCH"] # !
[linkme_macho_section =
"__DATA,__linkmehjtiMuFn,regular,no_dead_strip"] # !
[linkme_windows_section = ".linkme_MX_TRAIT_DISPATCH$b"] # !
[linkme_illumos_section = "set_linkme_MX_TRAIT_DISPATCH"] # !
[linkme_bsd_section = "linkme_MX_TRAIT_DISPATCH"] $item
}
};
(# ! [linkme_linux_section = $linux_section : expr] # !
[linkme_macho_section = $macho_section : expr] # !
[linkme_windows_section = $windows_section : expr] # !
[linkme_illumos_section = $illumos_section : expr] # !
[linkme_bsd_section = $bsd_section : expr] $item : item) =>
{
#[used]
#[cfg_attr(any(target_os = "none", target_os = "linux", target_os =
"android", target_os = "fuchsia", target_os = "psp"), unsafe
(link_section = $linux_section))]
#[cfg_attr(any(target_os = "macos", target_os = "ios", target_os =
"tvos"), unsafe (link_section = $macho_section))]
#[cfg_attr(any(target_os = "uefi", target_os = "windows"), unsafe
(link_section = $windows_section))]
#[cfg_attr(target_os = "illumos", unsafe
(link_section = $illumos_section))]
#[cfg_attr(any(target_os = "freebsd", target_os = "openbsd"), unsafe
(link_section = $bsd_section))] $item
};
}
#[doc(hidden)]
pub use _linkme_macro_MX_TRAIT_DISPATCH as MX_TRAIT_DISPATCH;#[distributed_slice]65pub static MX_TRAIT_DISPATCH: [TraitDispatchEntry];
6667/// Match-arg choices entries for R wrapper post-processing. **Host-only.**
68///
69/// Each `#[miniextendr]` function with `match_arg` params emits an entry.
70/// During `write_r_wrappers_to_file`, the placeholder in the R formal default
71/// is replaced with the actual choices from the enum's `MatchArg::CHOICES`.
72#[cfg(not(target_arch = "wasm32"))]
73#[doc =
" Match-arg choices entries for R wrapper post-processing. **Host-only.**"]
#[doc = ""]
#[doc =
" Each `#[miniextendr]` function with `match_arg` params emits an entry."]
#[doc =
" During `write_r_wrappers_to_file`, the placeholder in the R formal default"]
#[doc =
" is replaced with the actual choices from the enum\'s `MatchArg::CHOICES`."]
pub static MX_MATCH_ARG_CHOICES:
::linkme::DistributedSlice<[MatchArgChoicesEntry]> =
{
unsafe extern "Rust" {
#[link_name = "__start_linkme_MX_MATCH_ARG_CHOICES"]
static LINKME_START:
[<[MatchArgChoicesEntry] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__stop_linkme_MX_MATCH_ARG_CHOICES"]
static LINKME_STOP:
[<[MatchArgChoicesEntry] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__start_linkm2_MX_MATCH_ARG_CHOICES"]
static DUPCHECK_START: ();
#[link_name = "__stop_linkm2_MX_MATCH_ARG_CHOICES"]
static DUPCHECK_STOP: ();
}
#[used]
#[unsafe(link_section = "linkme_MX_MATCH_ARG_CHOICES")]
static mut LINKME_PLEASE:
[<[MatchArgChoicesEntry] as
::linkme::__private36::Slice>::Element; 0] =
[];
#[used]
#[unsafe(link_section = "linkm2_MX_MATCH_ARG_CHOICES")]
static DUPCHECK: ::linkme::__private36::isize = 1;
unsafe {
::linkme::DistributedSlice::private_new("MX_MATCH_ARG_CHOICES",
(&raw const LINKME_START).cast::<<[MatchArgChoicesEntry] as
::linkme::__private36::Slice>::Element>(),
(&raw const LINKME_STOP).cast::<<[MatchArgChoicesEntry] as
::linkme::__private36::Slice>::Element>(),
(&raw const DUPCHECK_START).cast::<::linkme::__private36::isize>(),
(&raw const DUPCHECK_STOP).cast::<::linkme::__private36::isize>())
}
};
#[doc(hidden)]
#[macro_export]
macro_rules! _linkme_macro_MX_MATCH_ARG_CHOICES {
(# ! [linkme_macro = $macro : path] # ! [linkme_sort_key = $key : tt]
$item : item) =>
{
$macro !
{
# !
[linkme_linux_section = concat!
("linkme_MX_MATCH_ARG_CHOICES", $key)] # !
[linkme_macho_section = concat!
("__DATA,__linkmefIX4YxkR,regular,no_dead_strip", $key)] # !
[linkme_windows_section = concat!
(".linkme_MX_MATCH_ARG_CHOICES$b", $key)] # !
[linkme_illumos_section = concat!
("set_linkme_MX_MATCH_ARG_CHOICES", $key)] # !
[linkme_bsd_section = concat!
("linkme_MX_MATCH_ARG_CHOICES", $key)] $item
}
}; (# ! [linkme_macro = $macro : path] $item : item) =>
{
$macro !
{
# ! [linkme_linux_section = "linkme_MX_MATCH_ARG_CHOICES"] # !
[linkme_macho_section =
"__DATA,__linkmefIX4YxkR,regular,no_dead_strip"] # !
[linkme_windows_section = ".linkme_MX_MATCH_ARG_CHOICES$b"] # !
[linkme_illumos_section = "set_linkme_MX_MATCH_ARG_CHOICES"] # !
[linkme_bsd_section = "linkme_MX_MATCH_ARG_CHOICES"] $item
}
};
(# ! [linkme_linux_section = $linux_section : expr] # !
[linkme_macho_section = $macho_section : expr] # !
[linkme_windows_section = $windows_section : expr] # !
[linkme_illumos_section = $illumos_section : expr] # !
[linkme_bsd_section = $bsd_section : expr] $item : item) =>
{
#[used]
#[cfg_attr(any(target_os = "none", target_os = "linux", target_os =
"android", target_os = "fuchsia", target_os = "psp"), unsafe
(link_section = $linux_section))]
#[cfg_attr(any(target_os = "macos", target_os = "ios", target_os =
"tvos"), unsafe (link_section = $macho_section))]
#[cfg_attr(any(target_os = "uefi", target_os = "windows"), unsafe
(link_section = $windows_section))]
#[cfg_attr(target_os = "illumos", unsafe
(link_section = $illumos_section))]
#[cfg_attr(any(target_os = "freebsd", target_os = "openbsd"), unsafe
(link_section = $bsd_section))] $item
};
}
#[doc(hidden)]
pub use _linkme_macro_MX_MATCH_ARG_CHOICES as MX_MATCH_ARG_CHOICES;#[distributed_slice]74pub static MX_MATCH_ARG_CHOICES: [MatchArgChoicesEntry];
7576/// Match-arg `@param` doc entries for R wrapper post-processing. **Host-only.**
77///
78/// Each `#[miniextendr]` function with `match_arg` params that has no
79/// user-written `@param` doc emits an entry here. During
80/// `write_r_wrappers_to_file`, the placeholder in the `@param` roxygen tag
81/// is replaced with e.g. `One of "Fast", "Safe", "Debug".`
82#[cfg(not(target_arch = "wasm32"))]
83#[doc =
" Match-arg `@param` doc entries for R wrapper post-processing. **Host-only.**"]
#[doc = ""]
#[doc = " Each `#[miniextendr]` function with `match_arg` params that has no"]
#[doc = " user-written `@param` doc emits an entry here. During"]
#[doc =
" `write_r_wrappers_to_file`, the placeholder in the `@param` roxygen tag"]
#[doc = " is replaced with e.g. `One of \"Fast\", \"Safe\", \"Debug\".`"]
pub static MX_MATCH_ARG_PARAM_DOCS:
::linkme::DistributedSlice<[MatchArgParamDocEntry]> =
{
unsafe extern "Rust" {
#[link_name = "__start_linkme_MX_MATCH_ARG_PARAM_DOCS"]
static LINKME_START:
[<[MatchArgParamDocEntry] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__stop_linkme_MX_MATCH_ARG_PARAM_DOCS"]
static LINKME_STOP:
[<[MatchArgParamDocEntry] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__start_linkm2_MX_MATCH_ARG_PARAM_DOCS"]
static DUPCHECK_START: ();
#[link_name = "__stop_linkm2_MX_MATCH_ARG_PARAM_DOCS"]
static DUPCHECK_STOP: ();
}
#[used]
#[unsafe(link_section = "linkme_MX_MATCH_ARG_PARAM_DOCS")]
static mut LINKME_PLEASE:
[<[MatchArgParamDocEntry] as
::linkme::__private36::Slice>::Element; 0] =
[];
#[used]
#[unsafe(link_section = "linkm2_MX_MATCH_ARG_PARAM_DOCS")]
static DUPCHECK: ::linkme::__private36::isize = 1;
unsafe {
::linkme::DistributedSlice::private_new("MX_MATCH_ARG_PARAM_DOCS",
(&raw const LINKME_START).cast::<<[MatchArgParamDocEntry] as
::linkme::__private36::Slice>::Element>(),
(&raw const LINKME_STOP).cast::<<[MatchArgParamDocEntry] as
::linkme::__private36::Slice>::Element>(),
(&raw const DUPCHECK_START).cast::<::linkme::__private36::isize>(),
(&raw const DUPCHECK_STOP).cast::<::linkme::__private36::isize>())
}
};
#[doc(hidden)]
#[macro_export]
macro_rules! _linkme_macro_MX_MATCH_ARG_PARAM_DOCS {
(# ! [linkme_macro = $macro : path] # ! [linkme_sort_key = $key : tt]
$item : item) =>
{
$macro !
{
# !
[linkme_linux_section = concat!
("linkme_MX_MATCH_ARG_PARAM_DOCS", $key)] # !
[linkme_macho_section = concat!
("__DATA,__linkmeV0zVCfXB,regular,no_dead_strip", $key)] # !
[linkme_windows_section = concat!
(".linkme_MX_MATCH_ARG_PARAM_DOCS$b", $key)] # !
[linkme_illumos_section = concat!
("set_linkme_MX_MATCH_ARG_PARAM_DOCS", $key)] # !
[linkme_bsd_section = concat!
("linkme_MX_MATCH_ARG_PARAM_DOCS", $key)] $item
}
}; (# ! [linkme_macro = $macro : path] $item : item) =>
{
$macro !
{
# ! [linkme_linux_section = "linkme_MX_MATCH_ARG_PARAM_DOCS"] # !
[linkme_macho_section =
"__DATA,__linkmeV0zVCfXB,regular,no_dead_strip"] # !
[linkme_windows_section = ".linkme_MX_MATCH_ARG_PARAM_DOCS$b"] # !
[linkme_illumos_section = "set_linkme_MX_MATCH_ARG_PARAM_DOCS"] #
! [linkme_bsd_section = "linkme_MX_MATCH_ARG_PARAM_DOCS"] $item
}
};
(# ! [linkme_linux_section = $linux_section : expr] # !
[linkme_macho_section = $macho_section : expr] # !
[linkme_windows_section = $windows_section : expr] # !
[linkme_illumos_section = $illumos_section : expr] # !
[linkme_bsd_section = $bsd_section : expr] $item : item) =>
{
#[used]
#[cfg_attr(any(target_os = "none", target_os = "linux", target_os =
"android", target_os = "fuchsia", target_os = "psp"), unsafe
(link_section = $linux_section))]
#[cfg_attr(any(target_os = "macos", target_os = "ios", target_os =
"tvos"), unsafe (link_section = $macho_section))]
#[cfg_attr(any(target_os = "uefi", target_os = "windows"), unsafe
(link_section = $windows_section))]
#[cfg_attr(target_os = "illumos", unsafe
(link_section = $illumos_section))]
#[cfg_attr(any(target_os = "freebsd", target_os = "openbsd"), unsafe
(link_section = $bsd_section))] $item
};
}
#[doc(hidden)]
pub use _linkme_macro_MX_MATCH_ARG_PARAM_DOCS as MX_MATCH_ARG_PARAM_DOCS;#[distributed_slice]84pub static MX_MATCH_ARG_PARAM_DOCS: [MatchArgParamDocEntry];
8586/// Class name entries mapping Rust type names to R-visible class names. **Host-only.**
87///
88/// Each `#[miniextendr]` impl block emits an entry. During
89/// `write_r_wrappers_to_file`, `.__MX_CLASS_REF_<RustName>__` placeholders
90/// in generated R wrapper strings are replaced with the registered R class name
91/// (which may differ when `class = "Override"` is set on the impl block).
92#[cfg(not(target_arch = "wasm32"))]
93#[doc =
" Class name entries mapping Rust type names to R-visible class names. **Host-only.**"]
#[doc = ""]
#[doc = " Each `#[miniextendr]` impl block emits an entry. During"]
#[doc =
" `write_r_wrappers_to_file`, `.__MX_CLASS_REF_<RustName>__` placeholders"]
#[doc =
" in generated R wrapper strings are replaced with the registered R class name"]
#[doc =
" (which may differ when `class = \"Override\"` is set on the impl block)."]
pub static MX_CLASS_NAMES: ::linkme::DistributedSlice<[ClassNameEntry]> =
{
unsafe extern "Rust" {
#[link_name = "__start_linkme_MX_CLASS_NAMES"]
static LINKME_START:
[<[ClassNameEntry] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__stop_linkme_MX_CLASS_NAMES"]
static LINKME_STOP:
[<[ClassNameEntry] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__start_linkm2_MX_CLASS_NAMES"]
static DUPCHECK_START: ();
#[link_name = "__stop_linkm2_MX_CLASS_NAMES"]
static DUPCHECK_STOP: ();
}
#[used]
#[unsafe(link_section = "linkme_MX_CLASS_NAMES")]
static mut LINKME_PLEASE:
[<[ClassNameEntry] as ::linkme::__private36::Slice>::Element; 0] =
[];
#[used]
#[unsafe(link_section = "linkm2_MX_CLASS_NAMES")]
static DUPCHECK: ::linkme::__private36::isize = 1;
unsafe {
::linkme::DistributedSlice::private_new("MX_CLASS_NAMES",
(&raw const LINKME_START).cast::<<[ClassNameEntry] as
::linkme::__private36::Slice>::Element>(),
(&raw const LINKME_STOP).cast::<<[ClassNameEntry] as
::linkme::__private36::Slice>::Element>(),
(&raw const DUPCHECK_START).cast::<::linkme::__private36::isize>(),
(&raw const DUPCHECK_STOP).cast::<::linkme::__private36::isize>())
}
};
#[doc(hidden)]
#[macro_export]
macro_rules! _linkme_macro_MX_CLASS_NAMES {
(# ! [linkme_macro = $macro : path] # ! [linkme_sort_key = $key : tt]
$item : item) =>
{
$macro !
{
# !
[linkme_linux_section = concat! ("linkme_MX_CLASS_NAMES", $key)] #
!
[linkme_macho_section = concat!
("__DATA,__linkmejXEM0NMl,regular,no_dead_strip", $key)] # !
[linkme_windows_section = concat!
(".linkme_MX_CLASS_NAMES$b", $key)] # !
[linkme_illumos_section = concat!
("set_linkme_MX_CLASS_NAMES", $key)] # !
[linkme_bsd_section = concat! ("linkme_MX_CLASS_NAMES", $key)]
$item
}
}; (# ! [linkme_macro = $macro : path] $item : item) =>
{
$macro !
{
# ! [linkme_linux_section = "linkme_MX_CLASS_NAMES"] # !
[linkme_macho_section =
"__DATA,__linkmejXEM0NMl,regular,no_dead_strip"] # !
[linkme_windows_section = ".linkme_MX_CLASS_NAMES$b"] # !
[linkme_illumos_section = "set_linkme_MX_CLASS_NAMES"] # !
[linkme_bsd_section = "linkme_MX_CLASS_NAMES"] $item
}
};
(# ! [linkme_linux_section = $linux_section : expr] # !
[linkme_macho_section = $macho_section : expr] # !
[linkme_windows_section = $windows_section : expr] # !
[linkme_illumos_section = $illumos_section : expr] # !
[linkme_bsd_section = $bsd_section : expr] $item : item) =>
{
#[used]
#[cfg_attr(any(target_os = "none", target_os = "linux", target_os =
"android", target_os = "fuchsia", target_os = "psp"), unsafe
(link_section = $linux_section))]
#[cfg_attr(any(target_os = "macos", target_os = "ios", target_os =
"tvos"), unsafe (link_section = $macho_section))]
#[cfg_attr(any(target_os = "uefi", target_os = "windows"), unsafe
(link_section = $windows_section))]
#[cfg_attr(target_os = "illumos", unsafe
(link_section = $illumos_section))]
#[cfg_attr(any(target_os = "freebsd", target_os = "openbsd"), unsafe
(link_section = $bsd_section))] $item
};
}
#[doc(hidden)]
pub use _linkme_macro_MX_CLASS_NAMES as MX_CLASS_NAMES;#[distributed_slice]94pub static MX_CLASS_NAMES: [ClassNameEntry];
9596/// The R class parts of every `#[miniextendr(env|r6|s3|s4|s7|vctrs)]` impl
97/// block. **Host-only.**
98///
99/// [`collect_r_wrappers`] merges the blocks of each Rust type into one class
100/// fragment: the class is defined once and every block's members attach to it
101/// (#1776).
102#[cfg(not(target_arch = "wasm32"))]
103#[doc =
" The R class parts of every `#[miniextendr(env|r6|s3|s4|s7|vctrs)]` impl"]
#[doc = " block. **Host-only.**"]
#[doc = ""]
#[doc =
" [`collect_r_wrappers`] merges the blocks of each Rust type into one class"]
#[doc =
" fragment: the class is defined once and every block\'s members attach to it"]
#[doc = " (#1776)."]
pub static MX_CLASS_BLOCKS: ::linkme::DistributedSlice<[ClassBlockEntry]> =
{
unsafe extern "Rust" {
#[link_name = "__start_linkme_MX_CLASS_BLOCKS"]
static LINKME_START:
[<[ClassBlockEntry] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__stop_linkme_MX_CLASS_BLOCKS"]
static LINKME_STOP:
[<[ClassBlockEntry] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__start_linkm2_MX_CLASS_BLOCKS"]
static DUPCHECK_START: ();
#[link_name = "__stop_linkm2_MX_CLASS_BLOCKS"]
static DUPCHECK_STOP: ();
}
#[used]
#[unsafe(link_section = "linkme_MX_CLASS_BLOCKS")]
static mut LINKME_PLEASE:
[<[ClassBlockEntry] as ::linkme::__private36::Slice>::Element; 0]
=
[];
#[used]
#[unsafe(link_section = "linkm2_MX_CLASS_BLOCKS")]
static DUPCHECK: ::linkme::__private36::isize = 1;
unsafe {
::linkme::DistributedSlice::private_new("MX_CLASS_BLOCKS",
(&raw const LINKME_START).cast::<<[ClassBlockEntry] as
::linkme::__private36::Slice>::Element>(),
(&raw const LINKME_STOP).cast::<<[ClassBlockEntry] as
::linkme::__private36::Slice>::Element>(),
(&raw const DUPCHECK_START).cast::<::linkme::__private36::isize>(),
(&raw const DUPCHECK_STOP).cast::<::linkme::__private36::isize>())
}
};
#[doc(hidden)]
#[macro_export]
macro_rules! _linkme_macro_MX_CLASS_BLOCKS {
(# ! [linkme_macro = $macro : path] # ! [linkme_sort_key = $key : tt]
$item : item) =>
{
$macro !
{
# !
[linkme_linux_section = concat! ("linkme_MX_CLASS_BLOCKS", $key)]
# !
[linkme_macho_section = concat!
("__DATA,__linkmeCAA1ohRR,regular,no_dead_strip", $key)] # !
[linkme_windows_section = concat!
(".linkme_MX_CLASS_BLOCKS$b", $key)] # !
[linkme_illumos_section = concat!
("set_linkme_MX_CLASS_BLOCKS", $key)] # !
[linkme_bsd_section = concat! ("linkme_MX_CLASS_BLOCKS", $key)]
$item
}
}; (# ! [linkme_macro = $macro : path] $item : item) =>
{
$macro !
{
# ! [linkme_linux_section = "linkme_MX_CLASS_BLOCKS"] # !
[linkme_macho_section =
"__DATA,__linkmeCAA1ohRR,regular,no_dead_strip"] # !
[linkme_windows_section = ".linkme_MX_CLASS_BLOCKS$b"] # !
[linkme_illumos_section = "set_linkme_MX_CLASS_BLOCKS"] # !
[linkme_bsd_section = "linkme_MX_CLASS_BLOCKS"] $item
}
};
(# ! [linkme_linux_section = $linux_section : expr] # !
[linkme_macho_section = $macho_section : expr] # !
[linkme_windows_section = $windows_section : expr] # !
[linkme_illumos_section = $illumos_section : expr] # !
[linkme_bsd_section = $bsd_section : expr] $item : item) =>
{
#[used]
#[cfg_attr(any(target_os = "none", target_os = "linux", target_os =
"android", target_os = "fuchsia", target_os = "psp"), unsafe
(link_section = $linux_section))]
#[cfg_attr(any(target_os = "macos", target_os = "ios", target_os =
"tvos"), unsafe (link_section = $macho_section))]
#[cfg_attr(any(target_os = "uefi", target_os = "windows"), unsafe
(link_section = $windows_section))]
#[cfg_attr(target_os = "illumos", unsafe
(link_section = $illumos_section))]
#[cfg_attr(any(target_os = "freebsd", target_os = "openbsd"), unsafe
(link_section = $bsd_section))] $item
};
}
#[doc(hidden)]
pub use _linkme_macro_MX_CLASS_BLOCKS as MX_CLASS_BLOCKS;#[distributed_slice]104pub static MX_CLASS_BLOCKS: [ClassBlockEntry];
105106/// S7 sidecar property documentation entries. **Host-only.**
107///
108/// Each `#[derive(ExternalPtr)] #[externalptr(s7)]` struct with `#[r_data]` fields
109/// emits one entry per public field. During `write_r_wrappers_to_file`, the
110/// `.__MX_S7_SIDECAR_PROP_DOCS_<TypeName>__` placeholder in the S7 class wrapper
111/// is replaced with the formatted `#' @prop {field} {doc}` roxygen lines.
112#[cfg(not(target_arch = "wasm32"))]
113#[doc = " S7 sidecar property documentation entries. **Host-only.**"]
#[doc = ""]
#[doc =
" Each `#[derive(ExternalPtr)] #[externalptr(s7)]` struct with `#[r_data]` fields"]
#[doc =
" emits one entry per public field. During `write_r_wrappers_to_file`, the"]
#[doc =
" `.__MX_S7_SIDECAR_PROP_DOCS_<TypeName>__` placeholder in the S7 class wrapper"]
#[doc =
" is replaced with the formatted `#\' @prop {field} {doc}` roxygen lines."]
pub static MX_S7_SIDECAR_PROPS: ::linkme::DistributedSlice<[SidecarPropEntry]>
=
{
unsafe extern "Rust" {
#[link_name = "__start_linkme_MX_S7_SIDECAR_PROPS"]
static LINKME_START:
[<[SidecarPropEntry] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__stop_linkme_MX_S7_SIDECAR_PROPS"]
static LINKME_STOP:
[<[SidecarPropEntry] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__start_linkm2_MX_S7_SIDECAR_PROPS"]
static DUPCHECK_START: ();
#[link_name = "__stop_linkm2_MX_S7_SIDECAR_PROPS"]
static DUPCHECK_STOP: ();
}
#[used]
#[unsafe(link_section = "linkme_MX_S7_SIDECAR_PROPS")]
static mut LINKME_PLEASE:
[<[SidecarPropEntry] as ::linkme::__private36::Slice>::Element; 0]
=
[];
#[used]
#[unsafe(link_section = "linkm2_MX_S7_SIDECAR_PROPS")]
static DUPCHECK: ::linkme::__private36::isize = 1;
unsafe {
::linkme::DistributedSlice::private_new("MX_S7_SIDECAR_PROPS",
(&raw const LINKME_START).cast::<<[SidecarPropEntry] as
::linkme::__private36::Slice>::Element>(),
(&raw const LINKME_STOP).cast::<<[SidecarPropEntry] as
::linkme::__private36::Slice>::Element>(),
(&raw const DUPCHECK_START).cast::<::linkme::__private36::isize>(),
(&raw const DUPCHECK_STOP).cast::<::linkme::__private36::isize>())
}
};
#[doc(hidden)]
#[macro_export]
macro_rules! _linkme_macro_MX_S7_SIDECAR_PROPS {
(# ! [linkme_macro = $macro : path] # ! [linkme_sort_key = $key : tt]
$item : item) =>
{
$macro !
{
# !
[linkme_linux_section = concat!
("linkme_MX_S7_SIDECAR_PROPS", $key)] # !
[linkme_macho_section = concat!
("__DATA,__linkmef0iLUGpd,regular,no_dead_strip", $key)] # !
[linkme_windows_section = concat!
(".linkme_MX_S7_SIDECAR_PROPS$b", $key)] # !
[linkme_illumos_section = concat!
("set_linkme_MX_S7_SIDECAR_PROPS", $key)] # !
[linkme_bsd_section = concat!
("linkme_MX_S7_SIDECAR_PROPS", $key)] $item
}
}; (# ! [linkme_macro = $macro : path] $item : item) =>
{
$macro !
{
# ! [linkme_linux_section = "linkme_MX_S7_SIDECAR_PROPS"] # !
[linkme_macho_section =
"__DATA,__linkmef0iLUGpd,regular,no_dead_strip"] # !
[linkme_windows_section = ".linkme_MX_S7_SIDECAR_PROPS$b"] # !
[linkme_illumos_section = "set_linkme_MX_S7_SIDECAR_PROPS"] # !
[linkme_bsd_section = "linkme_MX_S7_SIDECAR_PROPS"] $item
}
};
(# ! [linkme_linux_section = $linux_section : expr] # !
[linkme_macho_section = $macho_section : expr] # !
[linkme_windows_section = $windows_section : expr] # !
[linkme_illumos_section = $illumos_section : expr] # !
[linkme_bsd_section = $bsd_section : expr] $item : item) =>
{
#[used]
#[cfg_attr(any(target_os = "none", target_os = "linux", target_os =
"android", target_os = "fuchsia", target_os = "psp"), unsafe
(link_section = $linux_section))]
#[cfg_attr(any(target_os = "macos", target_os = "ios", target_os =
"tvos"), unsafe (link_section = $macho_section))]
#[cfg_attr(any(target_os = "uefi", target_os = "windows"), unsafe
(link_section = $windows_section))]
#[cfg_attr(target_os = "illumos", unsafe
(link_section = $illumos_section))]
#[cfg_attr(any(target_os = "freebsd", target_os = "openbsd"), unsafe
(link_section = $bsd_section))] $item
};
}
#[doc(hidden)]
pub use _linkme_macro_MX_S7_SIDECAR_PROPS as MX_S7_SIDECAR_PROPS;#[distributed_slice]114pub static MX_S7_SIDECAR_PROPS: [SidecarPropEntry];
115116/// The standalone sidecar accessors of each `#[derive(ExternalPtr)]` type
117/// with public `#[r_data]` fields. **Host-only.** [`collect_r_wrappers`]
118/// writes each as exported or internal, from the type's
119/// [`ClassNameEntry::field_syntax`] (#1900).
120#[cfg(not(target_arch = "wasm32"))]
121#[doc =
" The standalone sidecar accessors of each `#[derive(ExternalPtr)]` type"]
#[doc =
" with public `#[r_data]` fields. **Host-only.** [`collect_r_wrappers`]"]
#[doc = " writes each as exported or internal, from the type\'s"]
#[doc = " [`ClassNameEntry::field_syntax`] (#1900)."]
pub static MX_SIDECAR_ACCESSORS:
::linkme::DistributedSlice<[SidecarAccessorsEntry]> =
{
unsafe extern "Rust" {
#[link_name = "__start_linkme_MX_SIDECAR_ACCESSORS"]
static LINKME_START:
[<[SidecarAccessorsEntry] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__stop_linkme_MX_SIDECAR_ACCESSORS"]
static LINKME_STOP:
[<[SidecarAccessorsEntry] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__start_linkm2_MX_SIDECAR_ACCESSORS"]
static DUPCHECK_START: ();
#[link_name = "__stop_linkm2_MX_SIDECAR_ACCESSORS"]
static DUPCHECK_STOP: ();
}
#[used]
#[unsafe(link_section = "linkme_MX_SIDECAR_ACCESSORS")]
static mut LINKME_PLEASE:
[<[SidecarAccessorsEntry] as
::linkme::__private36::Slice>::Element; 0] =
[];
#[used]
#[unsafe(link_section = "linkm2_MX_SIDECAR_ACCESSORS")]
static DUPCHECK: ::linkme::__private36::isize = 1;
unsafe {
::linkme::DistributedSlice::private_new("MX_SIDECAR_ACCESSORS",
(&raw const LINKME_START).cast::<<[SidecarAccessorsEntry] as
::linkme::__private36::Slice>::Element>(),
(&raw const LINKME_STOP).cast::<<[SidecarAccessorsEntry] as
::linkme::__private36::Slice>::Element>(),
(&raw const DUPCHECK_START).cast::<::linkme::__private36::isize>(),
(&raw const DUPCHECK_STOP).cast::<::linkme::__private36::isize>())
}
};
#[doc(hidden)]
#[macro_export]
macro_rules! _linkme_macro_MX_SIDECAR_ACCESSORS {
(# ! [linkme_macro = $macro : path] # ! [linkme_sort_key = $key : tt]
$item : item) =>
{
$macro !
{
# !
[linkme_linux_section = concat!
("linkme_MX_SIDECAR_ACCESSORS", $key)] # !
[linkme_macho_section = concat!
("__DATA,__linkme7m9xCfja,regular,no_dead_strip", $key)] # !
[linkme_windows_section = concat!
(".linkme_MX_SIDECAR_ACCESSORS$b", $key)] # !
[linkme_illumos_section = concat!
("set_linkme_MX_SIDECAR_ACCESSORS", $key)] # !
[linkme_bsd_section = concat!
("linkme_MX_SIDECAR_ACCESSORS", $key)] $item
}
}; (# ! [linkme_macro = $macro : path] $item : item) =>
{
$macro !
{
# ! [linkme_linux_section = "linkme_MX_SIDECAR_ACCESSORS"] # !
[linkme_macho_section =
"__DATA,__linkme7m9xCfja,regular,no_dead_strip"] # !
[linkme_windows_section = ".linkme_MX_SIDECAR_ACCESSORS$b"] # !
[linkme_illumos_section = "set_linkme_MX_SIDECAR_ACCESSORS"] # !
[linkme_bsd_section = "linkme_MX_SIDECAR_ACCESSORS"] $item
}
};
(# ! [linkme_linux_section = $linux_section : expr] # !
[linkme_macho_section = $macho_section : expr] # !
[linkme_windows_section = $windows_section : expr] # !
[linkme_illumos_section = $illumos_section : expr] # !
[linkme_bsd_section = $bsd_section : expr] $item : item) =>
{
#[used]
#[cfg_attr(any(target_os = "none", target_os = "linux", target_os =
"android", target_os = "fuchsia", target_os = "psp"), unsafe
(link_section = $linux_section))]
#[cfg_attr(any(target_os = "macos", target_os = "ios", target_os =
"tvos"), unsafe (link_section = $macho_section))]
#[cfg_attr(any(target_os = "uefi", target_os = "windows"), unsafe
(link_section = $windows_section))]
#[cfg_attr(target_os = "illumos", unsafe
(link_section = $illumos_section))]
#[cfg_attr(any(target_os = "freebsd", target_os = "openbsd"), unsafe
(link_section = $bsd_section))] $item
};
}
#[doc(hidden)]
pub use _linkme_macro_MX_SIDECAR_ACCESSORS as MX_SIDECAR_ACCESSORS;#[distributed_slice]122pub static MX_SIDECAR_ACCESSORS: [SidecarAccessorsEntry];
123124/// The `all.equal()` methods `#[derive(AllEqual)]` generates (#2149).
125/// **Host-only.** [`collect_r_wrappers`] names and registers each for the
126/// type's R class, which the type's impl block gives in its
127/// [`ClassNameEntry`]: the derive sits on the struct, a separate expansion.
128#[cfg(not(target_arch = "wasm32"))]
129#[doc = " The `all.equal()` methods `#[derive(AllEqual)]` generates (#2149)."]
#[doc =
" **Host-only.** [`collect_r_wrappers`] names and registers each for the"]
#[doc = " type\'s R class, which the type\'s impl block gives in its"]
#[doc =
" [`ClassNameEntry`]: the derive sits on the struct, a separate expansion."]
pub static MX_ALL_EQUAL: ::linkme::DistributedSlice<[AllEqualEntry]> =
{
unsafe extern "Rust" {
#[link_name = "__start_linkme_MX_ALL_EQUAL"]
static LINKME_START:
[<[AllEqualEntry] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__stop_linkme_MX_ALL_EQUAL"]
static LINKME_STOP:
[<[AllEqualEntry] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__start_linkm2_MX_ALL_EQUAL"]
static DUPCHECK_START: ();
#[link_name = "__stop_linkm2_MX_ALL_EQUAL"]
static DUPCHECK_STOP: ();
}
#[used]
#[unsafe(link_section = "linkme_MX_ALL_EQUAL")]
static mut LINKME_PLEASE:
[<[AllEqualEntry] as ::linkme::__private36::Slice>::Element; 0] =
[];
#[used]
#[unsafe(link_section = "linkm2_MX_ALL_EQUAL")]
static DUPCHECK: ::linkme::__private36::isize = 1;
unsafe {
::linkme::DistributedSlice::private_new("MX_ALL_EQUAL",
(&raw const LINKME_START).cast::<<[AllEqualEntry] as
::linkme::__private36::Slice>::Element>(),
(&raw const LINKME_STOP).cast::<<[AllEqualEntry] as
::linkme::__private36::Slice>::Element>(),
(&raw const DUPCHECK_START).cast::<::linkme::__private36::isize>(),
(&raw const DUPCHECK_STOP).cast::<::linkme::__private36::isize>())
}
};
#[doc(hidden)]
#[macro_export]
macro_rules! _linkme_macro_MX_ALL_EQUAL {
(# ! [linkme_macro = $macro : path] # ! [linkme_sort_key = $key : tt]
$item : item) =>
{
$macro !
{
# ! [linkme_linux_section = concat! ("linkme_MX_ALL_EQUAL", $key)]
# !
[linkme_macho_section = concat!
("__DATA,__linkmeHq940RQF,regular,no_dead_strip", $key)] # !
[linkme_windows_section = concat!
(".linkme_MX_ALL_EQUAL$b", $key)] # !
[linkme_illumos_section = concat!
("set_linkme_MX_ALL_EQUAL", $key)] # !
[linkme_bsd_section = concat! ("linkme_MX_ALL_EQUAL", $key)] $item
}
}; (# ! [linkme_macro = $macro : path] $item : item) =>
{
$macro !
{
# ! [linkme_linux_section = "linkme_MX_ALL_EQUAL"] # !
[linkme_macho_section =
"__DATA,__linkmeHq940RQF,regular,no_dead_strip"] # !
[linkme_windows_section = ".linkme_MX_ALL_EQUAL$b"] # !
[linkme_illumos_section = "set_linkme_MX_ALL_EQUAL"] # !
[linkme_bsd_section = "linkme_MX_ALL_EQUAL"] $item
}
};
(# ! [linkme_linux_section = $linux_section : expr] # !
[linkme_macho_section = $macho_section : expr] # !
[linkme_windows_section = $windows_section : expr] # !
[linkme_illumos_section = $illumos_section : expr] # !
[linkme_bsd_section = $bsd_section : expr] $item : item) =>
{
#[used]
#[cfg_attr(any(target_os = "none", target_os = "linux", target_os =
"android", target_os = "fuchsia", target_os = "psp"), unsafe
(link_section = $linux_section))]
#[cfg_attr(any(target_os = "macos", target_os = "ios", target_os =
"tvos"), unsafe (link_section = $macho_section))]
#[cfg_attr(any(target_os = "uefi", target_os = "windows"), unsafe
(link_section = $windows_section))]
#[cfg_attr(target_os = "illumos", unsafe
(link_section = $illumos_section))]
#[cfg_attr(any(target_os = "freebsd", target_os = "openbsd"), unsafe
(link_section = $bsd_section))] $item
};
}
#[doc(hidden)]
pub use _linkme_macro_MX_ALL_EQUAL as MX_ALL_EQUAL;#[distributed_slice]130pub static MX_ALL_EQUAL: [AllEqualEntry];
131132/// The `pkg::name` items of every `reexport!` (#1910). **Host-only.**
133/// [`collect_r_wrappers`] writes roxygen2's re-export block for each
134/// `pkg::name` once, however many items name it, and
135/// [`write_r_wrappers_to_file`] refuses a name re-exported from two packages
136/// or bound by the package's own wrappers (`reexport_conflicts`).
137#[cfg(not(target_arch = "wasm32"))]
138#[doc = " The `pkg::name` items of every `reexport!` (#1910). **Host-only.**"]
#[doc = " [`collect_r_wrappers`] writes roxygen2\'s re-export block for each"]
#[doc = " `pkg::name` once, however many items name it, and"]
#[doc =
" [`write_r_wrappers_to_file`] refuses a name re-exported from two packages"]
#[doc = " or bound by the package\'s own wrappers (`reexport_conflicts`)."]
pub static MX_REEXPORTS: ::linkme::DistributedSlice<[ReexportEntry]> =
{
unsafe extern "Rust" {
#[link_name = "__start_linkme_MX_REEXPORTS"]
static LINKME_START:
[<[ReexportEntry] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__stop_linkme_MX_REEXPORTS"]
static LINKME_STOP:
[<[ReexportEntry] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__start_linkm2_MX_REEXPORTS"]
static DUPCHECK_START: ();
#[link_name = "__stop_linkm2_MX_REEXPORTS"]
static DUPCHECK_STOP: ();
}
#[used]
#[unsafe(link_section = "linkme_MX_REEXPORTS")]
static mut LINKME_PLEASE:
[<[ReexportEntry] as ::linkme::__private36::Slice>::Element; 0] =
[];
#[used]
#[unsafe(link_section = "linkm2_MX_REEXPORTS")]
static DUPCHECK: ::linkme::__private36::isize = 1;
unsafe {
::linkme::DistributedSlice::private_new("MX_REEXPORTS",
(&raw const LINKME_START).cast::<<[ReexportEntry] as
::linkme::__private36::Slice>::Element>(),
(&raw const LINKME_STOP).cast::<<[ReexportEntry] as
::linkme::__private36::Slice>::Element>(),
(&raw const DUPCHECK_START).cast::<::linkme::__private36::isize>(),
(&raw const DUPCHECK_STOP).cast::<::linkme::__private36::isize>())
}
};
#[doc(hidden)]
#[macro_export]
macro_rules! _linkme_macro_MX_REEXPORTS {
(# ! [linkme_macro = $macro : path] # ! [linkme_sort_key = $key : tt]
$item : item) =>
{
$macro !
{
# ! [linkme_linux_section = concat! ("linkme_MX_REEXPORTS", $key)]
# !
[linkme_macho_section = concat!
("__DATA,__linkmeZ89u477l,regular,no_dead_strip", $key)] # !
[linkme_windows_section = concat!
(".linkme_MX_REEXPORTS$b", $key)] # !
[linkme_illumos_section = concat!
("set_linkme_MX_REEXPORTS", $key)] # !
[linkme_bsd_section = concat! ("linkme_MX_REEXPORTS", $key)] $item
}
}; (# ! [linkme_macro = $macro : path] $item : item) =>
{
$macro !
{
# ! [linkme_linux_section = "linkme_MX_REEXPORTS"] # !
[linkme_macho_section =
"__DATA,__linkmeZ89u477l,regular,no_dead_strip"] # !
[linkme_windows_section = ".linkme_MX_REEXPORTS$b"] # !
[linkme_illumos_section = "set_linkme_MX_REEXPORTS"] # !
[linkme_bsd_section = "linkme_MX_REEXPORTS"] $item
}
};
(# ! [linkme_linux_section = $linux_section : expr] # !
[linkme_macho_section = $macho_section : expr] # !
[linkme_windows_section = $windows_section : expr] # !
[linkme_illumos_section = $illumos_section : expr] # !
[linkme_bsd_section = $bsd_section : expr] $item : item) =>
{
#[used]
#[cfg_attr(any(target_os = "none", target_os = "linux", target_os =
"android", target_os = "fuchsia", target_os = "psp"), unsafe
(link_section = $linux_section))]
#[cfg_attr(any(target_os = "macos", target_os = "ios", target_os =
"tvos"), unsafe (link_section = $macho_section))]
#[cfg_attr(any(target_os = "uefi", target_os = "windows"), unsafe
(link_section = $windows_section))]
#[cfg_attr(target_os = "illumos", unsafe
(link_section = $illumos_section))]
#[cfg_attr(any(target_os = "freebsd", target_os = "openbsd"), unsafe
(link_section = $bsd_section))] $item
};
}
#[doc(hidden)]
pub use _linkme_macro_MX_REEXPORTS as MX_REEXPORTS;#[distributed_slice]139pub static MX_REEXPORTS: [ReexportEntry];
140141/// The package crate's `conversion_error_class`. **Host-only.**
142///
143/// `miniextendr_init!` emits one entry, with the classes it reads from the
144/// crate's `[package.metadata.miniextendr]` (empty when unset). During
145/// `write_r_wrappers_to_file` it becomes `.miniextendr_conversion_error_class`,
146/// the classes `.miniextendr_arg_error` gives every R-side argument error, so
147/// they match the ones the macro emits into the Rust conversion arms (#1591).
148#[cfg(not(target_arch = "wasm32"))]
149#[doc = " The package crate\'s `conversion_error_class`. **Host-only.**"]
#[doc = ""]
#[doc =
" `miniextendr_init!` emits one entry, with the classes it reads from the"]
#[doc =
" crate\'s `[package.metadata.miniextendr]` (empty when unset). During"]
#[doc =
" `write_r_wrappers_to_file` it becomes `.miniextendr_conversion_error_class`,"]
#[doc =
" the classes `.miniextendr_arg_error` gives every R-side argument error, so"]
#[doc =
" they match the ones the macro emits into the Rust conversion arms (#1591)."]
pub static MX_CONVERSION_ERROR_CLASS:
::linkme::DistributedSlice<[ConversionErrorClassEntry]> =
{
unsafe extern "Rust" {
#[link_name = "__start_linkme_MX_CONVERSION_ERROR_CLASS"]
static LINKME_START:
[<[ConversionErrorClassEntry] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__stop_linkme_MX_CONVERSION_ERROR_CLASS"]
static LINKME_STOP:
[<[ConversionErrorClassEntry] as
::linkme::__private36::Slice>::Element; 0];
#[link_name = "__start_linkm2_MX_CONVERSION_ERROR_CLASS"]
static DUPCHECK_START: ();
#[link_name = "__stop_linkm2_MX_CONVERSION_ERROR_CLASS"]
static DUPCHECK_STOP: ();
}
#[used]
#[unsafe(link_section = "linkme_MX_CONVERSION_ERROR_CLASS")]
static mut LINKME_PLEASE:
[<[ConversionErrorClassEntry] as
::linkme::__private36::Slice>::Element; 0] =
[];
#[used]
#[unsafe(link_section = "linkm2_MX_CONVERSION_ERROR_CLASS")]
static DUPCHECK: ::linkme::__private36::isize = 1;
unsafe {
::linkme::DistributedSlice::private_new("MX_CONVERSION_ERROR_CLASS",
(&raw const LINKME_START).cast::<<[ConversionErrorClassEntry]
as ::linkme::__private36::Slice>::Element>(),
(&raw const LINKME_STOP).cast::<<[ConversionErrorClassEntry]
as ::linkme::__private36::Slice>::Element>(),
(&raw const DUPCHECK_START).cast::<::linkme::__private36::isize>(),
(&raw const DUPCHECK_STOP).cast::<::linkme::__private36::isize>())
}
};
#[doc(hidden)]
#[macro_export]
macro_rules! _linkme_macro_MX_CONVERSION_ERROR_CLASS {
(# ! [linkme_macro = $macro : path] # ! [linkme_sort_key = $key : tt]
$item : item) =>
{
$macro !
{
# !
[linkme_linux_section = concat!
("linkme_MX_CONVERSION_ERROR_CLASS", $key)] # !
[linkme_macho_section = concat!
("__DATA,__linkmesGGwUYY5,regular,no_dead_strip", $key)] # !
[linkme_windows_section = concat!
(".linkme_MX_CONVERSION_ERROR_CLASS$b", $key)] # !
[linkme_illumos_section = concat!
("set_linkme_MX_CONVERSION_ERROR_CLASS", $key)] # !
[linkme_bsd_section = concat!
("linkme_MX_CONVERSION_ERROR_CLASS", $key)] $item
}
}; (# ! [linkme_macro = $macro : path] $item : item) =>
{
$macro !
{
# ! [linkme_linux_section = "linkme_MX_CONVERSION_ERROR_CLASS"] #
!
[linkme_macho_section =
"__DATA,__linkmesGGwUYY5,regular,no_dead_strip"] # !
[linkme_windows_section = ".linkme_MX_CONVERSION_ERROR_CLASS$b"] #
!
[linkme_illumos_section = "set_linkme_MX_CONVERSION_ERROR_CLASS"]
# ! [linkme_bsd_section = "linkme_MX_CONVERSION_ERROR_CLASS"]
$item
}
};
(# ! [linkme_linux_section = $linux_section : expr] # !
[linkme_macho_section = $macho_section : expr] # !
[linkme_windows_section = $windows_section : expr] # !
[linkme_illumos_section = $illumos_section : expr] # !
[linkme_bsd_section = $bsd_section : expr] $item : item) =>
{
#[used]
#[cfg_attr(any(target_os = "none", target_os = "linux", target_os =
"android", target_os = "fuchsia", target_os = "psp"), unsafe
(link_section = $linux_section))]
#[cfg_attr(any(target_os = "macos", target_os = "ios", target_os =
"tvos"), unsafe (link_section = $macho_section))]
#[cfg_attr(any(target_os = "uefi", target_os = "windows"), unsafe
(link_section = $windows_section))]
#[cfg_attr(target_os = "illumos", unsafe
(link_section = $illumos_section))]
#[cfg_attr(any(target_os = "freebsd", target_os = "openbsd"), unsafe
(link_section = $bsd_section))] $item
};
}
#[doc(hidden)]
pub use _linkme_macro_MX_CONVERSION_ERROR_CLASS as MX_CONVERSION_ERROR_CLASS;#[distributed_slice]150pub static MX_CONVERSION_ERROR_CLASS: [ConversionErrorClassEntry];
151152// endregion
153154// region: Runtime slice access (cfg-uniform)
155156/// Native: read directly from the linkme distributed slice.
157/// wasm32: read from a `OnceLock` populated by `install_wasm_runtime_slices`.
158#[inline]
159pub(crate) fn call_defs() -> &'static [R_CallMethodDef] {
160#[cfg(not(target_arch = "wasm32"))]
161{
162&MX_CALL_DEFS163 }
164#[cfg(target_arch = "wasm32")]
165{
166 wasm_runtime::call_defs()
167 }
168}
169170#[inline]
171pub(crate) fn altrep_regs() -> &'static [AltrepRegistration] {
172#[cfg(not(target_arch = "wasm32"))]
173{
174&MX_ALTREP_REGISTRATIONS175 }
176#[cfg(target_arch = "wasm32")]
177{
178 wasm_runtime::altrep_regs()
179 }
180}
181182#[inline]
183pub(crate) fn trait_dispatch() -> &'static [TraitDispatchEntry] {
184#[cfg(not(target_arch = "wasm32"))]
185{
186&MX_TRAIT_DISPATCH187 }
188#[cfg(target_arch = "wasm32")]
189{
190 wasm_runtime::trait_dispatch()
191 }
192}
193194/// The package crate's `conversion_error_class` (empty when it sets none).
195///
196/// Native: the [`MX_CONVERSION_ERROR_CLASS`] entry `miniextendr_init!`
197/// registers (a package has one). wasm32: the copy the `wasm_registry.rs`
198/// snapshot carries, installed by `install_wasm_runtime_slices`.
199#[inline]
200pub(crate) fn conversion_error_class() -> &'static [&'static str] {
201#[cfg(not(target_arch = "wasm32"))]
202{
203MX_CONVERSION_ERROR_CLASS204 .first()
205 .map_or(&[][..], |entry| entry.classes)
206 }
207#[cfg(target_arch = "wasm32")]
208{
209 wasm_runtime::conversion_error_class()
210 }
211}
212213#[cfg(target_arch = "wasm32")]
214mod wasm_runtime {
215use super::{AltrepRegistration, R_CallMethodDef, TraitDispatchEntry};
216use std::sync::OnceLock;
217218static CALL_DEFS: OnceLock<&'static [R_CallMethodDef]> = OnceLock::new();
219static ALTREP_REGS: OnceLock<&'static [AltrepRegistration]> = OnceLock::new();
220static TRAIT_DISPATCH: OnceLock<&'static [TraitDispatchEntry]> = OnceLock::new();
221static CONVERSION_ERROR_CLASS: OnceLock<&'static [&'static str]> = OnceLock::new();
222223pub(super) fn install(
224 c: &'static [R_CallMethodDef],
225 a: &'static [AltrepRegistration],
226 t: &'static [TraitDispatchEntry],
227 k: &'static [&'static str],
228 ) {
229// Double-install is a programmer error but not memory-unsafe — silently
230 // ignore so a second `R_init_*` (e.g. dyn.unload + dyn.load) doesn't
231 // panic. The first install wins.
232let _ = CALL_DEFS.set(c);
233let _ = ALTREP_REGS.set(a);
234let _ = TRAIT_DISPATCH.set(t);
235let _ = CONVERSION_ERROR_CLASS.set(k);
236 }
237238pub(super) fn call_defs() -> &'static [R_CallMethodDef] {
239 CALL_DEFS.get().copied().unwrap_or(&[])
240 }
241pub(super) fn altrep_regs() -> &'static [AltrepRegistration] {
242 ALTREP_REGS.get().copied().unwrap_or(&[])
243 }
244pub(super) fn trait_dispatch() -> &'static [TraitDispatchEntry] {
245 TRAIT_DISPATCH.get().copied().unwrap_or(&[])
246 }
247pub(super) fn conversion_error_class() -> &'static [&'static str] {
248 CONVERSION_ERROR_CLASS.get().copied().unwrap_or(&[])
249 }
250}
251252/// Install the runtime-critical slice data on `wasm32-*`.
253///
254/// Called from the user crate's `R_init_<pkg>` (generated by
255/// `miniextendr_init!`) before `package_init` runs. The slices are typically
256/// the `MX_*_WASM` constants emitted into `<crate>/src/rust/<crate>/src/wasm_registry.rs`
257/// by [`crate::wasm_registry_writer::write_wasm_registry_to_file`].
258/// `conversion_error_class` is the crate's `conversion_error_class` from the
259/// same snapshot, which the raising guards give an `arg_error!` (#1768).
260///
261/// Calling more than once is harmless — the first install wins (the assumption
262/// being all calls supply the same data; second-install is treated as a
263/// re-init from `dyn.unload` + `dyn.load`).
264#[cfg(target_arch = "wasm32")]
265pub fn install_wasm_runtime_slices(
266 call_defs: &'static [R_CallMethodDef],
267 altrep_regs: &'static [AltrepRegistration],
268 trait_dispatch: &'static [TraitDispatchEntry],
269 conversion_error_class: &'static [&'static str],
270) {
271 wasm_runtime::install(
272 call_defs,
273 altrep_regs,
274 trait_dispatch,
275 conversion_error_class,
276 );
277}
278279// endregion
280281// region: Entry Types
282283/// Ordering priority for R wrapper code fragments.
284///
285/// Variant declaration order = output order. The order matters because
286/// R evaluates the wrapper file top-to-bottom, so dependencies must come first:
287/// sidecar accessors before class definitions, classes before functions, etc.
288#[derive(#[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for RWrapperPriority { }
#[automatically_derived]
impl ::core::clone::Clone for RWrapperPriority {
#[inline]
fn clone(&self) -> Self { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for RWrapperPriority { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for RWrapperPriority { }
#[automatically_derived]
impl ::core::cmp::PartialEq for RWrapperPriority {
#[inline]
fn eq(&self, other: &Self) -> bool {
::core::intrinsics::discriminant_value(self) ==
::core::intrinsics::discriminant_value(other)
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for RWrapperPriority { }Eq, #[automatically_derived]
impl ::core::cmp::PartialOrd for RWrapperPriority {
#[inline]
fn partial_cmp(&self, other: &Self)
-> ::core::option::Option<::core::cmp::Ordering> {
::core::option::Option::Some(::core::cmp::Ord::cmp(self, other))
}
}PartialOrd, #[automatically_derived]
impl ::core::cmp::Ord for RWrapperPriority {
#[inline]
fn cmp(&self, other: &Self) -> ::core::cmp::Ordering {
::core::cmp::Ord::cmp(&::core::intrinsics::discriminant_value(self),
&::core::intrinsics::discriminant_value(other))
}
}Ord)]
289pub enum RWrapperPriority {
290/// `#[r_data]` getters/setters — must come before class definitions.
291Sidecar,
292/// Class definitions (impl blocks: env/R6/S3/S4/S7).
293Class,
294/// Standalone `#[miniextendr]` functions.
295Function,
296/// Trait impl wrappers (`impl Trait for Type`).
297TraitImpl,
298/// Vctrs S3 method wrappers (`#[derive(Vctrs)]`).
299Vctrs,
300/// The re-export blocks of `reexport!` (#1910). They bind nothing, so
301 /// their place is free; last keeps them together.
302Reexport,
303}
304305/// R wrapper code with priority for ordering.
306pub struct RWrapperEntry {
307/// Ordering priority (lower = earlier in output file).
308pub priority: RWrapperPriority,
309/// R source code fragment.
310pub content: &'static str,
311/// Source file path (from `file!()`). Used to derive a default `@rdname`
312 /// for standalone functions that don't have an explicit one, so that all
313 /// functions from the same source file share a single .Rd page.
314pub source_file: &'static str,
315/// Line the generating item starts on in `source_file`. Together with the
316 /// file it fixes the emission order within a priority group at source
317 /// order, so the blocks merged onto a shared `.Rd` page read as written
318 /// rather than in link order (#1476).
319pub source_line: u32,
320}
321322// SAFETY: All fields are immutable and valid for 'static lifetime.
323unsafe impl Syncfor RWrapperEntry {}
324325/// The package crate's `conversion_error_class`, registered by
326/// `miniextendr_init!` in [`MX_CONVERSION_ERROR_CLASS`].
327pub struct ConversionErrorClassEntry {
328/// The classes, most specific first; empty when the crate sets none.
329pub classes: &'static [&'static str],
330}
331332/// Entry for replacing match_arg placeholder defaults with actual choices.
333pub struct MatchArgChoicesEntry {
334/// Placeholder string in the R formal default, e.g. `".__MX_MATCH_ARG_CHOICES_mode__"`.
335pub placeholder: &'static str,
336/// The parameter type's `MatchArg::CHOICES`, unescaped.
337pub choices: &'static [&'static str],
338/// User-supplied `default = "..."` value (unquoted, e.g. `"zstd"`), or `""`
339 /// if the user did not supply one. When non-empty, the write-time pass
340 /// moves this choice to the front of the formal ([`match_arg_formal`]),
341 /// so `.miniextendr_match_arg()` picks it for `NULL` and for the omitted
342 /// argument.
343pub preferred_default: &'static str,
344/// Placeholder for the trailing `aliases =` (and `listed =`) arguments
345 /// of the parameter's `.miniextendr_match_arg` /
346 /// `.miniextendr_match_arg_several` call, e.g.
347 /// `".__MX_MATCH_ARG_ALIASES_run_mode__"`. The writer replaces `, ` and
348 /// it with [`match_arg_aliases_arg`]: nothing for a type without aliases.
349pub aliases_placeholder: &'static str,
350/// The parameter type's `MatchArg::ALIASES`, unescaped.
351pub aliases: &'static [crate::match_arg::MatchArgAlias],
352}
353354/// Entry for replacing match_arg `@param` doc placeholders with human-readable
355/// choice descriptions.
356pub struct MatchArgParamDocEntry {
357/// Placeholder string in the `@param` roxygen tag, e.g.
358 /// `".__MX_MATCH_ARG_PARAM_DOC_match_arg_set_mode_mode__"`.
359pub placeholder: &'static str,
360/// `true` for `several_ok` params (emits "One or more of …");
361 /// `false` for plain `match_arg` (emits "One of …").
362pub several_ok: bool,
363/// Text after the rendered choice list, before the closing period:
364 /// `""` for a plain param, `", or NULL for no choice"` for an `Option<T>`
365 /// one (#1473), an omission note for `Missing<T>` (#1551). Composed by
366 /// the macro from the parameter type.
367pub suffix: &'static str,
368/// Function that returns the choices as a comma-separated quoted string,
369 /// e.g. `"\"Fast\", \"Safe\", \"Debug\""`.
370pub choices_str: fn() -> String,
371}
372373// SAFETY: function pointer, bools, and &'static str are Send+Sync.
374unsafe impl Syncfor MatchArgParamDocEntry {}
375376/// Entry mapping a Rust type name to its R-visible class name and class system.
377///
378/// Emitted by every `#[miniextendr(env|r6|s3|s4|s7|vctrs)]` impl block.
379/// Used by the resolver in `write_r_wrappers_to_file` to replace
380/// `.__MX_CLASS_REF_<RustName>__` placeholders with the actual R class name.
381pub struct ClassNameEntry {
382/// Rust type identifier, e.g. `"S7Shape"`.
383pub rust_type: &'static str,
384/// R-visible class name. Equals `rust_type` unless `class = "Override"` was
385 /// set on the impl block, in which case it is the override string.
386pub r_class_name: &'static str,
387/// Class system tag: `"env"` | `"r6"` | `"s3"` | `"s4"` | `"s7"` | `"vctrs"`.
388pub class_system: &'static str,
389/// The impl block gives the class `$` / `[[` field syntax for the type's
390 /// `#[r_data]` fields (`s3(r_data_accessors)`, `s4(...)`, `env(...)`).
391 /// The type's standalone `Type_get_f()` / `Type_set_f()` are then
392 /// written without an export or a page ([`SidecarAccessorsEntry`],
393 /// #1900).
394pub field_syntax: bool,
395}
396397// SAFETY: &'static str fields and a bool — immutable and valid for program lifetime.
398unsafe impl Syncfor ClassNameEntry {}
399400/// The line of [`ClassBlockEntry::head`] where the class's roxygen block
401/// takes the [`ClassBlockEntry::class_doc_lines`] of every impl block of the
402/// type. Spelled the same as `miniextendr_macros`'s `CLASS_DOC_LINES_MARKER`.
403pub const CLASS_DOC_LINES_MARKER: &str = ".__MX_CLASS_DOC_LINES__";
404405/// The line of [`ClassBlockEntry::head`] where the class's roxygen block
406/// takes the [`ClassBlockEntry::class_docs`] of every impl block of the
407/// type, the defining block's first. Spelled the same as
408/// `miniextendr_macros`'s `CLASS_DOCS_MARKER`.
409pub const CLASS_DOCS_MARKER: &str = ".__MX_CLASS_DOCS__";
410411/// Opens a class's roxygen block, in [`ClassBlockEntry::head`], that only
412/// carries the impl docs: the block of an S3 or vctrs class without a
413/// constructor that the package names and builds itself (`class = "..."`).
414/// The writer writes the block, up to its closing `NULL`, only when the
415/// type's [`class_docs`](ClassBlockEntry::class_docs) or
416/// [`class_doc_lines`](ClassBlockEntry::class_doc_lines) say something beyond
417/// the page and export tags, so a readers-only impl adds no page (#1918).
418/// Spelled the same as `miniextendr_macros`'s `CLASS_DOCS_ONLY_MARKER`.
419pub const CLASS_DOCS_ONLY_MARKER: &str = ".__MX_CLASS_DOCS_ONLY__";
420421/// Opens a generated line of a class's roxygen block, in
422/// [`ClassBlockEntry::head`] or [`ClassBlockEntry::class_doc_lines`], that a
423/// doc tag on another impl block of the type replaces: `@title`, `@name`,
424/// `@rdname`, `@export`, a constructor's `@param` or an R6 `@field`. The
425/// writer drops the line when another block's
426/// [`class_docs`](ClassBlockEntry::class_docs) have the tag (for `@param` and
427/// `@field`, for the same name), and otherwise writes it without the prefix.
428/// Spelled the same as `miniextendr_macros`'s `CLASS_DEFAULT_PREFIX`.
429pub const CLASS_DEFAULT_PREFIX: &str = ".__MX_CLASS_DEFAULT__";
430431/// Generated member page/parameter/prose defaults. The class assembler resolves
432/// these against the defining impl's topic, preserving explicit member tags.
433/// Spelled the same as the proc-macro's `CLASS_MEMBER_DEFAULT_PREFIX`.
434pub const CLASS_MEMBER_DEFAULT_PREFIX: &str = ".__MX_CLASS_MEMBER_DEFAULT__";
435436/// One `#[miniextendr]` impl block's share of its Rust type's R class (#1776).
437///
438/// A type's methods may be split over several impl blocks. Each block
439/// registers its R wrapper in parts, and [`collect_r_wrappers`] writes one
440/// class per type: the defining block's [`head`](Self::head), every block's
441/// [`members`](Self::members) in source order, then the defining block's
442/// [`tail`](Self::tail). The defining block is the one with a
443/// [`defines`](Self::defines) entry (at most one block may have any), else
444/// the first in source order. The class's roxygen block has the
445/// [`class_docs`](Self::class_docs) of every block, the defining block's
446/// first. Every part is R source, framed by the newline the macro's raw
447/// string literal adds at each end.
448pub struct ClassBlockEntry {
449/// Rust type identifier as written in the impl, e.g. `"Counter"`.
450pub rust_type: &'static str,
451/// The type's `core::any::type_name`, which tells apart two types that
452 /// share an identifier. The blocks of one type are merged by it.
453pub type_path: fn() -> &'static str,
454/// R-visible class name (`class = "..."`, else the type's identifier).
455pub r_class_name: &'static str,
456/// Source file of the impl block (`file!()`).
457pub source_file: &'static str,
458/// Line of the impl's type in `source_file`.
459pub source_line: u32,
460/// Column of the impl's type, for the `# Generated from` header.
461pub source_column: u32,
462/// The class-level settings, rendered: class system and its options,
463 /// `class = "..."`, `internal` / `noexport`, and the `@noRd` /
464 /// `@keywords internal` tags. They shape every block's members, so all
465 /// blocks of a type must have the same.
466pub settings: &'static str,
467/// The Rust name of the block's class constructor, `""` when it has
468 /// none: the method marked `#[miniextendr(<system>(constructor))]`, else a
469 /// receiver-less `new` returning `Self`, else
470 /// ([`constructor_inferred`](Self::constructor_inferred)) the block's one
471 /// [candidate](Self::constructor_candidates).
472pub constructor: &'static str,
473/// Whether [`constructor`](Self::constructor) was inferred: the block
474 /// declares none and has one candidate. The type's constructor is then
475 /// decided across its impl blocks when [`collect_r_wrappers`] merges them.
476pub constructor_inferred: bool,
477/// The Rust names of the block's receiver-less methods returning `Self`
478 /// other than a declared constructor: the methods that could be the
479 /// constructor of a type that declares none.
480pub constructor_candidates: &'static [&'static str],
481/// The block as it reads when another impl block of the type declares
482 /// the constructor: present when the block infers one, which there stays
483 /// a static method.
484pub demoted: Option<&'static ClassBlockEntry>,
485/// What this block has that only the defining block may: `"a
486 /// constructor"` (or `` "the inferred constructor `name`" ``), `"S7
487 /// properties"`, `"the field fallback"` (`s3(r_data_accessors, fallback
488 /// = ...)`, whose `.Call` entry the block emits and whose field methods
489 /// call it).
490pub defines: &'static [&'static str],
491/// The class definition as this block would write it: the class's
492 /// roxygen block (with the [`CLASS_DOCS_MARKER`] and
493 /// [`CLASS_DOC_LINES_MARKER`] lines, and [`CLASS_DEFAULT_PREFIX`]
494 /// lines), the class object, the constructor, and the class-level
495 /// helpers.
496pub head: &'static str,
497/// The block's doc comment as roxygen lines (with the constructor's for
498 /// S3 and vctrs, whose class block is the constructor's). The class's
499 /// roxygen block has every block's, the defining block's first; a tag
500 /// repeated exactly is written once, and two blocks giving the class
501 /// different `@title`s, or a page other than the defining block's
502 /// (`@name`, `@rdname`), are refused.
503pub class_docs: &'static str,
504/// Roxygen lines this block's members add to the class's roxygen block,
505 /// such as an R6 active binding's `@field` or a lifecycle import.
506pub class_doc_lines: &'static str,
507/// The block's members: its methods, static methods and coercions.
508pub members: &'static str,
509/// Names the members bind in the class's own namespaces (`$name` of an
510 /// env class or of an R6 generator, R6 members, S4 and S7 method
511 /// registrations), for the check that two blocks don't bind the same one.
512 /// Top-level functions are left to the duplicate-definition scan of the
513 /// written file.
514pub member_names: &'static [&'static str],
515/// What follows every block's members, such as R6's `$lock()`.
516pub tail: &'static str,
517}
518519// SAFETY: &'static str fields, a fn pointer, integers and a reference to
520// another such entry — immutable and valid for program lifetime.
521unsafe impl Syncfor ClassBlockEntry {}
522523/// Implemented by `#[miniextendr]` for each type with an impl block that
524/// declares its class constructor (`#[miniextendr(<system>(constructor))]`,
525/// or a receiver-less `new` returning `Self`). `Block` is a type private to that
526/// impl block, so two such blocks don't conflict here; the wrapper writer
527/// refuses them ([`merge_class_blocks`]).
528///
529/// An impl block with several methods that could be the constructor and
530/// none declared requires it ([`require_class_constructor`]): the type then
531/// compiles only when another of its impl blocks declares the constructor.
532#[doc(hidden)]
533#[diagnostic::on_unimplemented(
534 message = "`{Self}` has several methods that could be its constructor, and none is declared",
535 label = "this method returns `Self` and takes no receiver, like another method of `{Self}`",
536 note = "mark the constructor with the class system's `constructor` option, e.g. \
537 `#[miniextendr(r6(constructor))]`, or name it `new`; the other methods stay static \
538 factory methods"
539)]
540pub trait ClassConstructor<Block> {}
541542/// Compiles only when `T` has an impl block that declares its class
543/// constructor ([`ClassConstructor`]). Never called.
544#[doc(hidden)]
545pub const fn require_class_constructor<T: ?Sized + ClassConstructor<Block>, Block>() {}
546547/// The standalone R accessors `#[derive(ExternalPtr)]` generates for a
548/// type's public `#[r_data]` fields, `Type_get_f(x)` / `Type_set_f(x,
549/// value)`, in two renderings: exported on a "Type Sidecar Accessors" page,
550/// or internal (`@noRd`, no export). [`collect_r_wrappers`] writes the
551/// internal one for a type whose class has field syntax
552/// ([`ClassNameEntry::field_syntax`]): the class's `$` / `[[` are its R
553/// interface to the fields there, and the setters would write in place
554/// through every copy (#1900). The internal rendering also carries the field
555/// helpers that those methods call. Written with the sidecar fragments.
556pub struct SidecarAccessorsEntry {
557/// Rust type identifier, matched against [`ClassNameEntry::rust_type`].
558pub rust_type: &'static str,
559/// The page and the exported accessors.
560pub exported: &'static str,
561/// The same accessors with `@noRd` and no export, plus field-syntax helpers.
562pub internal: &'static str,
563/// Source file of the derive (`file!()`), for the fragment order.
564pub source_file: &'static str,
565/// Line of the derived type in `source_file`.
566pub source_line: u32,
567}
568569// SAFETY: &'static str fields and a u32 — immutable and valid for program lifetime.
570unsafe impl Syncfor SidecarAccessorsEntry {}
571572/// The `all.equal()` method `#[derive(AllEqual)]` generates for a type
573/// (#2149), without the parts that depend on the type's R class: its name,
574/// its title and its registration, which [`collect_r_wrappers`] adds.
575pub struct AllEqualEntry {
576/// Rust type identifier, matched against [`ClassNameEntry::rust_type`].
577pub rust_type: &'static str,
578/// The method, `function(target, current, ...) { ... }`.
579pub method: &'static str,
580/// The roxygen lines of the method's page after its title: the
581 /// description, the arguments and the value.
582pub docs: &'static str,
583/// Source file of the derive (`file!()`).
584pub source_file: &'static str,
585/// Line of the derived type in `source_file`.
586pub source_line: u32,
587/// Column of the derived type, for the `# Generated from` header.
588pub source_column: u32,
589}
590591// SAFETY: &'static str fields and integers — immutable and valid for program lifetime.
592unsafe impl Syncfor AllEqualEntry {}
593594/// One `pkg::name` item of a `reexport!` invocation (#1910): `name`, an
595/// export of the R package `package`, which this package exports too.
596/// The macro has checked both names.
597pub struct ReexportEntry {
598/// The R package the name comes from, e.g. `"generics"`.
599pub package: &'static str,
600/// The re-exported name as R spells it, unquoted, e.g. `"tidy"` or
601 /// `"%>%"`.
602pub name: &'static str,
603/// Source file of the invocation (`file!()`).
604pub source_file: &'static str,
605/// Line of the item in `source_file`.
606pub source_line: u32,
607/// Column of the item, for the `# Generated from` header.
608pub source_column: u32,
609}
610611/// Build the `rust_type → ClassNameEntry` lookup used by every placeholder
612/// resolver in `write_r_wrappers_to_file`.
613///
614/// Each `#[miniextendr]` impl block of a type registers a `ClassNameEntry`
615/// (#1242, #1776) — the class name is a property of the *type*, so identical
616/// duplicates collapse here. *Conflicting* registrations (same `rust_type`,
617/// different `r_class_name` or `class_system` — e.g. two blocks with
618/// disagreeing `class = "..."` overrides) panic: silently keeping
619/// whichever entry linkme ordered last would resolve `.__MX_CLASS_REF_*__`
620/// placeholders nondeterministically. `field_syntax` may differ between the
621/// blocks ([`sidecar_accessor_fragments`] reads every entry). Distinct Rust
622/// types must also have distinct R class names: otherwise their class bindings
623/// and generated methods overwrite each other when the package loads (#1677).
624///
625/// Host-only like `MX_CLASS_NAMES` itself — the consumers all live in the
626/// wrapper-writing path, which never runs on wasm32.
627#[cfg(not(target_arch = "wasm32"))]
628fn build_class_name_index<'a>(
629 entries: impl IntoIterator<Item = &'a ClassNameEntry>,
630) -> std::collections::HashMap<&'a str, &'a ClassNameEntry> {
631use std::collections::hash_map::Entry;
632let mut index = std::collections::HashMap::new();
633let mut r_class_names = std::collections::HashMap::new();
634for e in entries {
635match index.entry(e.rust_type) {
636 Entry::Vacant(slot) => {
637 slot.insert(e);
638 }
639 Entry::Occupied(slot) => {
640let prev = *slot.get();
641if prev.r_class_name != e.r_class_name || prev.class_system != e.class_system {
642{
::core::panicking::panic_fmt(format_args!("conflicting class registrations for Rust type `{0}`: `{1}` ({2}) vs `{3}` ({4}). Multiple #[miniextendr] impl blocks on one type must agree on the class system and any `class = \"...\"` override.",
e.rust_type, prev.r_class_name, prev.class_system, e.r_class_name,
e.class_system));
};panic!(
643"conflicting class registrations for Rust type `{}`: \
644 `{}` ({}) vs `{}` ({}). Multiple #[miniextendr] impl blocks \
645 on one type must agree on the class system and any \
646 `class = \"...\"` override.",
647 e.rust_type,
648 prev.r_class_name,
649 prev.class_system,
650 e.r_class_name,
651 e.class_system,
652 );
653 }
654 }
655 }
656if let Some(prev) = r_class_names.insert(e.r_class_name, e) {
657if !(prev.rust_type == e.rust_type) {
{
::core::panicking::panic_fmt(format_args!("R class `{0}` is registered for both Rust type `{1}` ({2}) and `{3}` ({4}). Distinct Rust types must use distinct R class names; change one `class = \"...\"` override to avoid overwriting its class and methods.",
e.r_class_name, prev.rust_type, prev.class_system,
e.rust_type, e.class_system));
}
};assert!(
658 prev.rust_type == e.rust_type,
659"R class `{}` is registered for both Rust type `{}` ({}) and `{}` ({}). \
660 Distinct Rust types must use distinct R class names; change one \
661 `class = \"...\"` override to avoid overwriting its class and methods.",
662 e.r_class_name,
663 prev.rust_type,
664 prev.class_system,
665 e.rust_type,
666 e.class_system,
667 );
668 }
669 }
670index671}
672673/// Entry documenting a sidecar (`#[r_data]`) property on an S7 ExternalPtr type.
674///
675/// Emitted by `#[derive(ExternalPtr)] #[externalptr(s7)]` for each public `#[r_data]`
676/// field. Used by `write_r_wrappers_to_file` to substitute the
677/// `.__MX_S7_SIDECAR_PROP_DOCS_<TypeName>__` placeholder with `#' @prop` lines.
678pub struct SidecarPropEntry {
679/// Rust type name, e.g. `"SidecarS7"`.
680pub rust_type: &'static str,
681/// Field name, e.g. `"prop_int"`.
682pub field_name: &'static str,
683/// Documentation string for this property.
684 /// Defaults to `"(undocumented sidecar property)"` when no `prop_doc` was supplied.
685pub prop_doc: &'static str,
686}
687688// SAFETY: All fields are &'static str — immutable and valid for program lifetime.
689unsafe impl Syncfor SidecarPropEntry {}
690691/// Trait dispatch entry mapping (concrete_tag, trait_tag) → vtable.
692#[repr(C)]
693pub struct TraitDispatchEntry {
694/// Tag identifying the concrete type.
695pub concrete_tag: mx_tag,
696/// Tag identifying the trait interface.
697pub trait_tag: mx_tag,
698/// Pointer to the trait's vtable (cast from `&'static SomeVTable`).
699pub vtable: *const c_void,
700/// Symbol name of the `#[no_mangle]` vtable static
701 /// (e.g. `"__VTABLE_mypkg_Counter_FOR_MyType"`). Consumed by the host-time WASM
702 /// snapshot writer to emit `extern "C" { static <symbol>: u8; }`
703 /// declarations in `wasm_registry.rs`.
704pub vtable_symbol: &'static str,
705}
706707// SAFETY: vtable points to a static vtable valid for program lifetime.
708// Tags are Copy values. All fields are safe to read from any thread.
709unsafe impl Syncfor TraitDispatchEntry {}
710unsafe impl Sendfor TraitDispatchEntry {}
711712/// ALTREP class registration entry: fn pointer + `#[no_mangle]` symbol name.
713///
714/// See [`MX_ALTREP_REGISTRATIONS`] for context.
715#[repr(C)]
716pub struct AltrepRegistration {
717/// Registration function called inside the package-init unwind boundary.
718pub register: extern "C-unwind" fn(),
719/// Symbol name of `register` (e.g. `"__mx_altrep_reg_MyType"`). Consumed by
720 /// the host-time WASM snapshot writer to emit `extern "C-unwind" { fn <symbol>(); }`
721 /// declarations in `wasm_registry.rs`.
722pub symbol: &'static str,
723}
724725// SAFETY: register is a static fn pointer; symbol is a `'static` string.
726unsafe impl Syncfor AltrepRegistration {}
727unsafe impl Sendfor AltrepRegistration {}
728// endregion
729730// region: Universal Query
731732/// Universal query function for trait dispatch.
733///
734/// Scans [`MX_TRAIT_DISPATCH`] for a matching `(concrete_tag, trait_tag)` pair.
735/// Returns the vtable pointer, or null if the trait is not implemented.
736///
737/// This replaces per-type query functions — a single function handles all types
738/// by reading from the global dispatch table.
739///
740/// # Safety
741///
742/// - `ptr` must point to a valid `mx_erased` with a valid base vtable.
743/// - Must be called on R's main thread.
744pub unsafe extern "C" fn universal_query(ptr: *mut mx_erased, trait_tag: mx_tag) -> *const c_void {
745let concrete_tag = unsafe { (*(*ptr).base).concrete_tag };
746for entry in trait_dispatch().iter() {
747if entry.concrete_tag == concrete_tag && entry.trait_tag == trait_tag {
748return entry.vtable;
749 }
750 }
751 std::ptr::null()
752}
753// endregion
754755// region: Initialization
756757/// Register all `#[miniextendr]` routines and ALTREP classes with R.
758///
759/// Called from `package_init()` during `R_init_*` (via `miniextendr_init!`).
760/// Everything else is automatic.
761///
762/// # Safety
763///
764/// Must be called from R's main thread during `R_init_*`.
765/// `dll` must be a valid pointer provided by R.
766#[unsafe(no_mangle)]
767pub unsafe extern "C-unwind" fn miniextendr_register_routines(dll: *mut DllInfo) {
768// 1. Register ALTREP classes (skip during wrapper generation)
769 //
770 // During wrapper-gen, the installed shared object is loaded temporarily via
771 // dyn.load() then unloaded via dyn.unload(). ALTREP class registration creates
772 // R-global entries with method pointers into the loaded code. After dyn.unload(),
773 // those pointers become dangling. When R later loads the installed package and
774 // re-registers, it may still have the stale entries, leading to heap corruption
775 // (e.g., "malloc(): unsorted double linked list corrupted" on Linux).
776 // Shares the single read-site with package_init (see init::WRAPPER_GEN_ENV) so
777 // the env-var name cannot drift between the two consumers.
778let wrapper_gen = crate::init::wrapper_gen_mode();
779if !wrapper_gen {
780// All ALTREP classes — both user-defined (#[miniextendr] structs) and
781 // builtins (Vec, Box, Range, Cow, Arrow) — register via linkme
782 // MX_ALTREP_REGISTRATIONS. Each call site emits a
783 // `#[distributed_slice(MX_ALTREP_REGISTRATIONS)]` entry, so a single
784 // iteration here covers everything. No hand-enumerated builtin list needed.
785for reg in altrep_regs().iter() {
786 (reg.register)();
787 }
788789// Verify no two ALTREP types registered the same class name.
790 // Duplicates cause silent overwrites in R — the wrong type gets
791 // reconstructed on readRDS, leading to memory corruption.
792crate::altrep::assert_altrep_class_uniqueness();
793 }
794795// 2. Build call method defs with null sentinel
796let mut call_defs: Vec<R_CallMethodDef> = self::call_defs().to_vec();
797// Always register the wrapper-gen entry points so they're visible
798 // via getNativeSymbolInfo even when R_forceSymbols(TRUE) is set. wasm32
799 // doesn't run wrapper-gen (it's host-only), so these are gated off there.
800 // SAFETY: DL_FUNC is Option<extern "C-unwind" fn() -> *mut c_void> — R's
801 // standard erased function pointer. The actual signature (SEXP -> SEXP) is
802 // ABI-compatible; R dispatches based on numArgs.
803#[cfg(not(target_arch = "wasm32"))]
804{
805call_defs.push(R_CallMethodDef {
806 name: c"miniextendr_write_wrappers".as_ptr(),
807 fun: unsafe {
808 std::mem::transmute::<
809*const (),
810Option<unsafe extern "C-unwind" fn() -> *mut c_void>,
811 >(miniextendr_write_wrappersas *const ())
812 },
813 numArgs: 1,
814 });
815call_defs.push(R_CallMethodDef {
816 name: c"miniextendr_write_wasm_registry".as_ptr(),
817 fun: unsafe {
818 std::mem::transmute::<
819*const (),
820Option<unsafe extern "C-unwind" fn() -> *mut c_void>,
821 >(miniextendr_write_wasm_registryas *const ())
822 },
823 numArgs: 1,
824 });
825 }
826call_defs.push(R_CallMethodDef {
827 name: std::ptr::null(),
828 fun: None,
829 numArgs: 0,
830 });
831832// 3. Register routines
833 // R copies every entry into the DLL's own table (`R_addCallRoutine`
834 // strdups the name and stores the function pointer), so `call_defs` is
835 // freed on return. Leaking it showed up as ~114 KB "definitely lost" per
836 // load under Valgrind.
837unsafe {
838crate::sys::R_registerRoutines_unchecked(
839dll,
840 std::ptr::null(),
841call_defs.as_ptr(),
842 std::ptr::null(),
843 std::ptr::null(),
844 );
845 }
846}
847848/// The standalone sidecar accessors as sidecar fragments: each type's
849/// internal rendering when one of its impl blocks gives its class field
850/// syntax, else the exported one (#1900).
851#[cfg(not(target_arch = "wasm32"))]
852fn sidecar_accessor_fragments(
853 accessors: &[SidecarAccessorsEntry],
854 classes: &[ClassNameEntry],
855) -> Vec<WrapperFragment> {
856accessors857 .iter()
858 .map(|entry| {
859let field_syntax = classes860 .iter()
861 .any(|class| class.rust_type == entry.rust_type && class.field_syntax);
862WrapperFragment {
863 priority: RWrapperPriority::Sidecar,
864 content: std::borrow::Cow::Borrowed(if field_syntax {
865entry.internal
866 } else {
867entry.exported
868 }),
869 source_file: entry.source_file,
870 source_line: entry.source_line,
871 }
872 })
873 .collect()
874}
875876/// The `all.equal()` methods of `#[derive(AllEqual)]` as fragments (#2149):
877/// each named `all.equal.<Class>` for the R class of the type's
878/// [`ClassNameEntry`], on its own page, and registered with
879/// `S3method(all.equal, <Class>)`, which an env, R6, S3 or S4 object
880/// dispatches on (S4 through R's S3 dispatch on the S4 class, which also
881/// reaches callers of `base::all.equal()` such as testthat, where an S4
882/// method would not). An S7 object's class attribute is `pkg::Class`, so an
883/// S7 class also registers the method with `S7::method(all.equal, Class)`,
884/// followed by the unbinding every such replacement call on a base generic
885/// needs (see `miniextendr_macros`'s `s7_base_operator_unbind`).
886///
887/// The fragments are written with the trait impls, after every class
888/// definition, which the S7 registration names.
889///
890/// Every problem is collected into one message: a type no impl block gives
891/// an R class, and a vctrs class, whose objects are R vectors with no pointer
892/// to the Rust value.
893#[cfg(not(target_arch = "wasm32"))]
894fn all_equal_fragments(
895 entries: &[AllEqualEntry],
896 classes: &[ClassNameEntry],
897) -> Result<Vec<WrapperFragment>, String> {
898let index = build_class_name_index(classes);
899let mut errors = Vec::new();
900let mut fragments = Vec::new();
901for entry in entries {
902let derive = ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("`#[derive(AllEqual)]` on `{0}` ({1}:{2})",
entry.rust_type, entry.source_file, entry.source_line))
})format!(
903"`#[derive(AllEqual)]` on `{}` ({}:{})",
904 entry.rust_type, entry.source_file, entry.source_line
905 );
906match index.get(entry.rust_type) {
907None => errors.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}: no `#[miniextendr(env | r6 | s3 | s4 | s7)]` impl block gives the type an R class, so there is no class to give the `all.equal()` method",
derive))
})format!(
908"{derive}: no `#[miniextendr(env | r6 | s3 | s4 | s7)]` impl block gives the \
909 type an R class, so there is no class to give the `all.equal()` method"
910)),
911Some(class) if class.class_system == "vctrs" => errors.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}: the objects of a vctrs class are R vectors, not pointers to the Rust value, and `all.equal()` already compares them",
derive))
})format!(
912"{derive}: the objects of a vctrs class are R vectors, not pointers to the Rust \
913 value, and `all.equal()` already compares them"
914)),
915Some(class) => fragments.push(WrapperFragment {
916 priority: RWrapperPriority::TraitImpl,
917 source_file: entry.source_file,
918 source_line: entry.source_line,
919 content: std::borrow::Cow::Owned(all_equal_method(entry, class)),
920 }),
921 }
922 }
923if errors.is_empty() {
924Ok(fragments)
925 } else {
926Err(errors.join("\n"))
927 }
928}
929930/// The text of one `#[derive(AllEqual)]` method: see [`all_equal_fragments`].
931#[cfg(not(target_arch = "wasm32"))]
932fn all_equal_method(entry: &AllEqualEntry, class: &ClassNameEntry) -> String {
933let class_name = class.r_class_name;
934let method = ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("all.equal.{0}", class_name))
})format!("all.equal.{class_name}");
935// `all.equal.` makes the name start with a letter; a class name with
936 // other characters than R's name characters needs backticks.
937let symbol = if class_name938 .bytes()
939 .all(|c| c.is_ascii_alphanumeric() || c == b'.' || c == b'_')
940 {
941method.clone()
942 } else {
943::alloc::__export::must_use({
::alloc::fmt::format(format_args!("`{0}`", method))
})format!("`{method}`")944 };
945let mut out = ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("# Generated from Rust derive `AllEqual` on `{0}` ({1}:{2}:{3})\n#\' Compare two `{6}` objects\n#\'\n{4}\n#\' @method all.equal {6}\n#\' @export\n{7} <- {5}",
entry.rust_type, entry.source_file, entry.source_line,
entry.source_column, entry.docs.trim_matches('\n'),
entry.method.trim_matches('\n'), class_name, symbol))
})format!(
946"# Generated from Rust derive `AllEqual` on `{}` ({}:{}:{})\n\
947 #' Compare two `{class_name}` objects\n\
948 #'\n\
949 {}\n\
950 #' @method all.equal {class_name}\n\
951 #' @export\n\
952 {symbol} <- {}",
953 entry.rust_type,
954 entry.source_file,
955 entry.source_line,
956 entry.source_column,
957 entry.docs.trim_matches('\n'),
958 entry.method.trim_matches('\n'),
959 );
960if class.class_system == "s7" {
961out.push_str(&::alloc::__export::must_use({
::alloc::fmt::format(format_args!("\nS7::method(all.equal, {0}) <- {1}\nbase::rm(list = \"all.equal\")",
class_name, symbol))
})format!(
962"\nS7::method(all.equal, {class_name}) <- {symbol}\nbase::rm(list = \"all.equal\")"
963));
964 }
965out966}
967968// region: re-exports (#1910)
969970/// The re-exports of every `reexport!` as one fragment, `None` without any:
971/// for each `pkg::name`, roxygen2's re-export block
972///
973/// ```r
974/// # Generated from Rust `reexport!` of `generics::tidy` (fits.rs:3:11)
975/// #' @importFrom generics tidy
976/// #' @export
977/// generics::tidy
978/// ```
979///
980/// from which roxygen2 writes `importFrom(generics,tidy)` and `export(tidy)`
981/// to `NAMESPACE` and an alias to `man/reexports.Rd`. A `pkg::name` that
982/// several items name is written once, its header listing their sites. The
983/// blocks are sorted by package, then name.
984///
985/// A name re-exported from two packages gets a block for each here;
986/// [`reexport_conflicts`] refuses it with the other problems once the file
987/// is assembled.
988#[cfg(not(target_arch = "wasm32"))]
989fn reexport_fragment(entries: &[ReexportEntry]) -> Option<WrapperFragment> {
990let mut by_item: std::collections::BTreeMap<(&str, &str), Vec<&ReexportEntry>> =
991 std::collections::BTreeMap::new();
992for entry in entries {
993 by_item
994 .entry((entry.package, entry.name))
995 .or_default()
996 .push(entry);
997 }
998let first = *by_item.values().next()?.first()?;
999let blocks: Vec<String> = by_item1000 .iter()
1001 .map(|((package, name), sites)| {
1002let object = if is_syntactic_r_name(name) {
1003::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}::{1}", package, name))
})format!("{package}::{name}")1004 } else {
1005::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}::`{1}`", package, name))
})format!("{package}::`{name}`")1006 };
1007::alloc::__export::must_use({
::alloc::fmt::format(format_args!("# Generated from Rust `reexport!` of `{2}::{3}` {0}\n#\' @importFrom {2} {1}\n#\' @export\n{4}",
reexport_sites(sites), r_namespace_name(name), package, name,
object))
})format!(
1008"# Generated from Rust `reexport!` of `{package}::{name}` {}\n\
1009 #' @importFrom {package} {}\n\
1010 #' @export\n\
1011 {object}",
1012 reexport_sites(sites),
1013 r_namespace_name(name),
1014 )1015 })
1016 .collect();
1017Some(WrapperFragment {
1018 priority: RWrapperPriority::Reexport,
1019 source_file: first.source_file,
1020 source_line: first.source_line,
1021 content: std::borrow::Cow::Owned(blocks.join("\n\n")),
1022 })
1023}
10241025/// The sites of `entries`, `(file.rs:line:column)` each, in source order.
1026#[cfg(not(target_arch = "wasm32"))]
1027fn reexport_sites(entries: &[&ReexportEntry]) -> String {
1028let mut sites: Vec<(&str, u32, u32)> = entries1029 .iter()
1030 .map(|entry| (entry.source_file, entry.source_line, entry.source_column))
1031 .collect();
1032sites.sort_unstable();
1033sites.dedup();
1034sites1035 .iter()
1036 .map(|(file, line, column)| ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("({0}:{1}:{2})", file, line,
column))
})format!("({file}:{line}:{column})"))
1037 .collect::<Vec<_>>()
1038 .join(" ")
1039}
10401041/// Refuse the re-exports of `entries` that can't be written, all in one
1042/// message: a name re-exported from two packages (a package exports one
1043/// object under a name), and a name that `content`, the assembled wrappers,
1044/// also binds at top level or exports. The package's own binding would hide
1045/// the imported one inside its namespace, so `export(name)` would export it
1046/// instead; an export alone repeats the one the re-export writes.
1047///
1048/// Only the generated wrappers are read, not the package's hand-written R
1049/// files.
1050#[cfg(not(target_arch = "wasm32"))]
1051fn reexport_conflicts(content: &str, entries: &[ReexportEntry]) -> Result<(), String> {
1052use std::collections::BTreeMap;
1053let mut by_name: BTreeMap<&str, BTreeMap<&str, Vec<&ReexportEntry>>> = BTreeMap::new();
1054for entry in entries {
1055 by_name
1056 .entry(entry.name)
1057 .or_default()
1058 .entry(entry.package)
1059 .or_default()
1060 .push(entry);
1061 }
1062if by_name.is_empty() {
1063return Ok(());
1064 }
1065let reexports = |name: &str, packages: &BTreeMap<&str, Vec<&ReexportEntry>>| {
1066packages1067 .iter()
1068 .map(|(package, sites)| ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("`{1}::{2}` {0}",
reexport_sites(sites), package, name))
})format!("`{package}::{name}` {}", reexport_sites(sites)))
1069 .collect::<Vec<_>>()
1070 .join(", ")
1071 };
10721073let mut errors = Vec::new();
1074for (name, packages) in &by_name {
1075if packages.len() > 1 {
1076 errors.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("`reexport!` re-exports `{2}` from {0} packages: {1}. A package exports one object under a name; re-export it from one of them.",
packages.len(), reexports(name, packages), name))
})format!(
1077"`reexport!` re-exports `{name}` from {} packages: {}. A package exports one \
1078 object under a name; re-export it from one of them.",
1079 packages.len(),
1080 reexports(name, packages),
1081 ));
1082 }
1083 }
10841085// Each fragment that binds or exports a re-exported name, once, by its
1086 // binding when it both binds and exports it.
1087let mut own: BTreeMap<&str, Vec<(&'static str, std::rc::Rc<str>)>> = BTreeMap::new();
1088for WrapperName {
1089 name,
1090 kind,
1091 producer,
1092 } in wrapper_names(content)
1093 {
1094if !by_name.contains_key(name) {
1095continue;
1096 }
1097let uses = own.entry(name).or_default();
1098match uses.iter_mut().find(|(_, other)| *other == producer) {
1099Some(used) if used.0 == "export" => used.0 = kind,
1100Some(_) => {}
1101None => uses.push((kind, producer)),
1102 }
1103 }
1104for (name, uses) in own {
1105let binds = uses.iter().any(|(kind, _)| *kind != "export");
1106let what = uses
1107 .iter()
1108 .map(|(kind, producer)| {
1109if *kind == "export" {
1110::alloc::__export::must_use({
::alloc::fmt::format(format_args!("an export from {0}", producer))
})format!("an export from {producer}")1111 } else {
1112::alloc::__export::must_use({
::alloc::fmt::format(format_args!("a {0} from {1}", kind, producer))
})format!("a {kind} from {producer}")1113 }
1114 })
1115 .collect::<Vec<_>>()
1116 .join(", ");
1117let (does, why) = if binds {
1118 (
1119"bind",
1120::alloc::__export::must_use({
::alloc::fmt::format(format_args!("The package\'s own `{0}` would hide the imported one in its namespace, so `export({0})` would export it instead. Remove the re-export, or rename the package\'s binding (`r_name = \"...\"`, `class = \"...\"`).",
name))
})format!(
1121"The package's own `{name}` would hide the imported one in its namespace, \
1122 so `export({name})` would export it instead. Remove the re-export, or \
1123 rename the package's binding (`r_name = \"...\"`, `class = \"...\"`)."
1124),
1125 )
1126 } else {
1127 (
1128"export",
1129"`reexport!` writes the export itself; drop the other one.".to_string(),
1130 )
1131 };
1132 errors.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("`reexport!` re-exports {0}, and the package\'s wrappers also {1} `{2}` ({3}). {4}",
reexports(name, &by_name[name]), does, name, what, why))
})format!(
1133"`reexport!` re-exports {}, and the package's wrappers also {does} `{name}` ({what}). \
1134 {why}",
1135 reexports(name, &by_name[name]),
1136 ));
1137 }
11381139if errors.is_empty() {
1140Ok(())
1141 } else {
1142Err(errors1143 .iter()
1144 .map(|error| ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("miniextendr: {0}", error))
})format!("miniextendr: {error}"))
1145 .collect::<Vec<_>>()
1146 .join("\n"))
1147 }
1148}
11491150// endregion
11511152// region: impl blocks merged per type (#1776)
11531154/// A wrapper fragment to write: an [`RWrapperEntry`]'s text, or a fragment
1155/// assembled at write time (a class merged from its impl blocks, a type's
1156/// sidecar accessors).
1157#[cfg(not(target_arch = "wasm32"))]
1158struct WrapperFragment {
1159 priority: RWrapperPriority,
1160 source_file: &'static str,
1161 source_line: u32,
1162 content: std::borrow::Cow<'static, str>,
1163}
11641165#[cfg(not(target_arch = "wasm32"))]
1166impl From<&RWrapperEntry> for WrapperFragment {
1167fn from(entry: &RWrapperEntry) -> Self {
1168WrapperFragment {
1169 priority: entry.priority,
1170 source_file: entry.source_file,
1171 source_line: entry.source_line,
1172 content: std::borrow::Cow::Borrowed(entry.content),
1173 }
1174 }
1175}
11761177/// A [`ClassBlockEntry`] part without the newline its raw string literal
1178/// adds at each end.
1179#[cfg(not(target_arch = "wasm32"))]
1180fn class_part(part: &str) -> &str {
1181let part = part.strip_prefix('\n').unwrap_or(part);
1182part.strip_suffix('\n').unwrap_or(part)
1183}
11841185/// How a merge error names an impl block: `` `impl Counter` (lib.rs:12) ``.
1186#[cfg(not(target_arch = "wasm32"))]
1187fn class_block_name(block: &ClassBlockEntry) -> String {
1188::alloc::__export::must_use({
::alloc::fmt::format(format_args!("`impl {0}` ({1}:{2})",
block.rust_type, block.source_file, block.source_line))
})format!(
1189"`impl {}` ({}:{})",
1190 block.rust_type, block.source_file, block.source_line
1191 )1192}
11931194/// Merge the impl blocks of each Rust type into one class fragment (#1776).
1195///
1196/// Blocks are grouped by [`ClassBlockEntry::type_path`] and taken in source
1197/// order. A type's fragment is the defining block's head, with the
1198/// `class_docs` of every block (the defining block's first) at its
1199/// [`CLASS_DOCS_MARKER`] line and the `class_doc_lines` of every block at its
1200/// [`CLASS_DOC_LINES_MARKER`] line, then each block's members, then the
1201/// defining block's tail. A tag repeated exactly is written once, and a
1202/// generated [`CLASS_DEFAULT_PREFIX`] line gives way to the same tag on
1203/// another block. A `# Generated from Rust impl` header precedes each
1204/// part whose block differs from the previous part's, so the
1205/// duplicate-definition scan of the written file
1206/// ([`detect_duplicate_wrapper_defs`]) names the block a definition comes from.
1207///
1208/// Every problem is collected into one message:
1209///
1210/// - two Rust types claim one R class name: their class objects and methods
1211/// would overwrite each other when the package loads (#1677);
1212/// - the blocks of a type disagree on the class settings
1213/// ([`ClassBlockEntry::settings`]), which shape every block's members;
1214/// - two blocks of a type each have something only the defining block may
1215/// have ([`ClassBlockEntry::defines`]);
1216/// - the doc comments of a type's blocks give the class two titles, or a
1217/// page other than the defining block's ([`class_doc_conflicts`]);
1218/// - two blocks of a type bind the same member name
1219/// ([`ClassBlockEntry::member_names`]), the later replacing the earlier;
1220/// - a type without a declared constructor has several methods that could be
1221/// one ([`resolve_class_constructor`], which first picks each block's
1222/// rendering).
1223#[cfg(not(target_arch = "wasm32"))]
1224fn merge_class_blocks(blocks: &[ClassBlockEntry]) -> Result<Vec<WrapperFragment>, String> {
1225use std::collections::HashMap;
12261227let mut ordered: Vec<&ClassBlockEntry> = blocks.iter().collect();
1228ordered.sort_by_key(|b| (b.source_file, b.source_line, b.source_column));
12291230// The blocks of each type, in source order; types in order of their first block.
1231let mut types: Vec<(&'static str, Vec<&ClassBlockEntry>)> = Vec::new();
1232let mut type_index: HashMap<&'static str, usize> = HashMap::new();
1233for block in ordered {
1234let path = (block.type_path)();
1235let i = *type_index.entry(path).or_insert_with(|| {
1236 types.push((path, Vec::new()));
1237 types.len() - 1
1238});
1239 types[i].1.push(block);
1240 }
12411242let mut errors: Vec<String> = Vec::new();
12431244// One Rust type per R class name.
1245let mut class_owner: HashMap<&'static str, (&'static str, &ClassBlockEntry)> = HashMap::new();
1246for (path, type_blocks) in &types {
1247for block in type_blocks {
1248match class_owner.get(block.r_class_name) {
1249Some(&(owner, first)) if owner != *path => {
1250 errors.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("miniextendr: R class `{0}` is claimed by two Rust types, `{3}` ({1}) and `{4}` ({2}). Their class objects and methods would overwrite each other when the package loads; give one a distinct `class = \"...\"`.",
block.r_class_name, class_block_name(first),
class_block_name(block), owner, path))
})format!(
1251"miniextendr: R class `{}` is claimed by two Rust types, `{owner}` \
1252 ({}) and `{path}` ({}). Their class objects and methods would \
1253 overwrite each other when the package loads; give one a distinct \
1254 `class = \"...\"`.",
1255 block.r_class_name,
1256 class_block_name(first),
1257 class_block_name(block),
1258 ));
1259 }
1260Some(_) => {}
1261None => {
1262 class_owner.insert(block.r_class_name, (path, block));
1263 }
1264 }
1265 }
1266 }
12671268let mut fragments = Vec::with_capacity(types.len());
1269for (_, type_blocks) in &types {
1270let type_blocks = match resolve_class_constructor(type_blocks) {
1271Ok(blocks) => blocks,
1272Err(error) => {
1273 errors.push(error);
1274continue;
1275 }
1276 };
1277let first = type_blocks[0];
1278let type_errors = errors.len();
12791280for block in &type_blocks[1..] {
1281if block.settings != first.settings {
1282 errors.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("miniextendr: the impl blocks of `{0}` disagree on the class settings: `{1}` on {2}, `{3}` on {4}. The blocks are merged into one class, and these settings shape every block\'s methods: spell the same class system and options, `class = \"...\"`, `internal` / `noexport` and `@noRd` / `@keywords internal` on each block (#1776).",
first.rust_type, first.settings, class_block_name(first),
block.settings, class_block_name(block)))
})format!(
1283"miniextendr: the impl blocks of `{}` disagree on the class settings: \
1284 `{}` on {}, `{}` on {}. The blocks are merged into one class, and \
1285 these settings shape every block's methods: spell the same class \
1286 system and options, `class = \"...\"`, `internal` / `noexport` and \
1287 `@noRd` / `@keywords internal` on each block (#1776).",
1288 first.rust_type,
1289 first.settings,
1290 class_block_name(first),
1291 block.settings,
1292 class_block_name(block),
1293 ));
1294 }
1295 }
12961297let defining: Vec<&ClassBlockEntry> = type_blocks
1298 .iter()
1299 .copied()
1300 .filter(|b| !b.defines.is_empty())
1301 .collect();
1302if let [a, b, ..] = defining.as_slice() {
1303 errors.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("miniextendr: two impl blocks of `{0}` each define the class: {1} has {2}, {3} has {4}. A type\'s impl blocks merge into one class, and only one of them may have a constructor or S7 properties: move what {5} has into {6} (#1776).",
first.rust_type, class_block_name(a), a.defines.join(" and "),
class_block_name(b), b.defines.join(" and "),
class_block_name(b), class_block_name(a)))
})format!(
1304"miniextendr: two impl blocks of `{}` each define the class: {} has {}, \
1305 {} has {}. A type's impl blocks merge into one class, and only one of \
1306 them may have a constructor or S7 properties: move what {} has into \
1307 {} (#1776).",
1308 first.rust_type,
1309 class_block_name(a),
1310 a.defines.join(" and "),
1311 class_block_name(b),
1312 b.defines.join(" and "),
1313 class_block_name(b),
1314 class_block_name(a),
1315 ));
1316 }
13171318let definer = defining.first().copied().unwrap_or(first);
1319 errors.extend(class_doc_conflicts(definer, &type_blocks));
13201321let mut members: HashMap<&str, &ClassBlockEntry> = HashMap::new();
1322for &block in &type_blocks {
1323for &name in block.member_names {
1324match members.get(name) {
1325Some(&previous) if !std::ptr::eq(previous, block) => {
1326 errors.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("miniextendr: two impl blocks of `{0}` define {3}: {1} and {2}. The later definition would replace the earlier one when the package loads; rename one with `#[miniextendr(r_name = \"...\")]` (#1776).",
first.rust_type, class_block_name(previous),
class_block_name(block), name))
})format!(
1327"miniextendr: two impl blocks of `{}` define {name}: {} and {}. \
1328 The later definition would replace the earlier one when the \
1329 package loads; rename one with \
1330 `#[miniextendr(r_name = \"...\")]` (#1776).",
1331 first.rust_type,
1332 class_block_name(previous),
1333 class_block_name(block),
1334 ));
1335 }
1336Some(_) => {}
1337None => {
1338 members.insert(name, block);
1339 }
1340 }
1341 }
1342 }
13431344if errors.len() > type_errors {
1345continue;
1346 }
1347 fragments.push(WrapperFragment {
1348 priority: RWrapperPriority::Class,
1349 source_file: first.source_file,
1350 source_line: first.source_line,
1351 content: std::borrow::Cow::Owned(merged_class_content(definer, &type_blocks)),
1352 });
1353 }
13541355if errors.is_empty() {
1356Ok(fragments)
1357 } else {
1358Err(errors.join("\n"))
1359 }
1360}
13611362/// One roxygen tag of a class's docs: its `#'` lines, the first opening the
1363/// tag (`#' @name ...`), or the lines before the first tag. `default` marks a
1364/// generated line ([`CLASS_DEFAULT_PREFIX`], stripped from `lines`).
1365#[cfg(not(target_arch = "wasm32"))]
1366#[derive(#[automatically_derived]
impl<'a> ::core::clone::Clone for DocTag<'a> {
#[inline]
fn clone(&self) -> Self {
DocTag {
default: ::core::clone::Clone::clone(&self.default),
lines: ::core::clone::Clone::clone(&self.lines),
}
}
}Clone, #[automatically_derived]
impl<'a> ::core::marker::StructuralPartialEq for DocTag<'a> { }
#[automatically_derived]
impl<'a> ::core::cmp::PartialEq for DocTag<'a> {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.default == other.default && self.lines == other.lines
}
}PartialEq)]
1367struct DocTag<'a> {
1368 default: bool,
1369 lines: Vec<&'a str>,
1370}
13711372#[cfg(not(target_arch = "wasm32"))]
1373impl<'a> DocTag<'a> {
1374/// The tags of `text`, a run of roxygen lines: a tag runs from its
1375 /// `#' @tag` line to the next one, as roxygen2 reads it.
1376fn parse(text: &'a str) -> Vec<Self> {
1377let mut tags: Vec<Self> = Vec::new();
1378for line in text.lines() {
1379let (default, line) = match line.strip_prefix(CLASS_DEFAULT_PREFIX) {
1380Some(line) => (true, line),
1381None => (false, line),
1382 };
1383match tags.last_mut() {
1384Some(tag) if !default && !line.starts_with("#' @") => tag.lines.push(line),
1385_ => tags.push(Self {
1386 default,
1387 lines: ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
[line]))vec![line],
1388 }),
1389 }
1390 }
1391tags1392 }
13931394/// A head's generated default line, as a one-line tag; `None` for
1395 /// any other line.
1396fn default_line(line: &'a str) -> Option<Self> {
1397line.strip_prefix(CLASS_DEFAULT_PREFIX).map(|line| Self {
1398 default: true,
1399 lines: ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
[line]))vec![line],
1400 })
1401 }
14021403/// The tag's name (`title` for `#' @title ...`), or `""` before the
1404 /// first tag.
1405fn name(&self) -> &'a str {
1406self.lines[0]
1407 .strip_prefix("#' @")
1408 .and_then(|rest| rest.split_whitespace().next())
1409 .unwrap_or("")
1410 }
14111412/// The tag's text after its name, continuation lines included.
1413fn value(&self) -> String {
1414let first = self.lines[0]
1415 .strip_prefix("#' @")
1416 .map_or("", |rest| rest.trim_start()[self.name().len()..].trim());
1417let rest = self.lines[1..]
1418 .iter()
1419 .map(|line| line.strip_prefix("#'").unwrap_or(line).trim());
1420 std::iter::once(first)
1421 .chain(rest)
1422 .collect::<Vec<_>>()
1423 .join("\n")
1424 .trim()
1425 .to_string()
1426 }
14271428/// Whether this tag replaces the generated `default` (a tag of the same
1429 /// name; for `@param` and `@field`, naming the same argument or field,
1430 /// and `@inheritParams` for any `@param`).
1431fn replaces(&self, default: &DocTag<'_>) -> bool {
1432match default.name() {
1433 name @ ("param" | "field") => {
1434let value = default.value();
1435let arg = value.split_whitespace().next().unwrap_or("");
1436 (self.name() == name1437 && self1438 .value()
1439 .split_whitespace()
1440 .next()
1441 .is_some_and(|names| names.split(',').any(|n| n.trim() == arg)))
1442 || (name == "param" && self.name() == "inheritParams")
1443 }
1444 name => self.name() == name,
1445 }
1446 }
1447}
14481449/// Every block's [`class_docs`](ClassBlockEntry::class_docs) as tags, the
1450/// defining block's first, then the others' in source order.
1451#[cfg(not(target_arch = "wasm32"))]
1452fn block_docs<'a>(
1453 definer: &'a ClassBlockEntry,
1454 blocks: &[&'a ClassBlockEntry],
1455) -> Vec<(&'a ClassBlockEntry, Vec<DocTag<'a>>)> {
1456let others = blocks1457 .iter()
1458 .copied()
1459 .filter(|b| !std::ptr::eq(*b, definer));
1460 std::iter::once(definer)
1461 .chain(others)
1462 .map(|b| (b, DocTag::parse(class_part(b.class_docs))))
1463 .collect()
1464}
14651466/// The problems the doc comments of a type's impl blocks make for its merged
1467/// roxygen block (#1776): every block's doc tags are written, so two
1468/// different `@title`s give the class two titles (roxygen2 silently keeps
1469/// the first), and a `@name` or `@rdname` other than the defining block's
1470/// would move the class block to a page the defining block's generated
1471/// documentation was not written for.
1472#[cfg(not(target_arch = "wasm32"))]
1473fn class_doc_conflicts(definer: &ClassBlockEntry, blocks: &[&ClassBlockEntry]) -> Vec<String> {
1474let mut errors = Vec::new();
1475let docs = block_docs(definer, blocks);
1476let tag_value =
1477 |tags: &[DocTag<'_>], name: &str| tags.iter().find(|t| t.name() == name).map(|t| t.value());
14781479let mut title: Option<(&ClassBlockEntry, String)> = None;
1480for (block, tags) in &docs {
1481let Some(value) = tag_value(tags, "title") else {
1482continue;
1483 };
1484match &title {
1485None => title = Some((block, value)),
1486Some((first, first_value)) if *first_value != value => {
1487 errors.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("miniextendr: the impl blocks of `{0}` give the class two titles: {1} has `@title {3}`, {2} has `@title {4}`. A type\'s impl blocks merge into one class with one help page: keep one `@title` (#1776).",
definer.rust_type, class_block_name(first),
class_block_name(block), first_value, value))
})format!(
1488"miniextendr: the impl blocks of `{}` give the class two titles: {} \
1489 has `@title {first_value}`, {} has `@title {value}`. A type's impl \
1490 blocks merge into one class with one help page: keep one `@title` \
1491 (#1776).",
1492 definer.rust_type,
1493 class_block_name(first),
1494 class_block_name(block),
1495 ));
1496break;
1497 }
1498Some(_) => {}
1499 }
1500 }
15011502// The page the defining block documents the class on: its own tag, else
1503 // the generated default.
1504let head_defaults: Vec<DocTag<'_>> = class_part(definer.head)
1505 .lines()
1506 .filter_map(DocTag::default_line)
1507 .collect();
1508for name in ["name", "rdname"] {
1509let page = tag_value(&docs[0].1, name).or_else(|| tag_value(&head_defaults, name));
1510for (block, tags) in &docs[1..] {
1511let Some(value) = tag_value(tags, name) else {
1512continue;
1513 };
1514if page.as_ref() == Some(&value) {
1515continue;
1516 }
1517let page = match &page {
1518Some(page) => ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("`@{0} {1}`", name, page))
})format!("`@{name} {page}`"),
1519None => ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("no `@{0}`", name))
})format!("no `@{name}`"),
1520 };
1521 errors.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("miniextendr: the impl blocks of `{0}` put the class on two help pages: {1}, which defines the class, has {4}, and {2} has `@{5} {6}`. The block that defines the class decides its page: move `@{5} {6}` into {3}, or remove it (#1776).",
definer.rust_type, class_block_name(definer),
class_block_name(block), class_block_name(definer), page,
name, value))
})format!(
1522"miniextendr: the impl blocks of `{}` put the class on two help pages: {}, \
1523 which defines the class, has {page}, and {} has `@{name} {value}`. The \
1524 block that defines the class decides its page: move `@{name} {value}` \
1525 into {}, or remove it (#1776).",
1526 definer.rust_type,
1527 class_block_name(definer),
1528 class_block_name(block),
1529 class_block_name(definer),
1530 ));
1531 }
1532 }
1533errors1534}
15351536/// Decide a type's class constructor across its impl blocks: the blocks to
1537/// merge, each in the rendering that agrees with the decision.
1538///
1539/// - A block that declares the constructor
1540/// (`#[miniextendr(<system>(constructor))]`, or a receiver-less `new`
1541/// returning `Self`) has it. A block that would
1542/// infer one takes its [`demoted`](ClassBlockEntry::demoted) rendering,
1543/// where that method is a static method. Two declaring blocks are refused
1544/// with the other two-definer cases ([`merge_class_blocks`]).
1545/// - Without one, the type's one
1546/// [candidate](ClassBlockEntry::constructor_candidates) is its constructor,
1547/// whichever block has it. Several candidates are an error naming each
1548/// with its block (the compiler already refuses several in one block).
1549#[cfg(not(target_arch = "wasm32"))]
1550fn resolve_class_constructor<'a>(
1551 blocks: &[&'a ClassBlockEntry],
1552) -> Result<Vec<&'a ClassBlockEntry>, String> {
1553let declared = blocks1554 .iter()
1555 .any(|b| !b.constructor.is_empty() && !b.constructor_inferred);
1556if declared {
1557return Ok(blocks1558 .iter()
1559 .map(|&b| match b.demoted {
1560Some(demoted) if b.constructor_inferred => demoted,
1561_ => b,
1562 })
1563 .collect());
1564 }
1565let candidates: usize = blocks.iter().map(|b| b.constructor_candidates.len()).sum();
1566if candidates > 1 {
1567let listed: Vec<String> = blocks1568 .iter()
1569 .filter(|b| !b.constructor_candidates.is_empty())
1570 .map(|b| {
1571let names: Vec<String> = b1572 .constructor_candidates
1573 .iter()
1574 .map(|name| ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("`{0}`", name))
})format!("`{name}`"))
1575 .collect();
1576::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0} in {1}", names.join(", "),
class_block_name(b)))
})format!("{} in {}", names.join(", "), class_block_name(b))1577 })
1578 .collect();
1579// The class system is the first word of the settings, `r6(...)`.
1580let settings = blocks[0].settings;
1581let system = &settings[..settings
1582 .find(|c: char| !c.is_ascii_alphanumeric())
1583 .unwrap_or(settings.len())];
1584return Err(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("miniextendr: `{0}` has several methods that could be its constructor, and none is declared: {1}. Mark the constructor with `#[miniextendr({2}(constructor))]` (or name it `new`); the others stay static factory methods.",
blocks[0].rust_type, listed.join("; "), system))
})format!(
1585"miniextendr: `{}` has several methods that could be its constructor, and none \
1586 is declared: {}. Mark the constructor with `#[miniextendr({system}(constructor))]` \
1587 (or name it `new`); the others stay static factory methods.",
1588 blocks[0].rust_type,
1589 listed.join("; "),
1590 ));
1591 }
1592Ok(blocks.to_vec())
1593}
15941595/// Resolve each member's generated page after the defining impl is known.
1596/// A default page follows that impl's `@rdname`; explicit member pages do not.
1597/// On an inherited author topic, its block supplies the title and parameter
1598/// docs, so generated prose/parameters are removed and the member sorts last.
1599#[cfg(not(target_arch = "wasm32"))]
1600fn resolve_class_member_docs(content: &str, class: &str, topic: &str) -> String {
1601fn unmark(line: &str) -> &str {
1602line.strip_prefix(CLASS_MEMBER_DEFAULT_PREFIX)
1603 .unwrap_or(line)
1604 }
1605let mut lines = content.split('\n').peekable();
1606let mut result = Vec::new();
1607while let Some(line) = lines.next() {
1608if !unmark(line).trim_start().starts_with("#'") {
1609 result.push(line.to_owned());
1610continue;
1611 }
1612let mut block = ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
[line]))vec![line];
1613while lines
1614 .peek()
1615 .is_some_and(|line| unmark(line).trim_start().starts_with("#'"))
1616 {
1617 block.push(lines.next().unwrap());
1618 }
1619let default_page = block.iter().any(|line| {
1620 line.strip_prefix(CLASS_MEMBER_DEFAULT_PREFIX)
1621 .is_some_and(|line| line.starts_with("#' @rdname "))
1622 });
1623let page = if default_page {
1624Some(topic)
1625 } else {
1626 block
1627 .iter()
1628 .find_map(|line| line.strip_prefix("#' @rdname "))
1629 };
1630let joined = page.is_some_and(|page| page != class)
1631 || block.iter().any(|line| line.starts_with("#' @describeIn "));
1632let has_order = block
1633 .iter()
1634 .any(|line| unmark(line).starts_with("#' @order "));
1635let no_rd = block.iter().any(|line| unmark(line).trim() == "#' @noRd");
1636for line in block {
1637match line.strip_prefix(CLASS_MEMBER_DEFAULT_PREFIX) {
1638Some(default) if joined => {
1639if default.starts_with("#' @rdname ") {
1640 result.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @rdname {0}", topic))
})format!("#' @rdname {topic}"));
1641 }
1642 }
1643Some(default) => result.push(default.to_owned()),
1644None => result.push(line.to_owned()),
1645 }
1646 }
1647if joined && !has_order && !no_rd && default_page {
1648 result.push("#' @order NaN".to_owned());
1649 }
1650 }
1651result.join("\n")
1652}
16531654/// The text of a type's merged class: see [`merge_class_blocks`].
1655#[cfg(not(target_arch = "wasm32"))]
1656fn merged_class_content(definer: &ClassBlockEntry, blocks: &[&ClassBlockEntry]) -> String {
1657let block_docs = block_docs(definer, blocks);
1658// A default of `owner` gives way to the same tag on another block; its
1659 // own docs were seen when it was generated.
1660let replaced = |owner: &ClassBlockEntry, default: &DocTag<'_>| {
1661block_docs.iter().any(|(block, tags)| {
1662 !std::ptr::eq(*block, owner) && tags.iter().any(|t| t.replaces(default))
1663 })
1664 };
1665let mut class_docs: Vec<DocTag<'_>> = Vec::new();
1666for tag in block_docs.iter().flat_map(|(_, tags)| tags) {
1667if !class_docs.contains(tag) {
1668 class_docs.push(tag.clone());
1669 }
1670 }
1671let mut class_doc_lines: Vec<DocTag<'_>> = Vec::new();
1672for block in blocks {
1673for mut tag in DocTag::parse(class_part(block.class_doc_lines)) {
1674if tag.default && replaced(block, &tag) {
1675continue;
1676 }
1677 tag.default = false;
1678if !class_doc_lines.contains(&tag) {
1679 class_doc_lines.push(tag);
1680 }
1681 }
1682 }
1683// A docs-only block (`CLASS_DOCS_ONLY_MARKER`) is written when the docs
1684 // say something beyond the page and export tags.
1685let says_something = !class_doc_lines.is_empty()
1686 || class_docs.iter().any(|t| {
1687 !#[allow(non_exhaustive_omitted_patterns)] match t.name() {
"rdname" | "noRd" | "keywords" | "order" | "export" => true,
_ => false,
}matches!(
1688 t.name(),
1689"rdname" | "noRd" | "keywords" | "order" | "export"
1690)1691 });
1692let mut skipping = false;
1693// `split`, not `lines`: a blank last line separates the head from the
1694 // members.
1695let mut head_lines: Vec<&str> = Vec::new();
1696for line in class_part(definer.head).split('\n') {
1697if skipping {
1698 skipping = line != "NULL";
1699continue;
1700 }
1701if line == CLASS_DOCS_ONLY_MARKER {
1702 skipping = !says_something;
1703 } else if line == CLASS_DOCS_MARKER {
1704 head_lines.extend(class_docs.iter().flat_map(|t| t.lines.iter().copied()));
1705 } else if line == CLASS_DOC_LINES_MARKER {
1706 head_lines.extend(class_doc_lines.iter().flat_map(|t| t.lines.iter().copied()));
1707 } else if let Some(tag) = DocTag::default_line(line) {
1708if !replaced(definer, &tag) {
1709 head_lines.push(tag.lines[0]);
1710 }
1711 } else {
1712 head_lines.push(line);
1713 }
1714 }
1715let head = head_lines.join("\n");
17161717let mut parts: Vec<(&ClassBlockEntry, &str)> = ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
[(definer, &head)]))vec![(definer, &head)];
1718parts.extend(blocks.iter().map(|&b| (b, class_part(b.members))));
1719parts.push((definer, class_part(definer.tail)));
17201721let mut content = String::new();
1722let mut previous: Option<&ClassBlockEntry> = None;
1723for (block, text) in parts {
1724if text.is_empty() {
1725continue;
1726 }
1727if previous.is_some_and(|p| std::ptr::eq(p, block)) {
1728 content.push('\n');
1729 content.push_str(text);
1730continue;
1731 }
1732if previous.is_some() {
1733 content.truncate(content.trim_end_matches('\n').len());
1734 content.push_str("\n\n");
1735 }
1736 content.push_str(&::alloc::__export::must_use({
::alloc::fmt::format(format_args!("# Generated from Rust impl `{0}` ({1}:{2}:{3})\n",
block.rust_type, block.source_file, block.source_line,
block.source_column))
})format!(
1737"# Generated from Rust impl `{}` ({}:{}:{})\n",
1738 block.rust_type, block.source_file, block.source_line, block.source_column
1739 ));
1740 content.push_str(text.trim_start_matches('\n'));
1741 previous = Some(block);
1742 }
1743let topic = block_docs[0]
1744 .1
1745.iter()
1746 .find(|tag| tag.name() == "rdname")
1747 .map(|tag| tag.value())
1748 .unwrap_or_else(|| definer.r_class_name.to_owned());
1749resolve_class_member_docs(&content, definer.r_class_name, &topic)
1750}
17511752// endregion
17531754/// Collect all R wrapper entries, sorted and deduplicated.
1755///
1756/// Each type's impl blocks are first merged into one class fragment
1757/// (`merge_class_blocks`, #1776); a conflict between them panics with every
1758/// problem found. The `reexport!` blocks are one more fragment
1759/// (`reexport_fragment`). Entries are then ordered by `sort_wrapper_entries`: priority
1760/// first, then source file, then source line. Within each priority group, S7
1761/// class definitions are additionally topologically sorted so parents are
1762/// defined before children (S7 `parent = X` requires X to exist).
1763///
1764/// Host-only — wasm32 doesn't run wrapper-gen.
1765#[cfg(not(target_arch = "wasm32"))]
1766pub fn collect_r_wrappers() -> Vec<std::borrow::Cow<'static, str>> {
1767let classes = merge_class_blocks(&MX_CLASS_BLOCKS).unwrap_or_else(|msg| { ::core::panicking::panic_fmt(format_args!("{0}", msg)); }panic!("{msg}"));
1768let mut entries: Vec<WrapperFragment> = MX_R_WRAPPERS1769 .iter()
1770 .map(WrapperFragment::from)
1771 .chain(classes)
1772 .chain(sidecar_accessor_fragments(
1773&MX_SIDECAR_ACCESSORS,
1774&MX_CLASS_NAMES,
1775 ))
1776 .chain(
1777all_equal_fragments(&MX_ALL_EQUAL, &MX_CLASS_NAMES)
1778 .unwrap_or_else(|msg| { ::core::panicking::panic_fmt(format_args!("{0}", msg)); }panic!("{msg}")),
1779 )
1780 .chain(reexport_fragment(&MX_REEXPORTS))
1781 .collect();
1782sort_wrapper_entries(&mut entries);
17831784let mut seen = std::collections::HashSet::<String>::new();
1785let mut fragments: Vec<(
1786RWrapperPriority,
1787&'static str,
1788 std::borrow::Cow<'static, str>,
1789 )> = Vec::with_capacity(entries.len());
1790for entry in entries {
1791let trimmed = match entry.content {
1792 std::borrow::Cow::Borrowed(content) => std::borrow::Cow::Borrowed(content.trim()),
1793 std::borrow::Cow::Owned(content) => std::borrow::Cow::Owned(content.trim().to_string()),
1794 };
1795if !trimmed.is_empty() && seen.insert(trimmed.to_string()) {
1796 fragments.push((entry.priority, entry.source_file, trimmed));
1797 }
1798 }
17991800// Standalone functions: the default `@title`, the generated `@param`
1801 // fillers and the `@order` of blocks on an author topic are decided
1802 // together, per page.
1803let standalone: Vec<StandaloneFragment<'_>> = fragments1804 .iter()
1805 .filter(|(priority, _, _)| *priority == RWrapperPriority::Function)
1806 .map(|(_, source_file, content)| StandaloneFragment {
1807content,
1808 stem: source_file_stem(source_file),
1809 })
1810 .collect();
1811let mut resolved = resolve_standalone_pages(&standalone).into_iter();
18121813let mut result: Vec<std::borrow::Cow<'static, str>> = fragments1814 .into_iter()
1815 .map(|(priority, _, content)| {
1816if priority == RWrapperPriority::Function {
1817 std::borrow::Cow::Owned(
1818resolved1819 .next()
1820 .expect("one resolved fragment per standalone function"),
1821 )
1822 } else {
1823content1824 }
1825 })
1826 .collect();
18271828// Topological sort for S7 inheritance ordering
1829sort_s7_classes(&mut result);
18301831result1832}
18331834// region: standalone-function pages (#1590)
18351836/// Prefix of a generated `@param` line in a standalone function's fragment,
1837/// after the `#' ` roxygen lead. Spelled the same as
1838/// `miniextendr_macros::roxygen::PARAM_FILLER_MARKER`, which emits it.
1839#[cfg(not(target_arch = "wasm32"))]
1840const PARAM_FILLER_MARKER: &str = ".__MX_PARAM_FILLER__ ";
18411842/// The `@order` line for a block on an author topic. Spelled the same as
1843/// `miniextendr_macros::roxygen::ORDER_AFTER_TOPIC_BLOCKS`, which documents
1844/// why `NaN` sorts the block after the topic's own block.
1845#[cfg(not(target_arch = "wasm32"))]
1846const ORDER_AFTER_TOPIC_BLOCKS: &str = "#' @order NaN";
18471848/// A standalone function's wrapper fragment and the stem of its source file
1849/// (`None` for `lib.rs` / `mod.rs`, see [`source_file_stem`]).
1850///
1851/// The stem is the topic an explicit `@rdname <stem>` names to group the
1852/// file's functions on one page (#1289): such a block is on its own file's
1853/// page, not joining a topic defined elsewhere, so it keeps its generated
1854/// `@param` lines where no function on the page documents the argument.
1855#[cfg(not(target_arch = "wasm32"))]
1856struct StandaloneFragment<'a> {
1857 content: &'a str,
1858 stem: Option<String>,
1859}
18601861/// Where a standalone function's block lands, read from its own tags.
1862#[cfg(not(target_arch = "wasm32"))]
1863struct StandalonePage {
1864/// The `.Rd` topic, `None` for an `@noRd` block.
1865page: Option<String>,
1866/// The block joins a topic defined elsewhere: `@describeIn`, or an
1867 /// `@rdname` naming neither its file's stem page nor the block's own
1868 /// `@name` / function name.
1869joined: bool,
1870/// The `@title` to inject: the function name, for a roxygen block that
1871 /// documents its own page and has no `@title` (roxygen2 renders no page
1872 /// without one).
1873title: Option<String>,
1874}
18751876#[cfg(not(target_arch = "wasm32"))]
1877fn standalone_page(fragment: &StandaloneFragment<'_>) -> StandalonePage {
1878let content = fragment.content;
1879let stem = fragment.stem.as_deref();
1880if has_no_rd_tag(content) {
1881return StandalonePage {
1882 page: None,
1883 joined: false,
1884 title: None,
1885 };
1886 }
1887let rdname = roxygen_tag_value(content, "rdname");
1888let describe_in = roxygen_tag_value(content, "describeIn");
1889let own_name = roxygen_tag_value(content, "name");
1890let fn_name = content.lines().find_map(parse_top_level_fn_def_name);
1891let own_page =
1892 |topic: &str| Some(topic) == stem || Some(topic) == own_name || Some(topic) == fn_name;
1893let joined = describe_in.is_some() || rdname.is_some_and(|topic| !own_page(topic));
1894let page = describe_in1895 .or(rdname)
1896 .or(own_name)
1897 .or(fn_name)
1898 .map(str::to_string);
1899let is_roxygen_block = content.lines().any(|l| l.trim_start().starts_with("#'"));
1900let has_title = content.lines().any(|l| l.trim().starts_with("#' @title "));
1901let title = fn_name1902 .filter(|_| is_roxygen_block && !has_title && !has_page_assignment_tag(content))
1903 .map(str::to_string);
1904StandalonePage {
1905page,
1906joined,
1907title,
1908 }
1909}
19101911/// Resolve the standalone functions' fragments, in emission order (#1590):
1912///
1913/// - give a roxygen block that picks no page itself (see
1914/// [`has_page_assignment_tag`]) and has no `@title` the function name as
1915/// its title. Such a block documents the function's own page, as in
1916/// roxygen2; grouping functions on one page is opt-in through an explicit
1917/// `@rdname` (#1289);
1918/// - resolve each generated `@param` filler (marked with
1919/// [`PARAM_FILLER_MARKER`] by the macro): keep it, unmarked, when the block
1920/// stays on its own page or its file's stem page (`@rdname <file stem>`)
1921/// and no other block of that page documents the argument; drop it when
1922/// the block joins a topic defined elsewhere (that topic's block documents
1923/// the arguments) or another function on the page documents the argument
1924/// (roxygen2 keeps the later of two `@param` lines for a name, so a filler
1925/// would replace the real text);
1926/// - give a block that joins a topic defined elsewhere `@order NaN`
1927/// ([`ORDER_AFTER_TOPIC_BLOCKS`]), so the topic's own block names and
1928/// titles the merged page whatever the R file order (an author `@order`
1929/// is kept).
1930///
1931/// Only the wrapper file is visible here: an R-file block that joins a
1932/// file-stem page does not count as documenting its arguments. Reading the
1933/// package's `R/*.R` blocks would tie the generated wrappers to R sources the
1934/// build does not track: they are regenerated only when the shared library is
1935/// relinked, so they would not follow an R-file edit.
1936#[cfg(not(target_arch = "wasm32"))]
1937fn resolve_standalone_pages(fragments: &[StandaloneFragment<'_>]) -> Vec<String> {
1938use std::collections::{HashMap, HashSet};
19391940let pages: Vec<StandalonePage> = fragments.iter().map(standalone_page).collect();
19411942// Arguments each page documents for real (the author's `@param` lines).
1943let mut documented: HashMap<&str, HashSet<&str>> = HashMap::new();
1944for (fragment, page) in fragments.iter().zip(&pages) {
1945let Some(topic) = page.page.as_deref() else {
1946continue;
1947 };
1948let names = documented.entry(topic).or_default();
1949for line in fragment.content.lines() {
1950if let Some(rest) = line.trim().strip_prefix("#' @param ")
1951 && let Some(token) = rest.split_whitespace().next()
1952 {
1953 names.extend(token.split(',').map(str::trim));
1954 }
1955 }
1956 }
19571958let filler_prefix = ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' {0}", PARAM_FILLER_MARKER))
})format!("#' {PARAM_FILLER_MARKER}");
1959fragments1960 .iter()
1961 .zip(&pages)
1962 .map(|(fragment, page)| {
1963let mut kept = Vec::new();
1964for line in fragment.content.lines() {
1965let Some(param) = line.trim().strip_prefix(filler_prefix.as_str()) else {
1966 kept.push(line.to_string());
1967continue;
1968 };
1969let name = param
1970 .strip_prefix("@param ")
1971 .and_then(|rest| rest.split_whitespace().next());
1972let documented_on_page = page
1973 .page
1974 .as_deref()
1975 .zip(name)
1976 .is_some_and(|(topic, name)| documented[topic].contains(name));
1977if page.page.is_some() && !page.joined && !documented_on_page {
1978 kept.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' {0}", param))
})format!("#' {param}"));
1979 }
1980 }
1981let mut content = kept.join("\n");
1982if page.joined && !content.lines().any(|l| l.trim().starts_with("#' @order ")) {
1983content = insert_roxygen_line(&content, ORDER_AFTER_TOPIC_BLOCKS);
1984 }
1985match page.title.as_deref() {
1986Some(title) => insert_roxygen_line(&content, &::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @title {0}", title))
})format!("#' @title {title}")),
1987None => content,
1988 }
1989 })
1990 .collect()
1991}
19921993/// The value of a fragment's first `#' @<tag> <value>` line (its first word).
1994#[cfg(not(target_arch = "wasm32"))]
1995fn roxygen_tag_value<'a>(content: &'a str, tag: &str) -> Option<&'a str> {
1996content.lines().find_map(|line| {
1997let rest = line.trim().strip_prefix("#' @")?.strip_prefix(tag)?;
1998if rest.starts_with(char::is_whitespace) {
1999rest.split_whitespace().next()
2000 } else {
2001None2002 }
2003 })
2004}
20052006/// Insert a roxygen line into a fragment at a tag boundary: before the first
2007/// `@export` / `@keywords` / `@source` line, else after the last roxygen line.
2008#[cfg(not(target_arch = "wasm32"))]
2009fn insert_roxygen_line(content: &str, new_line: &str) -> String {
2010let lines: Vec<&str> = content.lines().collect();
2011let at = lines2012 .iter()
2013 .position(|l| {
2014let t = l.trim();
2015t.starts_with("#' @export")
2016 || t.starts_with("#' @keywords")
2017 || t.starts_with("#' @source")
2018 })
2019 .or_else(|| {
2020lines2021 .iter()
2022 .rposition(|l| l.trim().starts_with("#'"))
2023 .map(|i| i + 1)
2024 })
2025 .unwrap_or(0);
2026let mut result = lines;
2027result.insert(at, new_line);
2028result.join("\n")
2029}
20302031// endregion
20322033/// Order wrapper entries for emission: by priority (the load-order
2034/// dependencies between sidecar accessors, classes, functions, trait impls and
2035/// vctrs methods), then by source file, then by the line the item starts on.
2036///
2037/// Within one source file that is source order, so the `@description`
2038/// paragraphs and `\usage` entries roxygen merges onto a shared `@rdname` /
2039/// `@describeIn` page appear in the order the Rust file defines them, not in
2040/// link order (#1476). The sort is stable, so entries generated at the same
2041/// file and line (a `macro_rules!` expansion) keep their registration order.
2042#[cfg(not(target_arch = "wasm32"))]
2043fn sort_wrapper_entries(entries: &mut [WrapperFragment]) {
2044entries.sort_by_key(|e| (e.priority, e.source_file, e.source_line));
2045}
20462047#[cfg(not(target_arch = "wasm32"))]
2048/// Check if an R wrapper fragment already assigns itself to a documentation
2049/// page: `@rdname <topic>`, `@describeIn <topic> <desc>`, or `@name <topic>`.
2050///
2051/// A fragment without one documents the function's own page, and only such a
2052/// fragment gets the default `@title` (the function name) when it has none
2053/// (#1289).
2054fn has_page_assignment_tag(content: &str) -> bool {
2055content.lines().any(|line| {
2056let trimmed = line.trim();
2057trimmed.starts_with("#' @rdname ")
2058 || trimmed.starts_with("#' @describeIn ")
2059 || trimmed.starts_with("#' @name ")
2060 })
2061}
20622063#[cfg(not(target_arch = "wasm32"))]
2064/// Check if an R wrapper fragment has `@noRd`.
2065fn has_no_rd_tag(content: &str) -> bool {
2066content.lines().any(|line| {
2067let trimmed = line.trim();
2068trimmed == "#' @noRd"
2069})
2070}
20712072#[cfg(not(target_arch = "wasm32"))]
2073/// The stem of a standalone function's source file: the topic an explicit
2074/// `@rdname` names to group the file's functions on one page (see
2075/// [`StandaloneFragment`]). `None` for `lib.rs` / `mod.rs`, whose stems name
2076/// no page of their own.
2077///
2078/// `"src/rust/zero_copy_tests.rs"` → `Some("zero_copy_tests")`
2079/// `"lib.rs"` → `None`
2080fn source_file_stem(path: &str) -> Option<String> {
2081let file_name = path.rsplit(['/', '\\']).next()?;
2082let stem = file_name.strip_suffix(".rs").unwrap_or(file_name);
2083if stem.is_empty() || stem == "lib" || stem == "mod" {
2084return None;
2085 }
2086Some(stem.to_string())
2087}
20882089#[cfg(not(target_arch = "wasm32"))]
2090/// Sort S7 class definitions so parents come before children.
2091///
2092/// Detects `S7::new_class()` calls, extracts `parent = ClassName` relationships,
2093/// and performs topological sort. Non-S7 entries keep their relative order.
2094fn sort_s7_classes(entries: &mut [std::borrow::Cow<'static, str>]) {
2095use std::collections::HashMap;
20962097// Parse S7 class definitions: find (index, name, parent)
2098let mut s7_info: Vec<(usize, String, Option<String>)> = Vec::new();
20992100for (i, entry) in entries.iter().enumerate() {
2101if let Some(nc_pos) = entry.find("S7::new_class(") {
2102// Extract class name: "NAME <- S7::new_class("
2103let before = entry[..nc_pos].trim_end();
2104let name = before
2105 .strip_suffix("<-")
2106 .or_else(|| before.rsplit_once("<-").map(|(_, r)| r))
2107 .map(|s| s.trim())
2108 .and_then(|s| s.split_whitespace().last());
21092110let Some(name) = name else { continue };
21112112// Extract parent: "parent = ParentName,"
2113let after = &entry[nc_pos..];
2114let parent = after.find("parent = ").and_then(|p| {
2115let rest = &after[p + "parent = ".len()..];
2116let end = rest.find([',', ')', '\n']).unwrap_or(rest.len());
2117let p = rest[..end].trim();
2118if p.is_empty() {
2119None
2120} else {
2121Some(p.to_string())
2122 }
2123 });
21242125 s7_info.push((i, name.to_string(), parent));
2126 }
2127 }
21282129if s7_info.len() <= 1 {
2130return;
2131 }
21322133// Build name → position-in-s7_info map.
2134 // Also map placeholder .__MX_CLASS_REF_<Name>__ to the same position, since
2135 // the placeholder carries the Rust type name which equals the R class name
2136 // (before any `class = "Override"` resolution — good enough for ordering).
2137let mut name_to_pos: HashMap<String, usize> = HashMap::new();
2138for (pos, (_, name, _)) in s7_info.iter().enumerate() {
2139 name_to_pos.insert(name.clone(), pos);
2140// Also register the placeholder form so a child class that references
2141 // `parent = .__MX_CLASS_REF_ParentName__` can still be sorted correctly.
2142 name_to_pos.insert(::alloc::__export::must_use({
::alloc::fmt::format(format_args!(".__MX_CLASS_REF_{0}__", name))
})format!(".__MX_CLASS_REF_{name}__"), pos);
2143 }
21442145// Topological sort: repeatedly emit classes whose parent is already placed
2146let n = s7_info.len();
2147let mut order: Vec<usize> = Vec::with_capacity(n);
2148let mut placed = ::alloc::vec::from_elem(false, n)vec![false; n];
21492150for _ in 0..n {
2151for (pos, (_, _, parent)) in s7_info.iter().enumerate() {
2152if placed[pos] {
2153continue;
2154 }
2155let ready = match parent {
2156None => true,
2157Some(pname) => match name_to_pos.get(pname.as_str()) {
2158None => true, // external parent
2159Some(&pp) => placed[pp],
2160 },
2161 };
2162if ready {
2163 order.push(pos);
2164 placed[pos] = true;
2165 }
2166 }
2167 }
21682169// Fallback: add any remaining (cycles) in original order
2170for (pos, &is_placed) in placed.iter().enumerate().take(n) {
2171if !is_placed {
2172 order.push(pos);
2173 }
2174 }
21752176// Apply: place sorted S7 entries back at their original indices
2177let s7_indices: Vec<usize> = s7_info.iter().map(|(i, _, _)| *i).collect();
2178let original: Vec<std::borrow::Cow<'static, str>> =
2179s7_indices.iter().map(|&i| entries[i].clone()).collect();
21802181for (slot, &src) in order.iter().enumerate() {
2182 entries[s7_indices[slot]] = original[src].clone();
2183 }
2184}
2185// endregion
21862187/// The R formal of a `match_arg` parameter, `c("a", "b")`, that the wrappers
2188/// writer splices in place of its placeholder: `choices` (the type's
2189/// `MatchArg::CHOICES`), each quoted and escaped for an R string literal
2190/// ([`escape_r_string`](crate::match_arg::escape_r_string)).
2191///
2192/// With a `default = "..."` (`preferred`: the literal's text without its
2193/// quotes, so escaped as R source is), that choice comes first and the others
2194/// keep their order ([`crate::match_arg::default_first`], the order
2195/// [`crate::match_arg::match_arg_param_with_default`] matches against,
2196/// #1767). An empty `preferred` keeps the declaration order.
2197///
2198/// A `preferred` that names no choice panics with the placeholder: the
2199/// wrappers writer is the only caller, so the panic surfaces as a load-time
2200/// error in the host R session rather than a broken wrapper.
2201#[cfg(not(target_arch = "wasm32"))]
2202#[doc(hidden)]
2203pub fn match_arg_formal(choices: &[&str], preferred: &str, placeholder: &str) -> String {
2204use crate::match_arg::{default_first, escape_r_string};
2205let formal = if preferred.is_empty() {
2206choices.to_vec()
2207 } else {
2208default_first(choices, |choice| escape_r_string(choice) == preferred).unwrap_or_else(|| {
2209{
::core::panicking::panic_fmt(format_args!("miniextendr: preferred default `{0}` for placeholder `{1}` does not match any choice in {2:?}",
preferred, placeholder, choices));
}panic!(
2210"miniextendr: preferred default `{preferred}` for placeholder `{placeholder}` \
2211 does not match any choice in {choices:?}"
2212)2213 })
2214 };
2215let quoted: Vec<String> = formal2216 .iter()
2217 .map(|choice| ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("\"{0}\"", escape_r_string(choice)))
})format!("\"{}\"", escape_r_string(choice)))
2218 .collect();
2219::alloc::__export::must_use({
::alloc::fmt::format(format_args!("c({0})", quoted.join(", ")))
})format!("c({})", quoted.join(", "))2220}
22212222/// The trailing arguments a `match_arg` parameter's check passes to
2223/// `.miniextendr_match_arg` / `.miniextendr_match_arg_several` for the
2224/// type's `MatchArg::ALIASES` (#1843), which the wrappers writer splices in
2225/// place of `, <placeholder>`: `, aliases = base::c("grey" = "gray")`, each
2226/// alias and choice escaped for an R string literal. For a type without
2227/// aliases it is empty, so that wrapper keeps the text it had before aliases
2228/// existed. `base::c` is spelled out because the argument is written after
2229/// the shadowing pass, which would otherwise qualify it for a parameter
2230/// named `c`.
2231///
2232/// When an alias is listed, `, listed = " (or \"grey\" for \"gray\")"`
2233/// follows: the clause the check's message appends after the choices,
2234/// rendered here by the same function as the Rust side's messages
2235/// (`match_arg::listed_aliases_clause`), so the two word it alike (#2148). A
2236/// type whose aliases are all unlisted passes no `listed`, and its check
2237/// words its message as without aliases.
2238///
2239/// Aliases a hand-written impl got wrong (empty, `"NA"`, a choice, repeated,
2240/// a prefix of another choice, or naming no choice; the rules
2241/// `#[derive(MatchArg)]` enforces at compile time) panic with the
2242/// placeholder, as [`match_arg_formal`] does for a bad default.
2243#[cfg(not(target_arch = "wasm32"))]
2244#[doc(hidden)]
2245pub fn match_arg_aliases_arg(
2246 choices: &[&str],
2247 aliases: &[crate::match_arg::MatchArgAlias],
2248 placeholder: &str,
2249) -> String {
2250use crate::match_arg::{alias_problems, escape_r_string, listed_aliases_clause, r_quote};
2251if aliases.is_empty() {
2252return String::new();
2253 }
2254let problems = alias_problems(choices, aliases);
2255if !problems.is_empty() {
2256{
::core::panicking::panic_fmt(format_args!("miniextendr: the `MatchArg::ALIASES` behind placeholder `{1}` can\'t be used (choices {2:?}): {0}",
problems.join("; "), placeholder, choices));
};panic!(
2257"miniextendr: the `MatchArg::ALIASES` behind placeholder `{placeholder}` \
2258 can't be used (choices {choices:?}): {}",
2259 problems.join("; ")
2260 );
2261 }
2262let pairs: Vec<String> = aliases2263 .iter()
2264 .map(|alias| {
2265::alloc::__export::must_use({
::alloc::fmt::format(format_args!("\"{0}\" = \"{1}\"",
escape_r_string(alias.alias), escape_r_string(alias.choice)))
})format!(
2266"\"{}\" = \"{}\"",
2267 escape_r_string(alias.alias),
2268 escape_r_string(alias.choice)
2269 )2270 })
2271 .collect();
2272let mut arg = ::alloc::__export::must_use({
::alloc::fmt::format(format_args!(", aliases = base::c({0})",
pairs.join(", ")))
})format!(", aliases = base::c({})", pairs.join(", "));
2273let listed = listed_aliases_clause(choices, aliases, r_quote);
2274if !listed.is_empty() {
2275arg.push_str(&::alloc::__export::must_use({
::alloc::fmt::format(format_args!(", listed = \"{0}\"",
escape_r_string(&listed)))
})format!(", listed = \"{}\"", escape_r_string(&listed)));
2276 }
2277arg2278}
22792280/// `content` with one `match_arg` parameter's placeholders resolved from its
2281/// [`MatchArgChoicesEntry`]: every choices placeholder (the formal and the
2282/// check) by [`match_arg_formal`], and the check's `, <aliases placeholder>`
2283/// by [`match_arg_aliases_arg`], which is empty for a type without aliases.
2284#[cfg(not(target_arch = "wasm32"))]
2285fn splice_match_arg_entry(content: &str, entry: &MatchArgChoicesEntry) -> String {
2286let formal = match_arg_formal(entry.choices, entry.preferred_default, entry.placeholder);
2287let aliases = match_arg_aliases_arg(entry.choices, entry.aliases, entry.aliases_placeholder);
2288content2289 .replace(entry.placeholder, &formal)
2290 .replace(&::alloc::__export::must_use({
::alloc::fmt::format(format_args!(", {0}", entry.aliases_placeholder))
})format!(", {}", entry.aliases_placeholder), &aliases)
2291}
22922293// region: R Wrapper File Generation
22942295// region: Generic doc marker resolution
22962297/// Parse the payload of a `.__MX_GENERIC_DOC__(...)` marker line.
2298///
2299/// Returns `(kind, generic_name, class_name, export, dispatch, no_dots)` on success.
2300/// The marker format (emitted by proc-macros) is:
2301/// ```text
2302/// .__MX_GENERIC_DOC__(kind="S7", generic="get_value", class="MyClass",
2303/// export=true, dispatch="x", no_dots=false)
2304/// ```
2305/// S4 markers omit `dispatch` and `no_dots` (always `"x"` / `false`).
2306#[cfg(not(target_arch = "wasm32"))]
2307fn parse_generic_doc_marker(marker: &str) -> Option<(String, String, String, bool, String, bool)> {
2308// Strip prefix and trailing ')'
2309let inner = marker
2310 .strip_prefix(".__MX_GENERIC_DOC__(")?
2311.strip_suffix(')')?;
23122313let mut kind = String::new();
2314let mut generic = String::new();
2315let mut class = String::new();
2316let mut export = false;
2317let mut dispatch = "x".to_string();
2318let mut no_dots = false;
23192320for part in inner.split(", ") {
2321let part = part.trim();
2322if let Some(val) = part.strip_prefix("kind=") {
2323 kind = val.trim_matches('"').to_string();
2324 } else if let Some(val) = part.strip_prefix("generic=") {
2325 generic = val.trim_matches('"').to_string();
2326 } else if let Some(val) = part.strip_prefix("class=") {
2327 class = val.trim_matches('"').to_string();
2328 } else if let Some(val) = part.strip_prefix("export=") {
2329 export = val == "true";
2330 } else if let Some(val) = part.strip_prefix("dispatch=") {
2331 dispatch = val.trim_matches('"').to_string();
2332 } else if let Some(val) = part.strip_prefix("no_dots=") {
2333 no_dots = val == "true";
2334 }
2335 }
23362337if kind.is_empty() || generic.is_empty() || class.is_empty() {
2338return None;
2339 }
23402341Some((kind, generic, class, export, dispatch, no_dots))
2342}
23432344/// Whether `name` is a syntactic R name (letters, digits, `.` and `_`; starts
2345/// with a letter or a `.` not followed by a digit; not a reserved word).
2346///
2347/// Mirrors `is_syntactic_r_name` in `miniextendr-macros`' `naming` module,
2348/// which decides how the class generators spell the same names.
2349#[cfg(not(target_arch = "wasm32"))]
2350fn is_syntactic_r_name(name: &str) -> bool {
2351const RESERVED: &[&str] = &[
2352"if",
2353"else",
2354"repeat",
2355"while",
2356"function",
2357"for",
2358"next",
2359"break",
2360"TRUE",
2361"FALSE",
2362"NULL",
2363"Inf",
2364"NaN",
2365"NA",
2366"NA_integer_",
2367"NA_real_",
2368"NA_character_",
2369"NA_complex_",
2370"in",
2371 ];
2372let mut chars = name.chars();
2373match chars.next() {
2374Some(c) if c.is_ascii_alphabetic() => {}
2375Some('.') => {
2376if chars.next().is_some_and(|c| c.is_ascii_digit()) {
2377return false;
2378 }
2379 }
2380_ => return false,
2381 }
2382name.chars()
2383 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_')
2384 && !RESERVED.contains(&name)
2385}
23862387/// The argument of a raw `export(...)` NAMESPACE directive: bare for a
2388/// syntactic name, a double-quoted string for an operator such as `[[`
2389/// (`export([[)` does not parse). Must match `r_namespace_name` in
2390/// `miniextendr-macros`' `naming` module, which writes the method blocks'
2391/// directive for the same generic; roxygen2 only merges identical lines.
2392#[cfg(not(target_arch = "wasm32"))]
2393fn r_namespace_name(name: &str) -> String {
2394if is_syntactic_r_name(name) {
2395name.to_string()
2396 } else {
2397::alloc::__export::must_use({
::alloc::fmt::format(format_args!("\"{0}\"",
name.replace('\\', "\\\\").replace('"', "\\\"")))
})format!("\"{}\"", name.replace('\\', "\\\\").replace('"', "\\\""))2398 }
2399}
24002401/// Synthesise a standalone roxygen doc block for a generic.
2402///
2403/// Produces a block with:
2404/// - `@title` / `@description` describing the generic
2405/// - `@param` lines for each dispatch argument (plus `...` unless no_dots)
2406/// - `@name <generic>` — yields the bare `\alias{<generic>}` that `?generic` resolves
2407/// - `@export` or `@rawNamespace export(<generic>)` as appropriate
2408/// - `NULL` anchor
2409///
2410/// The method blocks emitted by the class generators keep their qualified
2411/// `@name Class-generic` so there is no duplicate-alias regression.
2412#[cfg(not(target_arch = "wasm32"))]
2413fn synthesise_generic_doc_block(
2414 kind: &str,
2415 generic: &str,
2416 classes: &[String],
2417 export: bool,
2418 dispatch: &str,
2419 no_dots: bool,
2420) -> String {
2421let mut lines: Vec<String> = Vec::new();
24222423// An operator generic (`[[`, `%op%`) is not called as `name()`.
2424let display = if is_syntactic_r_name(generic) {
2425::alloc::__export::must_use({
::alloc::fmt::format(format_args!("`{0}()`", generic))
})format!("`{generic}()`")2426 } else {
2427::alloc::__export::must_use({
::alloc::fmt::format(format_args!("`{0}`", generic))
})format!("`{generic}`")2428 };
24292430// Title
2431lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' The {0} generic", display))
})format!("#' The {display} generic"));
2432lines.push("#'".to_string());
24332434// Description
2435let kind_phrase = if kind == "S4" {
2436"an S4 generic"
2437} else {
2438"an S7 generic"
2439};
2440lines.push("#' @description".to_string());
2441lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' {0} is {1} generated by miniextendr. Methods in this",
display, kind_phrase))
})format!(
2442"#' {display} is {kind_phrase} generated by miniextendr. Methods in this"
2443));
2444lines.push("#' package are available for:".to_string());
2445lines.push("#' \\itemize{".to_string());
2446for cls in classes {
2447 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' \\item \\code{{\\link{{{0}}}}}",
cls))
})format!("#' \\item \\code{{\\link{{{cls}}}}}"));
2448 }
2449lines.push("#' }".to_string());
24502451// @param lines for dispatch formals
2452let dispatch_args: Vec<&str> = dispatch.split(',').map(|s| s.trim()).collect();
2453for arg in &dispatch_args {
2454 lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @param {0} An object.", arg))
})format!("#' @param {arg} An object."));
2455 }
2456if !no_dots {
2457lines.push("#' @param ... Passed on to methods.".to_string());
2458 }
24592460// Bare @name — this is what produces the bare \\alias{<generic>} in Rd
2461lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @name {0}", generic))
})format!("#' @name {generic}"));
24622463// Export directive
2464if export {
2465if kind == "S4" {
2466// S4 generics use @exportMethod (matches the method blocks)
2467lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @exportMethod {0}", generic))
})format!("#' @exportMethod {generic}"));
2468 } else {
2469// S7 generics use @rawNamespace export(...) (matches the method blocks,
2470 // including the quoting of operator names).
2471lines.push(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @rawNamespace export({0})",
r_namespace_name(generic)))
})format!(
2472"#' @rawNamespace export({})",
2473 r_namespace_name(generic)
2474 ));
2475 }
2476 }
24772478lines.push("NULL".to_string());
24792480lines.join("\n")
2481}
24822483/// Scan `content` for `.__MX_GENERIC_DOC__(...)` marker lines, group by
2484/// generic name, replace the first marker per generic with a synthesised
2485/// standalone doc block, and delete all remaining markers.
2486///
2487/// This is the write-time counterpart to the proc-macro marker emission in
2488/// `s7_class.rs` and `s4_class.rs`.
2489#[cfg(not(target_arch = "wasm32"))]
2490fn resolve_generic_doc_markers(content: String) -> String {
2491use std::collections::HashMap;
24922493const MARKER_PREFIX: &str = ".__MX_GENERIC_DOC__(";
24942495// First pass: collect all (generic, kind, classes[], export, dispatch, no_dots) grouped
2496 // by generic name. Preserve insertion order (first class encountered wins for kind/export).
2497let mut by_generic: HashMap<String, (String, Vec<String>, bool, String, bool)> = HashMap::new();
2498let mut generic_order: Vec<String> = Vec::new(); // tracks first-seen order
24992500for line in content.lines() {
2501let trimmed = line.trim();
2502if !trimmed.starts_with(MARKER_PREFIX) {
2503continue;
2504 }
2505if let Some((kind, generic, class, export, dispatch, no_dots)) =
2506 parse_generic_doc_marker(trimmed)
2507 {
2508let entry = by_generic.entry(generic.clone()).or_insert_with(|| {
2509 generic_order.push(generic.clone());
2510 (kind, Vec::new(), export, dispatch, no_dots)
2511 });
2512 entry.1.push(class);
2513 }
2514 }
25152516if by_generic.is_empty() {
2517return content;
2518 }
25192520// Second pass: walk the content line by line. For each generic, the first
2521 // marker line is replaced by the synthesised doc block; subsequent markers
2522 // for the same generic are dropped entirely.
2523let mut first_seen: std::collections::HashSet<String> = std::collections::HashSet::new();
2524let mut result = String::with_capacity(content.len() + 512 * by_generic.len());
25252526for line in content.lines() {
2527let trimmed = line.trim();
2528if trimmed.starts_with(MARKER_PREFIX) {
2529if let Some((_kind, generic, _class, _export, _dispatch, _no_dots)) =
2530 parse_generic_doc_marker(trimmed)
2531 {
2532if first_seen.insert(generic.clone()) {
2533// First occurrence: replace with synthesised block
2534if let Some((kind, classes, export, dispatch, no_dots)) =
2535 by_generic.get(&generic)
2536 {
2537let block = synthesise_generic_doc_block(
2538 kind, &generic, classes, *export, dispatch, *no_dots,
2539 );
2540 result.push_str(&block);
2541 result.push('\n');
2542 }
2543 }
2544// Subsequent occurrences: drop the line entirely (no push)
2545continue;
2546 }
2547 }
2548 result.push_str(line);
2549 result.push('\n');
2550 }
25512552// Trim trailing newlines added by the line-by-line loop and restore the
2553 // original termination (content typically ends with \n).
2554result2555}
25562557// endregion
25582559// region: Inherited param marker resolution
25602561/// Parse a `.__MX_INHERITED_PARAM__(...)` marker line.
2562///
2563/// The marker format is:
2564/// ```text
2565/// .__MX_INHERITED_PARAM__(class="R6Dog", parent="R6Animal", method="speak", param="times")
2566/// ```
2567/// Returns `(class, parent, method, param)` on success.
2568#[cfg(not(target_arch = "wasm32"))]
2569fn parse_inherited_param_marker(marker: &str) -> Option<(String, String, String, String)> {
2570let inner = marker
2571 .strip_prefix(".__MX_INHERITED_PARAM__(")?
2572.strip_suffix(')')?;
25732574let mut class = String::new();
2575let mut parent = String::new();
2576let mut method = String::new();
2577let mut param = String::new();
25782579for part in inner.split(", ") {
2580let part = part.trim();
2581if let Some(val) = part.strip_prefix("class=") {
2582 class = val.trim_matches('"').to_string();
2583 } else if let Some(val) = part.strip_prefix("parent=") {
2584 parent = val.trim_matches('"').to_string();
2585 } else if let Some(val) = part.strip_prefix("method=") {
2586 method = val.trim_matches('"').to_string();
2587 } else if let Some(val) = part.strip_prefix("param=") {
2588 param = val.trim_matches('"').to_string();
2589 }
2590 }
25912592if class.is_empty() || parent.is_empty() || method.is_empty() || param.is_empty() {
2593return None;
2594 }
25952596Some((class, parent, method, param))
2597}
25982599/// Resolve `.__MX_INHERITED_PARAM__(...)` markers.
2600///
2601/// For each marker, checks whether the parent class is in-package and has
2602/// `@param <param>` documented for either:
2603/// - The same method name (in any of its method doc blocks), or
2604/// - The class-level `@param` (emitted in the class header).
2605///
2606/// If the parent is documented → delete the marker line (roxygen2 8.0.0 inherits
2607/// the parent method's `@param` into the subclass method).
2608///
2609/// If the parent is cross-package or the param is not documented there → replace
2610/// with `#' @param {param} (no documentation available)` to preserve the zero-warning
2611/// guarantee on rendered Rd.
2612///
2613/// Cross-package parents: markers are always replaced with the fallback text because
2614/// we cannot inspect foreign packages' wrapper content at write time.
2615#[cfg(not(target_arch = "wasm32"))]
2616fn resolve_inherited_param_markers(content: String) -> String {
2617use std::collections::{HashMap, HashSet};
26182619const MARKER_PREFIX: &str = ".__MX_INHERITED_PARAM__(";
26202621// Check if the marker is present at all (fast path).
2622if !content.contains(MARKER_PREFIX) {
2623return content;
2624 }
26252626// Build an index of in-package class param documentation:
2627 // class_name → Set<param_name>
2628 // We scan the content for:
2629 // 1. Class-level @param tags: lines like `#' @param <name> ...` appearing
2630 // between `#' @name <ClassName>` and the R6Class(...) definition.
2631 // 2. Method-level @param tags for a specific method: lines like
2632 // ` #' @param <name> ...` inside the method's doc block.
2633 //
2634 // Strategy: build a map of (class_name, method_name_or_class) → Set<param_name>
2635 // where method_name_or_class = "" means class-level.
2636let mut class_method_params: HashMap<(String, String), HashSet<String>> = HashMap::new();
26372638// We need to scan the wrapper content to find documented params.
2639 // Simple heuristic: find `#' @name ClassName` then collect `#' @param` lines
2640 // until the next non-doc line (the R6Class definition).
2641let lines: Vec<&str> = content.lines().collect();
2642let mut i = 0;
2643while i < lines.len() {
2644let line = lines[i].trim();
2645// Class-level: `#' @name ClassName` followed by class-level @param lines
2646if let Some(rest) = line.strip_prefix("#' @name ") {
2647let class_name = rest.trim().to_string();
2648// Scan forward for @param lines in the class header (until non-doc)
2649let mut j = i + 1;
2650while j < lines.len() {
2651let next = lines[j].trim();
2652if next.starts_with("#'") {
2653if let Some(param_rest) = next.strip_prefix("#' @param ") {
2654if let Some(param_name) = param_rest.split_whitespace().next() {
2655 class_method_params
2656 .entry((class_name.clone(), String::new()))
2657 .or_default()
2658 .insert(param_name.to_string());
2659 }
2660 }
2661 j += 1;
2662 } else {
2663break;
2664 }
2665 }
2666 }
2667// Method-level: ` #' @description Method \`method_name\`.` or source comment
2668 // Simpler: look for ` #' @param` lines at method indent — attribute to the
2669 // nearest preceding `#' @rdname ClassName` (method blocks carry this).
2670 // Actually, method-level @param inside an R6 class block are indented with 4 spaces.
2671 // We track the current class context from the most recent R6Class definition.
2672i += 1;
2673 }
26742675// Walk the content to track current R6 class + method context, and also
2676 // build method-level param maps. We do a second pass specifically for method params.
2677let mut current_class: Option<String> = None;
2678let mut current_method: Option<String> = None;
2679i = 0;
2680while i < lines.len() {
2681let raw = lines[i];
2682let trimmed = raw.trim();
2683// Detect R6Class definition: `ClassName <- R6::R6Class("ClassName",`
2684if trimmed.contains("R6::R6Class(\"") {
2685if let Some(start) = trimmed.find("R6::R6Class(\"") {
2686let rest = &trimmed[start + "R6::R6Class(\"".len()..];
2687if let Some(end) = rest.find('"') {
2688 current_class = Some(rest[..end].to_string());
2689 current_method = None;
2690 }
2691 }
2692 }
2693// Detect method source comment: ` # ClassName::method_name (file:line)`
2694if let Some(ref cls) = current_class.clone() {
2695let prefix = ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("# {0}::", cls))
})format!("# {}::", cls);
2696if trimmed.starts_with(&prefix) {
2697let rest = &trimmed[prefix.len()..];
2698let method_name = rest.split_whitespace().next().unwrap_or("").to_string();
2699if !method_name.is_empty() {
2700 current_method = Some(method_name);
2701 }
2702 }
2703// Detect indented method @param: ` #' @param name ...`
2704if let Some(ref method) = current_method.clone() {
2705if let Some(rest) = raw.strip_prefix(" #' @param ") {
2706if let Some(param_name) = rest.split_whitespace().next() {
2707 class_method_params
2708 .entry((cls.clone(), method.clone()))
2709 .or_default()
2710 .insert(param_name.to_string());
2711 }
2712 }
2713 }
2714 }
2715 i += 1;
2716 }
27172718// Second pass: resolve markers.
2719let mut result = String::with_capacity(content.len());
2720for line in content.lines() {
2721let trimmed = line.trim();
2722// Marker format: ` #' .__MX_INHERITED_PARAM__(class="...", parent="...", method="...", param="...")`
2723let inner = trimmed.strip_prefix("#' ").unwrap_or(trimmed);
2724if inner.starts_with(MARKER_PREFIX) {
2725if let Some((_class, parent, method, param)) = parse_inherited_param_marker(inner) {
2726// Check if parent is in-package and has the param documented:
2727 // Either at method level (parent, method) or class level (parent, "").
2728let parent_method_key = (parent.clone(), method.clone());
2729let parent_class_key = (parent.clone(), String::new());
2730let documented_in_method = class_method_params
2731 .get(&parent_method_key)
2732 .map(|s| s.contains(¶m))
2733 .unwrap_or(false);
2734let documented_at_class = class_method_params
2735 .get(&parent_class_key)
2736 .map(|s| s.contains(¶m))
2737 .unwrap_or(false);
27382739if documented_in_method || documented_at_class {
2740// Parent is documented in-package → drop this line entirely.
2741 // roxygen2 8.0.0 inherits the parent method's @param.
2742continue;
2743 } else {
2744// Parent not found or param not documented → keep fallback.
2745 // Reconstruct indentation from the original line.
2746let indent: String = line.chars().take_while(|c| c.is_whitespace()).collect();
2747 result.push_str(&indent);
2748 result.push_str(&::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @param {0} (no documentation available)",
param))
})format!("#' @param {} (no documentation available)", param));
2749 result.push('\n');
2750continue;
2751 }
2752 }
2753 }
2754 result.push_str(line);
2755 result.push('\n');
2756 }
27572758result2759}
27602761// endregion
27622763// region: cross-class return wrapper resolvers
27642765/// Scan `content` for `<prefix><RustName>__(<expr>)` markers and replace each
2766/// with whatever `emit` pushes for the resolved class entry (or `None` when
2767/// `RustName` is not registered).
2768///
2769/// Shared scan loop behind [`resolve_scalar_return_wrappers`] and
2770/// [`resolve_list_return_wrappers`]. The two marker families use
2771/// non-overlapping prefixes (`.__MX_WRAP_RETURN_` vs `.__MX_WRAP_LIST_RETURN_`)
2772/// so each resolver only ever consumes its own markers — the scalar resolver
2773/// rewrites *unrecognized* names of its own prefix to the bare expression, so
2774/// a shared prefix would destroy list markers before the list pass ran (#1284).
2775///
2776/// Malformed markers (missing `__(` or the closing `)`) are emitted as-is and
2777/// scanning stops, matching the other placeholder resolvers in this file.
2778#[cfg(not(target_arch = "wasm32"))]
2779fn resolve_wrap_return_markers(
2780 content: &str,
2781 prefix: &str,
2782 class_index: &std::collections::HashMap<&str, &ClassNameEntry>,
2783 emit: impl Fn(&mut String, Option<&ClassNameEntry>, &str),
2784) -> String {
2785const ARG_OPEN: &str = "__(";
2786const ARG_CLOSE: char = ')';
27872788let mut result = String::with_capacity(content.len());
2789let mut remaining = content;
2790while let Some(start) = remaining.find(prefix) {
2791 result.push_str(&remaining[..start]);
2792let rest_from_prefix = &remaining[start..];
2793let marker_body = &rest_from_prefix[prefix.len()..];
2794let Some(name_end) = marker_body.find(ARG_OPEN) else {
2795// Malformed placeholder (no opening `__(`) — emit as-is and stop scanning.
2796result.push_str(prefix);
2797 remaining = marker_body;
2798break;
2799 };
2800let rust_name = &marker_body[..name_end];
2801let after_open = &marker_body[name_end + ARG_OPEN.len()..];
2802let Some(expr_end) = after_open.find(ARG_CLOSE) else {
2803// Malformed placeholder (no closing `)`) — emit as-is and stop scanning.
2804result.push_str(prefix);
2805 remaining = marker_body;
2806break;
2807 };
2808let expr = &after_open[..expr_end];
2809 remaining = &after_open[expr_end + ARG_CLOSE.len_utf8()..];
2810 emit(&mut result, class_index.get(rust_name).copied(), expr);
2811 }
2812result.push_str(remaining);
2813result2814}
28152816/// Resolve scalar `.__MX_WRAP_RETURN_<RustName>__(<expr>)` placeholders
2817/// emitted by class-method wrappers whose Rust return type may name a
2818/// different ExternalPtr-backed class.
2819///
2820/// Unlike `CLASS_REF`, an unresolved return wrapper is a quiet no-op (the bare
2821/// `<expr>`): the syntactic macro-time heuristic intentionally allows false
2822/// positives, and write time is where we know whether `<RustName>` is
2823/// registered in this package.
2824#[cfg(not(target_arch = "wasm32"))]
2825fn resolve_scalar_return_wrappers(
2826 content: &str,
2827 class_index: &std::collections::HashMap<&str, &ClassNameEntry>,
2828) -> String {
2829resolve_wrap_return_markers(
2830content,
2831".__MX_WRAP_RETURN_",
2832class_index,
2833 |result, entry, expr| match entry {
2834Some(entry) => match entry.class_system {
2835"r6" => {
2836result.push_str(&::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}$new(.ptr = {1})",
entry.r_class_name, expr))
})format!("{}$new(.ptr = {})", entry.r_class_name, expr));
2837 }
2838"s7" => {
2839result.push_str(&::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}(.ptr = {1})",
entry.r_class_name, expr))
})format!("{}(.ptr = {})", entry.r_class_name, expr));
2840 }
2841"s4" => {
2842result.push_str(&::alloc::__export::must_use({
::alloc::fmt::format(format_args!("methods::new(\"{0}\", ptr = {1})",
entry.r_class_name, expr))
})format!(
2843"methods::new(\"{}\", ptr = {})",
2844 entry.r_class_name, expr
2845 ));
2846 }
2847_ => {
2848result.push_str(&::alloc::__export::must_use({
::alloc::fmt::format(format_args!("structure({0}, class = \"{1}\")",
expr, entry.r_class_name))
})format!(
2849"structure({}, class = \"{}\")",
2850 expr, entry.r_class_name
2851 ));
2852 }
2853 },
2854None => result.push_str(expr),
2855 },
2856 )
2857}
28582859/// Resolve list-shaped `.__MX_WRAP_LIST_RETURN_<RustName>__(<expr>)`
2860/// placeholders emitted for `Vec<Class>`-returning class methods (#1284).
2861///
2862/// `<expr>` is a bare R list of external pointers (the `IntoRVecElement`
2863/// conversion emitted by `#[derive(ExternalPtr)]`); each element gets the same
2864/// per-element wrap the scalar resolver applies, via `lapply`. Unregistered
2865/// element types fall back to the bare list, mirroring the scalar fallback.
2866#[cfg(not(target_arch = "wasm32"))]
2867fn resolve_list_return_wrappers(
2868 content: &str,
2869 class_index: &std::collections::HashMap<&str, &ClassNameEntry>,
2870) -> String {
2871resolve_wrap_return_markers(
2872content,
2873".__MX_WRAP_LIST_RETURN_",
2874class_index,
2875 |result, entry, expr| match entry {
2876Some(entry) => match entry.class_system {
2877"r6" => {
2878result.push_str(&::alloc::__export::must_use({
::alloc::fmt::format(format_args!("lapply({0}, function(.el) {1}$new(.ptr = .el))",
expr, entry.r_class_name))
})format!(
2879"lapply({}, function(.el) {}$new(.ptr = .el))",
2880 expr, entry.r_class_name
2881 ));
2882 }
2883"s7" => {
2884result.push_str(&::alloc::__export::must_use({
::alloc::fmt::format(format_args!("lapply({0}, function(.el) {1}(.ptr = .el))",
expr, entry.r_class_name))
})format!(
2885"lapply({}, function(.el) {}(.ptr = .el))",
2886 expr, entry.r_class_name
2887 ));
2888 }
2889"s4" => {
2890result.push_str(&::alloc::__export::must_use({
::alloc::fmt::format(format_args!("lapply({0}, function(.el) methods::new(\"{1}\", ptr = .el))",
expr, entry.r_class_name))
})format!(
2891"lapply({}, function(.el) methods::new(\"{}\", ptr = .el))",
2892 expr, entry.r_class_name
2893 ));
2894 }
2895_ => {
2896result.push_str(&::alloc::__export::must_use({
::alloc::fmt::format(format_args!("lapply({0}, function(.el) structure(.el, class = \"{1}\"))",
expr, entry.r_class_name))
})format!(
2897"lapply({}, function(.el) structure(.el, class = \"{}\"))",
2898 expr, entry.r_class_name
2899 ));
2900 }
2901 },
2902None => result.push_str(expr),
2903 },
2904 )
2905}
29062907// endregion
29082909/// R source of the `.miniextendr_caller_call` helper, with its comment
2910/// block, as written into the wrappers preamble by
2911/// [`write_r_wrappers_to_file`]: the call a `call = caller` wrapper reports
2912/// (#1450, #1548, #1552), including the frame or call a hand-written helper
2913/// passed as the wrapper's `.call` formal (#1613), and the frame or call a
2914/// `call_arg` wrapper was passed (`own = TRUE`, #1834).
2915#[cfg(not(target_arch = "wasm32"))]
2916const CALLER_CALL_HELPER: &str = r#"# Internal helper: the call a wrapper with a `.call` formal attributes its
2917# conditions to, as written. A `#[miniextendr(noexport, call = caller)]`
2918# wrapper calls it as its first statement, `.mx_call <- .miniextendr_caller_call(.call)`,
2919# with its trailing `.call = NULL` formal (an S3 method has none and calls it
2920# without an argument). A `#[miniextendr(call_arg)]` wrapper binds
2921# `.mx_call <- if (is.null(.call)) sys.call() else .miniextendr_caller_call(.call, own = TRUE)`,
2922# so it calls the helper only for a `.call` that isn't NULL. The calling chain
2923# is [caller's caller] <- [caller] <- [wrapper] <- [this helper]:
2924# `sys.parent(1L)` is the wrapper's frame, `sys.parent(2L)` its caller's.
2925# `call` is what the wrapper's caller passed as `.call`:
2926# - an environment: a hand-written helper between the public function and the
2927# wrapper passes its caller's frame (`call = parent.frame()` in the helper's
2928# formals), or a function composing the wrapper its own (`environment()`).
2929# The result is the call of the closure owning that frame.
2930# `match(TRUE, ...)` takes the oldest frame with that environment, the
2931# closure's own, ahead of a later `eval()` over it. An environment that is no
2932# closure's live frame (`globalenv()`) counts as NULL.
2933# - a call object: used as is.
2934# - NULL, the default: under `own = TRUE`, the wrapper's own call, without
2935# `.call`. Otherwise, when the wrapper's caller is a closure, that caller's
2936# call; else (top level, or an `eval()` frame such as a testthat block,
2937# `source()` or `local()`, whose frame function is not a closure and whose
2938# call names `eval`) the wrapper's own call, without `.call`.
2939# - anything else: an argument error on `.call` against the wrapper's own call,
2940# which also catches a positional argument too many.
2941.miniextendr_caller_call <- function(call = NULL, own = FALSE) {
2942 wrapper <- sys.parent(1L)
2943 if (is.environment(call)) {
2944 frames <- sys.frames()
2945 i <- match(TRUE, vapply(frames, identical, NA, call))
2946 if (!is.na(i) && typeof(sys.function(i)) == "closure") return(sys.call(i))
2947 } else if (is.call(call)) {
2948 return(call)
2949 } else if (!is.null(call)) {
2950 .miniextendr_arg_error(".call", "must be NULL, an environment or a call", sys.call(wrapper))
2951 }
2952 if (!own) {
2953 parent <- sys.parent(2L)
2954 if (parent > 0L && typeof(sys.function(parent)) == "closure") return(sys.call(parent))
2955 }
2956 call <- sys.call(wrapper)
2957 call$.call <- NULL
2958 call
2959}
2960"#;
29612962/// R source of `.miniextendr_frame_call` as a bare `function(env)`
2963/// expression: the call a generated S3 method's conditions report, resolved
2964/// from the method's frame (#1851).
2965///
2966/// A generated S3 method passes `environment()` as its `.call` and as the
2967/// raise fallback (`CallAttribution::Generic` in the macros), so the success
2968/// path pays no more than for `sys.call()`, and the frame becomes a call only
2969/// when a condition is raised. [`write_r_wrappers_to_file`] binds the
2970/// expression under that name in the preamble ([`FRAME_CALL_HELPER`]) for the
2971/// package's raise helper, the R-side checks (`.miniextendr_arg_error`) and a
2972/// method with a `Call` parameter, which resolves it up front;
2973/// [`RAISE_CONDITION_HELPER_FN`] inlines it, because that copy of the raise
2974/// helper signals deferred conditions from the base namespace, where the
2975/// preamble's binding is not visible.
2976///
2977/// The frame is looked up among the live frames (`sys.frames()`), newest
2978/// first, so the helper works from the method itself, from the raise helper
2979/// the method calls, and from the deferred-condition path, which R runs from
2980/// C while the method's `.Call()` is still on the stack (a `.Call` context
2981/// is not a function frame, so the frame numbers line up). `.Generic` is
2982/// bound in the frame of a method R dispatched (`UseMethod()`, an internal
2983/// generic such as `[[`, `NextMethod()`, a group generic such as `Ops`) and
2984/// not in a method called by name, so a direct call keeps the method's name.
2985/// For a replacement generic R's call is
2986/// `` `$<-.cls`(`*tmp*`, name, value = <value>) ``: the result is the
2987/// assignment as written, `x$name <- value`, which never deparses the value.
2988/// No comment inside: the text is inlined into another function. A macro,
2989/// not a `const`, because `concat!` takes literals only.
2990macro_rules!frame_call_helper_fn {
2991 () => {
2992r#"function(env) {
2993 frames <- sys.frames()
2994 i <- length(frames)
2995 while (i > 0L && !identical(frames[[i]], env)) i <- i - 1L
2996 if (i == 0L) return(NULL)
2997 call <- sys.call(i)
2998 generic <- env[[".Generic"]]
2999 if (!is.character(generic) || length(generic) != 1L) return(call)
3000 if (length(call) > 1L && identical(call[[2L]], quote(`*tmp*`))) {
3001 call[[2L]] <- as.name(names(formals(sys.function(i)))[[1L]])
3002 }
3003 if (!endsWith(generic, "<-")) {
3004 call[[1L]] <- as.name(generic)
3005 return(call)
3006 }
3007 call$value <- NULL
3008 call[[1L]] <- as.name(substr(generic, 1L, nchar(generic) - 2L))
3009 call("<-", call, quote(value))
3010}"#
3011};
3012}
30133014/// The preamble's `.miniextendr_frame_call` binding of
3015/// [`frame_call_helper_fn!`], with its comment block, as written by
3016/// [`write_r_wrappers_to_file`].
3017#[cfg(not(target_arch = "wasm32"))]
3018const FRAME_CALL_HELPER: &str = "# Internal helper: the call a generated S3 method\'s conditions report, from\n# the method\'s frame (`environment()`, which the method passes as `.call`).\n# Under R\'s dispatch the method\'s own call names the method,\n# `summary.cls(x, a = 1)`, and `.Generic` is bound in its frame; the result\n# names the generic the user called instead: `summary(x, a = 1)`, `x[i]`,\n# `x + 1` for an `Ops` method. For a replacement generic (`$<-`, `[[<-`, `[<-`,\n# `names<-`, ...), which R calls as `` `$<-.cls`(`*tmp*`, name, value = <v>) ``,\n# the result is the assignment as written, `x$name <- value`, with the\n# method\'s first formal as the receiver and the symbol `value` in place of the\n# value, so a large value is never deparsed into the call. A method called\n# directly by name has no `.Generic` and keeps its own call. The frame is\n# looked up among the live frames, so the helper serves the method\'s own\n# checks, the raise helper and the conditions deferred while the method\'s\n# `.Call()` runs; a frame that is no longer live gives NULL.\n.miniextendr_frame_call <- function(env) {\n frames <- sys.frames()\n i <- length(frames)\n while (i > 0L && !identical(frames[[i]], env)) i <- i - 1L\n if (i == 0L) return(NULL)\n call <- sys.call(i)\n generic <- env[[\".Generic\"]]\n if (!is.character(generic) || length(generic) != 1L) return(call)\n if (length(call) > 1L && identical(call[[2L]], quote(`*tmp*`))) {\n call[[2L]] <- as.name(names(formals(sys.function(i)))[[1L]])\n }\n if (!endsWith(generic, \"<-\")) {\n call[[1L]] <- as.name(generic)\n return(call)\n }\n call$value <- NULL\n call[[1L]] <- as.name(substr(generic, 1L, nchar(generic) - 2L))\n call(\"<-\", call, quote(value))\n}\n"concat!(
3019r#"# Internal helper: the call a generated S3 method's conditions report, from
3020# the method's frame (`environment()`, which the method passes as `.call`).
3021# Under R's dispatch the method's own call names the method,
3022# `summary.cls(x, a = 1)`, and `.Generic` is bound in its frame; the result
3023# names the generic the user called instead: `summary(x, a = 1)`, `x[i]`,
3024# `x + 1` for an `Ops` method. For a replacement generic (`$<-`, `[[<-`, `[<-`,
3025# `names<-`, ...), which R calls as `` `$<-.cls`(`*tmp*`, name, value = <v>) ``,
3026# the result is the assignment as written, `x$name <- value`, with the
3027# method's first formal as the receiver and the symbol `value` in place of the
3028# value, so a large value is never deparsed into the call. A method called
3029# directly by name has no `.Generic` and keeps its own call. The frame is
3030# looked up among the live frames, so the helper serves the method's own
3031# checks, the raise helper and the conditions deferred while the method's
3032# `.Call()` runs; a frame that is no longer live gives NULL.
3033.miniextendr_frame_call <- "#,
3034frame_call_helper_fn!(),
3035"\n"
3036);
30373038/// R source of the `.miniextendr_field_at` helper, with its comment block,
3039/// as written into the wrappers preamble by [`write_r_wrappers_to_file`]: the
3040/// field a position picks in the generated `[[.Class` of an S3 class with
3041/// `s3(r_data_accessors)`, on the bare classed pointer (#1891). The method
3042/// checks for `NA` first (a list's `[[NA]]` is `NULL`, a character vector's
3043/// is `NA`). A position is truncated and a logical is taken, as by a list's
3044/// `[[`; every position that picks no field (past the last one, `0`, a
3045/// negative one) raises `subscriptOutOfBoundsError` with the type's
3046/// field-error classes in front (`class`, the derive's
3047/// `.rdata_error_class_<Type>`, #1899), on the generic's call. Also
3048/// `.miniextendr_field_index_error`, the same method's refusal of an index
3049/// that is neither one name nor one position (#1920), and under
3050/// `lookup = "strict"` of a non-whole number, a logical or `NA` (#1921).
3051#[cfg(not(target_arch = "wasm32"))]
3052const FIELD_AT_HELPER: &str = r#"# Internal helper: the field a position picks on a bare classed pointer, as
3053# `[[` picks a list element: `2.9` is the second field, `TRUE` the first. A
3054# position that picks no field (past the last one, `0`, a negative one)
3055# raises `subscriptOutOfBoundsError`, with the type's own classes (`class`)
3056# in front, reporting the generic's call, `x[[9]]`, resolved from the
3057# method's frame (`env`).
3058.miniextendr_field_at <- function(fields, i, env, class = NULL) {
3059 k <- trunc(i)
3060 if (k < 1 || k > length(fields)) stop(errorCondition("subscript out of bounds", class = c(class, "subscriptOutOfBoundsError"), call = .miniextendr_frame_call(env)))
3061 fields[[k]]
3062}
30633064# Internal helper: refuses a `[[` index on a bare classed pointer that is
3065# neither one field name nor one position (`x[[1:2]]`, `x[[list(1)]]`, and
3066# under strict lookup `x[[TRUE]]`, `x[[NA]]` or `x[[1.5]]` too), where R's
3067# `[[.default` would report itself. The type's own classes (`class`) go in
3068# front of R's `notSubsettableError`; the call is the generic's, `x[[1:2]]`,
3069# resolved from the method's frame (`env`). `cls` is the R class.
3070.miniextendr_field_index_error <- function(cls, env, class = NULL, strict = FALSE) {
3071 stop(errorCondition(sprintf("`[[` on a `%s` object takes one field name or one %sposition", cls, if (strict) "whole-number " else ""), class = c(class, "notSubsettableError"), call = .miniextendr_frame_call(env)))
3072}
3073"#;
30743075/// R source blocks for the argument-check helpers of the wrappers preamble,
3076/// `.miniextendr_arg_error`, `.miniextendr_match_arg_several` and
3077/// `.miniextendr_match_arg`, with their comment blocks, as written by
3078/// [`write_r_wrappers_to_file`] (they read `.miniextendr_conversion_error_class`,
3079/// bound after them, and `.miniextendr_frame_call`, bound before).
3080/// Each block is emitted only when needed; this ordered array lets the
3081/// equivalence tests evaluate the same source as the wrapper writer.
3082///
3083/// `.miniextendr_match_arg`'s messages are the ones
3084/// [`crate::match_arg::match_arg_param`] and the generated conversion of a
3085/// `match_arg` type word in Rust (#1767): all take their words from
3086/// `match_arg::match_arg_wording!`, and the API tests
3087/// (`miniextendr-api/tests/match_arg_param.rs`) evaluate this source and
3088/// compare them on every input class (#1741).
3089///
3090/// History, kept here because the R comments ship in every package and cite
3091/// no PR numbers (#1832): `.miniextendr_arg_error` is #1591, the `call`
3092/// argument a `call = caller` wrapper passes is #1548, the frame an S3 method
3093/// passes there and the `NULL` of `call = none` are #1851, the strict
3094/// `several_ok` match is #1472, reading a factor as its labels is #1552, the
3095/// `aliases` argument ([`match_arg_aliases_arg`]) is #1843, and the `listed`
3096/// argument, the clause naming the listed aliases, is #2148.
3097#[cfg(not(target_arch = "wasm32"))]
3098#[doc(hidden)]
3099pub const ARG_CHECK_HELPERS: [&str; 3] =
3100 [ARG_ERROR_HELPER, MATCH_ARG_SEVERAL_HELPER, MATCH_ARG_HELPER];
31013102#[cfg(not(target_arch = "wasm32"))]
3103const ARG_ERROR_HELPER: &str = r#"# Internal helper: raise an argument error from an R-side check: a
3104# type / length precondition, `no_na`, `inherits`, or a `match_arg` /
3105# `choices` value. The condition is the one a failed Rust conversion raises:
3106# the crate's `conversion_error_class` (`.miniextendr_conversion_error_class`,
3107# at the end of this preamble), then `rust_error`, with
3108# `kind = "conversion"` and `e$param`, so one handler catches an argument
3109# error whichever side finds it. The message is `'<param>' <what>`, or the
3110# author's own `message` (`inherits(..., message = )` / `no_na(message = )`),
3111# used as given. `call` defaults to the wrapper's own call, as `stopifnot()`
3112# reported it; a `call = caller` wrapper passes `.mx_call`, by name
3113# next to a named `message`; an S3 method passes its frame, resolved here to
3114# the generic's call (`.miniextendr_frame_call`); a `call = none` wrapper
3115# passes NULL. Only a failing check calls this: the passing path is one
3116# `isTRUE()` test per check.
3117.miniextendr_arg_error <- function(param, what, call = sys.call(-1L), message = sprintf("'%s' %s", param, what)) {
3118 if (is.environment(call)) call <- .miniextendr_frame_call(call)
3119 stop(structure(
3120 list(message = message, call = call, kind = "conversion", param = param),
3121 class = c(.miniextendr_conversion_error_class, "rust_error", "simpleError", "error", "condition")
3122 ))
3123}
3124"#;
31253126/// The check of `#[miniextendr(dots = empty | empty(allow = ..) | forward(allow
3127/// = ..))]` (#2157): a wrapper with such an attribute calls it as
3128/// `if (...length()) .miniextendr_check_dots(...names(), ...length(),
3129/// <formals>, allow = .., forward = TRUE, class = .., call = ..)`, the
3130/// statement `miniextendr-macros`' `dots_policy::DotsPolicy::r_check` writes
3131/// (`forward = TRUE` for `forward` only). The `[[`, `as.list()` and `[[<-`
3132/// field methods of `s3(r_data_accessors)` and an env class's `[[` make the
3133/// `empty` check on a bare pointer (#2174). It builds its own condition, an
3134/// argument error with two more fields, and depends only on the helpers every
3135/// preamble has.
3136#[cfg(not(target_arch = "wasm32"))]
3137const DOTS_CHECK_HELPER: &str = r#"# Internal helper: refuse the arguments in a wrapper's `...` that its
3138# `dots = empty | empty(allow = ...) | forward(allow = ...)` attribute does not
3139# take, or that a generated field method or env class `[[` (`empty`) gets.
3140# The wrapper calls it only when `...length()` is nonzero, with
3141# `...names()` and `...length()`, so no argument is forced. Every name outside
3142# `allow` is refused. An unnamed argument stands for an allowed name given by
3143# position: under `forward` (`forward = TRUE`) it passes, since the function
3144# the dots go on to matches it by position itself; under `empty` nothing reads
3145# it, so only the count matters, and the unnamed arguments past the number of
3146# allowed names not given by name are refused. All of them go in one
3147# condition: `argument` holds each one (`..<i>` for an unnamed one, its
3148# position in `...`) and `suggestion` the formal or allowed name each one most
3149# likely meant, or NA. That is the one name it is a prefix of, else the first
3150# one closest by edit distance (a swap of two neighbours counts as one edit),
3151# at most a third of its length in edits but at least one, and fewer than its
3152# length. The condition is an argument error (`kind = "conversion"`,
3153# `param = "..."`) classed `miniextendr_error_unused_argument` after the
3154# attribute's own `class`. `call` is the wrapper's call by default; a
3155# `call = caller` wrapper passes `.mx_call`, an S3 method its frame (resolved
3156# to the generic's call), a `call = none` wrapper NULL.
3157.miniextendr_check_dots <- function(names, n, formals, allow = character(), forward = FALSE, class = character(), call = sys.call(-1L)) {
3158 if (is.null(names)) names <- character(n)
3159 names[is.na(names)] <- ""
3160 unnamed <- !nzchar(names)
3161 bad <- !unnamed & !(names %in% allow)
3162 if (!forward) bad <- bad | (unnamed & cumsum(unnamed) > length(setdiff(allow, names)))
3163 bad <- which(bad)
3164 if (!length(bad)) return(invisible())
3165 if (is.environment(call)) call <- .miniextendr_frame_call(call)
3166 candidates <- c(formals, allow)
3167 distance <- function(a, b) {
3168 a <- strsplit(a, "", fixed = TRUE)[[1L]]
3169 b <- strsplit(b, "", fixed = TRUE)[[1L]]
3170 d <- matrix(0L, length(a) + 1L, length(b) + 1L)
3171 d[, 1L] <- seq.int(0L, length(a))
3172 d[1L, ] <- seq.int(0L, length(b))
3173 for (i in seq_along(a)) for (j in seq_along(b)) {
3174 d[i + 1L, j + 1L] <- min(d[i, j + 1L] + 1L, d[i + 1L, j] + 1L, d[i, j] + (a[[i]] != b[[j]]))
3175 if (i > 1L && j > 1L && a[[i]] == b[[j - 1L]] && a[[i - 1L]] == b[[j]]) {
3176 d[i + 1L, j + 1L] <- min(d[i + 1L, j + 1L], d[i - 1L, j - 1L] + 1L)
3177 }
3178 }
3179 d[[length(a) + 1L, length(b) + 1L]]
3180 }
3181 suggest <- function(name) {
3182 if (!nzchar(name) || !length(candidates)) return(NA_character_)
3183 prefixed <- candidates[startsWith(candidates, name)]
3184 if (length(prefixed) == 1L) return(prefixed)
3185 edits <- vapply(candidates, distance, 0L, b = name, USE.NAMES = FALSE)
3186 best <- which(edits == min(edits) & edits <= max(1L, nchar(name) %/% 3L) & edits < nchar(name))
3187 if (length(best)) candidates[[best[[1L]]]] else NA_character_
3188 }
3189 unnamed <- !nzchar(names[bad])
3190 argument <- ifelse(unnamed, paste0("..", bad), names[bad])
3191 suggestion <- vapply(names[bad], suggest, "", USE.NAMES = FALSE)
3192 what <- sprintf("'%s'%s", argument, ifelse(unnamed, " (unnamed)", ""))
3193 hint <- !is.na(suggestion)
3194 what[hint] <- sprintf("%s (did you mean '%s'?)", what[hint], suggestion[hint])
3195 message <- sprintf("unused argument%s %s", if (length(bad) > 1L) "s" else "", paste(what, collapse = ", "))
3196 stop(structure(
3197 list(message = message, call = call, kind = "conversion", param = "...", argument = argument, suggestion = suggestion),
3198 class = c(class, "miniextendr_error_unused_argument", .miniextendr_conversion_error_class, "rust_error", "simpleError", "error", "condition")
3199 ))
3200}
3201"#;
32023203#[cfg(not(target_arch = "wasm32"))]
3204const MATCH_ARG_SEVERAL_HELPER: &str = "# Internal helper: strict `match.arg(several.ok = TRUE)` for `several_ok` params.\n# Base R keeps only the elements that match as long as one of them does, so a\n# misspelled entry silently shortens the selection and the per-element check on\n# the Rust side never sees it. Here every element has to match a choice\n# (exactly or as a unique prefix), the first that does not is reported with its\n# position, and `NULL` selects every choice, like an omitted argument does. A\n# factor is read as its labels. `aliases`, a named character vector\n# (`c(grey = \"gray\")`), maps each element typed exactly as an alias to its\n# choice first; a prefix of an alias is not one. `listed` is the text the\n# message names the listed aliases with after the choices\n# (` (or \"grey\" for \"gray\")`, empty when none is listed). The error is an\n# argument error (`.miniextendr_arg_error`) attributed to `call`: by default\n# the wrapper\'s own call, not this helper; a `call = caller` wrapper passes\n# its caller\'s matched call.\n.miniextendr_match_arg_several <- function(arg, choices, arg_name, call = sys.call(-1L), aliases = NULL, listed = \"\") {\n if (is.null(arg)) return(choices)\n if (is.factor(arg)) arg <- as.character(arg)\n if (!is.character(arg)) .miniextendr_arg_error(arg_name, \"must be NULL or a character vector\", call)\n if (length(arg) == 0L) .miniextendr_arg_error(arg_name, \"must be of length >= 1\", call)\n if (!is.null(aliases)) {\n a <- match(arg, names(aliases))\n arg[!is.na(a)] <- aliases[a[!is.na(a)]]\n }\n i <- pmatch(arg, choices, nomatch = 0L, duplicates.ok = TRUE)\n bad <- which(is.na(i) | i == 0L)\n if (length(bad)) {\n .miniextendr_arg_error(arg_name, sprintf(\"element %d (\\\"%s\\\") should be one of %s%s\", bad[[1L]], arg[[bad[[1L]]]], paste(dQuote(choices, FALSE), collapse = \", \"), listed), call)\n }\n choices[i]\n}\n"concat!(
3205r#"# Internal helper: strict `match.arg(several.ok = TRUE)` for `several_ok` params.
3206# Base R keeps only the elements that match as long as one of them does, so a
3207# misspelled entry silently shortens the selection and the per-element check on
3208# the Rust side never sees it. Here every element has to match a choice
3209# (exactly or as a unique prefix), the first that does not is reported with its
3210# position, and `NULL` selects every choice, like an omitted argument does. A
3211# factor is read as its labels. `aliases`, a named character vector
3212# (`c(grey = "gray")`), maps each element typed exactly as an alias to its
3213# choice first; a prefix of an alias is not one. `listed` is the text the
3214# message names the listed aliases with after the choices
3215# (` (or "grey" for "gray")`, empty when none is listed). The error is an
3216# argument error (`.miniextendr_arg_error`) attributed to `call`: by default
3217# the wrapper's own call, not this helper; a `call = caller` wrapper passes
3218# its caller's matched call.
3219.miniextendr_match_arg_several <- function(arg, choices, arg_name, call = sys.call(-1L), aliases = NULL, listed = "") {
3220 if (is.null(arg)) return(choices)
3221 if (is.factor(arg)) arg <- as.character(arg)
3222 if (!is.character(arg)) .miniextendr_arg_error(arg_name, ""#,
3223crate::match_arg::match_arg_wording!(not_character),
3224r#"", call)
3225 if (length(arg) == 0L) .miniextendr_arg_error(arg_name, "must be of length >= 1", call)
3226 if (!is.null(aliases)) {
3227 a <- match(arg, names(aliases))
3228 arg[!is.na(a)] <- aliases[a[!is.na(a)]]
3229 }
3230 i <- pmatch(arg, choices, nomatch = 0L, duplicates.ok = TRUE)
3231 bad <- which(is.na(i) | i == 0L)
3232 if (length(bad)) {
3233 .miniextendr_arg_error(arg_name, sprintf("element %d (\"%s\") "#,
3234crate::match_arg::match_arg_wording!(not_a_choice),
3235r#" %s%s", bad[[1L]], arg[[bad[[1L]]]], paste(dQuote(choices, FALSE), collapse = ", "), listed), call)
3236 }
3237 choices[i]
3238}
3239"#
3240);
32413242#[cfg(not(target_arch = "wasm32"))]
3243const MATCH_ARG_HELPER: &str = "# Internal helper: scalar `match.arg()` for `match_arg` / `choices` params.\n# `base::match.arg()` says `\'arg\'` in its messages and reports its own frame;\n# this names the argument and raises an argument error\n# (`.miniextendr_arg_error`) attributed to `call`: by default the wrapper\'s own\n# call, for a `call = caller` wrapper its caller\'s matched call.\n# Semantics follow `match.arg(arg, choices)`: `NULL` and the full choice vector\n# (the formal default) select the first choice; otherwise exactly one string\n# that matches a choice exactly or as a unique prefix. A factor is read as its\n# labels. `aliases`, a named character vector (`c(grey = \"gray\")`), maps a\n# string typed exactly as an alias to its choice; a prefix of an alias is not\n# one. `listed` is the text the message names the listed aliases with after\n# the choices (` (or \"grey\" for \"gray\")`, empty when none is listed). The\n# Rust side\'s `match_arg_param()` gives a body the same results and messages\n# for a choice argument it matches itself.\n.miniextendr_match_arg <- function(arg, choices, arg_name, call = sys.call(-1L), aliases = NULL, listed = \"\") {\n if (is.null(arg)) return(choices[[1L]])\n if (is.factor(arg)) arg <- as.character(arg)\n if (!is.character(arg)) .miniextendr_arg_error(arg_name, \"must be NULL or a character vector\", call)\n if (identical(arg, choices)) return(arg[[1L]])\n if (length(arg) != 1L) .miniextendr_arg_error(arg_name, \"must be of length 1\", call)\n if (!is.null(aliases) && !is.na(a <- match(arg, names(aliases)))) return(aliases[[a]])\n i <- pmatch(arg, choices, nomatch = 0L, duplicates.ok = TRUE)\n if (is.na(i) || i == 0L) {\n .miniextendr_arg_error(arg_name, sprintf(\"should be one of %s%s\", paste(dQuote(choices, FALSE), collapse = \", \"), listed), call)\n }\n choices[[i]]\n}\n"concat!(
3244r#"# Internal helper: scalar `match.arg()` for `match_arg` / `choices` params.
3245# `base::match.arg()` says `'arg'` in its messages and reports its own frame;
3246# this names the argument and raises an argument error
3247# (`.miniextendr_arg_error`) attributed to `call`: by default the wrapper's own
3248# call, for a `call = caller` wrapper its caller's matched call.
3249# Semantics follow `match.arg(arg, choices)`: `NULL` and the full choice vector
3250# (the formal default) select the first choice; otherwise exactly one string
3251# that matches a choice exactly or as a unique prefix. A factor is read as its
3252# labels. `aliases`, a named character vector (`c(grey = "gray")`), maps a
3253# string typed exactly as an alias to its choice; a prefix of an alias is not
3254# one. `listed` is the text the message names the listed aliases with after
3255# the choices (` (or "grey" for "gray")`, empty when none is listed). The
3256# Rust side's `match_arg_param()` gives a body the same results and messages
3257# for a choice argument it matches itself.
3258.miniextendr_match_arg <- function(arg, choices, arg_name, call = sys.call(-1L), aliases = NULL, listed = "") {
3259 if (is.null(arg)) return(choices[[1L]])
3260 if (is.factor(arg)) arg <- as.character(arg)
3261 if (!is.character(arg)) .miniextendr_arg_error(arg_name, ""#,
3262crate::match_arg::match_arg_wording!(not_character),
3263r#"", call)
3264 if (identical(arg, choices)) return(arg[[1L]])
3265 if (length(arg) != 1L) .miniextendr_arg_error(arg_name, ""#,
3266crate::match_arg::match_arg_wording!(not_scalar),
3267r#"", call)
3268 if (!is.null(aliases) && !is.na(a <- match(arg, names(aliases)))) return(aliases[[a]])
3269 i <- pmatch(arg, choices, nomatch = 0L, duplicates.ok = TRUE)
3270 if (is.na(i) || i == 0L) {
3271 .miniextendr_arg_error(arg_name, sprintf(""#,
3272crate::match_arg::match_arg_wording!(not_a_choice),
3273r#" %s%s", paste(dQuote(choices, FALSE), collapse = ", "), listed), call)
3274 }
3275 choices[[i]]
3276}
3277"#
3278);
32793280/// One condition-transport body for the package and standalone Rust helpers.
3281/// The resolver is a package binding or a self-contained function expression.
3282macro_rules!raise_condition_helper_fn {
3283 ($resolver:expr) => {
3284concat!(
3285r#"function(.val, .call_default) {
3286 # The condition object for one tagged value `.v`.
3287 .cond <- function(.v) {
3288 .msg <- .v$error
3289 # `.v$call` is the call the Rust side captured, NULL to use the wrapper's
3290 # call, or FALSE for a condition raised without one (`call = none`, R's
3291 # `call. = FALSE`), which is signalled with `call = NULL`.
3292 .call <- if (isFALSE(.v$call)) NULL else if (is.null(.v$call)) .call_default else .v$call
3293 # A generated S3 method passes its frame instead of a call, so that the
3294 # generic's call (`summary(x)` for `summary.cls(x)`, the assignment for a
3295 # replacement method) is built only now, when a condition is raised. The
3296 # resolver recovers the generic's call from that method frame.
3297 if (is.environment(.call)) .call <- "#,
3298$resolver,
3299r#"(.call)
3300 .class <- .v$class
3301 # `arg_error!` writes the class ".miniextendr_conversion_error_class"
3302 # where the crate's `conversion_error_class` belongs: only the generated R
3303 # knows those classes, bound under that name at the end of the wrappers
3304 # preamble, so they replace it here. The copy of this helper that signals
3305 # deferred conditions lives in the base namespace, where the name is
3306 # unbound and the marker is dropped.
3307 .at <- match(".miniextendr_conversion_error_class", .class)
3308 if (!is.na(.at)) {
3309 .class <- append(.class[-.at], get0(".miniextendr_conversion_error_class", ifnotfound = NULL), after = .at - 1L)
3310 }
3311 # `.v$data` is an optional named list of structured fields (from the
3312 # macros' `data = ...` form). When present, splice its named elements into
3313 # the condition object alongside message/call/kind so handlers can read
3314 # `e$<name>`. `utils::modifyList` keeps the base fields and appends the
3315 # data fields; a malformed (non-list / unnamed) payload is ignored.
3316 # `keep.null = TRUE`: a field the Rust side set to `RValue::Null` is a
3317 # present field whose value is NULL, and must stay in `names(e)`; the
3318 # default would silently drop it (found downstream via a
3319 # `RConditionData::data()` returning an explicit NULL entry).
3320 .data <- .v$data
3321 # A cause the Rust side attached (`RCondition::with_parent`) travels as
3322 # the tagged value `.mx_parent` and becomes the condition object
3323 # `e$parent`, as `rlang::abort(parent = )` chains conditions. It is not
3324 # signalled, and its classes stay out of this condition's.
3325 if (is.list(.data) && !is.null(.data[[".mx_parent", exact = TRUE]])) {
3326 .parent <- .cond(.data[[".mx_parent", exact = TRUE]])
3327 .data[[".mx_parent"]] <- NULL
3328 .data[["parent"]] <- .parent
3329 }
3330 .cond_fields <- function(base) {
3331 if (is.null(.data) || !is.list(.data) || is.null(names(.data))) {
3332 base
3333 } else {
3334 utils::modifyList(base, .data, keep.null = TRUE)
3335 }
3336 }
3337 switch(.v$kind,
3338 warning = structure(.cond_fields(list(message = .msg, call = .call, kind = "warning")),
3339 class = c(.class, "rust_warning", "simpleWarning", "warning", "condition")),
3340 message = structure(.cond_fields(list(message = paste0(.msg, "\n"), call = NULL, kind = "message")),
3341 class = c(.class, "rust_message", "simpleMessage", "message", "condition")),
3342 condition = structure(.cond_fields(list(message = .msg, call = .call, kind = "condition")),
3343 class = c(.class, "rust_condition", "simpleCondition", "condition")),
3344 panic = structure(list(message = .msg, call = .call, kind = "panic"),
3345 class = c("rust_error", "simpleError", "error", "condition")),
3346 # error / result_err / none_err / conversion / other: an error whose class
3347 # vector and data (if any) come from the Rust side, e.g. a
3348 # `Result<T, E: RConditionData>`.
3349 structure(.cond_fields(list(message = .msg, call = .call, kind = .v$kind)),
3350 class = c(.class, "rust_error", "simpleError", "error", "condition")))
3351 }
3352 .c <- .cond(.val)
3353 switch(.val$kind,
3354 warning = warning(.c),
3355 message = message(.c),
3356 condition = signalCondition(.c),
3357 stop(.c)
3358 )
3359 invisible(NULL)
3360}"#
3361)
3362 };
3363}
33643365/// R source of the `.miniextendr_raise_condition` helper as a bare
3366/// `function(.val, .call_default)` expression.
3367///
3368/// [`crate::deferred_condition`] evaluates this self-contained expression
3369/// once (in the base namespace) to signal queued conditions from Rust without
3370/// depending on any package namespace. [`write_r_wrappers_to_file`] renders
3371/// the same template with the preamble's `.miniextendr_frame_call` binding as
3372/// the resolver instead of a second inline copy. Keeping one source
3373/// guarantees identical class layering and `data` splicing.
3374///
3375/// The helper dispatches on `.val$kind` (see [`crate::error_value::kind`]) and
3376/// re-raises the tagged Rust condition value as the matching R condition:
3377/// `stop()` for the error kinds, `warning()` / `message()` /
3378/// `signalCondition()` for the non-fatal ones (returning `invisible(NULL)`).
3379/// The marker class it replaces is the one `arg_error!` writes (#1740). A
3380/// frame in the call slot or as the default (a generated S3 method passes
3381/// `environment()`, #1851) is resolved to the generic's call with an inline
3382/// copy of [`frame_call_helper_fn!`], since the base-namespace copy of this
3383/// helper cannot see the preamble's `.miniextendr_frame_call`.
3384pub(crate) const RAISE_CONDITION_HELPER_FN: &str =
3385"function(.val, .call_default) {\n # The condition object for one tagged value `.v`.\n .cond <- function(.v) {\n .msg <- .v$error\n # `.v$call` is the call the Rust side captured, NULL to use the wrapper\'s\n # call, or FALSE for a condition raised without one (`call = none`, R\'s\n # `call. = FALSE`), which is signalled with `call = NULL`.\n .call <- if (isFALSE(.v$call)) NULL else if (is.null(.v$call)) .call_default else .v$call\n # A generated S3 method passes its frame instead of a call, so that the\n # generic\'s call (`summary(x)` for `summary.cls(x)`, the assignment for a\n # replacement method) is built only now, when a condition is raised. The\n # resolver recovers the generic\'s call from that method frame.\n if (is.environment(.call)) .call <- (function(env) {\n frames <- sys.frames()\n i <- length(frames)\n while (i > 0L && !identical(frames[[i]], env)) i <- i - 1L\n if (i == 0L) return(NULL)\n call <- sys.call(i)\n generic <- env[[\".Generic\"]]\n if (!is.character(generic) || length(generic) != 1L) return(call)\n if (length(call) > 1L && identical(call[[2L]], quote(`*tmp*`))) {\n call[[2L]] <- as.name(names(formals(sys.function(i)))[[1L]])\n }\n if (!endsWith(generic, \"<-\")) {\n call[[1L]] <- as.name(generic)\n return(call)\n }\n call$value <- NULL\n call[[1L]] <- as.name(substr(generic, 1L, nchar(generic) - 2L))\n call(\"<-\", call, quote(value))\n})(.call)\n .class <- .v$class\n # `arg_error!` writes the class \".miniextendr_conversion_error_class\"\n # where the crate\'s `conversion_error_class` belongs: only the generated R\n # knows those classes, bound under that name at the end of the wrappers\n # preamble, so they replace it here. The copy of this helper that signals\n # deferred conditions lives in the base namespace, where the name is\n # unbound and the marker is dropped.\n .at <- match(\".miniextendr_conversion_error_class\", .class)\n if (!is.na(.at)) {\n .class <- append(.class[-.at], get0(\".miniextendr_conversion_error_class\", ifnotfound = NULL), after = .at - 1L)\n }\n # `.v$data` is an optional named list of structured fields (from the\n # macros\' `data = ...` form). When present, splice its named elements into\n # the condition object alongside message/call/kind so handlers can read\n # `e$<name>`. `utils::modifyList` keeps the base fields and appends the\n # data fields; a malformed (non-list / unnamed) payload is ignored.\n # `keep.null = TRUE`: a field the Rust side set to `RValue::Null` is a\n # present field whose value is NULL, and must stay in `names(e)`; the\n # default would silently drop it (found downstream via a\n # `RConditionData::data()` returning an explicit NULL entry).\n .data <- .v$data\n # A cause the Rust side attached (`RCondition::with_parent`) travels as\n # the tagged value `.mx_parent` and becomes the condition object\n # `e$parent`, as `rlang::abort(parent = )` chains conditions. It is not\n # signalled, and its classes stay out of this condition\'s.\n if (is.list(.data) && !is.null(.data[[\".mx_parent\", exact = TRUE]])) {\n .parent <- .cond(.data[[\".mx_parent\", exact = TRUE]])\n .data[[\".mx_parent\"]] <- NULL\n .data[[\"parent\"]] <- .parent\n }\n .cond_fields <- function(base) {\n if (is.null(.data) || !is.list(.data) || is.null(names(.data))) {\n base\n } else {\n utils::modifyList(base, .data, keep.null = TRUE)\n }\n }\n switch(.v$kind,\n warning = structure(.cond_fields(list(message = .msg, call = .call, kind = \"warning\")),\n class = c(.class, \"rust_warning\", \"simpleWarning\", \"warning\", \"condition\")),\n message = structure(.cond_fields(list(message = paste0(.msg, \"\\n\"), call = NULL, kind = \"message\")),\n class = c(.class, \"rust_message\", \"simpleMessage\", \"message\", \"condition\")),\n condition = structure(.cond_fields(list(message = .msg, call = .call, kind = \"condition\")),\n class = c(.class, \"rust_condition\", \"simpleCondition\", \"condition\")),\n panic = structure(list(message = .msg, call = .call, kind = \"panic\"),\n class = c(\"rust_error\", \"simpleError\", \"error\", \"condition\")),\n # error / result_err / none_err / conversion / other: an error whose class\n # vector and data (if any) come from the Rust side, e.g. a\n # `Result<T, E: RConditionData>`.\n structure(.cond_fields(list(message = .msg, call = .call, kind = .v$kind)),\n class = c(.class, \"rust_error\", \"simpleError\", \"error\", \"condition\")))\n }\n .c <- .cond(.val)\n switch(.val$kind,\n warning = warning(.c),\n message = message(.c),\n condition = signalCondition(.c),\n stop(.c)\n )\n invisible(NULL)\n}"raise_condition_helper_fn!(concat!("(", frame_call_helper_fn!(), ")"));
33863387/// The package preamble already binds the resolver; reuse it there.
3388#[cfg(not(target_arch = "wasm32"))]
3389const WRAPPER_RAISE_CONDITION_HELPER_FN: &str =
3390"function(.val, .call_default) {\n # The condition object for one tagged value `.v`.\n .cond <- function(.v) {\n .msg <- .v$error\n # `.v$call` is the call the Rust side captured, NULL to use the wrapper\'s\n # call, or FALSE for a condition raised without one (`call = none`, R\'s\n # `call. = FALSE`), which is signalled with `call = NULL`.\n .call <- if (isFALSE(.v$call)) NULL else if (is.null(.v$call)) .call_default else .v$call\n # A generated S3 method passes its frame instead of a call, so that the\n # generic\'s call (`summary(x)` for `summary.cls(x)`, the assignment for a\n # replacement method) is built only now, when a condition is raised. The\n # resolver recovers the generic\'s call from that method frame.\n if (is.environment(.call)) .call <- .miniextendr_frame_call(.call)\n .class <- .v$class\n # `arg_error!` writes the class \".miniextendr_conversion_error_class\"\n # where the crate\'s `conversion_error_class` belongs: only the generated R\n # knows those classes, bound under that name at the end of the wrappers\n # preamble, so they replace it here. The copy of this helper that signals\n # deferred conditions lives in the base namespace, where the name is\n # unbound and the marker is dropped.\n .at <- match(\".miniextendr_conversion_error_class\", .class)\n if (!is.na(.at)) {\n .class <- append(.class[-.at], get0(\".miniextendr_conversion_error_class\", ifnotfound = NULL), after = .at - 1L)\n }\n # `.v$data` is an optional named list of structured fields (from the\n # macros\' `data = ...` form). When present, splice its named elements into\n # the condition object alongside message/call/kind so handlers can read\n # `e$<name>`. `utils::modifyList` keeps the base fields and appends the\n # data fields; a malformed (non-list / unnamed) payload is ignored.\n # `keep.null = TRUE`: a field the Rust side set to `RValue::Null` is a\n # present field whose value is NULL, and must stay in `names(e)`; the\n # default would silently drop it (found downstream via a\n # `RConditionData::data()` returning an explicit NULL entry).\n .data <- .v$data\n # A cause the Rust side attached (`RCondition::with_parent`) travels as\n # the tagged value `.mx_parent` and becomes the condition object\n # `e$parent`, as `rlang::abort(parent = )` chains conditions. It is not\n # signalled, and its classes stay out of this condition\'s.\n if (is.list(.data) && !is.null(.data[[\".mx_parent\", exact = TRUE]])) {\n .parent <- .cond(.data[[\".mx_parent\", exact = TRUE]])\n .data[[\".mx_parent\"]] <- NULL\n .data[[\"parent\"]] <- .parent\n }\n .cond_fields <- function(base) {\n if (is.null(.data) || !is.list(.data) || is.null(names(.data))) {\n base\n } else {\n utils::modifyList(base, .data, keep.null = TRUE)\n }\n }\n switch(.v$kind,\n warning = structure(.cond_fields(list(message = .msg, call = .call, kind = \"warning\")),\n class = c(.class, \"rust_warning\", \"simpleWarning\", \"warning\", \"condition\")),\n message = structure(.cond_fields(list(message = paste0(.msg, \"\\n\"), call = NULL, kind = \"message\")),\n class = c(.class, \"rust_message\", \"simpleMessage\", \"message\", \"condition\")),\n condition = structure(.cond_fields(list(message = .msg, call = .call, kind = \"condition\")),\n class = c(.class, \"rust_condition\", \"simpleCondition\", \"condition\")),\n panic = structure(list(message = .msg, call = .call, kind = \"panic\"),\n class = c(\"rust_error\", \"simpleError\", \"error\", \"condition\")),\n # error / result_err / none_err / conversion / other: an error whose class\n # vector and data (if any) come from the Rust side, e.g. a\n # `Result<T, E: RConditionData>`.\n structure(.cond_fields(list(message = .msg, call = .call, kind = .v$kind)),\n class = c(.class, \"rust_error\", \"simpleError\", \"error\", \"condition\")))\n }\n .c <- .cond(.val)\n switch(.val$kind,\n warning = warning(.c),\n message = message(.c),\n condition = signalCondition(.c),\n stop(.c)\n )\n invisible(NULL)\n}"raise_condition_helper_fn!(".miniextendr_frame_call");
33913392/// Shared method lookup for environment classes. Each returned closure gets
3393/// its own receiver-binding frame. An inherent method keeps its original
3394/// enclosure behind that frame, so native symbols and author-supplied R code
3395/// never resolve through the lookup helper's locals (#1677). Field lookup and
3396/// its condition call stay in the S3 method. Flat trait lookup must find a
3397/// unique method; inherent methods and explicit trait namespaces take priority.
3398#[cfg(not(target_arch = "wasm32"))]
3399const ENV_METHOD_HELPER: &str = r#"# Internal helper: bind an environment class's methods to one receiver.
3400# The class's `$` / `[[` methods handle fields before calling this lookup.
3401.miniextendr_env_method <- function(self, name, methods) {
3402 obj <- methods[[name]]
3403 if (is.environment(obj)) {
3404 bound <- new.env(parent = emptyenv())
3405 for (method_name in names(obj)) {
3406 method <- obj[[method_name]]
3407 if (is.function(method)) {
3408 if (isTRUE(attr(method, ".__mx_instance__"))) {
3409 local({
3410 m <- method
3411 bound[[method_name]] <<- function(...) m(self, ...)
3412 })
3413 } else {
3414 bound[[method_name]] <- method
3415 }
3416 }
3417 }
3418 bound
3419 } else if (is.null(obj)) {
3420 matches <- list()
3421 for (ns_name in names(methods)) {
3422 ns <- methods[[ns_name]]
3423 if (is.environment(ns) && exists(name, envir = ns, inherits = FALSE)) {
3424 method <- ns[[name]]
3425 if (is.function(method)) matches[[ns_name]] <- method
3426 }
3427 }
3428 if (!length(matches)) return(NULL)
3429 if (length(matches) > 1L) {
3430 traits <- sort(names(matches))
3431 stop(errorCondition(
3432 sprintf("Method '%s' is provided by multiple traits (%s). Use obj$Trait to select a trait explicitly.", name, paste(traits, collapse = ", ")),
3433 class = "miniextendr_ambiguous_method", method = name, traits = traits,
3434 call = .miniextendr_frame_call(parent.frame())
3435 ))
3436 }
3437 m <- matches[[1L]]
3438 if (isTRUE(attr(m, ".__mx_instance__"))) {
3439 s <- self
3440 function(...) m(s, ...)
3441 } else {
3442 m
3443 }
3444 } else {
3445 environment(obj) <- list2env(list(.mx_self = self), parent = environment(obj))
3446 obj
3447 }
3448}
3449"#;
34503451/// The `.miniextendr_conversion_error_class` binding that closes the wrappers
3452/// preamble: the classes `.miniextendr_arg_error` puts on every R-side
3453/// argument error, from the [`MX_CONVERSION_ERROR_CLASS`] entry that
3454/// `miniextendr_init!` registers for the package crate (#1591).
3455///
3456/// The Rust conversion arms carry the same classes, emitted by the macro from
3457/// the same `[package.metadata.miniextendr]` key, so the two paths agree for
3458/// every item of that crate. A package has one `miniextendr_init!`, hence one
3459/// entry; with none (or a crate that sets no class) the binding is `NULL`.
3460#[cfg(not(target_arch = "wasm32"))]
3461fn conversion_error_class_binding() -> String {
3462format_conversion_error_class_binding(conversion_error_class())
3463}
34643465/// [`conversion_error_class_binding`] for an explicit class list.
3466#[cfg(not(target_arch = "wasm32"))]
3467fn format_conversion_error_class_binding(classes: &[&str]) -> String {
3468let value = if classes.is_empty() {
3469"NULL".to_string()
3470 } else {
3471let quoted: Vec<String> = classes3472 .iter()
3473 .map(|c| ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("\"{0}\"",
c.replace('\\', "\\\\").replace('"', "\\\"")))
})format!("\"{}\"", c.replace('\\', "\\\\").replace('"', "\\\"")))
3474 .collect();
3475::alloc::__export::must_use({
::alloc::fmt::format(format_args!("c({0})", quoted.join(", ")))
})format!("c({})", quoted.join(", "))3476 };
3477::alloc::__export::must_use({
::alloc::fmt::format(format_args!("# Internal: the crate\'s `conversion_error_class` ([package.metadata.miniextendr]),\n# the classes of every argument error ahead of `rust_error`; NULL when unset.\n.miniextendr_conversion_error_class <- {0}",
value))
})format!(
3478"# Internal: the crate's `conversion_error_class` ([package.metadata.miniextendr]),\n\
3479 # the classes of every argument error ahead of `rust_error`; NULL when unset.\n\
3480 .miniextendr_conversion_error_class <- {value}"
3481)3482}
34833484/// The comment block that opens the wrappers preamble, ahead of the
3485/// `.miniextendr_raise_condition` binding, as written by
3486/// [`write_r_wrappers_to_file`].
3487#[cfg(not(target_arch = "wasm32"))]
3488const WRAPPERS_PREAMBLE_HEADER: &str =
3489"# This file is generated by the miniextendr proc-macro during package build.
3490# Any manual changes will be overwritten.
3491#
3492# To regenerate: rebuild the package (R CMD INSTALL or devtools::install).
3493# nolint start
3494# nocov start
34953496# Internal helper: re-raise a tagged Rust error/condition value as an R condition.
3497# Generated wrappers call this whenever `.Call()` returns a `rust_condition_value`.
3498# `.call_default` is the wrapper's `sys.call()` (or its `.mx_call`, the
3499# caller's call as written, for `#[miniextendr(noexport, call = caller)]` entry
3500# points, which honours a `.call` a helper passed on; an S3 method's frame,
3501# `environment()`, resolved to the generic's call here; NULL for a
3502# `call = none` wrapper), used as the fallback when the Rust panic payload
3503# didn't carry a captured call (e.g. lambda contexts that pass `.call = NULL`
3504# to `.Call`). For error/panic kinds `stop()` longjmps; for
3505# warning/message/condition the helper signals and returns invisible(NULL),
3506# which the wrapper's surrounding `return(...)` propagates as its result.
3507";
35083509/// Emit an optional preamble helper only when a collected fragment names it.
3510///
3511/// A name in a roxygen comment also keeps its helper, which is harmless.
3512/// The one dependency between optional helpers is explicit:
3513/// `.miniextendr_caller_call` raises through `.miniextendr_arg_error`. The
3514/// helpers every preamble has (condition transport, the frame resolver, the
3515/// conversion-error class) are written unconditionally.
3516#[cfg(not(target_arch = "wasm32"))]
3517fn append_used_wrapper_helpers(content: &mut String, fragments: &[std::borrow::Cow<'_, str>]) {
3518let uses = |name: &str| fragments.iter().any(|fragment| fragment.contains(name));
3519let caller = uses(".miniextendr_caller_call");
3520// Generated calls have no space before '('; include it to distinguish
3521 // the scalar matcher from the several-choice matcher's name.
3522let scalar = uses(".miniextendr_match_arg(");
3523let several = uses(".miniextendr_match_arg_several(");
3524for (needed, helper) in [
3525 (caller, CALLER_CALL_HELPER),
3526 (
3527 uses(".miniextendr_field_at") || uses(".miniextendr_field_index_error"),
3528 FIELD_AT_HELPER,
3529 ),
3530 (uses(".miniextendr_env_method"), ENV_METHOD_HELPER),
3531 (uses(".miniextendr_check_dots("), DOTS_CHECK_HELPER),
3532 (
3533 caller || scalar || several || uses(".miniextendr_arg_error"),
3534 ARG_ERROR_HELPER,
3535 ),
3536 (several, MATCH_ARG_SEVERAL_HELPER),
3537 (scalar, MATCH_ARG_HELPER),
3538 ] {
3539if needed {
3540 content.push_str(helper);
3541 content.push('\n');
3542 }
3543 }
3544}
35453546/// Write all R wrapper entries to a file.
3547///
3548/// Called from [`miniextendr_write_wrappers`] (via `dyn.load`/`.Call` of the
3549/// installed shared object). All distributed_slice entries from `#[miniextendr]`
3550/// items are available because stub.c force-loads the whole user crate.
3551///
3552/// Host-only — wasm32 doesn't run wrapper-gen.
3553#[cfg(not(target_arch = "wasm32"))]
3554pub fn write_r_wrappers_to_file(path: &str) {
3555// Build the new content in memory; `wrappers_file_content` prepends the
3556 // header (marker line + version/fingerprint line) once the body is final.
3557let mut content = String::from(WRAPPERS_PREAMBLE_HEADER);
3558content.push_str(".miniextendr_raise_condition <- ");
3559content.push_str(WRAPPER_RAISE_CONDITION_HELPER_FN);
3560content.push_str("\n\n");
3561content.push_str(FRAME_CALL_HELPER);
3562content.push('\n');
3563let fragments = collect_r_wrappers();
3564append_used_wrapper_helpers(&mut content, &fragments);
3565content.push_str(&conversion_error_class_binding());
3566content.push_str("\n\n");
35673568for fragment in fragments {
3569 content.push_str(fragment.as_ref());
3570 content.push_str("\n\n");
3571 }
35723573content.push_str("# nocov end\n# nolint end\n");
35743575// Replace match_arg choices placeholders with actual enum choices.
3576 // If the entry has a `preferred_default`, that choice comes first —
3577 // `.miniextendr_match_arg()` returns `arg[1]` when arg is the formal
3578 // default, so the position-0 element becomes the effective default. The
3579 // check's aliases placeholder becomes its `aliases =` argument, or
3580 // nothing (#1843).
3581for entry in MX_MATCH_ARG_CHOICES.iter() {
3582 content = splice_match_arg_entry(&content, entry);
3583 }
35843585// Replace match_arg @param doc placeholders with human-readable choice descriptions
3586for entry in MX_MATCH_ARG_PARAM_DOCS.iter() {
3587let choices = (entry.choices_str)();
3588let prefix = if entry.several_ok {
3589"One or more of"
3590} else {
3591"One of"
3592};
3593let replacement = ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{1} {2}{0}.", entry.suffix, prefix,
choices))
})format!("{prefix} {choices}{}.", entry.suffix);
3594 content = content.replace(entry.placeholder, &replacement);
3595 }
35963597// Replace .__MX_S7_SIDECAR_PROP_DOCS_<TypeName>__ placeholders with @prop lines.
3598 //
3599 // Each S7 class wrapper with `r_data_accessors` embeds one placeholder per type.
3600 // We collect all entries for each type name and emit `#' @prop field doc` lines.
3601{
3602use std::collections::HashMap;
3603// Group entries by rust_type
3604let mut by_type: HashMap<&'static str, Vec<&SidecarPropEntry>> = HashMap::new();
3605for entry in MX_S7_SIDECAR_PROPS.iter() {
3606 by_type.entry(entry.rust_type).or_default().push(entry);
3607 }
36083609const SIDECAR_PREFIX: &str = ".__MX_S7_SIDECAR_PROP_DOCS_";
3610const SIDECAR_SUFFIX: &str = "__";
3611let mut result = String::with_capacity(content.len());
3612let mut remaining = content.as_str();
3613while let Some(start) = remaining.find(SIDECAR_PREFIX) {
3614 result.push_str(&remaining[..start]);
3615 remaining = &remaining[start + SIDECAR_PREFIX.len()..];
3616if let Some(end) = remaining.find(SIDECAR_SUFFIX) {
3617let rust_name = &remaining[..end];
3618 remaining = &remaining[end + SIDECAR_SUFFIX.len()..];
3619// Emit @prop lines for each sidecar field registered for this type
3620if let Some(entries) = by_type.get(rust_name) {
3621let prop_lines: String = entries
3622 .iter()
3623 .map(|e| ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#\' @prop {0} {1}", e.field_name,
e.prop_doc))
})format!("#' @prop {} {}", e.field_name, e.prop_doc))
3624 .collect::<Vec<_>>()
3625 .join("\n");
3626 result.push_str(&prop_lines);
3627 }
3628// If no entries, the placeholder is simply removed (empty replacement)
3629} else {
3630// Malformed placeholder — emit prefix and stop
3631result.push_str(SIDECAR_PREFIX);
3632break;
3633 }
3634 }
3635result.push_str(remaining);
3636content = result;
3637 }
36383639// Replace .__MX_CLASS_REF_<RustName>__ placeholders with actual R class names.
3640 //
3641 // Build a lookup from rust_type → ClassNameEntry, then do a linear scan over
3642 // the content for each placeholder. We avoid pulling in a regex dependency
3643 // by scanning for the sentinel prefix directly.
3644{
3645let class_index = build_class_name_index(MX_CLASS_NAMES.iter());
36463647// Two placeholder forms:
3648 // .__MX_CLASS_REF_<RustName>__ → loud fallback on miss
3649 // (used by R6 `inherit =`, S7 parent class, convert_from / convert_to)
3650 // .__MX_CLASS_REF_OR_ANY_<RustName>__ → silent `S7::class_any` on miss
3651 // (used by S7 property class constraints — #203: a getter returning
3652 // `SEXP`, `PathBuf`, or an R6/S3/S4 type shouldn't break load-time
3653 // with "object '…' not found"; the property just falls back to the
3654 // permissive class_any it had pre-#154.)
3655const OR_ANY_PREFIX: &str = ".__MX_CLASS_REF_OR_ANY_";
3656const PREFIX: &str = ".__MX_CLASS_REF_";
3657const SUFFIX: &str = "__";
36583659let mut result = String::with_capacity(content.len());
3660let mut remaining = content.as_str();
3661while let Some(start) = remaining.find(PREFIX) {
3662 result.push_str(&remaining[..start]);
3663// Does this occurrence have the OR_ANY form?
3664let rest_from_prefix = &remaining[start..];
3665let (quiet_fallback, header_len) = if rest_from_prefix.starts_with(OR_ANY_PREFIX) {
3666 (true, OR_ANY_PREFIX.len())
3667 } else {
3668 (false, PREFIX.len())
3669 };
3670 remaining = &rest_from_prefix[header_len..];
3671// Find the closing __
3672if let Some(end) = remaining.find(SUFFIX) {
3673let rust_name = &remaining[..end];
3674 remaining = &remaining[end + SUFFIX.len()..];
3675match class_index.get(rust_name) {
3676Some(entry) if quiet_fallback => {
3677// S7 property resolved to an S7 class → use the
3678 // registered name. Registered-but-non-S7 types
3679 // (R6 / S3 / S4 / Env / Vctrs) fall through to
3680 // class_any because S7 can't use them as class
3681 // constraints.
3682if entry.class_system == "s7" {
3683 result.push_str(entry.r_class_name);
3684 } else {
3685 result.push_str("S7::class_any");
3686 }
3687 }
3688Some(entry) => {
3689 result.push_str(entry.r_class_name);
3690 }
3691None if quiet_fallback => {
3692// Unresolved S7 property type → silent class_any,
3693 // matching pre-#154 behavior.
3694result.push_str("S7::class_any");
3695 }
3696None => {
3697// Unresolved non-property CLASS_REF: fall back to the
3698 // bare Rust name with a warning.
3699{
::std::io::_eprint(format_args!("miniextendr: unresolved class reference `{0}` in R wrapper — is the class defined in a reachable crate?\n",
rust_name));
};eprintln!(
3700"miniextendr: unresolved class reference `{rust_name}` \
3701 in R wrapper — is the class defined in a reachable crate?"
3702);
3703 result.push_str(rust_name);
3704 }
3705 }
3706 } else {
3707// Malformed placeholder (no closing __): emit as-is and stop scanning.
3708result.push_str(PREFIX);
3709break;
3710 }
3711 }
3712result.push_str(remaining);
3713content = result;
3714 }
37153716// Resolve the cross-class return wrapper placeholders: the scalar
3717 // `.__MX_WRAP_RETURN_<RustName>__(<expr>)` family and the list-shaped
3718 // `.__MX_WRAP_LIST_RETURN_<RustName>__(<expr>)` family (#1284). The
3719 // prefixes are non-overlapping, so pass order does not matter — each
3720 // resolver only consumes its own markers.
3721{
3722let class_index = build_class_name_index(MX_CLASS_NAMES.iter());
3723content = resolve_scalar_return_wrappers(&content, &class_index);
3724content = resolve_list_return_wrappers(&content, &class_index);
3725 }
37263727// Resolve .__MX_GENERIC_DOC__(...) markers into standalone Rd pages.
3728 //
3729 // Each package-owned S7/S4 generic-creation site emits one marker line:
3730 // .__MX_GENERIC_DOC__(kind="S7", generic="get_value", class="MyClass",
3731 // export=true, dispatch="x", no_dots=false)
3732 //
3733 // This pass:
3734 // 1. Scans all fragments for marker lines.
3735 // 2. Groups markers by generic name.
3736 // 3. Replaces the FIRST marker per generic with a synthesised standalone
3737 // roxygen block (bare @name <generic> → bare \alias{<generic>}, listing
3738 // every in-package implementing class).
3739 // 4. Deletes all remaining markers (keeping none in the final file).
3740 //
3741 // Method blocks keep their qualified @name Class-generic so the PR #83
3742 // duplicate-alias fix is not re-introduced.
3743content = resolve_generic_doc_markers(content);
37443745// Resolve .__MX_INHERITED_PARAM__(...) markers.
3746 //
3747 // Each R6 subclass method param that might be inherited from a parent class
3748 // emits one marker line per undocumented param. The write-time pass:
3749 // 1. Scans the assembled content for method and class-level @param docs.
3750 // 2. For each marker: if the parent class is in-package and has the param
3751 // documented (at method or class level) → delete the marker line
3752 // (roxygen2 8.0.0 inherits the parent's @param into the subclass method).
3753 // 3. Otherwise → replace the marker with the fallback placeholder text.
3754 //
3755 // This guarantees zero markers remain in the final wrappers.R and zero
3756 // roxygen2 warnings about undocumented parameters.
3757content = resolve_inherited_param_markers(content);
37583759// Detect collisions between separately-expanded wrapper definitions (#991).
3760 //
3761 // Repeated top-level function or class/namespace definitions silently
3762 // clobber each other at load time (last write wins), dispatching calls to
3763 // the wrong implementation or replacing the class object altogether.
3764 // The compile-time `check_s7_shortcut_collisions` only sees one impl block;
3765 // collisions that span macro expansions --
3766 // an S7 shortcut vs a `#[derive(ExternalPtr)]` sidecar accessor, or two S7
3767 // impl blocks on the same type -- are only visible here, where every name
3768 // set is assembled. We resolve the sidecar-accessor prefix through
3769 // MX_CLASS_NAMES so a `class = "Override"` is honoured, then scan the final
3770 // content. This runs after all placeholder resolution so names are final.
3771{
3772let class_index = build_class_name_index(MX_CLASS_NAMES.iter());
3773let accessor_producers = sidecar_accessor_names(&MX_S7_SIDECAR_PROPS, &class_index);
3774if let Err(msg) = detect_duplicate_wrapper_defs(&content, &accessor_producers) {
3775{ ::core::panicking::panic_fmt(format_args!("{0}", msg)); };panic!("{msg}");
3776 }
3777 }
37783779// Refuse the `reexport!` names the package's wrappers bind or export
3780 // themselves, and a name re-exported from two packages (#1910). Like the
3781 // scan above, it reads the final names.
3782if let Err(msg) = reexport_conflicts(&content, &MX_REEXPORTS) {
3783{ ::core::panicking::panic_fmt(format_args!("{0}", msg)); };panic!("{msg}");
3784 }
37853786let content = wrappers_file_content(&content);
37873788// Only write if content changed (avoids unnecessary NAMESPACE/man regeneration).
3789 //
3790 // "Semantic equality" means equal after stripping source-position suffixes from
3791 // the source-attribution comments emitted by the `#[miniextendr]` proc-macro:
3792 //
3793 // # Generated from Rust fn `foo` (lib.rs:42:8)
3794 // ^^^^^ positional suffix — ignored here
3795 //
3796 // These positions shift whenever any unrelated Rust source above a wrapper is
3797 // edited. The NOTE and the file rewrite should only fire when the actual
3798 // wrapper code, exports, or docstrings change — not when line numbers move.
3799 // The written file still carries the real line:col for jump-to-source.
3800let existing = std::fs::read_to_string(path).unwrap_or_default();
3801if wrappers_semantically_equal(&existing, &content) {
3802return;
3803 }
38043805// Write via a sibling temp file then rename for atomicity.
3806 //
3807 // A plain `std::fs::write` truncates in place; a concurrent reader (e.g. a
3808 // second `R CMD INSTALL` process) can observe a torn file. `rename(2)` is
3809 // atomic on POSIX — the destination is replaced in a single syscall, so
3810 // readers always see either the old or the new content. On Windows, `rename`
3811 // fails when the destination exists, so we remove it first; that window is
3812 // benign (worst case the reader gets "file not found" and retries, which is
3813 // the same outcome as a torn write).
3814let dest = std::path::Path::new(path);
3815let tmp = dest.with_extension("tmp");
3816 std::fs::write(&tmp, content.as_bytes())
3817 .unwrap_or_else(|e| {
::core::panicking::panic_fmt(format_args!("failed to write {0}: {1}",
tmp.display(), e));
}panic!("failed to write {}: {e}", tmp.display()));
3818#[cfg(windows)]
3819let _ = std::fs::remove_file(dest);
3820 std::fs::rename(&tmp, dest).unwrap_or_else(|e| {
3821{
::core::panicking::panic_fmt(format_args!("failed to rename {0} → {1}: {2}",
tmp.display(), dest.display(), e));
}panic!(
3822"failed to rename {} → {}: {e}",
3823 tmp.display(),
3824 dest.display()
3825 )3826 });
38273828if !existing.is_empty() {
3829let filename = dest3830 .file_name()
3831 .and_then(|f| f.to_str())
3832 .unwrap_or("wrappers.R");
3833{ ::std::io::_eprint(format_args!("\n")); };eprintln!();
3834{
::std::io::_eprint(format_args!("NOTE: {0} changed — run devtools::document() to update NAMESPACE.\n",
filename));
};eprintln!("NOTE: {filename} changed — run devtools::document() to update NAMESPACE.");
3835{ ::std::io::_eprint(format_args!("\n")); };eprintln!();
3836 }
3837}
38383839/// Returns `true` when `a` and `b` are equal after normalising away
3840/// `(<file>.rs:LINE:COL)` positional suffixes in source-attribution comments.
3841///
3842/// The `#[miniextendr]` proc-macro emits comments of the form:
3843/// ```r
3844/// # Generated from Rust fn `foo` (lib.rs:42:8)
3845/// ```
3846/// The `42:8` part shifts whenever unrelated code above the wrapper is edited,
3847/// producing spurious wrapper-file rewrites and misleading NOTEs. Normalisation
3848/// replaces `(lib.rs:42:8)` → `(lib.rs:_:_)` for the comparison only; the
3849/// file on disk still carries the real positions.
3850///
3851/// First line of the generated wrappers file.
3852#[cfg(not(target_arch = "wasm32"))]
3853const WRAPPERS_FILE_MARKER: &str = "# ---- AUTO-GENERATED FILE - DO NOT EDIT ----";
38543855/// The complete wrappers file for a finished `body`: the marker line, a line
3856/// naming the `miniextendr-api` version that generated it and an FNV-1a 64
3857/// digest of the body, then the body (#1552).
3858///
3859/// The digest is taken over [`normalize_source_locs`]`(body)`, the same view
3860/// [`wrappers_semantically_equal`] compares, so a Rust edit that only shifts
3861/// line numbers leaves the header alone and the no-rewrite check keeps
3862/// holding; any change to wrapper code, exports or docstrings changes it.
3863/// It lets a reviewer or a build tool tell two copies of the file apart from
3864/// the first two lines instead of diffing ~40k lines, and identifies the
3865/// generator version when a shipped wrapper is reported against.
3866#[cfg(not(target_arch = "wasm32"))]
3867fn wrappers_file_content(body: &str) -> String {
3868let digest = crate::abi::fnv1a_64(
3869normalize_source_locs(body).as_bytes(),
3870crate::abi::FNV1A_64_OFFSET,
3871 );
3872::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{1}\n# miniextendr-api {0} | fnv1a-64 of the normalized lines below: {2:016x}\n{3}",
"0.1.0", WRAPPERS_FILE_MARKER, digest, body))
})format!(
3873"{WRAPPERS_FILE_MARKER}\n# miniextendr-api {} | fnv1a-64 of the normalized lines below: {digest:016x}\n{body}",
3874env!("CARGO_PKG_VERSION"),
3875 )3876}
38773878/// A match requires `(` + one or more non-`()`/non-newline chars + `.rs:` +
3879/// ASCII digits + `:` + ASCII digits + `)`. Malformed or non-`.rs` patterns
3880/// are left untouched.
3881///
3882/// Host-only: the sole caller is [`write_r_wrappers_to_file`], gated off on
3883/// wasm32 (the wrapper-gen pass doesn't run there).
3884#[cfg(not(target_arch = "wasm32"))]
3885fn wrappers_semantically_equal(a: &str, b: &str) -> bool {
3886normalize_source_locs(a) == normalize_source_locs(b)
3887}
38883889/// Replace every `(<stem>.rs:LINE:COL)` occurrence with `(<stem>.rs:_:_)`.
3890///
3891/// Returns a [`std::borrow::Cow::Borrowed`] slice when no replacements are
3892/// needed (zero-allocation fast path for the common case where the file has
3893/// never been written or is truly unchanged).
3894#[cfg(not(target_arch = "wasm32"))]
3895fn normalize_source_locs(s: &str) -> std::borrow::Cow<'_, str> {
3896// Fast path: bail early if there is no `.rs:` anywhere in the string.
3897if !s.contains(".rs:") {
3898return std::borrow::Cow::Borrowed(s);
3899 }
39003901let bytes = s.as_bytes();
3902let len = bytes.len();
3903let mut out = String::with_capacity(len);
3904let mut pos = 0usize;
3905let mut any_replaced = false;
39063907while pos < len {
3908// Look for the next `(`.
3909let Some(open) = memchr(bytes, pos, b'(') else {
3910break;
3911 };
39123913// After `(`, scan for `.rs:` without crossing `)` or `(` or a newline.
3914let inner_start = open + 1;
3915let Some(dot_rs) = find_substr(bytes, inner_start, b".rs:") else {
3916// No `.rs:` at all in the rest of the string — copy remainder and stop.
3917out.push_str(&s[pos..]);
3918return std::borrow::Cow::Owned(out);
3919 };
39203921// Make sure `.rs:` precedes any closing `)`, `(`, or newline (no nesting).
3922let intervening = &bytes[inner_start..dot_rs];
3923if intervening
3924 .iter()
3925 .any(|&b| b == b')' || b == b'(' || b == b'\n')
3926 {
3927// Guard crossed; copy up to and including `(` and continue after it.
3928out.push_str(&s[pos..=open]);
3929 pos = open + 1;
3930continue;
3931 }
39323933// After `.rs:`, consume digits for LINE.
3934let after_colon1 = dot_rs + 4; // skip `.rs:`
3935let Some(colon2) = scan_digits(bytes, after_colon1) else {
3936// Not digits after `.rs:` — not a source-attribution pattern.
3937out.push_str(&s[pos..=open]);
3938 pos = open + 1;
3939continue;
3940 };
3941if colon2 >= len || bytes[colon2] != b':' {
3942 out.push_str(&s[pos..=open]);
3943 pos = open + 1;
3944continue;
3945 }
39463947// After the second `:`, consume digits for COL.
3948let after_colon2 = colon2 + 1;
3949let Some(close_pos) = scan_digits(bytes, after_colon2) else {
3950 out.push_str(&s[pos..=open]);
3951 pos = open + 1;
3952continue;
3953 };
3954if close_pos >= len || bytes[close_pos] != b')' {
3955 out.push_str(&s[pos..=open]);
3956 pos = open + 1;
3957continue;
3958 }
39593960// We have a match: `(stem.rs:LINE:COL)` — emit `(stem.rs:_:_)`.
3961any_replaced = true;
3962 out.push_str(&s[pos..inner_start]); // text before inner_start (includes `(`)
3963out.push_str(&s[inner_start..dot_rs + 3]); // `stem.rs` (without the colon)
3964out.push_str(":_:_)");
3965 pos = close_pos + 1; // skip past the closing `)`
3966}
39673968if !any_replaced {
3969return std::borrow::Cow::Borrowed(s);
3970 }
39713972out.push_str(&s[pos..]);
3973 std::borrow::Cow::Owned(out)
3974}
39753976/// Find the first occurrence of `needle` in `haystack[from..]`.
3977/// Returns the absolute index in `haystack`, or `None`.
3978#[cfg(not(target_arch = "wasm32"))]
3979#[inline]
3980fn find_substr(haystack: &[u8], from: usize, needle: &[u8]) -> Option<usize> {
3981let window = haystack.get(from..)?;
3982window3983 .windows(needle.len())
3984 .position(|w| w == needle)
3985 .map(|rel| from + rel)
3986}
39873988/// Find the first byte equal to `needle` in `haystack[from..]`.
3989/// Returns the absolute index, or `None`.
3990#[cfg(not(target_arch = "wasm32"))]
3991#[inline]
3992fn memchr(haystack: &[u8], from: usize, needle: u8) -> Option<usize> {
3993haystack[from..]
3994 .iter()
3995 .position(|&b| b == needle)
3996 .map(|rel| from + rel)
3997}
39983999/// Advance past a run of ASCII digits starting at `haystack[from]`.
4000/// Returns the absolute index of the first non-digit byte, or `None` if
4001/// there are no digits at `from` (empty run is not valid).
4002#[cfg(not(target_arch = "wasm32"))]
4003#[inline]
4004fn scan_digits(haystack: &[u8], from: usize) -> Option<usize> {
4005let start = haystack.get(from..)?;
4006let count = start.iter().take_while(|&&b| b.is_ascii_digit()).count();
4007if count == 0 { None } else { Some(from + count) }
4008}
4009// endregion
40104011// region: Duplicate wrapper-definition detection (#991)
40124013/// Compute the sidecar accessor function names a `#[derive(ExternalPtr)]` emits
4014/// for each `#[r_data]` field, resolving the R-visible prefix through the
4015/// class-name registry.
4016///
4017/// The derive emits two top-level functions per field —
4018/// `<prefix>_get_<field>` and `<prefix>_set_<field>` — where `<prefix>` is the
4019/// R-visible class name. That equals the Rust type name unless the impl block
4020/// set `class = "Override"`, in which case `MX_CLASS_NAMES` carries the
4021/// override. We resolve through `class_index` so the names we attribute match
4022/// what actually lands in the wrapper file.
4023///
4024/// Returns a map from accessor function name -> the human-readable producer
4025/// description used in the collision message.
4026#[cfg(not(target_arch = "wasm32"))]
4027fn sidecar_accessor_names(
4028 sidecar_props: &[SidecarPropEntry],
4029 class_index: &std::collections::HashMap<&str, &ClassNameEntry>,
4030) -> std::collections::HashMap<String, String> {
4031let mut out = std::collections::HashMap::new();
4032for entry in sidecar_props {
4033// Resolve the R-visible prefix (honours `class = "Override"`).
4034let prefix = class_index
4035 .get(entry.rust_type)
4036 .map(|c| c.r_class_name)
4037 .unwrap_or(entry.rust_type);
4038let producer = ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("#[derive(ExternalPtr)] sidecar field `{0}` on `{1}`",
entry.field_name, entry.rust_type))
})format!(
4039"#[derive(ExternalPtr)] sidecar field `{}` on `{}`",
4040 entry.field_name, entry.rust_type
4041 );
4042 out.insert(
4043::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{1}_get_{0}", entry.field_name,
prefix))
})format!("{prefix}_get_{}", entry.field_name),
4044 producer.clone(),
4045 );
4046 out.insert(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{1}_set_{0}", entry.field_name,
prefix))
})format!("{prefix}_set_{}", entry.field_name), producer);
4047 }
4048out4049}
40504051/// Detect conflicting top-level function and class/namespace definitions in
4052/// the assembled R wrapper text.
4053///
4054/// This is the write-time counterpart to the compile-time
4055/// `check_s7_shortcut_collisions` (in `miniextendr-macros`): it catches name
4056/// collisions that span *separate* macro expansions and are therefore invisible
4057/// to any single proc-macro invocation. The two motivating cases (#991):
4058///
4059/// 1. An S7 fast-path shortcut `<Class>_<method>` colliding with a
4060/// `#[derive(ExternalPtr)]` sidecar accessor `<Class>_get_<field>` /
4061/// `<Class>_set_<field>` -- the sidecar field names live in the
4062/// `MX_S7_SIDECAR_PROPS` slice, which the impl-block macro can't see.
4063/// 2. Two `#[miniextendr(s7)]` impl blocks on the same type each emitting a
4064/// `<Class>_<method>` shortcut -- cross-impl-block, also invisible to a
4065/// single expansion.
4066///
4067/// Without this check the second definition silently clobbers the first
4068/// (last-write-wins in R), so a call dispatches to the wrong implementation.
4069///
4070/// `accessor_producers` maps a known sidecar-accessor name to a description of
4071/// its producing derive; when a duplicate matches one we name the derive in the
4072/// message, otherwise we report the generic "defined more than once" form.
4073///
4074/// Class and namespace bindings (`new.env`, `R6::R6Class`, `S7::new_class`)
4075/// occupy the same namespace as functions. Include them so a free function
4076/// cannot silently replace a class object (#1677). S4's `setClass()` is not a
4077/// binding assignment: its same-named constructor function remains valid.
4078/// Guarded S3/S4/S7 generic declarations may repeat across classes, but must
4079/// not share their binding with an ordinary function or class object (#1677).
4080/// A class object defined twice would replace the first together with the
4081/// methods bound to it. The impl blocks of one Rust type are merged into one
4082/// class before this scan, and two types claiming one class name are refused
4083/// there ([`merge_class_blocks`], #1776), so this is the last line of defence.
4084///
4085/// Every message names both producers, read from the
4086/// `# Generated from Rust …` header that opens each wrapper fragment.
4087///
4088/// Returns `Err(message)` naming the offending binding on the first collision
4089/// found; `Ok(())` when names are unique apart from shared generic declarations.
4090#[cfg(not(target_arch = "wasm32"))]
4091fn detect_duplicate_wrapper_defs(
4092 content: &str,
4093 accessor_producers: &std::collections::HashMap<String, String>,
4094) -> Result<(), String> {
4095use std::collections::HashMap;
40964097// Binding name -> (kind, producer) of its first definition.
4098let mut seen: HashMap<&str, (&str, std::rc::Rc<str>)> = HashMap::new();
40994100for WrapperName {
4101 name,
4102 kind,
4103 producer,
4104 } in wrapper_names(content)
4105 {
4106if kind == "export" {
4107continue;
4108 }
4109let Some((previous, first)) = seen.get(name) else {
4110 seen.insert(name, (kind, producer));
4111continue;
4112 };
4113let previous = *previous;
4114if previous == "guarded generic" && kind == "guarded generic" {
4115// Several classes intentionally dispatch through one generic.
4116continue;
4117 }
4118let both = ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0} from {1}, {2} from {3}",
previous, first, kind, producer))
})format!("{previous} from {first}, {kind} from {producer}");
4119if previous == "guarded generic" || kind == "guarded generic" {
4120return Err(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("miniextendr: wrapper name `{0}` is used by both a guarded generic and another generated binding ({1}). Package load order would replace one binding or prevent method dispatch. Rename the ordinary function with `r_name = \"...\"`, the class with `class = \"...\"`, or choose a distinct generic name.",
name, both))
})format!(
4121"miniextendr: wrapper name `{name}` is used by both a guarded generic \
4122 and another generated binding ({both}). Package load order would \
4123 replace one binding or prevent method dispatch. Rename the ordinary \
4124 function with `r_name = \"...\"`, the class with `class = \"...\"`, \
4125 or choose a distinct generic name."
4126));
4127 }
4128if previous != "function" && kind != "function" {
4129return Err(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("miniextendr: class `{0}` is defined more than once ({1}). The second definition replaces the first at load time, together with the methods bound to it. Give one of the classes a distinct `class = \"...\"`.",
name, both))
})format!(
4130"miniextendr: class `{name}` is defined more than once ({both}). The \
4131 second definition replaces the first at load time, together with the \
4132 methods bound to it. Give one of the classes a distinct \
4133 `class = \"...\"`."
4134));
4135 }
4136if previous != "function" || kind != "function" {
4137return Err(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("miniextendr: wrapper name `{0}` is defined more than once ({1}). The second definition overwrites the first at load time. Give the class a distinct `class = \"...\"` name or rename the colliding function with `r_name = \"...\"`.",
name, both))
})format!(
4138"miniextendr: wrapper name `{name}` is defined more than once ({both}). \
4139 The second definition overwrites the first at load time. Give the \
4140 class a distinct `class = \"...\"` name or rename the colliding \
4141 function with `r_name = \"...\"`."
4142));
4143 }
4144// Second function definition of this name -> collision.
4145if let Some(derive) = accessor_producers.get(name) {
4146return Err(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("miniextendr: wrapper function `{0}` is defined more than once ({1}). One definition comes from {2}; the other from an S7 fast-path shortcut or `#[miniextendr]` function of the same name. The second definition silently overwrites the first at load time. Rename the colliding S7 method (`#[miniextendr(s7(r_name = \"...\"))]`), opt it out of the shortcut (`#[miniextendr(s7(no_shortcut))]`), or rename the sidecar field.",
name, both, derive))
})format!(
4147"miniextendr: wrapper function `{name}` is defined more than once \
4148 ({both}). One definition comes from {derive}; the other from an S7 \
4149 fast-path shortcut or `#[miniextendr]` function of the same name. The \
4150 second definition silently overwrites the first at load time. Rename \
4151 the colliding S7 method (`#[miniextendr(s7(r_name = \"...\"))]`), opt \
4152 it out of the shortcut (`#[miniextendr(s7(no_shortcut))]`), or rename \
4153 the sidecar field."
4154));
4155 }
4156return Err(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("miniextendr: wrapper function `{0}` is defined more than once in the generated R wrappers ({1}). Two `#[miniextendr]`-generated top-level functions emit the same name and the second silently overwrites the first at load time. Common causes: two `#[miniextendr(s7)]` impl blocks (or an S7 shortcut and another generated function) sharing a `<Class>_<method>` name — rename one (`#[miniextendr(s7(r_name = \"...\"))]`) or opt it out of the shortcut (`#[miniextendr(s7(no_shortcut))]`); or two trait impls whose generated standalone wrapper names are not class-qualified — rename the colliding method (`#[miniextendr(<system>(r_name = \"...\"))]`).",
name, both))
})format!(
4157"miniextendr: wrapper function `{name}` is defined more than once in the \
4158 generated R wrappers ({both}). Two `#[miniextendr]`-generated top-level \
4159 functions emit the same name and the second silently overwrites the first \
4160 at load time. Common causes: two `#[miniextendr(s7)]` impl blocks (or an S7 \
4161 shortcut and another generated function) sharing a `<Class>_<method>` \
4162 name — rename one (`#[miniextendr(s7(r_name = \"...\"))]`) or opt it out \
4163 of the shortcut (`#[miniextendr(s7(no_shortcut))]`); or two trait impls \
4164 whose generated standalone wrapper names are not class-qualified — rename \
4165 the colliding method (`#[miniextendr(<system>(r_name = \"...\"))]`)."
4166));
4167 }
41684169Ok(())
4170}
41714172/// A name the assembled wrappers bind at top level or export, and the
4173/// fragment that does, as the `# Generated from Rust …` header opening each
4174/// fragment names it.
4175#[cfg(not(target_arch = "wasm32"))]
4176struct WrapperName<'a> {
4177 name: &'a str,
4178/// `"function"`, `"class/namespace object"` (`new.env`, `R6::R6Class`,
4179 /// `S7::new_class`), `"guarded generic"` ([`parse_guarded_generic`]), or
4180 /// `"export"`: an `@export <name>` target or an
4181 /// `@rawNamespace export(<name>)` argument.
4182kind: &'static str,
4183 producer: std::rc::Rc<str>,
4184}
41854186/// The names of the assembled wrappers `content` in file order: see
4187/// [`WrapperName`]. Read by [`detect_duplicate_wrapper_defs`] and
4188/// [`reexport_conflicts`].
4189#[cfg(not(target_arch = "wasm32"))]
4190fn wrapper_names(content: &str) -> Vec<WrapperName<'_>> {
4191let mut names = Vec::new();
4192let mut producer: std::rc::Rc<str> = std::rc::Rc::from("an unattributed wrapper fragment");
4193let mut lines = content.lines();
4194while let Some(line) = lines.next() {
4195if let Some(header) = line.strip_prefix("# Generated from ") {
4196 producer = std::rc::Rc::from(header);
4197continue;
4198 }
4199// A sidecar accessor's header gives only a source position; the line
4200 // after it names the field and the Rust type.
4201if let Some(wraps) = line.strip_prefix("# Wraps ") {
4202let what = wraps.split_once(" via ").map_or(wraps, |(what, _)| what);
4203 producer = std::rc::Rc::from(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0} ({1})", what, producer))
})format!("{what} ({producer})"));
4204continue;
4205 }
4206if let Some(targets) = line.strip_prefix("#' @export ") {
4207 names.extend(targets.split_whitespace().map(|target| WrapperName {
4208 name: unquote_r_name(target),
4209 kind: "export",
4210 producer: producer.clone(),
4211 }));
4212continue;
4213 }
4214if let Some(directives) = line.strip_prefix("#' @rawNamespace ") {
4215 names.extend(raw_namespace_exports(directives).map(|name| WrapperName {
4216 name,
4217 kind: "export",
4218 producer: producer.clone(),
4219 }));
4220continue;
4221 }
4222let (name, kind) = if let Some(name) = parse_guarded_generic(line, lines.clone()) {
4223 (name, "guarded generic")
4224 } else {
4225// Method assignments and indented closures are not bare top-level
4226 // bindings. Only the known generic guards above expose nested defs.
4227let Some((name, rhs)) = parse_top_level_assignment(line) else {
4228continue;
4229 };
4230let kind = if rhs.starts_with("function") {
4231"function"
4232} else if ["new.env(", "R6::R6Class(", "S7::new_class("]
4233 .iter()
4234 .any(|prefix| rhs.starts_with(prefix))
4235 {
4236"class/namespace object"
4237} else {
4238continue;
4239 };
4240 (name, kind)
4241 };
4242 names.push(WrapperName {
4243 name,
4244 kind,
4245 producer: producer.clone(),
4246 });
4247 }
4248names4249}
42504251/// The names the `export(...)` directives of an `@rawNamespace` tag's text
4252/// export: `export(a, "[[")` gives `a` and `[[`. `exportPattern(...)`,
4253/// `exportClasses(...)` and the like are not `export(`.
4254#[cfg(not(target_arch = "wasm32"))]
4255fn raw_namespace_exports(directives: &str) -> impl Iterator<Item = &str> {
4256directives4257 .match_indices("export(")
4258 .filter(move |(at, _)| {
4259 !directives[..*at]
4260 .chars()
4261 .next_back()
4262 .is_some_and(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_')
4263 })
4264 .flat_map(move |(at, open)| {
4265let args = &directives[at + open.len()..];
4266let args = args.split_once(')').map_or(args, |(args, _)| args);
4267args.split(',')
4268 .map(|arg| unquote_r_name(arg.trim()))
4269 .filter(|name| !name.is_empty())
4270 })
4271}
42724273/// `name` without the double quotes, single quotes or backticks around it.
4274#[cfg(not(target_arch = "wasm32"))]
4275fn unquote_r_name(name: &str) -> &str {
4276 ['"', '\'', '`']
4277 .iter()
4278 .find_map(|quote| {
4279name.strip_prefix(*quote)
4280 .and_then(|inner| inner.strip_suffix(*quote))
4281 })
4282 .unwrap_or(name)
4283}
42844285/// Recognize the generic guards emitted by `r_class_formatter` and the S4/S7
4286/// inherent/trait generators. This is a scan of generated forms, not an R parser:
4287/// the guard must start at column zero and its first statement must create a
4288/// generic. Never scan arbitrary indented assignments inside function bodies.
4289/// Documentation markers cannot stand in for this: `noexport` / `@noRd` classes
4290/// and trait implementations still declare generics without generic-doc markers.
4291#[cfg(not(target_arch = "wasm32"))]
4292fn parse_guarded_generic<'a>(line: &'a str, mut following: std::str::Lines<'a>) -> Option<&'a str> {
4293if !(line.starts_with("if (!base::exists(\"")
4294 || line.starts_with("if (!exists(\"")
4295 || line.starts_with("if (!base::inherits(base::get0(\""))
4296 {
4297return None;
4298 }
4299// S4 emits its complete guard and setGeneric call on one line.
4300if let Some((_, call)) = line.split_once(")) methods::setGeneric(\"") {
4301return call.split_once("\", ").map(|(name, _)| name);
4302 }
4303if !line.ends_with(" {") {
4304return None;
4305 }
4306// S3 and S7 put the assignment first in the guarded block, optionally
4307 // preceded by a generated comment. Their shadow-fallback branch need not
4308 // be scanned: it defines the same binding as the first branch.
4309let body = following.find(|line| {
4310let line = line.trim();
4311 !line.is_empty() && !line.starts_with('#')
4312 })?;
4313let (name, rhs) = parse_top_level_assignment(body.strip_prefix(" ")?)?;
4314// The wrapper's shadowed-formal pass can qualify UseMethod with base::.
4315let s3 = rhs.strip_prefix("function(x, ...) ").is_some_and(|body| {
4316body.strip_prefix("base::").unwrap_or(body) == ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("UseMethod(\"{0}\")", name))
})format!("UseMethod(\"{name}\")")4317 });
4318 (s3 || rhs.starts_with("S7::new_generic(") || rhs.starts_with("S7::new_external_generic("))
4319 .then_some(name)
4320}
43214322/// Parse a top-level `name <- function(...)` definition. Used both by the
4323/// duplicate scan and by standalone-function documentation grouping.
4324#[cfg(not(target_arch = "wasm32"))]
4325fn parse_top_level_fn_def_name(line: &str) -> Option<&str> {
4326let (name, rhs) = parse_top_level_assignment(line)?;
4327rhs.starts_with("function").then_some(name)
4328}
43294330/// Parse a bare top-level assignment, returning its name and trimmed RHS.
4331///
4332/// Ignore indented closures and targets containing `$`, `[[` or `::`, or a
4333/// call such as `S7::method(...)`. Backtick-quoted names are returned without
4334/// the quotes, with escapes kept as emitted. Find the closing backtick before
4335/// the arrow so replacement names such as `` `[<-.Foo` `` parse correctly.
4336#[cfg(not(target_arch = "wasm32"))]
4337fn parse_top_level_assignment(line: &str) -> Option<(&str, &str)> {
4338if line.starts_with([' ', '\t']) {
4339return None;
4340 }
4341if let Some(quoted) = line.strip_prefix('`') {
4342let mut escaped = false;
4343for (index, ch) in quoted.char_indices() {
4344if escaped {
4345 escaped = false;
4346 } else if ch == '\\' {
4347 escaped = true;
4348 } else if ch == '`' {
4349let rhs = quoted[index + 1..].trim_start().strip_prefix("<-")?;
4350return (!quoted[..index].is_empty())
4351 .then_some(("ed[..index], rhs.trim_start()));
4352 }
4353 }
4354return None;
4355 }
4356let (lhs, rhs) = line.split_once("<-")?;
4357let name = lhs.trim();
4358let first = name.chars().next()?;
4359if !(first.is_ascii_alphabetic() || first == '.') {
4360return None;
4361 }
4362name.chars()
4363 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_')
4364 .then_some((name, rhs.trim_start()))
4365}
4366// endregion
43674368// region: C-Callable Entry Points (wrapper generation)
4369//
4370// All wrapper-gen entry points are host-only — wasm32 builds skip
4371// them (the wrapper-gen pass itself doesn't run on wasm32; wrappers and
4372// `wasm_registry.rs` are pre-generated on native and shipped into the
4373// wasm32 install).
43744375/// C-callable entry point for R wrapper generation.
4376///
4377/// Called from Makevars via Rscript: loads the installed shared object with
4378/// `dyn.load()`, then `.Call("miniextendr_write_wrappers", path)` to write
4379/// `R/miniextendr-wrappers.R`. NAMESPACE generation is left to roxygen2
4380/// (`devtools::document()`).
4381///
4382/// A panic, such as a name collision found while assembling the wrappers, is
4383/// raised as an R error, so the install stops with the message instead of
4384/// aborting R at this non-unwinding boundary.
4385///
4386/// # Safety
4387///
4388/// `path_sexp` must be a valid STRSXP of length >= 1.
4389#[cfg(not(target_arch = "wasm32"))]
4390#[unsafe(no_mangle)]
4391pub unsafe extern "C" fn miniextendr_write_wrappers(path_sexp: crate::SEXP) -> crate::SEXP {
4392crate::unwind_protect::with_r_unwind_protect_or_raise(
4393 || unsafe {
4394use crate::{SEXP, SexpExt};
43954396let char_sexp = path_sexp.string_elt_unchecked(0);
4397let c_str = std::ffi::CStr::from_ptr(char_sexp.r_char_unchecked());
4398let path = c_str4399 .to_str()
4400 .unwrap_or_else(|e| {
::core::panicking::panic_fmt(format_args!("invalid UTF-8 in path: {0}",
e));
}panic!("invalid UTF-8 in path: {e}"));
44014402write_r_wrappers_to_file(path);
44034404SEXP::nil()
4405 },
4406None,
4407 )
4408}
44094410/// C-callable entry point for `wasm_registry.rs` generation.
4411///
4412/// Pairs with [`miniextendr_write_wrappers`]: same loaded object, separate `.Call`,
4413/// independent output path. Host-only; the generated file itself is then
4414/// consumed at compile time by the user crate's wasm32 build via
4415/// `install_wasm_runtime_slices`. A panic is raised as an R error, as in
4416/// [`miniextendr_write_wrappers`].
4417///
4418/// # Safety
4419///
4420/// `path_sexp` must be a valid STRSXP of length >= 1.
4421#[cfg(not(target_arch = "wasm32"))]
4422#[unsafe(no_mangle)]
4423pub unsafe extern "C" fn miniextendr_write_wasm_registry(path_sexp: crate::SEXP) -> crate::SEXP {
4424crate::unwind_protect::with_r_unwind_protect_or_raise(
4425 || unsafe {
4426use crate::{SEXP, SexpExt};
44274428let char_sexp = path_sexp.string_elt_unchecked(0);
4429let c_str = std::ffi::CStr::from_ptr(char_sexp.r_char_unchecked());
4430let path = c_str4431 .to_str()
4432 .unwrap_or_else(|e| {
::core::panicking::panic_fmt(format_args!("invalid UTF-8 in path: {0}",
e));
}panic!("invalid UTF-8 in path: {e}"));
44334434crate::wasm_registry_writer::write_wasm_registry_to_file(path);
44354436SEXP::nil()
4437 },
4438None,
4439 )
4440}
4441// endregion
44424443// region: Unit tests for class-ref placeholder resolution
44444445#[cfg(test)]
4446mod tests {
4447use super::*;
4448use crate::match_arg::MatchArgAlias;
44494450#[test]
4451 #[cfg(not(target_arch = "wasm32"))]
4452fn optional_preamble_helpers_follow_fragment_dependencies() {
4453use std::borrow::Cow;
4454let render = |fragment: &'static str| {
4455let mut text = String::new();
4456super::append_used_wrapper_helpers(&mut text, &[Cow::Borrowed(fragment)]);
4457 text
4458 };
4459assert!(render("f <- function() .Call(C_f)").is_empty());
44604461let caller = render("f <- function() .miniextendr_caller_call()");
4462assert!(caller.contains(".miniextendr_caller_call <- function"));
4463assert!(caller.contains(".miniextendr_arg_error <- function"));
4464assert!(!caller.contains(".miniextendr_field_at <- function"));
4465assert!(!caller.contains(".miniextendr_env_method <- function"));
4466assert!(!caller.contains(".miniextendr_match_arg"));
44674468let check = render("f <- function(x) .miniextendr_arg_error('x', 'must be positive')");
4469assert!(check.contains(".miniextendr_arg_error <- function"));
4470assert!(!check.contains(".miniextendr_match_arg"));
44714472let field = render("f <- function(x) .miniextendr_field_at(fields, x, environment())");
4473assert!(field.contains(".miniextendr_field_at <- function"));
4474assert!(!field.contains(".miniextendr_arg_error <- function"));
4475let index = render("f <- function(x) .miniextendr_field_index_error('cls', environment())");
4476assert!(index.contains(".miniextendr_field_index_error <- function"));
44774478let choices = render("f <- function(x) .miniextendr_match_arg_several(x, choices, 'x')");
4479assert!(choices.contains(".miniextendr_arg_error <- function"));
4480assert!(choices.contains(".miniextendr_match_arg_several <- function"));
4481assert!(!choices.contains(".miniextendr_match_arg <- function"));
44824483let scalar = render("f <- function(x) .miniextendr_match_arg(x, choices, 'x')");
4484assert!(scalar.contains(".miniextendr_arg_error <- function"));
4485assert!(scalar.contains(".miniextendr_match_arg <- function"));
4486assert!(!scalar.contains(".miniextendr_match_arg_several <- function"));
44874488// The `...` check builds its own condition (#2157).
4489let dots = render(
4490"f <- function(x, ...) {\n if (...length()) .miniextendr_check_dots(...names(), ...length(), \"x\")\n}",
4491 );
4492assert!(dots.contains(".miniextendr_check_dots <- function"));
4493assert!(!dots.contains(".miniextendr_arg_error <- function"));
4494assert!(!scalar.contains(".miniextendr_check_dots <- function"));
4495 }
44964497/// Run the class-ref placeholder resolver over `content` using `entries` as the registry.
4498 ///
4499 /// This mirrors the logic in `write_r_wrappers_to_file` but operates on a caller-supplied
4500 /// slice so it can be called from unit tests without a live distributed_slice.
4501fn resolve_class_refs(content: &str, entries: &[ClassNameEntry]) -> String {
4502let class_index = build_class_name_index(entries.iter());
45034504const OR_ANY_PREFIX: &str = ".__MX_CLASS_REF_OR_ANY_";
4505const PREFIX: &str = ".__MX_CLASS_REF_";
4506const SUFFIX: &str = "__";
45074508let mut result = String::with_capacity(content.len());
4509let mut remaining = content;
4510while let Some(start) = remaining.find(PREFIX) {
4511 result.push_str(&remaining[..start]);
4512let rest_from_prefix = &remaining[start..];
4513let (quiet_fallback, header_len) = if rest_from_prefix.starts_with(OR_ANY_PREFIX) {
4514 (true, OR_ANY_PREFIX.len())
4515 } else {
4516 (false, PREFIX.len())
4517 };
4518 remaining = &rest_from_prefix[header_len..];
4519if let Some(end) = remaining.find(SUFFIX) {
4520let rust_name = &remaining[..end];
4521 remaining = &remaining[end + SUFFIX.len()..];
4522match class_index.get(rust_name) {
4523Some(entry) if quiet_fallback => {
4524if entry.class_system == "s7" {
4525 result.push_str(entry.r_class_name);
4526 } else {
4527 result.push_str("S7::class_any");
4528 }
4529 }
4530Some(entry) => result.push_str(entry.r_class_name),
4531None if quiet_fallback => result.push_str("S7::class_any"),
4532None => result.push_str(rust_name),
4533 }
4534 } else {
4535 result.push_str(PREFIX);
4536break;
4537 }
4538 }
4539 result.push_str(remaining);
4540 result
4541 }
45424543/// Two impl blocks of one type register identical entries (#1242, #1776);
4544 /// the index collapses them instead of treating the duplicate key as an error.
4545#[test]
4546fn class_name_index_dedups_identical_entries() {
4547let entries = [
4548 ClassNameEntry {
4549 rust_type: "Foo",
4550 r_class_name: "Foo",
4551 class_system: "env",
4552 field_syntax: false,
4553 },
4554 ClassNameEntry {
4555 rust_type: "Foo",
4556 r_class_name: "Foo",
4557 class_system: "env",
4558 field_syntax: false,
4559 },
4560 ];
4561let index = build_class_name_index(entries.iter());
4562assert_eq!(index.len(), 1);
4563assert_eq!(index["Foo"].r_class_name, "Foo");
4564 }
45654566/// Disagreeing `class = "..."` overrides across blocks must fail loudly —
4567 /// placeholder resolution would otherwise pick whichever entry linkme
4568 /// ordered last.
4569#[test]
4570 #[should_panic(expected = "conflicting class registrations for Rust type `Foo`")]
4571fn class_name_index_panics_on_conflicting_override() {
4572let entries = [
4573 ClassNameEntry {
4574 rust_type: "Foo",
4575 r_class_name: "Foo",
4576 class_system: "env",
4577 field_syntax: false,
4578 },
4579 ClassNameEntry {
4580 rust_type: "Foo",
4581 r_class_name: "FooOverride",
4582 class_system: "env",
4583 field_syntax: false,
4584 },
4585 ];
4586let _ = build_class_name_index(entries.iter());
4587 }
45884589/// R class names belong to one Rust type, regardless of the selected
4590 /// class systems or the order linkme supplies the registrations.
4591#[test]
4592fn class_name_index_rejects_shared_r_class_names() {
4593for class_system in ["env", "r6", "s3", "s4", "s7", "vctrs"] {
4594let entries = [
4595 ClassNameEntry {
4596 rust_type: "First",
4597 r_class_name: "Shared",
4598 class_system: "env",
4599 field_syntax: false,
4600 },
4601 ClassNameEntry {
4602 rust_type: "Second",
4603 r_class_name: "Shared",
4604 class_system,
4605 field_syntax: true,
4606 },
4607 ];
4608for ordered in [[&entries[0], &entries[1]], [&entries[1], &entries[0]]] {
4609let error = std::panic::catch_unwind(|| build_class_name_index(ordered))
4610 .err()
4611 .expect("a shared R class would overwrite the other type's methods");
4612let message = error.downcast_ref::<String>().unwrap();
4613assert!(message.contains("R class `Shared`"), "{message}");
4614assert!(message.contains("`First` (env)"), "{message}");
4615assert!(
4616 message.contains(&format!("`Second` ({class_system})")),
4617"{message}"
4618);
4619assert!(message.contains("class = \"...\""), "{message}");
4620 }
4621 }
4622 }
46234624#[test]
4625fn class_name_index_preserves_case_distinct_r_class_names() {
4626let entries = [
4627 ClassNameEntry {
4628 rust_type: "First",
4629 r_class_name: "Widget",
4630 class_system: "env",
4631 field_syntax: false,
4632 },
4633 ClassNameEntry {
4634 rust_type: "Second",
4635 r_class_name: "widget",
4636 class_system: "env",
4637 field_syntax: false,
4638 },
4639 ];
4640let index = build_class_name_index(entries.iter());
4641assert_eq!(index["First"].r_class_name, "Widget");
4642assert_eq!(index["Second"].r_class_name, "widget");
4643 }
46444645/// A type's standalone sidecar accessors are written internal when one of
4646 /// its impl blocks gives the class field syntax, exported otherwise (#1900).
4647#[test]
4648fn sidecar_accessors_follow_the_field_syntax() {
4649let accessors = [
4650 SidecarAccessorsEntry {
4651 rust_type: "Fields",
4652 exported: "exported Fields",
4653 internal: "internal Fields",
4654 source_file: "lib.rs",
4655 source_line: 1,
4656 },
4657 SidecarAccessorsEntry {
4658 rust_type: "Plain",
4659 exported: "exported Plain",
4660 internal: "internal Plain",
4661 source_file: "lib.rs",
4662 source_line: 2,
4663 },
4664 ];
4665let class = |rust_type, field_syntax| ClassNameEntry {
4666 rust_type,
4667 r_class_name: rust_type,
4668 class_system: "s3",
4669 field_syntax,
4670 };
4671let classes = [
4672 class("Fields", false),
4673 class("Fields", true),
4674 class("Plain", false),
4675 ];
4676let fragments = sidecar_accessor_fragments(&accessors, &classes);
4677let contents: Vec<&str> = fragments.iter().map(|f| f.content.as_ref()).collect();
4678assert_eq!(contents, ["internal Fields", "exported Plain"]);
4679assert!(
4680 fragments
4681 .iter()
4682 .all(|f| f.priority == RWrapperPriority::Sidecar)
4683 );
4684// A type no impl block registers keeps its exported accessors.
4685assert_eq!(
4686 sidecar_accessor_fragments(&accessors, &[])[0].content,
4687"exported Fields"
4688);
4689 }
46904691/// The `all.equal()` method of `#[derive(AllEqual)]` takes the R class
4692 /// of the type's impl block: an S3 method for every class system, and an
4693 /// S7 class's own registration too (#2149).
4694#[test]
4695fn all_equal_methods_follow_the_class() {
4696let entry = |rust_type| AllEqualEntry {
4697 rust_type,
4698 method: "\nfunction(target, current, ...) TRUE\n",
4699 docs: "#' Docs.",
4700 source_file: "lib.rs",
4701 source_line: 3,
4702 source_column: 12,
4703 };
4704let class = |rust_type, r_class_name, class_system| ClassNameEntry {
4705 rust_type,
4706 r_class_name,
4707 class_system,
4708 field_syntax: false,
4709 };
4710let entries = [entry("Fit"), entry("Shape")];
4711let classes = [class("Fit", "fit_rec", "r6"), class("Shape", "Shape", "s7")];
4712let fragments = all_equal_fragments(&entries, &classes).unwrap();
4713assert!(
4714 fragments
4715 .iter()
4716 .all(|f| f.priority == RWrapperPriority::TraitImpl)
4717 );
4718assert_eq!(
4719 fragments[0].content,
4720"# Generated from Rust derive `AllEqual` on `Fit` (lib.rs:3:12)\n\
4721 #' Compare two `fit_rec` objects\n\
4722 #'\n\
4723 #' Docs.\n\
4724 #' @method all.equal fit_rec\n\
4725 #' @export\n\
4726 all.equal.fit_rec <- function(target, current, ...) TRUE"
4727);
4728assert!(fragments[1].content.ends_with(
4729"all.equal.Shape <- function(target, current, ...) TRUE\n\
4730 S7::method(all.equal, Shape) <- all.equal.Shape\n\
4731 base::rm(list = \"all.equal\")"
4732));
47334734// A class name R can't read as a name is quoted.
4735let quoted = all_equal_fragments(&[entry("Fit")], &[class("Fit", "fit-rec", "s3")]);
4736assert!(
4737 quoted.unwrap()[0]
4738 .content
4739 .contains("`all.equal.fit-rec` <- function")
4740 );
4741 }
47424743/// A type without a class, and a vctrs class, are refused together.
4744#[test]
4745fn all_equal_methods_need_a_pointer_class() {
4746let entry = |rust_type, source_line| AllEqualEntry {
4747 rust_type,
4748 method: "function(target, current, ...) TRUE",
4749 docs: "",
4750 source_file: "lib.rs",
4751 source_line,
4752 source_column: 1,
4753 };
4754let classes = [ClassNameEntry {
4755 rust_type: "Rec",
4756 r_class_name: "Rec",
4757 class_system: "vctrs",
4758 field_syntax: false,
4759 }];
4760let Err(msg) = all_equal_fragments(&[entry("Loose", 1), entry("Rec", 2)], &classes) else {
4761panic!("expected the derives to be refused");
4762 };
4763assert_eq!(
4764 msg,
4765"`#[derive(AllEqual)]` on `Loose` (lib.rs:1): no `#[miniextendr(env | r6 | s3 | s4 | s7)]` \
4766 impl block gives the type an R class, so there is no class to give the `all.equal()` method\n\
4767 `#[derive(AllEqual)]` on `Rec` (lib.rs:2): the objects of a vctrs class are R vectors, \
4768 not pointers to the Rust value, and `all.equal()` already compares them"
4769);
4770 }
47714772fn reexport(
4773 package: &'static str,
4774 name: &'static str,
4775 source_file: &'static str,
4776 source_line: u32,
4777 ) -> ReexportEntry {
4778 ReexportEntry {
4779 package,
4780 name,
4781 source_file,
4782 source_line,
4783 source_column: 11,
4784 }
4785 }
47864787/// One block per `pkg::name` whatever the number of sites, sorted by
4788 /// package then name, a non-syntactic name quoted (#1910).
4789#[test]
4790fn reexport_blocks_are_one_per_item_and_sorted() {
4791assert!(reexport_fragment(&[]).is_none());
4792let entries = [
4793 reexport("generics", "tidy", "models.rs", 8),
4794 reexport("magrittr", "%>%", "fits.rs", 4),
4795 reexport("generics", "tidy", "fits.rs", 3),
4796 reexport("generics", "glance", "fits.rs", 3),
4797 reexport("data.table", "fread", "io.rs", 1),
4798 reexport("generics", "tidy", "models.rs", 8),
4799 ];
4800let fragment = reexport_fragment(&entries).expect("a fragment");
4801assert!(fragment.priority == RWrapperPriority::Reexport);
4802assert_eq!(
4803 fragment.content,
4804"# Generated from Rust `reexport!` of `data.table::fread` (io.rs:1:11)\n\
4805 #' @importFrom data.table fread\n\
4806 #' @export\n\
4807 data.table::fread\n\
4808 \n\
4809 # Generated from Rust `reexport!` of `generics::glance` (fits.rs:3:11)\n\
4810 #' @importFrom generics glance\n\
4811 #' @export\n\
4812 generics::glance\n\
4813 \n\
4814 # Generated from Rust `reexport!` of `generics::tidy` (fits.rs:3:11) (models.rs:8:11)\n\
4815 #' @importFrom generics tidy\n\
4816 #' @export\n\
4817 generics::tidy\n\
4818 \n\
4819 # Generated from Rust `reexport!` of `magrittr::%>%` (fits.rs:4:11)\n\
4820 #' @importFrom magrittr \"%>%\"\n\
4821 #' @export\n\
4822 magrittr::`%>%`"
4823);
4824// The line numbers of the sites don't change the normalized file.
4825let moved = [reexport("generics", "tidy", "models.rs", 80)];
4826assert_eq!(
4827 normalize_source_locs(&reexport_fragment(&moved).unwrap().content),
4828 normalize_source_locs(
4829&reexport_fragment(&[reexport("generics", "tidy", "models.rs", 8)])
4830 .unwrap()
4831 .content
4832 )
4833 );
4834 }
48354836/// The blocks bind nothing, so they pass both scans of the file.
4837#[test]
4838fn reexport_blocks_pass_the_wrapper_scans() {
4839let entries = [
4840 reexport("generics", "tidy", "fits.rs", 3),
4841 reexport("magrittr", "%>%", "fits.rs", 4),
4842 ];
4843let content = format!(
4844"# Generated from Rust fn `fit` (fits.rs:9:8)\n\
4845 #' @export\n\
4846 fit <- function(x) .Call(C_fit, x)\n\n{}",
4847 reexport_fragment(&entries).unwrap().content
4848 );
4849let names: Vec<&str> = wrapper_names(&content).iter().map(|n| n.name).collect();
4850assert_eq!(names, ["fit"]);
4851assert!(detect_duplicate_wrapper_defs(&content, &std::collections::HashMap::new()).is_ok());
4852assert_eq!(reexport_conflicts(&content, &entries), Ok(()));
4853 }
48544855/// A name from two packages, and names the wrappers bind or export, are
4856 /// refused in one message naming every site (#1910).
4857#[test]
4858fn reexport_conflicts_are_refused_together() {
4859let content = concat!(
4860"# Generated from Rust impl `Fit` (fits.rs:20:6)\n",
4861"#' @export glance\n",
4862"if (!base::exists(\"glance\", mode = \"function\")) {\n",
4863" glance <- function(x, ...) UseMethod(\"glance\")\n",
4864"}\n",
4865"# Generated from Rust fn `augment` (fits.rs:30:8)\n",
4866"#' @export\n",
4867"augment <- function(x) .Call(C_augment, x)\n",
4868"# Generated from Rust fn `fit_print` (fits.rs:40:8)\n",
4869"#' @rawNamespace export(tidy, \"%>%\")\n",
4870"#' @method print fit\n",
4871"print.fit <- function(x, ...) invisible(x)\n",
4872 );
4873let entries = [
4874 reexport("generics", "tidy", "fits.rs", 3),
4875 reexport("broom", "tidy", "models.rs", 5),
4876 reexport("generics", "tidy", "models.rs", 6),
4877 reexport("generics", "glance", "fits.rs", 3),
4878 reexport("generics", "augment", "fits.rs", 3),
4879 reexport("generics", "interpolate", "fits.rs", 3),
4880 ];
4881assert_eq!(
4882 reexport_conflicts(content, &entries).unwrap_err(),
4883"miniextendr: `reexport!` re-exports `tidy` from 2 packages: `broom::tidy` \
4884 (models.rs:5:11), `generics::tidy` (fits.rs:3:11) (models.rs:6:11). A package \
4885 exports one object under a name; re-export it from one of them.\n\
4886 miniextendr: `reexport!` re-exports `generics::augment` (fits.rs:3:11), and the \
4887 package's wrappers also bind `augment` (a function from Rust fn `augment` \
4888 (fits.rs:30:8)). The package's own `augment` would hide the imported one in its \
4889 namespace, so `export(augment)` would export it instead. Remove the re-export, or \
4890 rename the package's binding (`r_name = \"...\"`, `class = \"...\"`).\n\
4891 miniextendr: `reexport!` re-exports `generics::glance` (fits.rs:3:11), and the \
4892 package's wrappers also bind `glance` (a guarded generic from Rust impl `Fit` \
4893 (fits.rs:20:6)). The package's own `glance` would hide the imported one in its \
4894 namespace, so `export(glance)` would export it instead. Remove the re-export, or \
4895 rename the package's binding (`r_name = \"...\"`, `class = \"...\"`).\n\
4896 miniextendr: `reexport!` re-exports `broom::tidy` (models.rs:5:11), \
4897 `generics::tidy` (fits.rs:3:11) (models.rs:6:11), and the package's wrappers also \
4898 export `tidy` (an export from Rust fn `fit_print` (fits.rs:40:8)). `reexport!` \
4899 writes the export itself; drop the other one."
4900);
4901 }
49024903#[test]
4904fn raw_namespace_exports_are_read_by_name() {
4905let names: Vec<&str> = raw_namespace_exports(
4906"export(a, \"[[\") exportPattern(\"^x\") S3method(print, foo) if (TRUE) export(`b`)",
4907 )
4908 .collect();
4909assert_eq!(names, ["a", "[[", "b"]);
4910 }
49114912/// Run the production scalar cross-class return wrapper resolver over
4913 /// `content` using `entries` as the class registry.
4914fn resolve_return_wrappers(content: &str, entries: &[ClassNameEntry]) -> String {
4915let class_index = build_class_name_index(entries.iter());
4916 resolve_scalar_return_wrappers(content, &class_index)
4917 }
49184919/// Run the production list cross-class return wrapper resolver (#1284)
4920 /// over `content` using `entries` as the class registry.
4921fn resolve_list_wrappers(content: &str, entries: &[ClassNameEntry]) -> String {
4922let class_index = build_class_name_index(entries.iter());
4923 resolve_list_return_wrappers(content, &class_index)
4924 }
49254926#[test]
4927fn test_class_ref_resolver_with_override() {
4928// S7Shape is registered with r_class_name = "Shape" (override)
4929let entries = [
4930 ClassNameEntry {
4931 rust_type: "S7Shape",
4932 r_class_name: "Shape",
4933 class_system: "s7",
4934 field_syntax: false,
4935 },
4936 ClassNameEntry {
4937 rust_type: "S7Circle",
4938 r_class_name: "S7Circle",
4939 class_system: "s7",
4940 field_syntax: false,
4941 },
4942 ];
49434944let input = r#"S7Circle <- S7::new_class("S7Circle", parent = .__MX_CLASS_REF_S7Shape__, properties = list())"#;
4945let output = resolve_class_refs(input, &entries);
4946assert_eq!(
4947 output,
4948r#"S7Circle <- S7::new_class("S7Circle", parent = Shape, properties = list())"#
4949);
4950 }
49514952#[test]
4953fn test_class_ref_resolver_multiple_placeholders() {
4954let entries = [
4955 ClassNameEntry {
4956 rust_type: "Point2D",
4957 r_class_name: "Point2D",
4958 class_system: "s7",
4959 field_syntax: false,
4960 },
4961 ClassNameEntry {
4962 rust_type: "Point3D",
4963 r_class_name: "Point3D",
4964 class_system: "s7",
4965 field_syntax: false,
4966 },
4967 ];
49684969let input = "S7::method(convert, list(.__MX_CLASS_REF_Point2D__, Point3D)) <- function(from, to) {}";
4970let output = resolve_class_refs(input, &entries);
4971assert_eq!(
4972 output,
4973"S7::method(convert, list(Point2D, Point3D)) <- function(from, to) {}"
4974);
4975 }
49764977#[test]
4978fn test_class_ref_resolver_unresolved_falls_back_to_rust_name() {
4979let entries: [ClassNameEntry; 0] = [];
4980let input = "parent = .__MX_CLASS_REF_UnknownClass__";
4981let output = resolve_class_refs(input, &entries);
4982// Falls back to the bare Rust name
4983assert_eq!(output, "parent = UnknownClass");
4984 }
49854986#[test]
4987fn test_class_ref_resolver_verbatim_passthrough() {
4988// Strings that are NOT bare identifiers should not have been wrapped in
4989 // a placeholder, so they pass through the resolver unchanged.
4990let entries: [ClassNameEntry; 0] = [];
4991let input = "parent = S7::class_any";
4992let output = resolve_class_refs(input, &entries);
4993assert_eq!(output, "parent = S7::class_any");
4994 }
49954996#[test]
4997fn test_is_bare_identifier_via_resolver() {
4998// A placeholder for a bare identifier should be resolved.
4999let entries = [ClassNameEntry {
5000 rust_type: "MyClass",
5001 r_class_name: "MyClass",
5002 class_system: "r6",
5003 field_syntax: false,
5004 }];
5005let input = "inherit = .__MX_CLASS_REF_MyClass__";
5006let output = resolve_class_refs(input, &entries);
5007assert_eq!(output, "inherit = MyClass");
5008 }
50095010// #203 — OR_ANY variant: S7 property class constraints should fall back
5011 // silently to S7::class_any when the type isn't a registered S7 class.
50125013#[test]
5014fn or_any_resolves_registered_s7_class() {
5015let entries = [ClassNameEntry {
5016 rust_type: "S7PropInner",
5017 r_class_name: "S7PropInner",
5018 class_system: "s7",
5019 field_syntax: false,
5020 }];
5021let input = "class = .__MX_CLASS_REF_OR_ANY_S7PropInner__";
5022let output = resolve_class_refs(input, &entries);
5023assert_eq!(output, "class = S7PropInner");
5024 }
50255026#[test]
5027fn or_any_unregistered_type_falls_back_to_class_any_silently() {
5028// No entry for SEXP, PathBuf, Json, etc.
5029let entries: [ClassNameEntry; 0] = [];
5030let input = "class = .__MX_CLASS_REF_OR_ANY_SEXP__";
5031let output = resolve_class_refs(input, &entries);
5032assert_eq!(output, "class = S7::class_any");
5033 }
50345035#[test]
5036fn or_any_registered_non_s7_type_falls_back_to_class_any() {
5037// A type registered as an R6 class can't serve as an S7 class
5038 // constraint — S7 would error at load time. Quiet fallback avoids
5039 // that footgun.
5040let entries = [ClassNameEntry {
5041 rust_type: "R6Counter",
5042 r_class_name: "R6Counter",
5043 class_system: "r6",
5044 field_syntax: false,
5045 }];
5046let input = "class = .__MX_CLASS_REF_OR_ANY_R6Counter__";
5047let output = resolve_class_refs(input, &entries);
5048assert_eq!(output, "class = S7::class_any");
5049 }
50505051#[test]
5052fn loud_class_ref_still_emits_bare_name_when_unresolved() {
5053// The existing CLASS_REF variant (without OR_ANY) keeps its loud
5054 // behavior: bare Rust name + compile-time warning. Regression test
5055 // for that path alongside the OR_ANY introduction.
5056let entries: [ClassNameEntry; 0] = [];
5057let input = "parent = .__MX_CLASS_REF_UnknownClass__";
5058let output = resolve_class_refs(input, &entries);
5059assert_eq!(output, "parent = UnknownClass");
5060 }
50615062#[test]
5063fn mixed_placeholders_resolve_independently() {
5064// A single wrapper can mix both variants — `inherit = parent` (loud)
5065 // and `class = prop_type` (quiet). Verify each takes its own path.
5066let entries = [
5067 ClassNameEntry {
5068 rust_type: "Parent",
5069 r_class_name: "Parent",
5070 class_system: "s7",
5071 field_syntax: false,
5072 },
5073// PropInner deliberately missing → should resolve to class_any.
5074];
5075let input =
5076"inherit = .__MX_CLASS_REF_Parent__, class = .__MX_CLASS_REF_OR_ANY_PropInner__";
5077let output = resolve_class_refs(input, &entries);
5078assert_eq!(output, "inherit = Parent, class = S7::class_any");
5079 }
50805081#[test]
5082fn return_wrapper_resolves_r6_constructor() {
5083let entries = [ClassNameEntry {
5084 rust_type: "R6Board",
5085 r_class_name: "R6Board",
5086 class_system: "r6",
5087 field_syntax: false,
5088 }];
5089let input = "return(.__MX_WRAP_RETURN_R6Board__(.val))";
5090let output = resolve_return_wrappers(input, &entries);
5091assert_eq!(output, "return(R6Board$new(.ptr = .val))");
5092 }
50935094#[test]
5095fn return_wrapper_resolves_s7_override_constructor() {
5096let entries = [ClassNameEntry {
5097 rust_type: "S7Board",
5098 r_class_name: "PrettyBoard",
5099 class_system: "s7",
5100 field_syntax: false,
5101 }];
5102let input = ".__MX_WRAP_RETURN_S7Board__(.val)";
5103let output = resolve_return_wrappers(input, &entries);
5104assert_eq!(output, "PrettyBoard(.ptr = .val)");
5105 }
51065107#[test]
5108fn return_wrapper_resolves_s4_constructor() {
5109let entries = [ClassNameEntry {
5110 rust_type: "S4Board",
5111 r_class_name: "S4Board",
5112 class_system: "s4",
5113 field_syntax: false,
5114 }];
5115let input = ".__MX_WRAP_RETURN_S4Board__(.val)";
5116let output = resolve_return_wrappers(input, &entries);
5117assert_eq!(output, "methods::new(\"S4Board\", ptr = .val)");
5118 }
51195120#[test]
5121fn return_wrapper_resolves_attribute_classes() {
5122let entries = [
5123 ClassNameEntry {
5124 rust_type: "S3Board",
5125 r_class_name: "S3Board",
5126 class_system: "s3",
5127 field_syntax: false,
5128 },
5129 ClassNameEntry {
5130 rust_type: "EnvBoard",
5131 r_class_name: "EnvBoard",
5132 class_system: "env",
5133 field_syntax: false,
5134 },
5135 ];
5136let input =
5137"a <- .__MX_WRAP_RETURN_S3Board__(.val)\nb <- .__MX_WRAP_RETURN_EnvBoard__(.ptr)";
5138let output = resolve_return_wrappers(input, &entries);
5139assert_eq!(
5140 output,
5141"a <- structure(.val, class = \"S3Board\")\nb <- structure(.ptr, class = \"EnvBoard\")"
5142);
5143 }
51445145#[test]
5146fn return_wrapper_unregistered_type_falls_back_to_value() {
5147let entries: [ClassNameEntry; 0] = [];
5148let input = ".__MX_WRAP_RETURN_JsonValue__(.val)";
5149let output = resolve_return_wrappers(input, &entries);
5150assert_eq!(output, ".val");
5151 }
51525153// #1284 — list-shaped cross-class return markers (`Vec<Class>` returns).
51545155/// CRITICAL non-overlap pin: the scalar resolver rewrites *unrecognized*
5156 /// names of its own prefix to the bare expression, so if the list marker
5157 /// shared the `.__MX_WRAP_RETURN_` prefix the scalar pass would consume
5158 /// and destroy it before the list pass ran. The `LIST_` infix keeps the
5159 /// families disjoint — the scalar resolver must leave list markers
5160 /// completely untouched (and vice versa).
5161#[test]
5162fn scalar_resolver_leaves_list_markers_untouched() {
5163let entries = [ClassNameEntry {
5164 rust_type: "R6Board",
5165 r_class_name: "R6Board",
5166 class_system: "r6",
5167 field_syntax: false,
5168 }];
5169let input = "a <- .__MX_WRAP_LIST_RETURN_R6Board__(.val)";
5170let output = resolve_return_wrappers(input, &entries);
5171assert_eq!(output, input, "scalar pass must not consume list markers");
5172 }
51735174#[test]
5175fn list_resolver_leaves_scalar_markers_untouched() {
5176let entries = [ClassNameEntry {
5177 rust_type: "R6Board",
5178 r_class_name: "R6Board",
5179 class_system: "r6",
5180 field_syntax: false,
5181 }];
5182let input = "a <- .__MX_WRAP_RETURN_R6Board__(.val)";
5183let output = resolve_list_wrappers(input, &entries);
5184assert_eq!(output, input, "list pass must not consume scalar markers");
5185 }
51865187/// Both passes over content mixing the two families resolve each marker
5188 /// with its own resolver, in either pass order.
5189#[test]
5190fn scalar_and_list_markers_resolve_independently() {
5191let entries = [ClassNameEntry {
5192 rust_type: "R6Board",
5193 r_class_name: "R6Board",
5194 class_system: "r6",
5195 field_syntax: false,
5196 }];
5197let input =
5198"a <- .__MX_WRAP_RETURN_R6Board__(.val)\nb <- .__MX_WRAP_LIST_RETURN_R6Board__(.val)";
5199let expected = "a <- R6Board$new(.ptr = .val)\nb <- lapply(.val, function(.el) R6Board$new(.ptr = .el))";
52005201let scalar_first =
5202 resolve_list_wrappers(&resolve_return_wrappers(input, &entries), &entries);
5203let list_first = resolve_return_wrappers(&resolve_list_wrappers(input, &entries), &entries);
5204assert_eq!(scalar_first, expected);
5205assert_eq!(list_first, expected);
5206 }
52075208#[test]
5209fn list_return_wrapper_resolves_r6_lapply() {
5210let entries = [ClassNameEntry {
5211 rust_type: "R6Board",
5212 r_class_name: "R6Board",
5213 class_system: "r6",
5214 field_syntax: false,
5215 }];
5216let input = "return(.__MX_WRAP_LIST_RETURN_R6Board__(.val))";
5217let output = resolve_list_wrappers(input, &entries);
5218assert_eq!(
5219 output,
5220"return(lapply(.val, function(.el) R6Board$new(.ptr = .el)))"
5221);
5222 }
52235224#[test]
5225fn list_return_wrapper_resolves_s7_override_lapply() {
5226let entries = [ClassNameEntry {
5227 rust_type: "S7Board",
5228 r_class_name: "PrettyBoard",
5229 class_system: "s7",
5230 field_syntax: false,
5231 }];
5232let input = ".__MX_WRAP_LIST_RETURN_S7Board__(.val)";
5233let output = resolve_list_wrappers(input, &entries);
5234assert_eq!(
5235 output,
5236"lapply(.val, function(.el) PrettyBoard(.ptr = .el))"
5237);
5238 }
52395240#[test]
5241fn list_return_wrapper_resolves_s4_lapply() {
5242let entries = [ClassNameEntry {
5243 rust_type: "S4Board",
5244 r_class_name: "S4Board",
5245 class_system: "s4",
5246 field_syntax: false,
5247 }];
5248let input = ".__MX_WRAP_LIST_RETURN_S4Board__(.val)";
5249let output = resolve_list_wrappers(input, &entries);
5250assert_eq!(
5251 output,
5252"lapply(.val, function(.el) methods::new(\"S4Board\", ptr = .el))"
5253);
5254 }
52555256#[test]
5257fn list_return_wrapper_resolves_attribute_classes_lapply() {
5258let entries = [
5259 ClassNameEntry {
5260 rust_type: "S3Board",
5261 r_class_name: "S3Board",
5262 class_system: "s3",
5263 field_syntax: false,
5264 },
5265 ClassNameEntry {
5266 rust_type: "EnvBoard",
5267 r_class_name: "EnvBoard",
5268 class_system: "env",
5269 field_syntax: false,
5270 },
5271 ];
5272let input = "a <- .__MX_WRAP_LIST_RETURN_S3Board__(.val)\nb <- .__MX_WRAP_LIST_RETURN_EnvBoard__(.ptr)";
5273let output = resolve_list_wrappers(input, &entries);
5274assert_eq!(
5275 output,
5276"a <- lapply(.val, function(.el) structure(.el, class = \"S3Board\"))\nb <- lapply(.ptr, function(.el) structure(.el, class = \"EnvBoard\"))"
5277);
5278 }
52795280#[test]
5281fn list_return_wrapper_unregistered_type_falls_back_to_bare_list() {
5282let entries: [ClassNameEntry; 0] = [];
5283let input = ".__MX_WRAP_LIST_RETURN_JsonValue__(.val)";
5284let output = resolve_list_wrappers(input, &entries);
5285assert_eq!(output, ".val");
5286 }
52875288// region: wrappers_file_content header (#1552)
52895290#[test]
5291fn wrappers_file_header_carries_version_and_body_digest() {
5292// FNV-1a 64 reference vectors: "" and "a".
5293assert_eq!(
5294crate::abi::fnv1a_64(b"", crate::abi::FNV1A_64_OFFSET),
52950xcbf29ce484222325
5296);
5297assert_eq!(
5298crate::abi::fnv1a_64(b"a", crate::abi::FNV1A_64_OFFSET),
52990xaf63dc4c8601ec8c
5300);
53015302let content = wrappers_file_content("a");
5303let mut lines = content.lines();
5304assert_eq!(lines.next(), Some(WRAPPERS_FILE_MARKER));
5305assert_eq!(
5306 lines.next(),
5307Some(concat!(
5308"# miniextendr-api ",
5309env!("CARGO_PKG_VERSION"),
5310" | fnv1a-64 of the normalized lines below: af63dc4c8601ec8c"
5311))
5312 );
5313assert_eq!(lines.next(), Some("a"));
5314assert_eq!(lines.next(), None);
5315 }
53165317#[test]
5318fn wrappers_file_header_ignores_source_position_shifts() {
5319let old = "# Generated from Rust fn `foo` (lib.rs:10:5)\nfoo <- function() 1\n";
5320let new = "# Generated from Rust fn `foo` (lib.rs:42:8)\nfoo <- function() 1\n";
5321let header = |s: &str| wrappers_file_content(s).lines().nth(1).unwrap().to_string();
5322assert_eq!(header(old), header(new));
5323assert!(wrappers_semantically_equal(
5324&wrappers_file_content(old),
5325&wrappers_file_content(new)
5326 ));
53275328let changed = "# Generated from Rust fn `foo` (lib.rs:10:5)\nfoo <- function() 2\n";
5329assert_ne!(header(old), header(changed));
5330 }
53315332// endregion
53335334 // region: normalize_source_locs unit tests (#528)
53355336#[test]
5337fn normalize_source_locs_noop_on_plain_text() {
5338// No `.rs:N:M` pattern — should return the input string unchanged (Borrowed).
5339let input = "# just a comment\nfoo <- function() {}\n";
5340let result = normalize_source_locs(input);
5341assert_eq!(result.as_ref(), input);
5342// Verify it's the zero-allocation borrowed path.
5343assert!(matches!(result, std::borrow::Cow::Borrowed(_)));
5344 }
53455346#[test]
5347fn normalize_source_locs_single_attribution() {
5348let input = "# Generated from Rust fn `foo` (lib.rs:42:8)";
5349let result = normalize_source_locs(input);
5350assert_eq!(
5351 result.as_ref(),
5352"# Generated from Rust fn `foo` (lib.rs:_:_)"
5353);
5354 }
53555356#[test]
5357fn normalize_source_locs_multiple_attributions() {
5358let input = concat!(
5359"# (conversions.rs:1:5)\n",
5360"foo <- function() {}\n",
5361"# (conversions.rs:158:8)\n",
5362"bar <- function() {}\n",
5363 );
5364let result = normalize_source_locs(input);
5365assert_eq!(
5366 result.as_ref(),
5367concat!(
5368"# (conversions.rs:_:_)\n",
5369"foo <- function() {}\n",
5370"# (conversions.rs:_:_)\n",
5371"bar <- function() {}\n",
5372 )
5373 );
5374 }
53755376#[test]
5377fn normalize_source_locs_does_not_match_extra_colon() {
5378// `(lib.rs:1:5:6)` — four components — is NOT a valid attribution; leave alone.
5379let input = "(lib.rs:1:5:6)";
5380let result = normalize_source_locs(input);
5381// The pattern `(lib.rs:1:5` matches, but then we expect `)` after the col
5382 // digits and find `:` instead — so it should NOT be replaced.
5383assert_eq!(result.as_ref(), input);
5384 }
53855386#[test]
5387fn normalize_source_locs_does_not_match_without_parens() {
5388// Bare `lib.rs:1:5` without surrounding parens — leave untouched.
5389let input = "lib.rs:1:5";
5390let result = normalize_source_locs(input);
5391assert_eq!(result.as_ref(), input);
5392assert!(matches!(result, std::borrow::Cow::Borrowed(_)));
5393 }
53945395#[test]
5396fn wrappers_semantically_equal_position_only_diff() {
5397// Two wrappers identical except for line:col are semantically equal.
5398let old = "# Generated from Rust fn `foo` (lib.rs:42:8)\nfoo <- function() {}\n";
5399let new = "# Generated from Rust fn `foo` (lib.rs:99:8)\nfoo <- function() {}\n";
5400assert!(wrappers_semantically_equal(old, new));
5401 }
54025403#[test]
5404fn wrappers_semantically_equal_content_diff() {
5405// A real semantic change (different function body) is NOT equal.
5406let old = "# Generated from Rust fn `foo` (lib.rs:42:8)\nfoo <- function() { 1L }\n";
5407let new = "# Generated from Rust fn `foo` (lib.rs:42:8)\nfoo <- function() { 2L }\n";
5408assert!(!wrappers_semantically_equal(old, new));
5409 }
54105411#[test]
5412fn wrappers_semantically_equal_both_position_and_content_diff() {
5413// Both positions and content differ — still NOT equal.
5414let old = "# Generated from Rust fn `foo` (lib.rs:1:1)\nfoo <- function() { 1L }\n";
5415let new = "# Generated from Rust fn `bar` (lib.rs:9:1)\nbar <- function() { 2L }\n";
5416assert!(!wrappers_semantically_equal(old, new));
5417 }
54185419// endregion
54205421 // region: generic doc marker tests
54225423#[test]
5424fn test_parse_generic_doc_marker_s7() {
5425let marker = r#".__MX_GENERIC_DOC__(kind="S7", generic="get_value", class="MyClass", export=true, dispatch="x", no_dots=false)"#;
5426let result = parse_generic_doc_marker(marker);
5427assert!(result.is_some());
5428let (kind, generic, class, export, dispatch, no_dots) = result.unwrap();
5429assert_eq!(kind, "S7");
5430assert_eq!(generic, "get_value");
5431assert_eq!(class, "MyClass");
5432assert!(export);
5433assert_eq!(dispatch, "x");
5434assert!(!no_dots);
5435 }
54365437#[test]
5438fn test_parse_generic_doc_marker_s4() {
5439let marker =
5440r#".__MX_GENERIC_DOC__(kind="S4", generic="s4_get", class="Counter", export=true)"#;
5441let result = parse_generic_doc_marker(marker);
5442assert!(result.is_some());
5443let (kind, generic, class, export, dispatch, no_dots) = result.unwrap();
5444assert_eq!(kind, "S4");
5445assert_eq!(generic, "s4_get");
5446assert_eq!(class, "Counter");
5447assert!(export);
5448assert_eq!(dispatch, "x"); // default
5449assert!(!no_dots); // default
5450}
54515452#[test]
5453fn test_resolve_generic_doc_markers_single_class() {
5454let input = concat!(
5455r#".__MX_GENERIC_DOC__(kind="S7", generic="get_value", class="MyClass", export=true, dispatch="x", no_dots=false)"#,
5456"\n",
5457"if (!exists(\"get_value\", mode = \"function\")) {\n",
5458" get_value <- S7::new_generic(\"get_value\", \"x\", function(x, ...) S7::S7_dispatch())\n",
5459"}\n",
5460 );
5461let output = resolve_generic_doc_markers(input.to_string());
54625463// Marker must be gone
5464assert!(!output.contains(".__MX_GENERIC_DOC__"));
5465// Standalone page must have bare @name
5466assert!(output.contains("#' @name get_value"));
5467// Must list the class
5468assert!(output.contains("\\link{MyClass}"));
5469// Must have NULL anchor
5470assert!(output.contains("\nNULL\n"));
5471// Generic guard must survive
5472assert!(output.contains("if (!exists(\"get_value\""));
5473 }
54745475#[test]
5476fn test_resolve_generic_doc_markers_two_classes_one_generic() {
5477// Two classes sharing one generic: both appear in the listing, only one doc page.
5478let input = concat!(
5479r#".__MX_GENERIC_DOC__(kind="S7", generic="get_value", class="ClassA", export=true, dispatch="x", no_dots=false)"#,
5480"\n",
5481"line_a\n",
5482r#".__MX_GENERIC_DOC__(kind="S7", generic="get_value", class="ClassB", export=true, dispatch="x", no_dots=false)"#,
5483"\n",
5484"line_b\n",
5485 );
5486let output = resolve_generic_doc_markers(input.to_string());
54875488// No markers remain
5489assert!(!output.contains(".__MX_GENERIC_DOC__"));
5490// Only one @name line
5491let name_count = output.matches("#' @name get_value").count();
5492assert_eq!(
5493 name_count, 1,
5494"expected exactly one @name get_value, got {name_count}"
5495);
5496// Both classes listed
5497assert!(output.contains("\\link{ClassA}"));
5498assert!(output.contains("\\link{ClassB}"));
5499// Other lines survive
5500assert!(output.contains("line_a"));
5501assert!(output.contains("line_b"));
5502 }
55035504#[test]
5505fn test_resolve_generic_doc_markers_external_generic_excluded() {
5506// External generics (has_generic_override) do NOT emit markers.
5507 // Verify that content without any markers passes through unchanged.
5508let input = "if (!exists(\"size\", mode = \"function\")) {\n size <- S7::new_external_generic(\"vctrs\", \"size\")\n}\n";
5509let output = resolve_generic_doc_markers(input.to_string());
5510// Content should be identical (the line-by-line reconstruction adds \n)
5511let output_trimmed: Vec<&str> = output.lines().collect();
5512let input_trimmed: Vec<&str> = input.lines().collect();
5513assert_eq!(output_trimmed, input_trimmed);
5514 }
55155516#[test]
5517fn test_resolve_generic_doc_markers_no_dots() {
5518let input = concat!(
5519r#".__MX_GENERIC_DOC__(kind="S7", generic="strict_fn", class="A", export=true, dispatch="x", no_dots=true)"#,
5520"\n",
5521 );
5522let output = resolve_generic_doc_markers(input.to_string());
5523// no_dots=true → @param ... must NOT appear in the standalone block
5524assert!(!output.contains("@param ..."));
5525// @param x must appear
5526assert!(output.contains("@param x"));
5527 }
55285529#[test]
5530fn test_resolve_generic_doc_markers_s4() {
5531let input = concat!(
5532r#".__MX_GENERIC_DOC__(kind="S4", generic="s4_compute", class="S4Counter", export=true)"#,
5533"\n",
5534"if (!methods::isGeneric(\"s4_compute\")) methods::setGeneric(\"s4_compute\", function(x, ...) standardGeneric(\"s4_compute\"))\n",
5535 );
5536let output = resolve_generic_doc_markers(input.to_string());
5537assert!(!output.contains(".__MX_GENERIC_DOC__"));
5538assert!(output.contains("#' @name s4_compute"));
5539assert!(output.contains("an S4 generic"));
5540assert!(output.contains("\\link{S4Counter}"));
5541// S4 export uses @exportMethod
5542assert!(output.contains("#' @exportMethod s4_compute"));
5543// setGeneric guard must survive
5544assert!(output.contains("if (!methods::isGeneric(\"s4_compute\")"));
5545 }
55465547#[test]
5548fn test_resolve_generic_doc_markers_quotes_operator_exports() {
5549for generic in ["[", "[[", "$", "%custom%"] {
5550let input = format!(
5551".__MX_GENERIC_DOC__(kind=\"S7\", generic=\"{generic}\", class=\"Foo\", export=true, dispatch=\"x\", no_dots=false)\n"
5552);
5553let output = resolve_generic_doc_markers(input);
5554// Must match the method block's directive (`naming::r_namespace_name`
5555 // in miniextendr-macros); `export([[)` would not parse.
5556assert!(
5557 output.contains(&format!("#' @rawNamespace export(\"{generic}\")")),
5558"{output}"
5559);
5560assert!(
5561 output.contains(&format!("#' @name {generic}\n")),
5562"{output}"
5563);
5564assert!(
5565 output.contains(&format!("#' The `{generic}` generic\n")),
5566"{output}"
5567);
5568 }
5569let output = resolve_generic_doc_markers(
5570r#".__MX_GENERIC_DOC__(kind="S7", generic="get", class="Foo", export=true, dispatch="x", no_dots=false)"#
5571.to_string(),
5572 );
5573assert!(
5574 output.contains("#' @rawNamespace export(get)\n"),
5575"{output}"
5576);
5577assert!(output.contains("#' The `get()` generic\n"), "{output}");
5578 }
55795580#[test]
5581fn test_is_syntactic_r_name_matches_r() {
5582for name in ["get", "var", ".hidden", "s7_trait_Ops_value", "a.b_c1"] {
5583assert!(is_syntactic_r_name(name), "{name}");
5584 }
5585for name in [
5586"[", "[[", "$", "%custom%", "Foo_[[", "if", "NA", ".1x", "1a", "_a", "",
5587 ] {
5588assert!(!is_syntactic_r_name(name), "{name}");
5589 }
5590assert_eq!(r_namespace_name(r#"a"b\c"#), r#""a\"b\\c""#);
5591 }
55925593// endregion
55945595 // region: duplicate wrapper-definition detection (#991)
55965597/// Build an accessor-producer map the way `write_r_wrappers_to_file` does,
5598 /// from synthetic sidecar + class registries, for unit testing.
5599fn accessor_map(
5600 sidecar: &[SidecarPropEntry],
5601 classes: &[ClassNameEntry],
5602 ) -> std::collections::HashMap<String, String> {
5603let class_index: std::collections::HashMap<&str, &ClassNameEntry> =
5604 classes.iter().map(|e| (e.rust_type, e)).collect();
5605 sidecar_accessor_names(sidecar, &class_index)
5606 }
56075608#[test]
5609fn parse_top_level_fn_def_name_accepts_bare_def() {
5610assert_eq!(
5611 parse_top_level_fn_def_name("Foo_get_value <- function(x) .Call(C, x)"),
5612Some("Foo_get_value")
5613 );
5614// Dotted/internal helper names are valid R identifiers.
5615assert_eq!(
5616 parse_top_level_fn_def_name(".miniextendr_raise_condition <- function(.val) {"),
5617Some(".miniextendr_raise_condition")
5618 );
5619 }
56205621#[test]
5622fn parse_top_level_fn_def_name_accepts_backticked_def() {
5623// Non-syntactic S3 method names are emitted backtick-quoted (#1475);
5624 // the scan sees the bare name so two `[.foo` definitions still collide.
5625assert_eq!(
5626 parse_top_level_fn_def_name("`[.mx_bag` <- function(x, i, ...) {"),
5627Some("[.mx_bag")
5628 );
5629assert_eq!(
5630 parse_top_level_fn_def_name("`$.mx_bag` <- function(x, name) {"),
5631Some("$.mx_bag")
5632 );
5633// Unterminated or empty quoting is not a definition we own.
5634assert_eq!(parse_top_level_fn_def_name("`[.foo <- function() {}"), None);
5635assert_eq!(parse_top_level_fn_def_name("`` <- function() {}"), None);
5636 }
56375638#[test]
5639fn detect_duplicate_wrapper_defs_sees_through_backticks() {
5640let content = "`[.foo` <- function(x, i) 1\n`[.foo` <- function(x, i) 2\n";
5641let err = detect_duplicate_wrapper_defs(content, &std::collections::HashMap::new())
5642 .expect_err("duplicate backticked definition must be reported");
5643assert!(err.contains("`[.foo`"), "{err}");
5644 }
56455646#[test]
5647fn has_page_assignment_tag_recognises_rdname_describein_and_name() {
5648assert!(has_page_assignment_tag(
5649"#' @rdname topic\nf <- function() 1"
5650));
5651assert!(has_page_assignment_tag(
5652"#' @describeIn topic Short text\n#' wrapped\nf <- function() 1"
5653));
5654// `@name topic` documents `topic.Rd` in roxygen2.
5655assert!(has_page_assignment_tag("#' @name topic\nf <- function() 1"));
5656// A `@name` with no topic is malformed and does not count.
5657assert!(!has_page_assignment_tag("#' @name\nf <- function() 1"));
5658assert!(!has_page_assignment_tag(
5659"#' @title T\n#' @export\nf <- function() 1"
5660));
5661// Prefix-only matches do not count.
5662assert!(!has_page_assignment_tag("#' @rdnamex\nf <- function() 1"));
5663 }
56645665// region: standalone-function pages (#1590)
56665667fn resolve_pages(fragments: &[(&str, Option<&str>)]) -> Vec<String> {
5668let fragments: Vec<StandaloneFragment<'_>> = fragments
5669 .iter()
5670 .map(|(content, stem)| StandaloneFragment {
5671 content,
5672 stem: stem.map(str::to_string),
5673 })
5674 .collect();
5675 resolve_standalone_pages(&fragments)
5676 }
56775678/// A generated `@param` line as the macro emits it.
5679fn filler(param: &str) -> String {
5680format!("#' {PARAM_FILLER_MARKER}@param {param} (no documentation available)")
5681 }
56825683fn filler_resolved(param: &str) -> String {
5684format!("#' @param {param} (no documentation available)")
5685 }
56865687/// The `.Rd` topic each fragment lands on, as `resolve_standalone_pages`
5688 /// reads it.
5689fn page_topics(fragments: &[(&str, Option<&str>)]) -> Vec<Option<String>> {
5690 fragments
5691 .iter()
5692 .map(|(content, stem)| {
5693 standalone_page(&StandaloneFragment {
5694 content,
5695 stem: stem.map(str::to_string),
5696 })
5697 .page
5698 })
5699 .collect()
5700 }
57015702/// Two free functions of one source file document a page each (#1289):
5703 /// no file-stem `@rdname` is injected, and each keeps the generated lines
5704 /// of the arguments it leaves undocumented, even an argument the other
5705 /// function documents.
5706#[test]
5707fn free_functions_of_one_file_get_a_page_each() {
5708let scale = format!(
5709"#' @title stem_scale\n#' @param values Numbers.\n{}\n#' @export\nstem_scale <- function(values, factor) 1",
5710 filler("factor")
5711 );
5712let floor = format!(
5713"#' @title stem_floor\n{}\n{}\n#' @export\nstem_floor <- function(values, floor) 1",
5714 filler("values"),
5715 filler("floor")
5716 );
5717let fragments = [
5718 (scale.as_str(), Some("stem_docs")),
5719 (&floor, Some("stem_docs")),
5720 ];
5721assert_eq!(
5722 page_topics(&fragments),
5723 [
5724Some("stem_scale".to_string()),
5725Some("stem_floor".to_string())
5726 ]
5727 );
5728let out = resolve_pages(&fragments);
5729for page in &out {
5730assert!(!page.contains("@rdname"), "{page}");
5731assert!(!page.contains(PARAM_FILLER_MARKER), "{page}");
5732assert!(!page.contains("@order"), "own page, no order: {page}");
5733 }
5734assert!(out[0].contains(&filler_resolved("factor")), "{}", out[0]);
5735assert!(out[1].contains(&filler_resolved("values")), "{}", out[1]);
5736assert!(out[1].contains(&filler_resolved("floor")), "{}", out[1]);
5737 }
57385739/// Grouping is opt-in through an explicit `@rdname <file stem>`. On that
5740 /// page a filler survives only for an argument no function on the page
5741 /// documents: roxygen2 keeps the later `@param` of a name, so a filler
5742 /// would replace the real description another block wrote. The blocks are
5743 /// the file's own page, so none of them gets `@order NaN`.
5744#[test]
5745fn explicit_stem_page_keeps_fillers_only_for_undocumented_arguments() {
5746let first = format!(
5747"#' @name stem_docs\n#' @rdname stem_docs\n#' @param values Numbers.\n{}\n#' @export\nstem_scale <- function(values, factor) 1",
5748 filler("factor")
5749 );
5750let second = format!(
5751"#' @rdname stem_docs\n#' @param offset Amount.\n{}\n#' @export\nstem_shift <- function(values, offset) 1",
5752 filler("values")
5753 );
5754let third = format!(
5755"#' @title stem_floor\n#' @rdname stem_docs\n{}\n{}\n#' @export\nstem_floor <- function(values, floor) 1",
5756 filler("values"),
5757 filler("floor")
5758 );
5759let fragments = [
5760 (first.as_str(), Some("stem_docs")),
5761 (&second, Some("stem_docs")),
5762 (&third, Some("stem_docs")),
5763 ];
5764assert!(
5765 page_topics(&fragments)
5766 .iter()
5767 .all(|page| page.as_deref() == Some("stem_docs"))
5768 );
5769let out = resolve_pages(&fragments);
5770for page in &out {
5771assert!(!page.contains(PARAM_FILLER_MARKER), "{page}");
5772assert!(!page.contains("@order"), "own page, no order: {page}");
5773assert_eq!(page.matches("#' @rdname").count(), 1, "{page}");
5774 }
5775assert!(out[0].contains(&filler_resolved("factor")), "{}", out[0]);
5776assert!(!out[1].contains("@param values"), "{}", out[1]);
5777assert!(!out[2].contains("@param values"), "{}", out[2]);
5778assert!(out[2].contains(&filler_resolved("floor")), "{}", out[2]);
5779 }
57805781/// The case that made a merged `.Rd` reach 3.7 MB and stall
5782 /// `R CMD INSTALL` (#1289): functions of one file with their own `@title`,
5783 /// the first defining a section the others `@inheritSection`. Each
5784 /// function keeps its own page, so the inheritance names another page
5785 /// instead of the merged page itself.
5786#[test]
5787fn titled_blocks_inheriting_a_section_keep_separate_pages() {
5788let first = "#' @title First setter\n#' @section Shared rules:\n#' Values are checked.\n#' @param x A value.\n#' @export\nfirst_setter <- function(x) 1";
5789let second = "#' @title Second setter\n#' @inheritSection first_setter Shared rules\n#' @param x A value.\n#' @export\nsecond_setter <- function(x) 1";
5790let third = "#' @title Third setter\n#' @inheritSection first_setter Shared rules\n#' @param x A value.\n#' @export\nthird_setter <- function(x) 1";
5791let fragments = [
5792 (first, Some("setters")),
5793 (second, Some("setters")),
5794 (third, Some("setters")),
5795 ];
5796assert_eq!(
5797 page_topics(&fragments),
5798 ["first_setter", "second_setter", "third_setter"].map(|t| Some(t.to_string()))
5799 );
5800// Nothing to resolve: the blocks are written as the macro emitted them.
5801assert_eq!(resolve_pages(&fragments), [first, second, third]);
5802 }
58035804/// A roxygen block that picks no page and has no `@title` gets the
5805 /// function name as its title, so roxygen2 renders its page; `lib.rs`
5806 /// functions (no stem) too. A block with no roxygen line, or `@noRd`,
5807 /// renders no page and gets none.
5808#[test]
5809fn untitled_own_page_blocks_get_the_function_name_as_title() {
5810let bare = "#' @export\nbare_fn <- function() 1";
5811let internal = "#' @keywords internal\ninternal_fn <- function() 1";
5812let undocumented = "plain_fn <- function() 1";
5813let no_rd = "#' @noRd\nhidden <- function() 1";
5814let out = resolve_pages(&[
5815 (bare, Some("helpers")),
5816 (internal, None),
5817 (undocumented, Some("helpers")),
5818 (no_rd, Some("helpers")),
5819 ]);
5820assert_eq!(
5821 out[0],
5822"#' @title bare_fn\n#' @export\nbare_fn <- function() 1"
5823);
5824assert_eq!(
5825 out[1],
5826"#' @title internal_fn\n#' @keywords internal\ninternal_fn <- function() 1"
5827);
5828assert_eq!(out[2], undocumented);
5829assert_eq!(out[3], no_rd);
5830 }
58315832/// A grouped `@param lower,upper` documents both names.
5833#[test]
5834fn grouped_param_names_count_as_documented() {
5835let a = "#' @rdname bounds\n#' @param lower,upper Bounds.\n#' @export\nf <- function(lower, upper) 1";
5836let b = format!(
5837"#' @rdname bounds\n{}\n{}\n#' @export\ng <- function(lower, upper) 1",
5838 filler("lower"),
5839 filler("upper")
5840 );
5841let out = resolve_pages(&[(a, Some("bounds")), (&b, Some("bounds"))]);
5842assert!(!out[1].contains("@param"), "{}", out[1]);
5843 }
58445845/// A block that joins a topic defined elsewhere drops its fillers (that
5846 /// topic's block documents the arguments) and sorts after the topic's own
5847 /// block, whatever the R file order. `@rdname` naming the function itself
5848 /// or its own `@name` is its own page.
5849#[test]
5850fn author_topic_blocks_drop_fillers_and_sort_last() {
5851let joined = format!(
5852"#' @title range_width\n#' @rdname range_summaries\n{}\n#' @export\nrange_width <- function(values) 1",
5853 filler("values")
5854 );
5855let own_fn = format!(
5856"#' @title mx_bag_sum\n#' @rdname mx_bag_sum\n{}\n#' @export\nmx_bag_sum <- function(x) 1",
5857 filler("x")
5858 );
5859let own_name = format!(
5860"#' @name bag_topic\n#' @rdname bag_topic\n#' @title Bags\n{}\nbag_len <- function(x) 1",
5861 filler("x")
5862 );
5863let ordered = "#' @rdname range_summaries\n#' @order 3\nrange_mid <- function(values) 1";
5864let out = resolve_pages(&[
5865 (&joined, Some("shared_param_docs")),
5866 (&own_fn, Some("s3_nonsyntactic_tests")),
5867 (&own_name, Some("bags")),
5868 (ordered, Some("shared_param_docs")),
5869 ]);
5870assert!(!out[0].contains("@param"), "{}", out[0]);
5871assert!(
5872 out[0].contains("#' @order NaN\n#' @export"),
5873"order inserted at a tag boundary: {}",
5874 out[0]
5875 );
5876for own in &out[1..3] {
5877assert!(own.contains(&filler_resolved("x")), "{own}");
5878assert!(!own.contains("@order"), "{own}");
5879 }
5880// The author's own `@order` is kept, not doubled.
5881assert_eq!(out[3].matches("@order").count(), 1, "{}", out[3]);
5882assert!(out[3].contains("#' @order 3"), "{}", out[3]);
5883 }
58845885/// `@describeIn` always joins another topic; an `@noRd` block has no page
5886 /// and keeps no fillers; `lib.rs` functions (no stem) keep theirs.
5887#[test]
5888fn describe_in_no_rd_and_stemless_blocks() {
5889let described = "#' @describeIn range_summaries Midpoint.\n#' wrapped\nrange_midpoint <- function(values) 1";
5890let no_rd = format!("{}\n#' @noRd\nhidden <- function(x) 1", filler("x"));
5891let stemless = format!(
5892"#' @title top\n{}\n#' @export\ntop <- function(x) 1",
5893 filler("x")
5894 );
5895let out = resolve_pages(&[
5896 (described, Some("shared_param_docs")),
5897 (&no_rd, Some("shared_param_docs")),
5898 (&stemless, None),
5899 ]);
5900assert_eq!(
5901 out[0],
5902"#' @describeIn range_summaries Midpoint.\n#' wrapped\n#' @order NaN\nrange_midpoint <- function(values) 1"
5903);
5904assert_eq!(out[1], "#' @noRd\nhidden <- function(x) 1");
5905assert!(out[2].contains(&filler_resolved("x")), "{}", out[2]);
5906assert!(!out[2].contains("@rdname"), "{}", out[2]);
5907 }
59085909// endregion
59105911#[test]
5912fn sort_wrapper_entries_is_priority_then_file_then_line() {
5913fn entry(
5914 priority: RWrapperPriority,
5915 source_file: &'static str,
5916 source_line: u32,
5917 content: &'static str,
5918 ) -> WrapperFragment {
5919 WrapperFragment {
5920 priority,
5921 content: std::borrow::Cow::Borrowed(content),
5922 source_file,
5923 source_line,
5924 }
5925 }
5926let mut entries = vec![
5927 entry(RWrapperPriority::Function, "b.rs", 10, "b10"),
5928 entry(RWrapperPriority::Function, "a.rs", 30, "a30"),
5929 entry(RWrapperPriority::Class, "z.rs", 1, "class"),
5930 entry(RWrapperPriority::Function, "a.rs", 5, "a5"),
5931 entry(RWrapperPriority::Function, "a.rs", 5, "a5-second"),
5932 ];
5933 sort_wrapper_entries(&mut entries);
5934let order: Vec<&str> = entries.iter().map(|e| e.content.as_ref()).collect();
5935// Classes first; then functions in file order, then source order; the
5936 // two entries at the same file:line keep their registration order.
5937assert_eq!(order, ["class", "a5", "a5-second", "a30", "b10"]);
5938 }
59395940#[test]
5941fn parse_top_level_fn_def_name_accepts_quoted_operators() {
5942for name in [
5943"[.Foo", "[[.Foo", "$.Foo", "==.Foo", "+.Foo", "[<-.Foo", r"a\`b\\c",
5944 ] {
5945let line = format!("`{name}` <- function(x, ...) {{}}");
5946assert_eq!(parse_top_level_fn_def_name(&line), Some(name));
5947 }
5948for line in [
5949"`obj`$method <- function() {}",
5950"`obj`[[1]] <- function() {}",
5951"`unclosed <- function() {}",
5952"`value` <- 1L",
5953 ] {
5954assert_eq!(parse_top_level_fn_def_name(line), None);
5955 }
5956 }
59575958#[test]
5959fn parse_top_level_fn_def_name_reads_escaped_backslashes() {
5960// `naming::r_def_name` (miniextendr-macros) writes `a\b` as `` `a\\b` ``
5961 // and `trail\` as `` `trail\\` ``: R reads backslash escapes inside
5962 // backticks. The scan returns that escaped spelling, and the doubled
5963 // backslash before the closing backtick must not hide it.
5964for (line, name) in [
5965 (r"`a\\b` <- function(x) {}", r"a\\b"),
5966 (r"`trail\\` <- function(x) {}", r"trail\\"),
5967 (r"`x\\\`y` <- function(x) {}", r"x\\\`y"),
5968 ] {
5969assert_eq!(parse_top_level_fn_def_name(line), Some(name), "{line}");
5970 }
5971// The pre-escaping spelling of `trail\` never closed its backtick.
5972assert_eq!(
5973 parse_top_level_fn_def_name(r"`trail\` <- function(x) {}"),
5974None
5975);
5976let map = std::collections::HashMap::new();
5977let twice = "`a\\\\b` <- function() 1\n`a\\\\b` <- function() 2\n";
5978assert!(detect_duplicate_wrapper_defs(twice, &map).is_err());
5979 }
59805981#[test]
5982fn detect_duplicate_quoted_operator_defs() {
5983let map = std::collections::HashMap::new();
5984let content = "`[.Foo` <- function(x, ...) {}\n`[.Foo` <- function(x, ...) {}\n";
5985let err = detect_duplicate_wrapper_defs(content, &map).unwrap_err();
5986assert!(err.contains("`[.Foo` is defined more than once"), "{err}");
5987let mixed = "foo <- function() {}\n`foo` <- function() {}\n";
5988assert!(detect_duplicate_wrapper_defs(mixed, &map).is_err());
5989let distinct = "`[.Foo` <- function(x, ...) {}\n`[[.Foo` <- function(x, ...) {}\n";
5990assert!(detect_duplicate_wrapper_defs(distinct, &map).is_ok());
5991 }
59925993#[test]
5994fn parse_top_level_fn_def_name_rejects_non_defs() {
5995// Indented closures (R6 methods, S7 getters) are not top-level wrappers.
5996assert_eq!(
5997 parse_top_level_fn_def_name(" get_value = function(self) self$x"),
5998None
5999);
6000// S7::method(...) <- function : LHS is a call, not a bare ident.
6001assert_eq!(
6002 parse_top_level_fn_def_name("S7::method(get_value, Shape) <- function(x) .Call(C, x)"),
6003None
6004);
6005// `$`-assignment target is not a bare ident.
6006assert_eq!(
6007 parse_top_level_fn_def_name("obj$method <- function() {}"),
6008None
6009);
6010// Not a function definition at all.
6011assert_eq!(parse_top_level_fn_def_name("x <- 1L"), None);
6012// Comment / arbitrary line.
6013assert_eq!(parse_top_level_fn_def_name("# a comment"), None);
6014 }
60156016/// The preamble ships in every package's `R/` and source tarball, where a
6017 /// bare `#1548` reads as that package's own issue, so its R text cites
6018 /// none (#1832). The citations live in the Rust comments around it.
6019#[test]
6020fn preamble_cites_no_issue_numbers() {
6021let binding = format_conversion_error_class_binding(&["pkg_error"]);
6022for (name, text) in [
6023 ("WRAPPERS_PREAMBLE_HEADER", WRAPPERS_PREAMBLE_HEADER),
6024 ("RAISE_CONDITION_HELPER_FN", RAISE_CONDITION_HELPER_FN),
6025 (
6026"WRAPPER_RAISE_CONDITION_HELPER_FN",
6027 WRAPPER_RAISE_CONDITION_HELPER_FN,
6028 ),
6029 ("CALLER_CALL_HELPER", CALLER_CALL_HELPER),
6030 ("FRAME_CALL_HELPER", FRAME_CALL_HELPER),
6031 ("FIELD_AT_HELPER", FIELD_AT_HELPER),
6032 ("ENV_METHOD_HELPER", ENV_METHOD_HELPER),
6033 ("ARG_ERROR_HELPER", ARG_ERROR_HELPER),
6034 ("DOTS_CHECK_HELPER", DOTS_CHECK_HELPER),
6035 ("MATCH_ARG_SEVERAL_HELPER", MATCH_ARG_SEVERAL_HELPER),
6036 ("MATCH_ARG_HELPER", MATCH_ARG_HELPER),
6037 ("the conversion_error_class binding", binding.as_str()),
6038 ] {
6039for line in text.lines() {
6040let cites = line
6041 .as_bytes()
6042 .windows(2)
6043 .any(|w| w[0] == b'#' && w[1].is_ascii_digit());
6044assert!(!cites, "{name} cites an issue number: {line}");
6045 }
6046 }
6047 }
60486049/// The preamble's caller-call helper (#1613) takes the wrapper's `.call`
6050 /// as an argument defaulting to `NULL` (an S3 method calls it without
6051 /// one) and `own`, which a `call_arg` wrapper sets so that a frame no
6052 /// closure owns gives its own call, not its caller's (#1834). It is the
6053 /// only top-level definition in its block, so the duplicate-name scan
6054 /// sees it once.
6055#[test]
6056fn caller_call_helper_takes_the_call_formal() {
6057let defs: Vec<&str> = CALLER_CALL_HELPER
6058 .lines()
6059 .filter_map(parse_top_level_fn_def_name)
6060 .collect();
6061assert_eq!(defs, [".miniextendr_caller_call"]);
6062assert!(
6063 CALLER_CALL_HELPER
6064 .contains(".miniextendr_caller_call <- function(call = NULL, own = FALSE) {")
6065 );
6066// `own` skips the caller lookup.
6067assert!(CALLER_CALL_HELPER.contains(" if (!own) {\n parent <- sys.parent(2L)"));
6068// The fallback call leaves the plumbing formal out.
6069assert!(CALLER_CALL_HELPER.contains("call$.call <- NULL"));
6070// Every call it returns is the call as written (`sys.call()`), never
6071 // a matched one.
6072assert!(!CALLER_CALL_HELPER.contains("match.call"));
6073assert!(CALLER_CALL_HELPER.ends_with("}\n"));
6074 }
60756076/// The preamble's frame-call helper (#1851) binds the bare
6077 /// `frame_call_helper_fn!()` expression, the same text the raise helper
6078 /// inlines for its base-namespace copy, and `.miniextendr_arg_error`
6079 /// resolves a frame with it before raising.
6080#[test]
6081fn frame_call_helper_is_shared_by_the_raise_helper_and_the_checks() {
6082let fn_text = frame_call_helper_fn!();
6083let defs: Vec<&str> = FRAME_CALL_HELPER
6084 .lines()
6085 .filter_map(parse_top_level_fn_def_name)
6086 .collect();
6087assert_eq!(defs, [".miniextendr_frame_call"]);
6088assert!(FRAME_CALL_HELPER.ends_with(&format!(".miniextendr_frame_call <- {fn_text}\n")));
6089assert!(RAISE_CONDITION_HELPER_FN.contains(&format!(
6090"if (is.environment(.call)) .call <- ({fn_text})(.call)\n"
6091)));
6092assert!(
6093 WRAPPER_RAISE_CONDITION_HELPER_FN
6094 .contains("if (is.environment(.call)) .call <- .miniextendr_frame_call(.call)\n")
6095 );
6096assert!(!WRAPPER_RAISE_CONDITION_HELPER_FN.contains(fn_text));
6097// No comment inside the shared text: it is inlined into the raise
6098 // helper, whose own comments sit outside it.
6099assert!(!fn_text.contains('#'));
6100// The generic's call replaces the method's; a replacement generic's
6101 // call is the assignment, with the value as the symbol `value`.
6102assert!(fn_text.contains("generic <- env[[\".Generic\"]]"));
6103assert!(fn_text.contains("call(\"<-\", call, quote(value))"));
6104assert!(
6105 ARG_ERROR_HELPER
6106 .contains(" if (is.environment(call)) call <- .miniextendr_frame_call(call)\n")
6107 );
6108 }
61096110/// The preamble's field-position helper (#1891) and the bad-index error
6111 /// (#1920) are the two definitions in their block. A position is
6112 /// truncated as `[[` truncates it on a list; one that picks no field
6113 /// (past the last, `0`, negative) raises R's own
6114 /// `subscriptOutOfBoundsError` with the type's `field_error` classes in
6115 /// front (#1899), reporting the generic's call resolved from the method's
6116 /// frame. An index that is no name or position raises the `field_error`
6117 /// classes, then `notSubsettableError`, on that call.
6118#[test]
6119fn field_at_helper_picks_a_field_as_a_list_does() {
6120let defs: Vec<&str> = FIELD_AT_HELPER
6121 .lines()
6122 .filter_map(parse_top_level_fn_def_name)
6123 .collect();
6124assert_eq!(
6125 defs,
6126 [".miniextendr_field_at", ".miniextendr_field_index_error"]
6127 );
6128assert!(FIELD_AT_HELPER.contains(
6129".miniextendr_field_index_error <- function(cls, env, class = NULL, strict = FALSE) {"
6130));
6131assert!(FIELD_AT_HELPER.contains(
6132"class = c(class, \"notSubsettableError\"), call = .miniextendr_frame_call(env)))"
6133));
6134assert!(
6135 FIELD_AT_HELPER
6136 .contains(".miniextendr_field_at <- function(fields, i, env, class = NULL) {")
6137 );
6138assert!(FIELD_AT_HELPER.contains(
6139"if (k < 1 || k > length(fields)) stop(errorCondition(\"subscript out of bounds\", \
6140 class = c(class, \"subscriptOutOfBoundsError\"), call = .miniextendr_frame_call(env)))"
6141));
6142assert!(FIELD_AT_HELPER.contains(" fields[[k]]\n"));
6143assert!(FIELD_AT_HELPER.starts_with("# Internal helper"));
6144assert!(FIELD_AT_HELPER.ends_with("}\n"));
6145 }
61466147/// The preamble's class binding (#1591): the crate classes as an R
6148 /// character vector, escaped, or `NULL` when the crate sets none. It is a
6149 /// plain assignment, not a function definition, so the duplicate-name
6150 /// scan skips it.
6151#[test]
6152fn conversion_error_class_binding_quotes_or_nulls() {
6153let last_line = |classes: &[&str]| {
6154 format_conversion_error_class_binding(classes)
6155 .lines()
6156 .last()
6157 .unwrap()
6158 .to_string()
6159 };
6160assert_eq!(
6161 last_line(&[]),
6162".miniextendr_conversion_error_class <- NULL"
6163);
6164assert_eq!(
6165 last_line(&["pkg_error_argument", "pkg_error"]),
6166".miniextendr_conversion_error_class <- c(\"pkg_error_argument\", \"pkg_error\")"
6167);
6168assert_eq!(
6169 last_line(&["a\"b\\c"]),
6170".miniextendr_conversion_error_class <- c(\"a\\\"b\\\\c\")"
6171);
6172let binding = format_conversion_error_class_binding(&["pkg_error"]);
6173assert!(
6174 binding
6175 .lines()
6176 .all(|l| parse_top_level_fn_def_name(l).is_none())
6177 );
6178 }
61796180/// The raise helper resolves `arg_error!`'s class marker (#1740) against
6181 /// the binding the preamble defines under the same name, and only inside
6182 /// the `if`, so the base-namespace copy that signals deferred conditions
6183 /// never needs the binding.
6184#[test]
6185fn raise_helper_resolves_the_conversion_class_marker() {
6186let marker = crate::condition::CONVERSION_ERROR_CLASS_MARKER;
6187assert_eq!(
6188format!("{marker} <- NULL"),
6189 format_conversion_error_class_binding(&[])
6190 .lines()
6191 .last()
6192 .unwrap()
6193 );
6194let lookup = format!(".at <- match(\"{marker}\", .class)");
6195let resolve = format!("get0(\"{marker}\", ifnotfound = NULL)");
6196let at = RAISE_CONDITION_HELPER_FN
6197 .find(&lookup)
6198 .expect("marker lookup");
6199let at_resolve = RAISE_CONDITION_HELPER_FN
6200 .find(&resolve)
6201 .expect("resolution");
6202let at_switch = RAISE_CONDITION_HELPER_FN.find("switch(.val$kind").unwrap();
6203assert!(at < at_resolve && at_resolve < at_switch);
6204// The bare binding name appears only as a string (no direct lookup).
6205assert_eq!(
6206 RAISE_CONDITION_HELPER_FN.matches(marker).count(),
6207 RAISE_CONDITION_HELPER_FN
6208 .matches(&format!("\"{marker}\""))
6209 .count()
6210 );
6211 }
62126213/// A `match_arg` check as the macro writes it, for `splice_match_arg_entry`.
6214const ALIASES_CHECK: &str = "f <- function(mode = .__MX_MATCH_ARG_CHOICES_f_mode__) {\n \
6215 mode <- .miniextendr_match_arg(mode, .__MX_MATCH_ARG_CHOICES_f_mode__, \"mode\", \
6216 .__MX_MATCH_ARG_ALIASES_f_mode__)\n}";
62176218fn aliases_entry(
6219 choices: &'static [&'static str],
6220 aliases: &'static [MatchArgAlias],
6221 ) -> MatchArgChoicesEntry {
6222 MatchArgChoicesEntry {
6223 placeholder: ".__MX_MATCH_ARG_CHOICES_f_mode__",
6224 choices,
6225 preferred_default: "",
6226 aliases_placeholder: ".__MX_MATCH_ARG_ALIASES_f_mode__",
6227 aliases,
6228 }
6229 }
62306231/// A type without aliases gets the check it had before aliases existed;
6232 /// one with aliases gets the `aliases =` argument, after the others
6233 /// (#1843), then the clause naming the listed ones (#2148), which a type
6234 /// whose aliases are all unlisted does without.
6235#[test]
6236fn splice_match_arg_entry_writes_the_aliases_argument() {
6237assert_eq!(
6238 splice_match_arg_entry(ALIASES_CHECK, &aliases_entry(&["red", "gray"], &[])),
6239"f <- function(mode = c(\"red\", \"gray\")) {\n \
6240 mode <- .miniextendr_match_arg(mode, c(\"red\", \"gray\"), \"mode\")\n}"
6241);
6242assert_eq!(
6243 splice_match_arg_entry(
6244 ALIASES_CHECK,
6245&aliases_entry(&["red", "gray"], {
6246const LISTED: &[MatchArgAlias] = &[
6247 MatchArgAlias::new("grey", "gray"),
6248 MatchArgAlias::new("rouge", "red"),
6249 ];
6250 LISTED
6251 })
6252 ),
6253"f <- function(mode = c(\"red\", \"gray\")) {\n \
6254 mode <- .miniextendr_match_arg(mode, c(\"red\", \"gray\"), \"mode\", \
6255 aliases = base::c(\"grey\" = \"gray\", \"rouge\" = \"red\"), \
6256 listed = \" (or \\\"rouge\\\" for \\\"red\\\", \\\"grey\\\" for \\\"gray\\\")\")\n}"
6257);
6258assert_eq!(
6259 splice_match_arg_entry(
6260 ALIASES_CHECK,
6261&aliases_entry(&["red", "gray"], {
6262const UNLISTED: &[MatchArgAlias] = &[MatchArgAlias::unlisted("grey", "gray")];
6263 UNLISTED
6264 })
6265 ),
6266"f <- function(mode = c(\"red\", \"gray\")) {\n \
6267 mode <- .miniextendr_match_arg(mode, c(\"red\", \"gray\"), \"mode\", \
6268 aliases = base::c(\"grey\" = \"gray\"))\n}"
6269);
6270// With a call argument before it, as under `call = caller`.
6271let caller = ALIASES_CHECK.replace("\"mode\", .__MX", "\"mode\", .mx_call, .__MX");
6272assert!(
6273 splice_match_arg_entry(&caller, &aliases_entry(&["red", "gray"], &[]))
6274 .contains("c(\"red\", \"gray\"), \"mode\", .mx_call)\n")
6275 );
6276 }
62776278/// Aliases and choices are escaped for an R string literal, and so is
6279 /// the clause, which quotes them as the R check quotes the choices.
6280#[test]
6281fn match_arg_aliases_arg_escapes() {
6282assert_eq!(match_arg_aliases_arg(&["a"], &[], "p"), "");
6283assert_eq!(
6284 match_arg_aliases_arg(
6285&["say \"hi\"", r"c:\path"],
6286&[
6287 MatchArgAlias::unlisted("hello", "say \"hi\""),
6288 MatchArgAlias::new(r"d:\", r"c:\path")
6289 ],
6290"p"
6291),
6292r#", aliases = base::c("hello" = "say \"hi\"", "d:\\" = "c:\\path"), listed = " (or \"d:\\\" for \"c:\\path\")""#
6293);
6294 }
62956296/// The wrappers writer refuses the aliases of a hand-written impl that the
6297 /// derive refuses at compile time, naming the placeholder, every problem
6298 /// at once.
6299#[test]
6300fn match_arg_aliases_arg_refuses_bad_aliases() {
6301let refused = |aliases: &'static [MatchArgAlias]| {
6302 std::panic::catch_unwind(|| {
6303 match_arg_aliases_arg(&["red", "gray", "green"], aliases, ".__MX_ALIASES_p__")
6304 })
6305 .expect_err("refused")
6306 .downcast::<String>()
6307 .map(|m| *m)
6308 .unwrap()
6309 };
6310const BAD: &[MatchArgAlias] = &[
6311 MatchArgAlias::new("", "red"),
6312 MatchArgAlias::new("NA", "red"),
6313 MatchArgAlias::new("red", "gray"),
6314 MatchArgAlias::new("grey", "gray"),
6315 MatchArgAlias::unlisted("grey", "gray"),
6316 MatchArgAlias::new("blue", "navy"),
6317 MatchArgAlias::new("gr", "green"),
6318 ];
6319let message = refused(BAD);
6320assert!(message.contains(".__MX_ALIASES_p__"), "{message}");
6321for problem in [
6322"an alias can't be empty",
6323"alias \"NA\" would read as a missing value",
6324"alias \"red\" is already a choice",
6325"alias \"grey\" is given more than once",
6326"alias \"blue\" names \"navy\", which is not a choice",
6327"alias \"gr\" for \"green\" is a prefix of the choice \"gray\"",
6328 ] {
6329assert!(message.contains(problem), "{problem:?} in {message}");
6330 }
6331// A prefix of its own choice only is fine.
6332assert_eq!(
6333 match_arg_aliases_arg(
6334&["red", "gray"],
6335&[MatchArgAlias::unlisted("gr", "gray")],
6336"p"
6337),
6338r#", aliases = base::c("gr" = "gray")"#
6339);
6340 }
63416342/// The argument-check helpers define exactly the three preamble functions
6343 /// and end on a closing brace.
6344#[test]
6345fn arg_check_helpers_define_the_three_functions() {
6346let defs: Vec<&str> = ARG_CHECK_HELPERS
6347 .iter()
6348 .flat_map(|helper| helper.lines())
6349 .filter_map(parse_top_level_fn_def_name)
6350 .collect();
6351assert_eq!(
6352 defs,
6353 [
6354".miniextendr_arg_error",
6355".miniextendr_match_arg_several",
6356".miniextendr_match_arg"
6357]
6358 );
6359for helper in ARG_CHECK_HELPERS {
6360assert!(helper.starts_with("# Internal helper"));
6361assert!(helper.ends_with("}\n"));
6362 }
6363 }
63646365#[test]
6366fn detect_duplicate_clean_content_passes() {
6367// A sidecar accessor and an unrelated S7 shortcut: distinct names, no clash.
6368let content = concat!(
6369"Shape_get_area <- function(x) .Call(C_get, x)\n",
6370"Shape_set_area <- function(x, value) { .Call(C_set, x, value); invisible(x) }\n",
6371"Shape_describe <- function(x, ...) .Call(C_describe, x)\n",
6372 );
6373let map = accessor_map(
6374&[SidecarPropEntry {
6375 rust_type: "Shape",
6376 field_name: "area",
6377 prop_doc: "",
6378 }],
6379&[ClassNameEntry {
6380 rust_type: "Shape",
6381 r_class_name: "Shape",
6382 class_system: "s7",
6383 field_syntax: false,
6384 }],
6385 );
6386assert!(detect_duplicate_wrapper_defs(content, &map).is_ok());
6387 }
63886389#[test]
6390fn detect_duplicate_shortcut_vs_sidecar_accessor_fails() {
6391// S7 method `get_area` emits `Shape_get_area`, colliding with the sidecar
6392 // accessor for field `area`. This is the core #991 case.
6393let content = concat!(
6394"Shape_get_area <- function(x) .Call(C_get, x)\n",
6395"Shape_set_area <- function(x, value) { .Call(C_set, x, value); invisible(x) }\n",
6396"Shape_get_area <- function(x, ...) .Call(C_method, x)\n",
6397 );
6398let map = accessor_map(
6399&[SidecarPropEntry {
6400 rust_type: "Shape",
6401 field_name: "area",
6402 prop_doc: "",
6403 }],
6404&[ClassNameEntry {
6405 rust_type: "Shape",
6406 r_class_name: "Shape",
6407 class_system: "s7",
6408 field_syntax: false,
6409 }],
6410 );
6411let err = detect_duplicate_wrapper_defs(content, &map).unwrap_err();
6412assert!(err.contains("Shape_get_area"), "msg: {err}");
6413// Message must name the sidecar producer and the field.
6414assert!(err.contains("sidecar field `area`"), "msg: {err}");
6415assert!(err.contains("Shape"), "msg: {err}");
6416 }
64176418#[test]
6419fn detect_duplicate_honours_class_override_prefix() {
6420// The impl block set `class = "Shape"` on Rust type `S7Shape`, so the
6421 // sidecar accessor is `Shape_get_area`, not `S7Shape_get_area`. The
6422 // collision must be attributed via the override prefix.
6423let content = concat!(
6424"Shape_get_area <- function(x) .Call(C_get, x)\n",
6425"Shape_get_area <- function(x, ...) .Call(C_method, x)\n",
6426 );
6427let map = accessor_map(
6428&[SidecarPropEntry {
6429 rust_type: "S7Shape",
6430 field_name: "area",
6431 prop_doc: "",
6432 }],
6433&[ClassNameEntry {
6434 rust_type: "S7Shape",
6435 r_class_name: "Shape",
6436 class_system: "s7",
6437 field_syntax: false,
6438 }],
6439 );
6440let err = detect_duplicate_wrapper_defs(content, &map).unwrap_err();
6441assert!(err.contains("Shape_get_area"), "msg: {err}");
6442assert!(err.contains("sidecar field `area`"), "msg: {err}");
6443 }
64446445#[test]
6446fn duplicate_class_and_function_bindings_fail_in_either_order() {
6447for class in [
6448"Widget <- new.env(parent = emptyenv())",
6449"Widget <- R6::R6Class(\"Widget\")",
6450"Widget <- S7::new_class(\"Widget\")",
6451"`Widget` <- new.env(parent = emptyenv())",
6452 ] {
6453let class = format!("# Generated from Rust impl `Widget` (lib.rs:10:6)\n{class}\n");
6454let function = "# Generated from Rust fn `widget` (lib.rs:30:8)\n\
6455 Widget <- function() .Call(C_widget)\n";
6456for content in [format!("{class}{function}"), format!("{function}{class}")] {
6457let error =
6458 detect_duplicate_wrapper_defs(&content, &std::collections::HashMap::new())
6459 .expect_err("a function must not replace a class or namespace object");
6460assert!(error.contains("`Widget`"), "{error}");
6461assert!(error.contains("class/namespace object"), "{error}");
6462assert!(error.contains("r_name"), "{error}");
6463assert!(
6464 error.contains("Rust impl `Widget` (lib.rs:10:6)"),
6465"{error}"
6466);
6467assert!(error.contains("Rust fn `widget` (lib.rs:30:8)"), "{error}");
6468 }
6469 }
6470 }
64716472/// Two class objects of one name in the written file: the second would
6473 /// drop the first one's methods (#1677, #1776).
6474#[test]
6475fn duplicate_class_objects_name_both_producers() {
6476for class in [
6477"Widget <- new.env(parent = emptyenv())",
6478"Widget <- R6::R6Class(\"Widget\",",
6479"Widget <- S7::new_class(\"Widget\",",
6480 ] {
6481let content = format!(
6482"# Generated from Rust impl `Widget` (lib.rs:10:6)\n{class}\n\
6483 Widget$new <- function() .Call(C_new)\n\
6484 # Generated from Rust impl `Widget` (lib.rs:40:6)\n{class}\n"
6485);
6486let error = detect_duplicate_wrapper_defs(&content, &std::collections::HashMap::new())
6487 .expect_err("a second class object drops the first one's methods");
6488assert!(
6489 error.contains("class `Widget` is defined more than once"),
6490"{error}"
6491);
6492assert!(error.contains("(lib.rs:10:6)"), "{error}");
6493assert!(error.contains("(lib.rs:40:6)"), "{error}");
6494assert!(error.contains("distinct `class = \"...\"`"), "{error}");
6495 }
6496 }
64976498#[test]
6499fn duplicate_scan_keeps_class_members_and_s4_constructors_separate() {
6500let content = concat!(
6501"Widget <- new.env(parent = emptyenv())\n",
6502"Widget$new <- function() .Call(C_new)\n",
6503"Widget$get <- function() .Call(C_get)\n",
6504"methods::setClass(\"S4Widget\", slots = c(ptr = \"externalptr\"))\n",
6505"S4Widget <- function() .Call(C_new_s4)\n",
6506"R6Widget <- R6::R6Class(\"R6Widget\",\n",
6507" public = list(\n",
6508" initialize = function() {}\n",
6509" )\n",
6510")\n",
6511"S7Widget <- S7::new_class(\"S7Widget\",\n",
6512" constructor = function() {}\n",
6513")\n",
6514"if (!base::exists(\"shared\", mode = \"function\")) {\n",
6515" shared <- function(x, ...) UseMethod(\"shared\")\n",
6516"}\n",
6517"if (!base::exists(\"shared\", mode = \"function\")) {\n",
6518" shared <- function(x, ...) UseMethod(\"shared\")\n",
6519"}\n",
6520 );
6521assert!(detect_duplicate_wrapper_defs(content, &std::collections::HashMap::new()).is_ok());
6522 }
65236524#[test]
6525fn guarded_generics_cannot_collide_with_plain_bindings() {
6526for generic in [
6527concat!(
6528"if (!base::exists(\"probe\", mode = \"function\")) {\n",
6529" probe <- function(x, ...) UseMethod(\"probe\")\n",
6530"}\n",
6531 ),
6532concat!(
6533"if (!base::exists(\"probe\", mode = \"function\")) {\n",
6534" probe <- function(x, ...) base::UseMethod(\"probe\")\n",
6535"}\n",
6536 ),
6537concat!(
6538"if (!exists(\"probe\", where = topenv(environment()), inherits = FALSE)) ",
6539"methods::setGeneric(\"probe\", function(x, ...) standardGeneric(\"probe\"))\n",
6540 ),
6541concat!(
6542"if (!base::exists(\"probe\", mode = \"function\")) {\n",
6543" # No existing binding; define a plain package-local S7 generic.\n",
6544" probe <- S7::new_generic(\"probe\", \"x\", function(x, ...) S7::S7_dispatch())\n",
6545"}\n",
6546 ),
6547concat!(
6548"if (!exists(\"probe\", mode = \"function\")) {\n",
6549" probe <- S7::new_generic(\"probe\", \"x\", function(x, ...) S7::S7_dispatch())\n",
6550"}\n",
6551 ),
6552concat!(
6553"if (!base::inherits(base::get0(\"probe\", inherits = FALSE), \"S7_external_generic\")) {\n",
6554" probe <- S7::new_external_generic(\"pkg\", \"probe\", \"x\")\n",
6555"}\n",
6556 ),
6557 ] {
6558// The same generic may be emitted by many inherent/trait impls.
6559let repeated = format!("{generic}{generic}");
6560assert!(
6561 detect_duplicate_wrapper_defs(&repeated, &std::collections::HashMap::new()).is_ok(),
6562"{repeated}"
6563);
6564let generic = format!("# Generated from Rust impl `Shape` (shape.rs:3:6)\n{generic}");
6565for binding in [
6566"# Generated from Rust fn `probe` (probe.rs:7:8)\n\
6567 probe <- function(x) .Call(C_probe, x)\n",
6568"# Generated from Rust impl `probe` (probe.rs:7:8)\n\
6569 probe <- new.env(parent = emptyenv())\n",
6570 ] {
6571// Classes precede free functions; trait impls follow them.
6572for content in [format!("{generic}{binding}"), format!("{binding}{generic}")] {
6573let error =
6574 detect_duplicate_wrapper_defs(&content, &std::collections::HashMap::new())
6575 .expect_err("a generated binding must not mask a generic");
6576assert!(error.contains("`probe`"), "{error}");
6577assert!(error.contains("guarded generic"), "{error}");
6578assert!(error.contains("r_name"), "{error}");
6579assert!(error.contains("`Shape` (shape.rs:3:6)"), "{error}");
6580assert!(error.contains("`probe` (probe.rs:7:8)"), "{error}");
6581 }
6582 }
6583 }
6584 }
65856586/// A generic shared by several classes is reported with the class that
6587 /// declared it first, and a sidecar accessor with its field and type.
6588#[test]
6589fn duplicate_scan_names_first_generic_and_sidecar_producers() {
6590let generic = "if (!base::exists(\"Shape_get_area\", mode = \"function\")) {\n \
6591 Shape_get_area <- function(x, ...) UseMethod(\"Shape_get_area\")\n}\n";
6592let content = format!(
6593"# Generated from Rust source line 4:5\n\
6594 # Wraps sidecar field `area` on Rust type `Shape` via `C_get` and `C_set`.\n\
6595 Shape_get_area <- function(x) .Call(C_get, x)\n\
6596 # Generated from Rust impl `Circle` (circle.rs:9:6)\n{generic}\
6597 # Generated from Rust impl `Square` (square.rs:9:6)\n{generic}"
6598);
6599let error = detect_duplicate_wrapper_defs(&content, &std::collections::HashMap::new())
6600 .expect_err("the generic would replace the sidecar accessor");
6601assert!(
6602 error.contains(
6603"function from sidecar field `area` on Rust type `Shape` (Rust source line 4:5)"
6604),
6605"{error}"
6606);
6607assert!(
6608 error.contains("guarded generic from Rust impl `Circle` (circle.rs:9:6)"),
6609"{error}"
6610);
6611assert!(!error.contains("Square"), "{error}");
66126613// Reported with the first declaration when the plain binding follows.
6614let content = format!(
6615"# Generated from Rust impl `Circle` (circle.rs:9:6)\n{generic}\
6616 # Generated from Rust impl `Square` (square.rs:9:6)\n{generic}\
6617 # Generated from Rust fn `Shape_get_area` (lib.rs:1:8)\n\
6618 Shape_get_area <- function(x) .Call(C_area, x)\n"
6619);
6620let error = detect_duplicate_wrapper_defs(&content, &std::collections::HashMap::new())
6621 .expect_err("the function would replace the generic");
6622assert!(error.contains("`Circle` (circle.rs:9:6)"), "{error}");
6623assert!(
6624 error.contains("Rust fn `Shape_get_area` (lib.rs:1:8)"),
6625"{error}"
6626);
6627assert!(!error.contains("Square"), "{error}");
6628 }
66296630#[test]
6631fn guarded_generics_recognize_quoted_operator_names() {
6632let generic = concat!(
6633"if (!base::exists(\"%probe%\", mode = \"function\")) {\n",
6634" `%probe%` <- function(x, ...) UseMethod(\"%probe%\")\n",
6635"}\n",
6636 );
6637let content = format!("{generic}`%probe%` <- function(x, y) .Call(C_probe, x, y)\n");
6638let error = detect_duplicate_wrapper_defs(&content, &std::collections::HashMap::new())
6639 .expect_err("quoted assignment names must match the generic name");
6640assert!(error.contains("`%probe%`"), "{error}");
6641 }
66426643#[test]
6644fn duplicate_scan_ignores_local_and_non_generic_guards() {
6645let content = concat!(
6646"probe <- function(x) {\n",
6647" if (!base::exists(\"probe\", mode = \"function\")) {\n",
6648" probe <- function(x, ...) UseMethod(\"probe\")\n",
6649" }\n",
6650"}\n",
6651"if (!base::exists(\"cached\", mode = \"function\")) {\n",
6652" cached <- function(x) x\n",
6653"}\n",
6654"cached <- function(x) .Call(C_cached, x)\n",
6655 );
6656assert!(detect_duplicate_wrapper_defs(content, &std::collections::HashMap::new()).is_ok());
6657 }
66586659#[test]
6660fn detect_duplicate_cross_impl_block_generic_message() {
6661// Two S7 impl blocks both emit `Counter_inc` -- no sidecar involved, so
6662 // the generic "more than once" message is used.
6663let content = concat!(
6664"Counter_inc <- function(x, ...) .Call(C_inc_a, x)\n",
6665"Counter_inc <- function(x, ...) .Call(C_inc_b, x)\n",
6666 );
6667let map = std::collections::HashMap::new();
6668let err = detect_duplicate_wrapper_defs(content, &map).unwrap_err();
6669assert!(err.contains("Counter_inc"), "msg: {err}");
6670assert!(err.contains("defined more than once"), "msg: {err}");
6671 }
66726673#[test]
6674fn detect_duplicate_ignores_repeated_s7_method_assignments() {
6675// `S7::method(...) <- function` lines are NOT top-level bare defs; a class
6676 // may legitimately have many method assignments. These must never trip.
6677let content = concat!(
6678"S7::method(describe, Shape) <- function(x) .Call(C1, x)\n",
6679"S7::method(area, Shape) <- function(x) .Call(C2, x)\n",
6680"S7::method(describe, Circle) <- function(x) .Call(C3, x)\n",
6681 );
6682let map = std::collections::HashMap::new();
6683assert!(detect_duplicate_wrapper_defs(content, &map).is_ok());
6684 }
66856686// endregion
66876688 // region: impl blocks merged per type (#1776)
66896690fn widget_path() -> &'static str {
6691"pkg::widgets::Widget"
6692}
66936694fn other_widget_path() -> &'static str {
6695"pkg::legacy::Widget"
6696}
66976698/// An env-class impl block of `Widget` at `lib.rs:<line>`, with the
6699 /// `parts` given as raw-literal text (framed by newlines) and no
6700 /// settings, definitions or member names.
6701fn widget_block(line: u32, head: &'static str, members: &'static str) -> ClassBlockEntry {
6702 ClassBlockEntry {
6703 rust_type: "Widget",
6704 type_path: widget_path,
6705 r_class_name: "Widget",
6706 source_file: "lib.rs",
6707 source_line: line,
6708 source_column: 6,
6709 settings: "env",
6710 constructor: "",
6711 constructor_inferred: false,
6712 constructor_candidates: &[],
6713 demoted: None,
6714 defines: &[],
6715 head,
6716 class_docs: "\n\n",
6717 class_doc_lines: "\n\n",
6718 members,
6719 member_names: &[],
6720 tail: "\n\n",
6721 }
6722 }
67236724fn merged(blocks: &[ClassBlockEntry]) -> Vec<String> {
6725 merge_class_blocks(blocks)
6726 .unwrap_or_else(|msg| panic!("{msg}"))
6727 .into_iter()
6728 .map(|f| f.content.into_owned())
6729 .collect()
6730 }
67316732/// The defining block's topic governs defaults from every block, while
6733 /// an explicit method page (including the original class page) wins.
6734#[test]
6735fn class_member_docs_inherit_the_defining_blocks_topic() {
6736let mut first = widget_block(10, "\n.__MX_CLASS_DOCS__\nWidget <- new.env()\n", "\n\n");
6737 first.class_docs = "\n#' @rdname widget_api\n";
6738 first.defines = &["constructor"];
6739let second = widget_block(
674020,
6741"\n\n",
6742concat!(
6743"\n#' @description Author prose.\n",
6744"#' @param n Author parameter.\n",
6745".__MX_CLASS_MEMBER_DEFAULT__#' @title Generated title\n",
6746".__MX_CLASS_MEMBER_DEFAULT__#' @param x An object.\n",
6747".__MX_CLASS_MEMBER_DEFAULT__#' @param missing (undocumented)\n",
6748".__MX_CLASS_MEMBER_DEFAULT__#' @rdname Widget\n",
6749"#' @name Widget_origin\nWidget_origin <- function(x, n, missing) 1\n\n",
6750"#' @rdname Widget\n",
6751".__MX_CLASS_MEMBER_DEFAULT__#' @param n (undocumented)\n",
6752"#' @name Widget_explicit\nWidget_explicit <- function(n) 1\n\n",
6753"#' @rdname own_page\n#' @order 2\n",
6754"Widget_own <- function() 1\n\n",
6755"#' @describeIn widget_api A described member.\n",
6756"Widget_described <- function() 1\n",
6757 ),
6758 );
6759let output = merged(&[second, first]).remove(0);
6760assert!(
6761 output.contains(concat!(
6762"#' @description Author prose.\n#' @param n Author parameter.\n",
6763"#' @rdname widget_api\n#' @name Widget_origin\n#' @order NaN\n",
6764"Widget_origin <- function(x, n, missing) 1"
6765)),
6766"{output}"
6767);
6768assert!(
6769 output.contains("#' @rdname Widget\n#' @param n (undocumented)"),
6770"{output}"
6771);
6772assert!(
6773 output.contains("#' @rdname own_page\n#' @order 2\nWidget_own"),
6774"{output}"
6775);
6776assert!(
6777 output.contains("#' @describeIn widget_api A described member.\nWidget_described"),
6778"{output}"
6779);
6780assert!(!output.contains(CLASS_MEMBER_DEFAULT_PREFIX), "{output}");
6781assert!(!output.contains("Generated title"), "{output}");
6782assert!(!output.contains("@param missing"), "{output}");
6783 }
67846785#[test]
6786fn class_member_docs_keep_default_page_and_author_order() {
6787let member = concat!(
6788".__MX_CLASS_MEMBER_DEFAULT__#' @rdname Widget\n",
6789".__MX_CLASS_MEMBER_DEFAULT__#' @param x An object.\n",
6790"#' @order 3\nWidget_value <- function(x) x\n",
6791 );
6792assert_eq!(
6793 resolve_class_member_docs(member, "Widget", "Widget"),
6794concat!(
6795"#' @rdname Widget\n#' @param x An object.\n",
6796"#' @order 3\nWidget_value <- function(x) x\n",
6797 )
6798 );
6799assert_eq!(
6800 resolve_class_member_docs(member, "Widget", "widget_api"),
6801"#' @rdname widget_api\n#' @order 3\nWidget_value <- function(x) x\n"
6802);
6803 }
68046805/// The docs-only block of a class the package names and builds itself
6806 /// is written when an impl block's docs say something, and dropped up
6807 /// to its `NULL` when they name a page at most (#1918).
6808#[test]
6809fn docs_only_class_block_needs_something_to_say() {
6810const HEAD: &str = "\n.__MX_CLASS_DOCS_ONLY__\n\
6811 .__MX_CLASS_DEFAULT__#' @title widget_rec S3 Class\n\
6812 .__MX_CLASS_DEFAULT__#' @name widget_rec\n\
6813 .__MX_CLASS_DOCS__\n.__MX_CLASS_DOC_LINES__\nNULL\n\n";
6814let with_docs = |first_docs: &'static str, second_docs: &'static str| {
6815let mut first = widget_block(10, HEAD, "\n# first\n");
6816 first.class_docs = first_docs;
6817let mut second = widget_block(20, "\n\n", "\n# second\n");
6818 second.class_docs = second_docs;
6819 merged(&[first, second]).remove(0)
6820 };
68216822let silent = with_docs("\n\n", "\n\n");
6823assert!(!silent.contains("widget_rec"), "{silent}");
6824assert!(!silent.contains("NULL"), "{silent}");
6825assert!(
6826 silent.contains("# first") && silent.contains("# second"),
6827"{silent}"
6828);
68296830let page_only = with_docs("\n#' @rdname widget_rec\n", "\n\n");
6831assert!(!page_only.contains("NULL"), "{page_only}");
68326833let documented = with_docs("\n\n", "\n#' @description Records the package builds.\n");
6834assert!(
6835 documented.contains(
6836"#' @title widget_rec S3 Class\n#' @name widget_rec\n\
6837 #' @description Records the package builds.\nNULL\n"
6838),
6839"{documented}"
6840);
68416842let titled = with_docs(
6843"\n\n",
6844"\n#' @title Package records\n#' @description Records.\n",
6845 );
6846assert!(!titled.contains("S3 Class"), "{titled}");
6847assert!(titled.contains("#' @title Package records\n"), "{titled}");
6848 }
68496850#[test]
6851fn merge_defines_the_class_once_with_every_blocks_members() {
6852let mut ctor = widget_block(
685310,
6854"\n#' Widget\n.__MX_CLASS_DOC_LINES__\n#' @export\nWidget <- new.env()\n",
6855"\nWidget$get <- function() 1\n",
6856 );
6857 ctor.defines = &["a constructor"];
6858 ctor.class_doc_lines = "\n#' @importFrom lifecycle deprecate_warn\n";
6859 ctor.tail = "\n`$.Widget` <- function(self, name) NULL\n";
6860let mut more = widget_block(
686140,
6862"\n#' Widget\n.__MX_CLASS_DOC_LINES__\n#' @export\nWidget <- new.env()\n",
6863"\nWidget$set <- function() 2\n",
6864 );
6865 more.class_doc_lines =
6866"\n#' @importFrom lifecycle deprecate_warn\n#' @importFrom lifecycle badge\n";
6867// The registration order of the blocks doesn't matter.
6868let classes = merged(&[more, ctor]);
6869assert_eq!(classes.len(), 1);
6870assert_eq!(
6871 classes[0],
6872"# Generated from Rust impl `Widget` (lib.rs:10:6)\n\
6873 #' Widget\n\
6874 #' @importFrom lifecycle deprecate_warn\n\
6875 #' @importFrom lifecycle badge\n\
6876 #' @export\n\
6877 Widget <- new.env()\n\
6878 Widget$get <- function() 1\n\
6879 \n\
6880 # Generated from Rust impl `Widget` (lib.rs:40:6)\n\
6881 Widget$set <- function() 2\n\
6882 \n\
6883 # Generated from Rust impl `Widget` (lib.rs:10:6)\n\
6884 `$.Widget` <- function(self, name) NULL"
6885);
6886 }
68876888#[test]
6889fn merge_defines_the_class_from_the_block_with_the_constructor() {
6890let first = widget_block(10, "\nWidget <- new.env() # first\n", "\nWidget$a <- 1\n");
6891let mut ctor = widget_block(40, "\nWidget <- new.env() # ctor\n", "\nWidget$b <- 2\n");
6892 ctor.defines = &["a constructor"];
6893let classes = merged(&[first, ctor]);
6894assert_eq!(
6895 classes[0],
6896"# Generated from Rust impl `Widget` (lib.rs:40:6)\n\
6897 Widget <- new.env() # ctor\n\
6898 \n\
6899 # Generated from Rust impl `Widget` (lib.rs:10:6)\n\
6900 Widget$a <- 1\n\
6901 \n\
6902 # Generated from Rust impl `Widget` (lib.rs:40:6)\n\
6903 Widget$b <- 2"
6904);
6905// Without one, the first block in source order defines the class.
6906let first = widget_block(10, "\nWidget <- new.env() # first\n", "\n\n");
6907let second = widget_block(40, "\nWidget <- new.env() # second\n", "\n\n");
6908assert_eq!(
6909 merged(&[second, first])[0],
6910"# Generated from Rust impl `Widget` (lib.rs:10:6)\nWidget <- new.env() # first"
6911);
6912 }
69136914#[test]
6915fn merge_keeps_one_block_as_written() {
6916// The head's blank last line separates it from the members.
6917let mut block = widget_block(
691810,
6919"\n#' Widget\n.__MX_CLASS_DOC_LINES__\n#' @export\nWidget <- new.env()\n\n",
6920"\nWidget$get <- function() 1\n",
6921 );
6922 block.tail = "\nWidget$lock()\n";
6923assert_eq!(
6924 merged(&[block])[0],
6925"# Generated from Rust impl `Widget` (lib.rs:10:6)\n\
6926 #' Widget\n\
6927 #' @export\n\
6928 Widget <- new.env()\n\
6929 \n\
6930 Widget$get <- function() 1\n\
6931 Widget$lock()"
6932);
6933 }
69346935#[test]
6936fn merge_refuses_blocks_with_different_settings() {
6937// `@noRd` on one block only: the other's methods would be documented
6938 // on a page that does not exist.
6939let mut hidden = widget_block(10, "\n\n", "\n\n");
6940 hidden.settings = "env, @noRd";
6941let plain = widget_block(40, "\n\n", "\n\n");
6942let msg = merge_class_blocks(&[hidden, plain])
6943 .err()
6944 .expect("settings differ");
6945assert!(
6946 msg.contains("the impl blocks of `Widget` disagree on the class settings"),
6947"{msg}"
6948);
6949assert!(
6950 msg.contains("`env, @noRd` on `impl Widget` (lib.rs:10)"),
6951"{msg}"
6952);
6953assert!(msg.contains("`env` on `impl Widget` (lib.rs:40)"), "{msg}");
6954 }
69556956#[test]
6957fn merge_refuses_two_defining_blocks() {
6958let mut ctor = widget_block(10, "\n\n", "\n\n");
6959 ctor.defines = &["a constructor"];
6960let mut props = widget_block(40, "\n\n", "\n\n");
6961 props.defines = &["a constructor", "S7 properties"];
6962let msg = merge_class_blocks(&[ctor, props])
6963 .err()
6964 .expect("two blocks define the class");
6965assert!(
6966 msg.contains(
6967"two impl blocks of `Widget` each define the class: `impl Widget` (lib.rs:10) \
6968 has a constructor, `impl Widget` (lib.rs:40) has a constructor and S7 \
6969 properties."
6970),
6971"{msg}"
6972);
6973assert!(
6974 msg.contains("move what `impl Widget` (lib.rs:40) has into `impl Widget` (lib.rs:10)"),
6975"{msg}"
6976);
6977 }
69786979#[test]
6980fn merge_refuses_a_member_bound_by_two_blocks() {
6981let mut first = widget_block(10, "\n\n", "\n\n");
6982 first.member_names = &["`Widget$get`", "`Widget$set`"];
6983let mut second = widget_block(40, "\n\n", "\n\n");
6984 second.member_names = &["`Widget$get`"];
6985let msg = merge_class_blocks(&[first, second])
6986 .err()
6987 .expect("`$get` is bound twice");
6988assert!(
6989 msg.contains(
6990"two impl blocks of `Widget` define `Widget$get`: `impl Widget` (lib.rs:10) \
6991 and `impl Widget` (lib.rs:40)."
6992),
6993"{msg}"
6994);
6995assert!(!msg.contains("Widget$set"), "{msg}");
6996 }
69976998#[test]
6999fn merge_refuses_two_types_with_one_class_name() {
7000// Same identifier, different modules: two types, not two blocks of one.
7001let mine = widget_block(10, "\nWidget <- new.env()\n", "\n\n");
7002let mut theirs = widget_block(40, "\nWidget <- new.env()\n", "\n\n");
7003 theirs.type_path = other_widget_path;
7004 theirs.source_file = "legacy.rs";
7005let msg = merge_class_blocks(&[mine, theirs])
7006 .err()
7007 .expect("one class, two types");
7008assert!(
7009 msg.contains(
7010"R class `Widget` is claimed by two Rust types, `pkg::legacy::Widget` \
7011 (`impl Widget` (legacy.rs:40)) and `pkg::widgets::Widget` \
7012 (`impl Widget` (lib.rs:10))"
7013),
7014"{msg}"
7015);
7016 }
70177018/// A class head as the class generators write it: generated defaults, the
7019 /// docs and doc-lines markers, then the class object.
7020const DOCUMENTED_HEAD: &str = "\n\
7021 .__MX_CLASS_DEFAULT__#' @title Widget Class\n\
7022 .__MX_CLASS_DEFAULT__#' @name Widget\n\
7023 .__MX_CLASS_DEFAULT__#' @rdname Widget\n\
7024 .__MX_CLASS_DOCS__\n\
7025 .__MX_CLASS_DEFAULT__#' @param .ptr Internal pointer.\n\
7026 #' @source Generated by miniextendr\n\
7027 .__MX_CLASS_DOC_LINES__\n\
7028 .__MX_CLASS_DEFAULT__#' @export\n\
7029 Widget <- new.env()\n";
70307031#[test]
7032fn merge_writes_every_blocks_docs_after_the_definers() {
7033let mut ctor = widget_block(40, DOCUMENTED_HEAD, "\n\n");
7034 ctor.defines = &["a constructor"];
7035 ctor.class_docs = "\n#' @description Widgets hold a value.\n#' @importFrom stats median\n";
7036let mut first = widget_block(10, DOCUMENTED_HEAD, "\n\n");
7037 first.class_docs = "\n#' @description More methods.\n#' @details One.\n#'\n#' }\n\
7038 #' @importFrom stats median\n#' @export\n";
7039let mut last = widget_block(70, DOCUMENTED_HEAD, "\n\n");
7040 last.class_docs = "\n#' @details Two.\n#'\n#' }\n#' @param .ptr Pointer.\n";
7041let classes = merged(&[last, first, ctor]);
7042// The defining block's docs, then the others' in source order; an
7043 // identical tag once, the blank and `}` lines of two tags each kept;
7044 // a default another block writes itself gives way.
7045assert_eq!(
7046 classes[0],
7047"# Generated from Rust impl `Widget` (lib.rs:40:6)\n\
7048 #' @title Widget Class\n\
7049 #' @name Widget\n\
7050 #' @rdname Widget\n\
7051 #' @description Widgets hold a value.\n\
7052 #' @importFrom stats median\n\
7053 #' @description More methods.\n\
7054 #' @details One.\n\
7055 #'\n\
7056 #' }\n\
7057 #' @export\n\
7058 #' @details Two.\n\
7059 #'\n\
7060 #' }\n\
7061 #' @param .ptr Pointer.\n\
7062 #' @source Generated by miniextendr\n\
7063 Widget <- new.env()"
7064);
7065 }
70667067#[test]
7068fn merge_takes_the_title_from_any_block() {
7069let mut ctor = widget_block(10, DOCUMENTED_HEAD, "\n\n");
7070 ctor.defines = &["a constructor"];
7071let mut more = widget_block(40, DOCUMENTED_HEAD, "\n\n");
7072 more.class_docs = "\n#' @title Widgets\n#' @rdname Widget\n";
7073let classes = merged(&[ctor, more]);
7074assert!(
7075 classes[0].starts_with(
7076"# Generated from Rust impl `Widget` (lib.rs:10:6)\n\
7077 #' @name Widget\n\
7078 #' @title Widgets\n\
7079 #' @rdname Widget\n\
7080 #' @param .ptr Internal pointer.\n"
7081),
7082"{}",
7083 classes[0]
7084 );
7085 }
70867087#[test]
7088fn merge_drops_a_member_default_another_block_documents() {
7089let mut ctor = widget_block(10, DOCUMENTED_HEAD, "\n\n");
7090 ctor.defines = &["a constructor"];
7091 ctor.class_docs = "\n#' @field doubled Twice the value.\n#' @inheritParams widget_ops\n";
7092let mut more = widget_block(40, DOCUMENTED_HEAD, "\n\n");
7093 more.class_doc_lines = "\n.__MX_CLASS_DEFAULT__#' @field doubled Active binding.\n\
7094 .__MX_CLASS_DEFAULT__#' @field halved Active binding.\n";
7095let class = &merged(&[ctor, more])[0];
7096assert!(
7097 class.contains("#' @field doubled Twice the value.\n"),
7098"{class}"
7099);
7100assert!(!class.contains("@field doubled Active binding."), "{class}");
7101assert!(
7102 class.contains("#' @field halved Active binding.\n"),
7103"{class}"
7104);
7105// The defining block's own docs were seen when its defaults were
7106 // generated: its `@param .ptr` default stays.
7107assert!(
7108 class.contains("#' @param .ptr Internal pointer.\n"),
7109"{class}"
7110);
7111 }
71127113#[test]
7114fn merge_refuses_two_titles() {
7115let mut ctor = widget_block(10, DOCUMENTED_HEAD, "\n\n");
7116 ctor.defines = &["a constructor"];
7117 ctor.class_docs = "\n#' @title Widgets\n";
7118let mut more = widget_block(40, DOCUMENTED_HEAD, "\n\n");
7119 more.class_docs = "\n#' @title Gadgets\n";
7120let same = widget_block(70, DOCUMENTED_HEAD, "\n\n");
7121let msg = merge_class_blocks(&[ctor, more, same])
7122 .err()
7123 .expect("two titles");
7124assert_eq!(
7125 msg,
7126"miniextendr: the impl blocks of `Widget` give the class two titles: \
7127 `impl Widget` (lib.rs:10) has `@title Widgets`, `impl Widget` (lib.rs:40) has \
7128 `@title Gadgets`. A type's impl blocks merge into one class with one help \
7129 page: keep one `@title` (#1776)."
7130);
7131 }
71327133#[test]
7134fn merge_refuses_a_page_other_than_the_definers() {
7135let mut ctor = widget_block(10, DOCUMENTED_HEAD, "\n\n");
7136 ctor.defines = &["a constructor"];
7137let mut more = widget_block(40, DOCUMENTED_HEAD, "\n\n");
7138 more.class_docs = "\n#' @rdname widget_ops\n";
7139let msg = merge_class_blocks(&[ctor, more]).err().expect("two pages");
7140assert_eq!(
7141 msg,
7142"miniextendr: the impl blocks of `Widget` put the class on two help pages: \
7143 `impl Widget` (lib.rs:10), which defines the class, has `@rdname Widget`, and \
7144 `impl Widget` (lib.rs:40) has `@rdname widget_ops`. The block that defines \
7145 the class decides its page: move `@rdname widget_ops` into `impl Widget` \
7146 (lib.rs:10), or remove it (#1776)."
7147);
7148 }
71497150#[test]
7151fn merge_reports_every_problem_at_once() {
7152let mut first = widget_block(10, "\n\n", "\n\n");
7153 first.defines = &["a constructor"];
7154 first.member_names = &["`Widget$get`"];
7155let mut second = widget_block(40, "\n\n", "\n\n");
7156 second.defines = &["a constructor"];
7157 second.member_names = &["`Widget$get`"];
7158 second.settings = "env, internal";
7159let msg = merge_class_blocks(&[first, second])
7160 .err()
7161 .expect("three problems");
7162assert_eq!(msg.lines().count(), 3, "{msg}");
7163 }
71647165// endregion
71667167 // region: the constructor decided per type
71687169/// A block of `Widget` that infers `from_value` as its constructor: its
7170 /// own rendering defines the class with it, its `demoted` one keeps it a
7171 /// static method.
7172fn inferring_block(line: u32) -> ClassBlockEntry {
7173let mut demoted = widget_block(
7174 line,
7175"\nWidget <- new.env() # no constructor\n",
7176"\nWidget$from_value <- function(x) 1\n",
7177 );
7178 demoted.constructor_candidates = &["from_value"];
7179let mut block = widget_block(
7180 line,
7181"\nWidget <- new.env() # from_value\nWidget$new <- function(x) 1\n",
7182"\n\n",
7183 );
7184 block.constructor = "from_value";
7185 block.constructor_inferred = true;
7186 block.constructor_candidates = &["from_value"];
7187 block.defines = &["the inferred constructor `from_value`"];
7188 block.demoted = Some(Box::leak(Box::new(demoted)));
7189 block
7190 }
71917192#[test]
7193fn a_lone_candidate_is_the_constructor_whichever_block_has_it() {
7194let methods = widget_block(10, "\nWidget <- new.env() # first\n", "\nWidget$get <- 2\n");
7195let classes = merged(&[methods, inferring_block(40)]);
7196assert_eq!(
7197 classes[0],
7198"# Generated from Rust impl `Widget` (lib.rs:40:6)\n\
7199 Widget <- new.env() # from_value\n\
7200 Widget$new <- function(x) 1\n\
7201 \n\
7202 # Generated from Rust impl `Widget` (lib.rs:10:6)\n\
7203 Widget$get <- 2"
7204);
7205 }
72067207#[test]
7208fn a_declared_constructor_keeps_the_candidate_a_static_method() {
7209let mut new = widget_block(10, "\nWidget <- new.env() # new\n", "\n\n");
7210 new.constructor = "new";
7211 new.constructor_candidates = &["from_file"];
7212 new.defines = &["a constructor"];
7213let classes = merged(&[inferring_block(40), new]);
7214assert_eq!(
7215 classes[0],
7216"# Generated from Rust impl `Widget` (lib.rs:10:6)\n\
7217 Widget <- new.env() # new\n\
7218 \n\
7219 # Generated from Rust impl `Widget` (lib.rs:40:6)\n\
7220 Widget$from_value <- function(x) 1"
7221);
7222 }
72237224#[test]
7225fn candidates_in_two_blocks_without_a_declared_constructor_are_refused() {
7226let mut other = widget_block(10, "\n\n", "\n\n");
7227 other.constructor_candidates = &["empty", "from_file"];
7228let msg = merge_class_blocks(&[other, inferring_block(40)])
7229 .err()
7230 .expect("three candidates");
7231assert!(
7232 msg.contains(
7233"`Widget` has several methods that could be its constructor, and none is \
7234 declared: `empty`, `from_file` in `impl Widget` (lib.rs:10); `from_value` in \
7235 `impl Widget` (lib.rs:40)."
7236),
7237"{msg}"
7238);
7239assert!(msg.contains("`#[miniextendr(env(constructor))]`"), "{msg}");
7240// The candidates are the only problem reported.
7241assert_eq!(msg.lines().count(), 1, "{msg}");
7242 }
72437244// endregion
7245}
7246// endregion