Skip to main content

miniextendr_api/
registry.rs

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.
7
8use 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;
13
14// 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.
27
28/// 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];
34
35/// 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];
43
44/// 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];
58
59/// 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];
66
67/// 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];
75
76/// 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];
85
86/// 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];
95
96/// 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];
105
106/// 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];
115
116/// 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];
123
124/// 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];
131
132/// 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];
140
141/// 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];
151
152// endregion
153
154// region: Runtime slice access (cfg-uniform)
155
156/// 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_DEFS
163    }
164    #[cfg(target_arch = "wasm32")]
165    {
166        wasm_runtime::call_defs()
167    }
168}
169
170#[inline]
171pub(crate) fn altrep_regs() -> &'static [AltrepRegistration] {
172    #[cfg(not(target_arch = "wasm32"))]
173    {
174        &MX_ALTREP_REGISTRATIONS
175    }
176    #[cfg(target_arch = "wasm32")]
177    {
178        wasm_runtime::altrep_regs()
179    }
180}
181
182#[inline]
183pub(crate) fn trait_dispatch() -> &'static [TraitDispatchEntry] {
184    #[cfg(not(target_arch = "wasm32"))]
185    {
186        &MX_TRAIT_DISPATCH
187    }
188    #[cfg(target_arch = "wasm32")]
189    {
190        wasm_runtime::trait_dispatch()
191    }
192}
193
194/// 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    {
203        MX_CONVERSION_ERROR_CLASS
204            .first()
205            .map_or(&[][..], |entry| entry.classes)
206    }
207    #[cfg(target_arch = "wasm32")]
208    {
209        wasm_runtime::conversion_error_class()
210    }
211}
212
213#[cfg(target_arch = "wasm32")]
214mod wasm_runtime {
215    use super::{AltrepRegistration, R_CallMethodDef, TraitDispatchEntry};
216    use std::sync::OnceLock;
217
218    static CALL_DEFS: OnceLock<&'static [R_CallMethodDef]> = OnceLock::new();
219    static ALTREP_REGS: OnceLock<&'static [AltrepRegistration]> = OnceLock::new();
220    static TRAIT_DISPATCH: OnceLock<&'static [TraitDispatchEntry]> = OnceLock::new();
221    static CONVERSION_ERROR_CLASS: OnceLock<&'static [&'static str]> = OnceLock::new();
222
223    pub(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.
232        let _ = CALL_DEFS.set(c);
233        let _ = ALTREP_REGS.set(a);
234        let _ = TRAIT_DISPATCH.set(t);
235        let _ = CONVERSION_ERROR_CLASS.set(k);
236    }
237
238    pub(super) fn call_defs() -> &'static [R_CallMethodDef] {
239        CALL_DEFS.get().copied().unwrap_or(&[])
240    }
241    pub(super) fn altrep_regs() -> &'static [AltrepRegistration] {
242        ALTREP_REGS.get().copied().unwrap_or(&[])
243    }
244    pub(super) fn trait_dispatch() -> &'static [TraitDispatchEntry] {
245        TRAIT_DISPATCH.get().copied().unwrap_or(&[])
246    }
247    pub(super) fn conversion_error_class() -> &'static [&'static str] {
248        CONVERSION_ERROR_CLASS.get().copied().unwrap_or(&[])
249    }
250}
251
252/// 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}
278
279// endregion
280
281// region: Entry Types
282
283/// 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.
291    Sidecar,
292    /// Class definitions (impl blocks: env/R6/S3/S4/S7).
293    Class,
294    /// Standalone `#[miniextendr]` functions.
295    Function,
296    /// Trait impl wrappers (`impl Trait for Type`).
297    TraitImpl,
298    /// Vctrs S3 method wrappers (`#[derive(Vctrs)]`).
299    Vctrs,
300    /// The re-export blocks of `reexport!` (#1910). They bind nothing, so
301    /// their place is free; last keeps them together.
302    Reexport,
303}
304
305/// R wrapper code with priority for ordering.
306pub struct RWrapperEntry {
307    /// Ordering priority (lower = earlier in output file).
308    pub priority: RWrapperPriority,
309    /// R source code fragment.
310    pub 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.
314    pub 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).
319    pub source_line: u32,
320}
321
322// SAFETY: All fields are immutable and valid for 'static lifetime.
323unsafe impl Sync for RWrapperEntry {}
324
325/// 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.
329    pub classes: &'static [&'static str],
330}
331
332/// 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__"`.
335    pub placeholder: &'static str,
336    /// The parameter type's `MatchArg::CHOICES`, unescaped.
337    pub 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.
343    pub 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.
349    pub aliases_placeholder: &'static str,
350    /// The parameter type's `MatchArg::ALIASES`, unescaped.
351    pub aliases: &'static [crate::match_arg::MatchArgAlias],
352}
353
354/// 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__"`.
359    pub placeholder: &'static str,
360    /// `true` for `several_ok` params (emits "One or more of …");
361    /// `false` for plain `match_arg` (emits "One of …").
362    pub 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.
367    pub suffix: &'static str,
368    /// Function that returns the choices as a comma-separated quoted string,
369    /// e.g. `"\"Fast\", \"Safe\", \"Debug\""`.
370    pub choices_str: fn() -> String,
371}
372
373// SAFETY: function pointer, bools, and &'static str are Send+Sync.
374unsafe impl Sync for MatchArgParamDocEntry {}
375
376/// 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"`.
383    pub 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.
386    pub r_class_name: &'static str,
387    /// Class system tag: `"env"` | `"r6"` | `"s3"` | `"s4"` | `"s7"` | `"vctrs"`.
388    pub 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).
394    pub field_syntax: bool,
395}
396
397// SAFETY: &'static str fields and a bool — immutable and valid for program lifetime.
398unsafe impl Sync for ClassNameEntry {}
399
400/// 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__";
404
405/// 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__";
410
411/// 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__";
420
421/// 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__";
430
431/// 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__";
435
436/// 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"`.
450    pub 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.
453    pub type_path: fn() -> &'static str,
454    /// R-visible class name (`class = "..."`, else the type's identifier).
455    pub r_class_name: &'static str,
456    /// Source file of the impl block (`file!()`).
457    pub source_file: &'static str,
458    /// Line of the impl's type in `source_file`.
459    pub source_line: u32,
460    /// Column of the impl's type, for the `# Generated from` header.
461    pub 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.
466    pub 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).
472    pub 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.
476    pub 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.
480    pub 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.
484    pub 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).
490    pub 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.
496    pub 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.
503    pub 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.
506    pub class_doc_lines: &'static str,
507    /// The block's members: its methods, static methods and coercions.
508    pub 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.
514    pub member_names: &'static [&'static str],
515    /// What follows every block's members, such as R6's `$lock()`.
516    pub tail: &'static str,
517}
518
519// SAFETY: &'static str fields, a fn pointer, integers and a reference to
520// another such entry — immutable and valid for program lifetime.
521unsafe impl Sync for ClassBlockEntry {}
522
523/// 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> {}
541
542/// 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>() {}
546
547/// 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`].
558    pub rust_type: &'static str,
559    /// The page and the exported accessors.
560    pub exported: &'static str,
561    /// The same accessors with `@noRd` and no export, plus field-syntax helpers.
562    pub internal: &'static str,
563    /// Source file of the derive (`file!()`), for the fragment order.
564    pub source_file: &'static str,
565    /// Line of the derived type in `source_file`.
566    pub source_line: u32,
567}
568
569// SAFETY: &'static str fields and a u32 — immutable and valid for program lifetime.
570unsafe impl Sync for SidecarAccessorsEntry {}
571
572/// 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`].
577    pub rust_type: &'static str,
578    /// The method, `function(target, current, ...) { ... }`.
579    pub method: &'static str,
580    /// The roxygen lines of the method's page after its title: the
581    /// description, the arguments and the value.
582    pub docs: &'static str,
583    /// Source file of the derive (`file!()`).
584    pub source_file: &'static str,
585    /// Line of the derived type in `source_file`.
586    pub source_line: u32,
587    /// Column of the derived type, for the `# Generated from` header.
588    pub source_column: u32,
589}
590
591// SAFETY: &'static str fields and integers — immutable and valid for program lifetime.
592unsafe impl Sync for AllEqualEntry {}
593
594/// 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"`.
599    pub package: &'static str,
600    /// The re-exported name as R spells it, unquoted, e.g. `"tidy"` or
601    /// `"%>%"`.
602    pub name: &'static str,
603    /// Source file of the invocation (`file!()`).
604    pub source_file: &'static str,
605    /// Line of the item in `source_file`.
606    pub source_line: u32,
607    /// Column of the item, for the `# Generated from` header.
608    pub source_column: u32,
609}
610
611/// 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> {
631    use std::collections::hash_map::Entry;
632    let mut index = std::collections::HashMap::new();
633    let mut r_class_names = std::collections::HashMap::new();
634    for e in entries {
635        match index.entry(e.rust_type) {
636            Entry::Vacant(slot) => {
637                slot.insert(e);
638            }
639            Entry::Occupied(slot) => {
640                let prev = *slot.get();
641                if 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        }
656        if let Some(prev) = r_class_names.insert(e.r_class_name, e) {
657            if !(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    }
670    index
671}
672
673/// 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"`.
680    pub rust_type: &'static str,
681    /// Field name, e.g. `"prop_int"`.
682    pub field_name: &'static str,
683    /// Documentation string for this property.
684    /// Defaults to `"(undocumented sidecar property)"` when no `prop_doc` was supplied.
685    pub prop_doc: &'static str,
686}
687
688// SAFETY: All fields are &'static str — immutable and valid for program lifetime.
689unsafe impl Sync for SidecarPropEntry {}
690
691/// Trait dispatch entry mapping (concrete_tag, trait_tag) → vtable.
692#[repr(C)]
693pub struct TraitDispatchEntry {
694    /// Tag identifying the concrete type.
695    pub concrete_tag: mx_tag,
696    /// Tag identifying the trait interface.
697    pub trait_tag: mx_tag,
698    /// Pointer to the trait's vtable (cast from `&'static SomeVTable`).
699    pub 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`.
704    pub vtable_symbol: &'static str,
705}
706
707// 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 Sync for TraitDispatchEntry {}
710unsafe impl Send for TraitDispatchEntry {}
711
712/// 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.
718    pub 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`.
722    pub symbol: &'static str,
723}
724
725// SAFETY: register is a static fn pointer; symbol is a `'static` string.
726unsafe impl Sync for AltrepRegistration {}
727unsafe impl Send for AltrepRegistration {}
728// endregion
729
730// region: Universal Query
731
732/// 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 {
745    let concrete_tag = unsafe { (*(*ptr).base).concrete_tag };
746    for entry in trait_dispatch().iter() {
747        if entry.concrete_tag == concrete_tag && entry.trait_tag == trait_tag {
748            return entry.vtable;
749        }
750    }
751    std::ptr::null()
752}
753// endregion
754
755// region: Initialization
756
757/// 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.
778    let wrapper_gen = crate::init::wrapper_gen_mode();
779    if !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.
785        for reg in altrep_regs().iter() {
786            (reg.register)();
787        }
788
789        // 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.
792        crate::altrep::assert_altrep_class_uniqueness();
793    }
794
795    // 2. Build call method defs with null sentinel
796    let 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    {
805        call_defs.push(R_CallMethodDef {
806            name: c"miniextendr_write_wrappers".as_ptr(),
807            fun: unsafe {
808                std::mem::transmute::<
809                    *const (),
810                    Option<unsafe extern "C-unwind" fn() -> *mut c_void>,
811                >(miniextendr_write_wrappers as *const ())
812            },
813            numArgs: 1,
814        });
815        call_defs.push(R_CallMethodDef {
816            name: c"miniextendr_write_wasm_registry".as_ptr(),
817            fun: unsafe {
818                std::mem::transmute::<
819                    *const (),
820                    Option<unsafe extern "C-unwind" fn() -> *mut c_void>,
821                >(miniextendr_write_wasm_registry as *const ())
822            },
823            numArgs: 1,
824        });
825    }
826    call_defs.push(R_CallMethodDef {
827        name: std::ptr::null(),
828        fun: None,
829        numArgs: 0,
830    });
831
832    // 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.
837    unsafe {
838        crate::sys::R_registerRoutines_unchecked(
839            dll,
840            std::ptr::null(),
841            call_defs.as_ptr(),
842            std::ptr::null(),
843            std::ptr::null(),
844        );
845    }
846}
847
848/// 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> {
856    accessors
857        .iter()
858        .map(|entry| {
859            let field_syntax = classes
860                .iter()
861                .any(|class| class.rust_type == entry.rust_type && class.field_syntax);
862            WrapperFragment {
863                priority: RWrapperPriority::Sidecar,
864                content: std::borrow::Cow::Borrowed(if field_syntax {
865                    entry.internal
866                } else {
867                    entry.exported
868                }),
869                source_file: entry.source_file,
870                source_line: entry.source_line,
871            }
872        })
873        .collect()
874}
875
876/// 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> {
898    let index = build_class_name_index(classes);
899    let mut errors = Vec::new();
900    let mut fragments = Vec::new();
901    for entry in entries {
902        let 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        );
906        match index.get(entry.rust_type) {
907            None => 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            )),
911            Some(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            )),
915            Some(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    }
923    if errors.is_empty() {
924        Ok(fragments)
925    } else {
926        Err(errors.join("\n"))
927    }
928}
929
930/// 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 {
933    let class_name = class.r_class_name;
934    let 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.
937    let symbol = if class_name
938        .bytes()
939        .all(|c| c.is_ascii_alphanumeric() || c == b'.' || c == b'_')
940    {
941        method.clone()
942    } else {
943        ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("`{0}`", method))
    })format!("`{method}`")
944    };
945    let 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    );
960    if class.class_system == "s7" {
961        out.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    }
965    out
966}
967
968// region: re-exports (#1910)
969
970/// 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> {
990    let mut by_item: std::collections::BTreeMap<(&str, &str), Vec<&ReexportEntry>> =
991        std::collections::BTreeMap::new();
992    for entry in entries {
993        by_item
994            .entry((entry.package, entry.name))
995            .or_default()
996            .push(entry);
997    }
998    let first = *by_item.values().next()?.first()?;
999    let blocks: Vec<String> = by_item
1000        .iter()
1001        .map(|((package, name), sites)| {
1002            let 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();
1017    Some(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}
1024
1025/// The sites of `entries`, `(file.rs:line:column)` each, in source order.
1026#[cfg(not(target_arch = "wasm32"))]
1027fn reexport_sites(entries: &[&ReexportEntry]) -> String {
1028    let mut sites: Vec<(&str, u32, u32)> = entries
1029        .iter()
1030        .map(|entry| (entry.source_file, entry.source_line, entry.source_column))
1031        .collect();
1032    sites.sort_unstable();
1033    sites.dedup();
1034    sites
1035        .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}
1040
1041/// 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> {
1052    use std::collections::BTreeMap;
1053    let mut by_name: BTreeMap<&str, BTreeMap<&str, Vec<&ReexportEntry>>> = BTreeMap::new();
1054    for entry in entries {
1055        by_name
1056            .entry(entry.name)
1057            .or_default()
1058            .entry(entry.package)
1059            .or_default()
1060            .push(entry);
1061    }
1062    if by_name.is_empty() {
1063        return Ok(());
1064    }
1065    let reexports = |name: &str, packages: &BTreeMap<&str, Vec<&ReexportEntry>>| {
1066        packages
1067            .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    };
1072
1073    let mut errors = Vec::new();
1074    for (name, packages) in &by_name {
1075        if 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    }
1084
1085    // Each fragment that binds or exports a re-exported name, once, by its
1086    // binding when it both binds and exports it.
1087    let mut own: BTreeMap<&str, Vec<(&'static str, std::rc::Rc<str>)>> = BTreeMap::new();
1088    for WrapperName {
1089        name,
1090        kind,
1091        producer,
1092    } in wrapper_names(content)
1093    {
1094        if !by_name.contains_key(name) {
1095            continue;
1096        }
1097        let uses = own.entry(name).or_default();
1098        match uses.iter_mut().find(|(_, other)| *other == producer) {
1099            Some(used) if used.0 == "export" => used.0 = kind,
1100            Some(_) => {}
1101            None => uses.push((kind, producer)),
1102        }
1103    }
1104    for (name, uses) in own {
1105        let binds = uses.iter().any(|(kind, _)| *kind != "export");
1106        let what = uses
1107            .iter()
1108            .map(|(kind, producer)| {
1109                if *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(", ");
1117        let (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    }
1138
1139    if errors.is_empty() {
1140        Ok(())
1141    } else {
1142        Err(errors
1143            .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}
1149
1150// endregion
1151
1152// region: impl blocks merged per type (#1776)
1153
1154/// 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}
1164
1165#[cfg(not(target_arch = "wasm32"))]
1166impl From<&RWrapperEntry> for WrapperFragment {
1167    fn from(entry: &RWrapperEntry) -> Self {
1168        WrapperFragment {
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}
1176
1177/// 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 {
1181    let part = part.strip_prefix('\n').unwrap_or(part);
1182    part.strip_suffix('\n').unwrap_or(part)
1183}
1184
1185/// 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}
1193
1194/// 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> {
1225    use std::collections::HashMap;
1226
1227    let mut ordered: Vec<&ClassBlockEntry> = blocks.iter().collect();
1228    ordered.sort_by_key(|b| (b.source_file, b.source_line, b.source_column));
1229
1230    // The blocks of each type, in source order; types in order of their first block.
1231    let mut types: Vec<(&'static str, Vec<&ClassBlockEntry>)> = Vec::new();
1232    let mut type_index: HashMap<&'static str, usize> = HashMap::new();
1233    for block in ordered {
1234        let path = (block.type_path)();
1235        let 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    }
1241
1242    let mut errors: Vec<String> = Vec::new();
1243
1244    // One Rust type per R class name.
1245    let mut class_owner: HashMap<&'static str, (&'static str, &ClassBlockEntry)> = HashMap::new();
1246    for (path, type_blocks) in &types {
1247        for block in type_blocks {
1248            match class_owner.get(block.r_class_name) {
1249                Some(&(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                }
1260                Some(_) => {}
1261                None => {
1262                    class_owner.insert(block.r_class_name, (path, block));
1263                }
1264            }
1265        }
1266    }
1267
1268    let mut fragments = Vec::with_capacity(types.len());
1269    for (_, type_blocks) in &types {
1270        let type_blocks = match resolve_class_constructor(type_blocks) {
1271            Ok(blocks) => blocks,
1272            Err(error) => {
1273                errors.push(error);
1274                continue;
1275            }
1276        };
1277        let first = type_blocks[0];
1278        let type_errors = errors.len();
1279
1280        for block in &type_blocks[1..] {
1281            if 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        }
1296
1297        let defining: Vec<&ClassBlockEntry> = type_blocks
1298            .iter()
1299            .copied()
1300            .filter(|b| !b.defines.is_empty())
1301            .collect();
1302        if 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        }
1317
1318        let definer = defining.first().copied().unwrap_or(first);
1319        errors.extend(class_doc_conflicts(definer, &type_blocks));
1320
1321        let mut members: HashMap<&str, &ClassBlockEntry> = HashMap::new();
1322        for &block in &type_blocks {
1323            for &name in block.member_names {
1324                match members.get(name) {
1325                    Some(&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                    }
1336                    Some(_) => {}
1337                    None => {
1338                        members.insert(name, block);
1339                    }
1340                }
1341            }
1342        }
1343
1344        if errors.len() > type_errors {
1345            continue;
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    }
1354
1355    if errors.is_empty() {
1356        Ok(fragments)
1357    } else {
1358        Err(errors.join("\n"))
1359    }
1360}
1361
1362/// 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}
1371
1372#[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.
1376    fn parse(text: &'a str) -> Vec<Self> {
1377        let mut tags: Vec<Self> = Vec::new();
1378        for line in text.lines() {
1379            let (default, line) = match line.strip_prefix(CLASS_DEFAULT_PREFIX) {
1380                Some(line) => (true, line),
1381                None => (false, line),
1382            };
1383            match tags.last_mut() {
1384                Some(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        }
1391        tags
1392    }
1393
1394    /// A head's generated default line, as a one-line tag; `None` for
1395    /// any other line.
1396    fn default_line(line: &'a str) -> Option<Self> {
1397        line.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    }
1402
1403    /// The tag's name (`title` for `#' @title ...`), or `""` before the
1404    /// first tag.
1405    fn name(&self) -> &'a str {
1406        self.lines[0]
1407            .strip_prefix("#' @")
1408            .and_then(|rest| rest.split_whitespace().next())
1409            .unwrap_or("")
1410    }
1411
1412    /// The tag's text after its name, continuation lines included.
1413    fn value(&self) -> String {
1414        let first = self.lines[0]
1415            .strip_prefix("#' @")
1416            .map_or("", |rest| rest.trim_start()[self.name().len()..].trim());
1417        let 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    }
1427
1428    /// 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`).
1431    fn replaces(&self, default: &DocTag<'_>) -> bool {
1432        match default.name() {
1433            name @ ("param" | "field") => {
1434                let value = default.value();
1435                let arg = value.split_whitespace().next().unwrap_or("");
1436                (self.name() == name
1437                    && self
1438                        .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}
1448
1449/// 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>>)> {
1456    let others = blocks
1457        .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}
1465
1466/// 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> {
1474    let mut errors = Vec::new();
1475    let docs = block_docs(definer, blocks);
1476    let tag_value =
1477        |tags: &[DocTag<'_>], name: &str| tags.iter().find(|t| t.name() == name).map(|t| t.value());
1478
1479    let mut title: Option<(&ClassBlockEntry, String)> = None;
1480    for (block, tags) in &docs {
1481        let Some(value) = tag_value(tags, "title") else {
1482            continue;
1483        };
1484        match &title {
1485            None => title = Some((block, value)),
1486            Some((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                ));
1496                break;
1497            }
1498            Some(_) => {}
1499        }
1500    }
1501
1502    // The page the defining block documents the class on: its own tag, else
1503    // the generated default.
1504    let head_defaults: Vec<DocTag<'_>> = class_part(definer.head)
1505        .lines()
1506        .filter_map(DocTag::default_line)
1507        .collect();
1508    for name in ["name", "rdname"] {
1509        let page = tag_value(&docs[0].1, name).or_else(|| tag_value(&head_defaults, name));
1510        for (block, tags) in &docs[1..] {
1511            let Some(value) = tag_value(tags, name) else {
1512                continue;
1513            };
1514            if page.as_ref() == Some(&value) {
1515                continue;
1516            }
1517            let page = match &page {
1518                Some(page) => ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("`@{0} {1}`", name, page))
    })format!("`@{name} {page}`"),
1519                None => ::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    }
1533    errors
1534}
1535
1536/// 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> {
1553    let declared = blocks
1554        .iter()
1555        .any(|b| !b.constructor.is_empty() && !b.constructor_inferred);
1556    if declared {
1557        return Ok(blocks
1558            .iter()
1559            .map(|&b| match b.demoted {
1560                Some(demoted) if b.constructor_inferred => demoted,
1561                _ => b,
1562            })
1563            .collect());
1564    }
1565    let candidates: usize = blocks.iter().map(|b| b.constructor_candidates.len()).sum();
1566    if candidates > 1 {
1567        let listed: Vec<String> = blocks
1568            .iter()
1569            .filter(|b| !b.constructor_candidates.is_empty())
1570            .map(|b| {
1571                let names: Vec<String> = b
1572                    .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(...)`.
1580        let settings = blocks[0].settings;
1581        let system = &settings[..settings
1582            .find(|c: char| !c.is_ascii_alphanumeric())
1583            .unwrap_or(settings.len())];
1584        return 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    }
1592    Ok(blocks.to_vec())
1593}
1594
1595/// 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 {
1601    fn unmark(line: &str) -> &str {
1602        line.strip_prefix(CLASS_MEMBER_DEFAULT_PREFIX)
1603            .unwrap_or(line)
1604    }
1605    let mut lines = content.split('\n').peekable();
1606    let mut result = Vec::new();
1607    while let Some(line) = lines.next() {
1608        if !unmark(line).trim_start().starts_with("#'") {
1609            result.push(line.to_owned());
1610            continue;
1611        }
1612        let mut block = ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [line]))vec![line];
1613        while lines
1614            .peek()
1615            .is_some_and(|line| unmark(line).trim_start().starts_with("#'"))
1616        {
1617            block.push(lines.next().unwrap());
1618        }
1619        let default_page = block.iter().any(|line| {
1620            line.strip_prefix(CLASS_MEMBER_DEFAULT_PREFIX)
1621                .is_some_and(|line| line.starts_with("#' @rdname "))
1622        });
1623        let page = if default_page {
1624            Some(topic)
1625        } else {
1626            block
1627                .iter()
1628                .find_map(|line| line.strip_prefix("#' @rdname "))
1629        };
1630        let joined = page.is_some_and(|page| page != class)
1631            || block.iter().any(|line| line.starts_with("#' @describeIn "));
1632        let has_order = block
1633            .iter()
1634            .any(|line| unmark(line).starts_with("#' @order "));
1635        let no_rd = block.iter().any(|line| unmark(line).trim() == "#' @noRd");
1636        for line in block {
1637            match line.strip_prefix(CLASS_MEMBER_DEFAULT_PREFIX) {
1638                Some(default) if joined => {
1639                    if default.starts_with("#' @rdname ") {
1640                        result.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @rdname {0}", topic))
    })format!("#' @rdname {topic}"));
1641                    }
1642                }
1643                Some(default) => result.push(default.to_owned()),
1644                None => result.push(line.to_owned()),
1645            }
1646        }
1647        if joined && !has_order && !no_rd && default_page {
1648            result.push("#' @order NaN".to_owned());
1649        }
1650    }
1651    result.join("\n")
1652}
1653
1654/// 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 {
1657    let 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.
1660    let replaced = |owner: &ClassBlockEntry, default: &DocTag<'_>| {
1661        block_docs.iter().any(|(block, tags)| {
1662            !std::ptr::eq(*block, owner) && tags.iter().any(|t| t.replaces(default))
1663        })
1664    };
1665    let mut class_docs: Vec<DocTag<'_>> = Vec::new();
1666    for tag in block_docs.iter().flat_map(|(_, tags)| tags) {
1667        if !class_docs.contains(tag) {
1668            class_docs.push(tag.clone());
1669        }
1670    }
1671    let mut class_doc_lines: Vec<DocTag<'_>> = Vec::new();
1672    for block in blocks {
1673        for mut tag in DocTag::parse(class_part(block.class_doc_lines)) {
1674            if tag.default && replaced(block, &tag) {
1675                continue;
1676            }
1677            tag.default = false;
1678            if !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.
1685    let 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        });
1692    let mut skipping = false;
1693    // `split`, not `lines`: a blank last line separates the head from the
1694    // members.
1695    let mut head_lines: Vec<&str> = Vec::new();
1696    for line in class_part(definer.head).split('\n') {
1697        if skipping {
1698            skipping = line != "NULL";
1699            continue;
1700        }
1701        if 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) {
1708            if !replaced(definer, &tag) {
1709                head_lines.push(tag.lines[0]);
1710            }
1711        } else {
1712            head_lines.push(line);
1713        }
1714    }
1715    let head = head_lines.join("\n");
1716
1717    let 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)];
1718    parts.extend(blocks.iter().map(|&b| (b, class_part(b.members))));
1719    parts.push((definer, class_part(definer.tail)));
1720
1721    let mut content = String::new();
1722    let mut previous: Option<&ClassBlockEntry> = None;
1723    for (block, text) in parts {
1724        if text.is_empty() {
1725            continue;
1726        }
1727        if previous.is_some_and(|p| std::ptr::eq(p, block)) {
1728            content.push('\n');
1729            content.push_str(text);
1730            continue;
1731        }
1732        if 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    }
1743    let 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());
1749    resolve_class_member_docs(&content, definer.r_class_name, &topic)
1750}
1751
1752// endregion
1753
1754/// 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>> {
1767    let classes = merge_class_blocks(&MX_CLASS_BLOCKS).unwrap_or_else(|msg| { ::core::panicking::panic_fmt(format_args!("{0}", msg)); }panic!("{msg}"));
1768    let mut entries: Vec<WrapperFragment> = MX_R_WRAPPERS
1769        .iter()
1770        .map(WrapperFragment::from)
1771        .chain(classes)
1772        .chain(sidecar_accessor_fragments(
1773            &MX_SIDECAR_ACCESSORS,
1774            &MX_CLASS_NAMES,
1775        ))
1776        .chain(
1777            all_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();
1782    sort_wrapper_entries(&mut entries);
1783
1784    let mut seen = std::collections::HashSet::<String>::new();
1785    let mut fragments: Vec<(
1786        RWrapperPriority,
1787        &'static str,
1788        std::borrow::Cow<'static, str>,
1789    )> = Vec::with_capacity(entries.len());
1790    for entry in entries {
1791        let 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        };
1795        if !trimmed.is_empty() && seen.insert(trimmed.to_string()) {
1796            fragments.push((entry.priority, entry.source_file, trimmed));
1797        }
1798    }
1799
1800    // 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.
1803    let standalone: Vec<StandaloneFragment<'_>> = fragments
1804        .iter()
1805        .filter(|(priority, _, _)| *priority == RWrapperPriority::Function)
1806        .map(|(_, source_file, content)| StandaloneFragment {
1807            content,
1808            stem: source_file_stem(source_file),
1809        })
1810        .collect();
1811    let mut resolved = resolve_standalone_pages(&standalone).into_iter();
1812
1813    let mut result: Vec<std::borrow::Cow<'static, str>> = fragments
1814        .into_iter()
1815        .map(|(priority, _, content)| {
1816            if priority == RWrapperPriority::Function {
1817                std::borrow::Cow::Owned(
1818                    resolved
1819                        .next()
1820                        .expect("one resolved fragment per standalone function"),
1821                )
1822            } else {
1823                content
1824            }
1825        })
1826        .collect();
1827
1828    // Topological sort for S7 inheritance ordering
1829    sort_s7_classes(&mut result);
1830
1831    result
1832}
1833
1834// region: standalone-function pages (#1590)
1835
1836/// 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__ ";
1841
1842/// 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";
1847
1848/// 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}
1860
1861/// 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.
1865    page: 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.
1869    joined: 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).
1873    title: Option<String>,
1874}
1875
1876#[cfg(not(target_arch = "wasm32"))]
1877fn standalone_page(fragment: &StandaloneFragment<'_>) -> StandalonePage {
1878    let content = fragment.content;
1879    let stem = fragment.stem.as_deref();
1880    if has_no_rd_tag(content) {
1881        return StandalonePage {
1882            page: None,
1883            joined: false,
1884            title: None,
1885        };
1886    }
1887    let rdname = roxygen_tag_value(content, "rdname");
1888    let describe_in = roxygen_tag_value(content, "describeIn");
1889    let own_name = roxygen_tag_value(content, "name");
1890    let fn_name = content.lines().find_map(parse_top_level_fn_def_name);
1891    let own_page =
1892        |topic: &str| Some(topic) == stem || Some(topic) == own_name || Some(topic) == fn_name;
1893    let joined = describe_in.is_some() || rdname.is_some_and(|topic| !own_page(topic));
1894    let page = describe_in
1895        .or(rdname)
1896        .or(own_name)
1897        .or(fn_name)
1898        .map(str::to_string);
1899    let is_roxygen_block = content.lines().any(|l| l.trim_start().starts_with("#'"));
1900    let has_title = content.lines().any(|l| l.trim().starts_with("#' @title "));
1901    let title = fn_name
1902        .filter(|_| is_roxygen_block && !has_title && !has_page_assignment_tag(content))
1903        .map(str::to_string);
1904    StandalonePage {
1905        page,
1906        joined,
1907        title,
1908    }
1909}
1910
1911/// 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> {
1938    use std::collections::{HashMap, HashSet};
1939
1940    let pages: Vec<StandalonePage> = fragments.iter().map(standalone_page).collect();
1941
1942    // Arguments each page documents for real (the author's `@param` lines).
1943    let mut documented: HashMap<&str, HashSet<&str>> = HashMap::new();
1944    for (fragment, page) in fragments.iter().zip(&pages) {
1945        let Some(topic) = page.page.as_deref() else {
1946            continue;
1947        };
1948        let names = documented.entry(topic).or_default();
1949        for line in fragment.content.lines() {
1950            if 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    }
1957
1958    let filler_prefix = ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' {0}", PARAM_FILLER_MARKER))
    })format!("#' {PARAM_FILLER_MARKER}");
1959    fragments
1960        .iter()
1961        .zip(&pages)
1962        .map(|(fragment, page)| {
1963            let mut kept = Vec::new();
1964            for line in fragment.content.lines() {
1965                let Some(param) = line.trim().strip_prefix(filler_prefix.as_str()) else {
1966                    kept.push(line.to_string());
1967                    continue;
1968                };
1969                let name = param
1970                    .strip_prefix("@param ")
1971                    .and_then(|rest| rest.split_whitespace().next());
1972                let documented_on_page = page
1973                    .page
1974                    .as_deref()
1975                    .zip(name)
1976                    .is_some_and(|(topic, name)| documented[topic].contains(name));
1977                if 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            }
1981            let mut content = kept.join("\n");
1982            if page.joined && !content.lines().any(|l| l.trim().starts_with("#' @order ")) {
1983                content = insert_roxygen_line(&content, ORDER_AFTER_TOPIC_BLOCKS);
1984            }
1985            match page.title.as_deref() {
1986                Some(title) => insert_roxygen_line(&content, &::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @title {0}", title))
    })format!("#' @title {title}")),
1987                None => content,
1988            }
1989        })
1990        .collect()
1991}
1992
1993/// 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> {
1996    content.lines().find_map(|line| {
1997        let rest = line.trim().strip_prefix("#' @")?.strip_prefix(tag)?;
1998        if rest.starts_with(char::is_whitespace) {
1999            rest.split_whitespace().next()
2000        } else {
2001            None
2002        }
2003    })
2004}
2005
2006/// 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 {
2010    let lines: Vec<&str> = content.lines().collect();
2011    let at = lines
2012        .iter()
2013        .position(|l| {
2014            let t = l.trim();
2015            t.starts_with("#' @export")
2016                || t.starts_with("#' @keywords")
2017                || t.starts_with("#' @source")
2018        })
2019        .or_else(|| {
2020            lines
2021                .iter()
2022                .rposition(|l| l.trim().starts_with("#'"))
2023                .map(|i| i + 1)
2024        })
2025        .unwrap_or(0);
2026    let mut result = lines;
2027    result.insert(at, new_line);
2028    result.join("\n")
2029}
2030
2031// endregion
2032
2033/// 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]) {
2044    entries.sort_by_key(|e| (e.priority, e.source_file, e.source_line));
2045}
2046
2047#[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 {
2055    content.lines().any(|line| {
2056        let trimmed = line.trim();
2057        trimmed.starts_with("#' @rdname ")
2058            || trimmed.starts_with("#' @describeIn ")
2059            || trimmed.starts_with("#' @name ")
2060    })
2061}
2062
2063#[cfg(not(target_arch = "wasm32"))]
2064/// Check if an R wrapper fragment has `@noRd`.
2065fn has_no_rd_tag(content: &str) -> bool {
2066    content.lines().any(|line| {
2067        let trimmed = line.trim();
2068        trimmed == "#' @noRd"
2069    })
2070}
2071
2072#[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> {
2081    let file_name = path.rsplit(['/', '\\']).next()?;
2082    let stem = file_name.strip_suffix(".rs").unwrap_or(file_name);
2083    if stem.is_empty() || stem == "lib" || stem == "mod" {
2084        return None;
2085    }
2086    Some(stem.to_string())
2087}
2088
2089#[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>]) {
2095    use std::collections::HashMap;
2096
2097    // Parse S7 class definitions: find (index, name, parent)
2098    let mut s7_info: Vec<(usize, String, Option<String>)> = Vec::new();
2099
2100    for (i, entry) in entries.iter().enumerate() {
2101        if let Some(nc_pos) = entry.find("S7::new_class(") {
2102            // Extract class name: "NAME <- S7::new_class("
2103            let before = entry[..nc_pos].trim_end();
2104            let 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());
2109
2110            let Some(name) = name else { continue };
2111
2112            // Extract parent: "parent = ParentName,"
2113            let after = &entry[nc_pos..];
2114            let parent = after.find("parent = ").and_then(|p| {
2115                let rest = &after[p + "parent = ".len()..];
2116                let end = rest.find([',', ')', '\n']).unwrap_or(rest.len());
2117                let p = rest[..end].trim();
2118                if p.is_empty() {
2119                    None
2120                } else {
2121                    Some(p.to_string())
2122                }
2123            });
2124
2125            s7_info.push((i, name.to_string(), parent));
2126        }
2127    }
2128
2129    if s7_info.len() <= 1 {
2130        return;
2131    }
2132
2133    // 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).
2137    let mut name_to_pos: HashMap<String, usize> = HashMap::new();
2138    for (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    }
2144
2145    // Topological sort: repeatedly emit classes whose parent is already placed
2146    let n = s7_info.len();
2147    let mut order: Vec<usize> = Vec::with_capacity(n);
2148    let mut placed = ::alloc::vec::from_elem(false, n)vec![false; n];
2149
2150    for _ in 0..n {
2151        for (pos, (_, _, parent)) in s7_info.iter().enumerate() {
2152            if placed[pos] {
2153                continue;
2154            }
2155            let ready = match parent {
2156                None => true,
2157                Some(pname) => match name_to_pos.get(pname.as_str()) {
2158                    None => true, // external parent
2159                    Some(&pp) => placed[pp],
2160                },
2161            };
2162            if ready {
2163                order.push(pos);
2164                placed[pos] = true;
2165            }
2166        }
2167    }
2168
2169    // Fallback: add any remaining (cycles) in original order
2170    for (pos, &is_placed) in placed.iter().enumerate().take(n) {
2171        if !is_placed {
2172            order.push(pos);
2173        }
2174    }
2175
2176    // Apply: place sorted S7 entries back at their original indices
2177    let s7_indices: Vec<usize> = s7_info.iter().map(|(i, _, _)| *i).collect();
2178    let original: Vec<std::borrow::Cow<'static, str>> =
2179        s7_indices.iter().map(|&i| entries[i].clone()).collect();
2180
2181    for (slot, &src) in order.iter().enumerate() {
2182        entries[s7_indices[slot]] = original[src].clone();
2183    }
2184}
2185// endregion
2186
2187/// 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 {
2204    use crate::match_arg::{default_first, escape_r_string};
2205    let formal = if preferred.is_empty() {
2206        choices.to_vec()
2207    } else {
2208        default_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    };
2215    let quoted: Vec<String> = formal
2216        .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}
2221
2222/// 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 {
2250    use crate::match_arg::{alias_problems, escape_r_string, listed_aliases_clause, r_quote};
2251    if aliases.is_empty() {
2252        return String::new();
2253    }
2254    let problems = alias_problems(choices, aliases);
2255    if !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    }
2262    let pairs: Vec<String> = aliases
2263        .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();
2272    let mut arg = ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!(", aliases = base::c({0})",
                pairs.join(", ")))
    })format!(", aliases = base::c({})", pairs.join(", "));
2273    let listed = listed_aliases_clause(choices, aliases, r_quote);
2274    if !listed.is_empty() {
2275        arg.push_str(&::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!(", listed = \"{0}\"",
                escape_r_string(&listed)))
    })format!(", listed = \"{}\"", escape_r_string(&listed)));
2276    }
2277    arg
2278}
2279
2280/// `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 {
2286    let formal = match_arg_formal(entry.choices, entry.preferred_default, entry.placeholder);
2287    let aliases = match_arg_aliases_arg(entry.choices, entry.aliases, entry.aliases_placeholder);
2288    content
2289        .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}
2292
2293// region: R Wrapper File Generation
2294
2295// region: Generic doc marker resolution
2296
2297/// 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 ')'
2309    let inner = marker
2310        .strip_prefix(".__MX_GENERIC_DOC__(")?
2311        .strip_suffix(')')?;
2312
2313    let mut kind = String::new();
2314    let mut generic = String::new();
2315    let mut class = String::new();
2316    let mut export = false;
2317    let mut dispatch = "x".to_string();
2318    let mut no_dots = false;
2319
2320    for part in inner.split(", ") {
2321        let part = part.trim();
2322        if 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    }
2336
2337    if kind.is_empty() || generic.is_empty() || class.is_empty() {
2338        return None;
2339    }
2340
2341    Some((kind, generic, class, export, dispatch, no_dots))
2342}
2343
2344/// 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 {
2351    const 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    ];
2372    let mut chars = name.chars();
2373    match chars.next() {
2374        Some(c) if c.is_ascii_alphabetic() => {}
2375        Some('.') => {
2376            if chars.next().is_some_and(|c| c.is_ascii_digit()) {
2377                return false;
2378            }
2379        }
2380        _ => return false,
2381    }
2382    name.chars()
2383        .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_')
2384        && !RESERVED.contains(&name)
2385}
2386
2387/// 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 {
2394    if is_syntactic_r_name(name) {
2395        name.to_string()
2396    } else {
2397        ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("\"{0}\"",
                name.replace('\\', "\\\\").replace('"', "\\\"")))
    })format!("\"{}\"", name.replace('\\', "\\\\").replace('"', "\\\""))
2398    }
2399}
2400
2401/// 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 {
2421    let mut lines: Vec<String> = Vec::new();
2422
2423    // An operator generic (`[[`, `%op%`) is not called as `name()`.
2424    let 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    };
2429
2430    // Title
2431    lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' The {0} generic", display))
    })format!("#' The {display} generic"));
2432    lines.push("#'".to_string());
2433
2434    // Description
2435    let kind_phrase = if kind == "S4" {
2436        "an S4 generic"
2437    } else {
2438        "an S7 generic"
2439    };
2440    lines.push("#' @description".to_string());
2441    lines.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    ));
2444    lines.push("#' package are available for:".to_string());
2445    lines.push("#' \\itemize{".to_string());
2446    for 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    }
2449    lines.push("#' }".to_string());
2450
2451    // @param lines for dispatch formals
2452    let dispatch_args: Vec<&str> = dispatch.split(',').map(|s| s.trim()).collect();
2453    for 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    }
2456    if !no_dots {
2457        lines.push("#' @param ... Passed on to methods.".to_string());
2458    }
2459
2460    // Bare @name — this is what produces the bare \\alias{<generic>} in Rd
2461    lines.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("#\' @name {0}", generic))
    })format!("#' @name {generic}"));
2462
2463    // Export directive
2464    if export {
2465        if kind == "S4" {
2466            // S4 generics use @exportMethod (matches the method blocks)
2467            lines.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).
2471            lines.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    }
2477
2478    lines.push("NULL".to_string());
2479
2480    lines.join("\n")
2481}
2482
2483/// 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 {
2491    use std::collections::HashMap;
2492
2493    const MARKER_PREFIX: &str = ".__MX_GENERIC_DOC__(";
2494
2495    // 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).
2497    let mut by_generic: HashMap<String, (String, Vec<String>, bool, String, bool)> = HashMap::new();
2498    let mut generic_order: Vec<String> = Vec::new(); // tracks first-seen order
2499
2500    for line in content.lines() {
2501        let trimmed = line.trim();
2502        if !trimmed.starts_with(MARKER_PREFIX) {
2503            continue;
2504        }
2505        if let Some((kind, generic, class, export, dispatch, no_dots)) =
2506            parse_generic_doc_marker(trimmed)
2507        {
2508            let 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    }
2515
2516    if by_generic.is_empty() {
2517        return content;
2518    }
2519
2520    // 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.
2523    let mut first_seen: std::collections::HashSet<String> = std::collections::HashSet::new();
2524    let mut result = String::with_capacity(content.len() + 512 * by_generic.len());
2525
2526    for line in content.lines() {
2527        let trimmed = line.trim();
2528        if trimmed.starts_with(MARKER_PREFIX) {
2529            if let Some((_kind, generic, _class, _export, _dispatch, _no_dots)) =
2530                parse_generic_doc_marker(trimmed)
2531            {
2532                if first_seen.insert(generic.clone()) {
2533                    // First occurrence: replace with synthesised block
2534                    if let Some((kind, classes, export, dispatch, no_dots)) =
2535                        by_generic.get(&generic)
2536                    {
2537                        let 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)
2545                continue;
2546            }
2547        }
2548        result.push_str(line);
2549        result.push('\n');
2550    }
2551
2552    // Trim trailing newlines added by the line-by-line loop and restore the
2553    // original termination (content typically ends with \n).
2554    result
2555}
2556
2557// endregion
2558
2559// region: Inherited param marker resolution
2560
2561/// 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)> {
2570    let inner = marker
2571        .strip_prefix(".__MX_INHERITED_PARAM__(")?
2572        .strip_suffix(')')?;
2573
2574    let mut class = String::new();
2575    let mut parent = String::new();
2576    let mut method = String::new();
2577    let mut param = String::new();
2578
2579    for part in inner.split(", ") {
2580        let part = part.trim();
2581        if 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    }
2591
2592    if class.is_empty() || parent.is_empty() || method.is_empty() || param.is_empty() {
2593        return None;
2594    }
2595
2596    Some((class, parent, method, param))
2597}
2598
2599/// 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 {
2617    use std::collections::{HashMap, HashSet};
2618
2619    const MARKER_PREFIX: &str = ".__MX_INHERITED_PARAM__(";
2620
2621    // Check if the marker is present at all (fast path).
2622    if !content.contains(MARKER_PREFIX) {
2623        return content;
2624    }
2625
2626    // 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.
2636    let mut class_method_params: HashMap<(String, String), HashSet<String>> = HashMap::new();
2637
2638    // 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).
2641    let lines: Vec<&str> = content.lines().collect();
2642    let mut i = 0;
2643    while i < lines.len() {
2644        let line = lines[i].trim();
2645        // Class-level: `#' @name ClassName` followed by class-level @param lines
2646        if let Some(rest) = line.strip_prefix("#' @name ") {
2647            let class_name = rest.trim().to_string();
2648            // Scan forward for @param lines in the class header (until non-doc)
2649            let mut j = i + 1;
2650            while j < lines.len() {
2651                let next = lines[j].trim();
2652                if next.starts_with("#'") {
2653                    if let Some(param_rest) = next.strip_prefix("#' @param ") {
2654                        if 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 {
2663                    break;
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.
2672        i += 1;
2673    }
2674
2675    // 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.
2677    let mut current_class: Option<String> = None;
2678    let mut current_method: Option<String> = None;
2679    i = 0;
2680    while i < lines.len() {
2681        let raw = lines[i];
2682        let trimmed = raw.trim();
2683        // Detect R6Class definition: `ClassName <- R6::R6Class("ClassName",`
2684        if trimmed.contains("R6::R6Class(\"") {
2685            if let Some(start) = trimmed.find("R6::R6Class(\"") {
2686                let rest = &trimmed[start + "R6::R6Class(\"".len()..];
2687                if 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)`
2694        if let Some(ref cls) = current_class.clone() {
2695            let prefix = ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("# {0}::", cls))
    })format!("# {}::", cls);
2696            if trimmed.starts_with(&prefix) {
2697                let rest = &trimmed[prefix.len()..];
2698                let method_name = rest.split_whitespace().next().unwrap_or("").to_string();
2699                if !method_name.is_empty() {
2700                    current_method = Some(method_name);
2701                }
2702            }
2703            // Detect indented method @param: `    #' @param name ...`
2704            if let Some(ref method) = current_method.clone() {
2705                if let Some(rest) = raw.strip_prefix("    #' @param ") {
2706                    if 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    }
2717
2718    // Second pass: resolve markers.
2719    let mut result = String::with_capacity(content.len());
2720    for line in content.lines() {
2721        let trimmed = line.trim();
2722        // Marker format: `    #' .__MX_INHERITED_PARAM__(class="...", parent="...", method="...", param="...")`
2723        let inner = trimmed.strip_prefix("#' ").unwrap_or(trimmed);
2724        if inner.starts_with(MARKER_PREFIX) {
2725            if 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, "").
2728                let parent_method_key = (parent.clone(), method.clone());
2729                let parent_class_key = (parent.clone(), String::new());
2730                let documented_in_method = class_method_params
2731                    .get(&parent_method_key)
2732                    .map(|s| s.contains(&param))
2733                    .unwrap_or(false);
2734                let documented_at_class = class_method_params
2735                    .get(&parent_class_key)
2736                    .map(|s| s.contains(&param))
2737                    .unwrap_or(false);
2738
2739                if 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.
2742                    continue;
2743                } else {
2744                    // Parent not found or param not documented → keep fallback.
2745                    // Reconstruct indentation from the original line.
2746                    let 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');
2750                    continue;
2751                }
2752            }
2753        }
2754        result.push_str(line);
2755        result.push('\n');
2756    }
2757
2758    result
2759}
2760
2761// endregion
2762
2763// region: cross-class return wrapper resolvers
2764
2765/// 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 {
2785    const ARG_OPEN: &str = "__(";
2786    const ARG_CLOSE: char = ')';
2787
2788    let mut result = String::with_capacity(content.len());
2789    let mut remaining = content;
2790    while let Some(start) = remaining.find(prefix) {
2791        result.push_str(&remaining[..start]);
2792        let rest_from_prefix = &remaining[start..];
2793        let marker_body = &rest_from_prefix[prefix.len()..];
2794        let Some(name_end) = marker_body.find(ARG_OPEN) else {
2795            // Malformed placeholder (no opening `__(`) — emit as-is and stop scanning.
2796            result.push_str(prefix);
2797            remaining = marker_body;
2798            break;
2799        };
2800        let rust_name = &marker_body[..name_end];
2801        let after_open = &marker_body[name_end + ARG_OPEN.len()..];
2802        let Some(expr_end) = after_open.find(ARG_CLOSE) else {
2803            // Malformed placeholder (no closing `)`) — emit as-is and stop scanning.
2804            result.push_str(prefix);
2805            remaining = marker_body;
2806            break;
2807        };
2808        let 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    }
2812    result.push_str(remaining);
2813    result
2814}
2815
2816/// 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 {
2829    resolve_wrap_return_markers(
2830        content,
2831        ".__MX_WRAP_RETURN_",
2832        class_index,
2833        |result, entry, expr| match entry {
2834            Some(entry) => match entry.class_system {
2835                "r6" => {
2836                    result.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" => {
2839                    result.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" => {
2842                    result.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                _ => {
2848                    result.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            },
2854            None => result.push_str(expr),
2855        },
2856    )
2857}
2858
2859/// 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 {
2871    resolve_wrap_return_markers(
2872        content,
2873        ".__MX_WRAP_LIST_RETURN_",
2874        class_index,
2875        |result, entry, expr| match entry {
2876            Some(entry) => match entry.class_system {
2877                "r6" => {
2878                    result.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" => {
2884                    result.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" => {
2890                    result.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                _ => {
2896                    result.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            },
2902            None => result.push_str(expr),
2903        },
2904    )
2905}
2906
2907// endregion
2908
2909/// 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"#;
2961
2962/// 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    () => {
2992        r#"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}
3013
3014/// 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!(
3019    r#"# 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 <- "#,
3034    frame_call_helper_fn!(),
3035    "\n"
3036);
3037
3038/// 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}
3063
3064# 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"#;
3074
3075/// 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];
3101
3102#[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"#;
3125
3126/// 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"#;
3202
3203#[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!(
3205    r#"# 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, ""#,
3223    crate::match_arg::match_arg_wording!(not_character),
3224    r#"", 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\") "#,
3234    crate::match_arg::match_arg_wording!(not_a_choice),
3235    r#" %s%s", bad[[1L]], arg[[bad[[1L]]]], paste(dQuote(choices, FALSE), collapse = ", "), listed), call)
3236  }
3237  choices[i]
3238}
3239"#
3240);
3241
3242#[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!(
3244    r#"# 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, ""#,
3262    crate::match_arg::match_arg_wording!(not_character),
3263    r#"", call)
3264  if (identical(arg, choices)) return(arg[[1L]])
3265  if (length(arg) != 1L) .miniextendr_arg_error(arg_name, ""#,
3266    crate::match_arg::match_arg_wording!(not_scalar),
3267    r#"", 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(""#,
3272    crate::match_arg::match_arg_wording!(not_a_choice),
3273    r#" %s%s", paste(dQuote(choices, FALSE), collapse = ", "), listed), call)
3274  }
3275  choices[[i]]
3276}
3277"#
3278);
3279
3280/// 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) => {
3284        concat!(
3285            r#"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,
3299            r#"(.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}
3364
3365/// 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!(), ")"));
3386
3387/// 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");
3391
3392/// 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"#;
3450
3451/// 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 {
3462    format_conversion_error_class_binding(conversion_error_class())
3463}
3464
3465/// [`conversion_error_class_binding`] for an explicit class list.
3466#[cfg(not(target_arch = "wasm32"))]
3467fn format_conversion_error_class_binding(classes: &[&str]) -> String {
3468    let value = if classes.is_empty() {
3469        "NULL".to_string()
3470    } else {
3471        let quoted: Vec<String> = classes
3472            .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}
3483
3484/// 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
3495
3496# 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";
3508
3509/// 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>]) {
3518    let uses = |name: &str| fragments.iter().any(|fragment| fragment.contains(name));
3519    let 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.
3522    let scalar = uses(".miniextendr_match_arg(");
3523    let several = uses(".miniextendr_match_arg_several(");
3524    for (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    ] {
3539        if needed {
3540            content.push_str(helper);
3541            content.push('\n');
3542        }
3543    }
3544}
3545
3546/// 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.
3557    let mut content = String::from(WRAPPERS_PREAMBLE_HEADER);
3558    content.push_str(".miniextendr_raise_condition <- ");
3559    content.push_str(WRAPPER_RAISE_CONDITION_HELPER_FN);
3560    content.push_str("\n\n");
3561    content.push_str(FRAME_CALL_HELPER);
3562    content.push('\n');
3563    let fragments = collect_r_wrappers();
3564    append_used_wrapper_helpers(&mut content, &fragments);
3565    content.push_str(&conversion_error_class_binding());
3566    content.push_str("\n\n");
3567
3568    for fragment in fragments {
3569        content.push_str(fragment.as_ref());
3570        content.push_str("\n\n");
3571    }
3572
3573    content.push_str("# nocov end\n# nolint end\n");
3574
3575    // 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).
3581    for entry in MX_MATCH_ARG_CHOICES.iter() {
3582        content = splice_match_arg_entry(&content, entry);
3583    }
3584
3585    // Replace match_arg @param doc placeholders with human-readable choice descriptions
3586    for entry in MX_MATCH_ARG_PARAM_DOCS.iter() {
3587        let choices = (entry.choices_str)();
3588        let prefix = if entry.several_ok {
3589            "One or more of"
3590        } else {
3591            "One of"
3592        };
3593        let 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    }
3596
3597    // 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    {
3602        use std::collections::HashMap;
3603        // Group entries by rust_type
3604        let mut by_type: HashMap<&'static str, Vec<&SidecarPropEntry>> = HashMap::new();
3605        for entry in MX_S7_SIDECAR_PROPS.iter() {
3606            by_type.entry(entry.rust_type).or_default().push(entry);
3607        }
3608
3609        const SIDECAR_PREFIX: &str = ".__MX_S7_SIDECAR_PROP_DOCS_";
3610        const SIDECAR_SUFFIX: &str = "__";
3611        let mut result = String::with_capacity(content.len());
3612        let mut remaining = content.as_str();
3613        while let Some(start) = remaining.find(SIDECAR_PREFIX) {
3614            result.push_str(&remaining[..start]);
3615            remaining = &remaining[start + SIDECAR_PREFIX.len()..];
3616            if let Some(end) = remaining.find(SIDECAR_SUFFIX) {
3617                let rust_name = &remaining[..end];
3618                remaining = &remaining[end + SIDECAR_SUFFIX.len()..];
3619                // Emit @prop lines for each sidecar field registered for this type
3620                if let Some(entries) = by_type.get(rust_name) {
3621                    let 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
3631                result.push_str(SIDECAR_PREFIX);
3632                break;
3633            }
3634        }
3635        result.push_str(remaining);
3636        content = result;
3637    }
3638
3639    // 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    {
3645        let class_index = build_class_name_index(MX_CLASS_NAMES.iter());
3646
3647        // 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.)
3655        const OR_ANY_PREFIX: &str = ".__MX_CLASS_REF_OR_ANY_";
3656        const PREFIX: &str = ".__MX_CLASS_REF_";
3657        const SUFFIX: &str = "__";
3658
3659        let mut result = String::with_capacity(content.len());
3660        let mut remaining = content.as_str();
3661        while let Some(start) = remaining.find(PREFIX) {
3662            result.push_str(&remaining[..start]);
3663            // Does this occurrence have the OR_ANY form?
3664            let rest_from_prefix = &remaining[start..];
3665            let (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 __
3672            if let Some(end) = remaining.find(SUFFIX) {
3673                let rust_name = &remaining[..end];
3674                remaining = &remaining[end + SUFFIX.len()..];
3675                match class_index.get(rust_name) {
3676                    Some(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.
3682                        if entry.class_system == "s7" {
3683                            result.push_str(entry.r_class_name);
3684                        } else {
3685                            result.push_str("S7::class_any");
3686                        }
3687                    }
3688                    Some(entry) => {
3689                        result.push_str(entry.r_class_name);
3690                    }
3691                    None if quiet_fallback => {
3692                        // Unresolved S7 property type → silent class_any,
3693                        // matching pre-#154 behavior.
3694                        result.push_str("S7::class_any");
3695                    }
3696                    None => {
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.
3708                result.push_str(PREFIX);
3709                break;
3710            }
3711        }
3712        result.push_str(remaining);
3713        content = result;
3714    }
3715
3716    // 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    {
3722        let class_index = build_class_name_index(MX_CLASS_NAMES.iter());
3723        content = resolve_scalar_return_wrappers(&content, &class_index);
3724        content = resolve_list_return_wrappers(&content, &class_index);
3725    }
3726
3727    // 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.
3743    content = resolve_generic_doc_markers(content);
3744
3745    // 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.
3757    content = resolve_inherited_param_markers(content);
3758
3759    // 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    {
3772        let class_index = build_class_name_index(MX_CLASS_NAMES.iter());
3773        let accessor_producers = sidecar_accessor_names(&MX_S7_SIDECAR_PROPS, &class_index);
3774        if let Err(msg) = detect_duplicate_wrapper_defs(&content, &accessor_producers) {
3775            { ::core::panicking::panic_fmt(format_args!("{0}", msg)); };panic!("{msg}");
3776        }
3777    }
3778
3779    // 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.
3782    if let Err(msg) = reexport_conflicts(&content, &MX_REEXPORTS) {
3783        { ::core::panicking::panic_fmt(format_args!("{0}", msg)); };panic!("{msg}");
3784    }
3785
3786    let content = wrappers_file_content(&content);
3787
3788    // 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.
3800    let existing = std::fs::read_to_string(path).unwrap_or_default();
3801    if wrappers_semantically_equal(&existing, &content) {
3802        return;
3803    }
3804
3805    // 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).
3814    let dest = std::path::Path::new(path);
3815    let 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)]
3819    let _ = 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    });
3827
3828    if !existing.is_empty() {
3829        let filename = dest
3830            .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}
3838
3839/// 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 ----";
3854
3855/// 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 {
3868    let digest = crate::abi::fnv1a_64(
3869        normalize_source_locs(body).as_bytes(),
3870        crate::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}",
3874        env!("CARGO_PKG_VERSION"),
3875    )
3876}
3877
3878/// 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 {
3886    normalize_source_locs(a) == normalize_source_locs(b)
3887}
3888
3889/// 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.
3897    if !s.contains(".rs:") {
3898        return std::borrow::Cow::Borrowed(s);
3899    }
3900
3901    let bytes = s.as_bytes();
3902    let len = bytes.len();
3903    let mut out = String::with_capacity(len);
3904    let mut pos = 0usize;
3905    let mut any_replaced = false;
3906
3907    while pos < len {
3908        // Look for the next `(`.
3909        let Some(open) = memchr(bytes, pos, b'(') else {
3910            break;
3911        };
3912
3913        // After `(`, scan for `.rs:` without crossing `)` or `(` or a newline.
3914        let inner_start = open + 1;
3915        let 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.
3917            out.push_str(&s[pos..]);
3918            return std::borrow::Cow::Owned(out);
3919        };
3920
3921        // Make sure `.rs:` precedes any closing `)`, `(`, or newline (no nesting).
3922        let intervening = &bytes[inner_start..dot_rs];
3923        if 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.
3928            out.push_str(&s[pos..=open]);
3929            pos = open + 1;
3930            continue;
3931        }
3932
3933        // After `.rs:`, consume digits for LINE.
3934        let after_colon1 = dot_rs + 4; // skip `.rs:`
3935        let Some(colon2) = scan_digits(bytes, after_colon1) else {
3936            // Not digits after `.rs:` — not a source-attribution pattern.
3937            out.push_str(&s[pos..=open]);
3938            pos = open + 1;
3939            continue;
3940        };
3941        if colon2 >= len || bytes[colon2] != b':' {
3942            out.push_str(&s[pos..=open]);
3943            pos = open + 1;
3944            continue;
3945        }
3946
3947        // After the second `:`, consume digits for COL.
3948        let after_colon2 = colon2 + 1;
3949        let Some(close_pos) = scan_digits(bytes, after_colon2) else {
3950            out.push_str(&s[pos..=open]);
3951            pos = open + 1;
3952            continue;
3953        };
3954        if close_pos >= len || bytes[close_pos] != b')' {
3955            out.push_str(&s[pos..=open]);
3956            pos = open + 1;
3957            continue;
3958        }
3959
3960        // We have a match: `(stem.rs:LINE:COL)` — emit `(stem.rs:_:_)`.
3961        any_replaced = true;
3962        out.push_str(&s[pos..inner_start]); // text before inner_start (includes `(`)
3963        out.push_str(&s[inner_start..dot_rs + 3]); // `stem.rs` (without the colon)
3964        out.push_str(":_:_)");
3965        pos = close_pos + 1; // skip past the closing `)`
3966    }
3967
3968    if !any_replaced {
3969        return std::borrow::Cow::Borrowed(s);
3970    }
3971
3972    out.push_str(&s[pos..]);
3973    std::borrow::Cow::Owned(out)
3974}
3975
3976/// 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> {
3981    let window = haystack.get(from..)?;
3982    window
3983        .windows(needle.len())
3984        .position(|w| w == needle)
3985        .map(|rel| from + rel)
3986}
3987
3988/// 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> {
3993    haystack[from..]
3994        .iter()
3995        .position(|&b| b == needle)
3996        .map(|rel| from + rel)
3997}
3998
3999/// 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> {
4005    let start = haystack.get(from..)?;
4006    let count = start.iter().take_while(|&&b| b.is_ascii_digit()).count();
4007    if count == 0 { None } else { Some(from + count) }
4008}
4009// endregion
4010
4011// region: Duplicate wrapper-definition detection (#991)
4012
4013/// 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> {
4031    let mut out = std::collections::HashMap::new();
4032    for entry in sidecar_props {
4033        // Resolve the R-visible prefix (honours `class = "Override"`).
4034        let prefix = class_index
4035            .get(entry.rust_type)
4036            .map(|c| c.r_class_name)
4037            .unwrap_or(entry.rust_type);
4038        let 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    }
4048    out
4049}
4050
4051/// 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> {
4095    use std::collections::HashMap;
4096
4097    // Binding name -> (kind, producer) of its first definition.
4098    let mut seen: HashMap<&str, (&str, std::rc::Rc<str>)> = HashMap::new();
4099
4100    for WrapperName {
4101        name,
4102        kind,
4103        producer,
4104    } in wrapper_names(content)
4105    {
4106        if kind == "export" {
4107            continue;
4108        }
4109        let Some((previous, first)) = seen.get(name) else {
4110            seen.insert(name, (kind, producer));
4111            continue;
4112        };
4113        let previous = *previous;
4114        if previous == "guarded generic" && kind == "guarded generic" {
4115            // Several classes intentionally dispatch through one generic.
4116            continue;
4117        }
4118        let 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}");
4119        if previous == "guarded generic" || kind == "guarded generic" {
4120            return 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        }
4128        if previous != "function" && kind != "function" {
4129            return 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        }
4136        if previous != "function" || kind != "function" {
4137            return 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.
4145        if let Some(derive) = accessor_producers.get(name) {
4146            return 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        }
4156        return 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    }
4168
4169    Ok(())
4170}
4171
4172/// 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.
4182    kind: &'static str,
4183    producer: std::rc::Rc<str>,
4184}
4185
4186/// 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<'_>> {
4191    let mut names = Vec::new();
4192    let mut producer: std::rc::Rc<str> = std::rc::Rc::from("an unattributed wrapper fragment");
4193    let mut lines = content.lines();
4194    while let Some(line) = lines.next() {
4195        if let Some(header) = line.strip_prefix("# Generated from ") {
4196            producer = std::rc::Rc::from(header);
4197            continue;
4198        }
4199        // A sidecar accessor's header gives only a source position; the line
4200        // after it names the field and the Rust type.
4201        if let Some(wraps) = line.strip_prefix("# Wraps ") {
4202            let 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})"));
4204            continue;
4205        }
4206        if 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            }));
4212            continue;
4213        }
4214        if 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            }));
4220            continue;
4221        }
4222        let (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.
4227            let Some((name, rhs)) = parse_top_level_assignment(line) else {
4228                continue;
4229            };
4230            let 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 {
4238                continue;
4239            };
4240            (name, kind)
4241        };
4242        names.push(WrapperName {
4243            name,
4244            kind,
4245            producer: producer.clone(),
4246        });
4247    }
4248    names
4249}
4250
4251/// 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> {
4256    directives
4257        .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)| {
4265            let args = &directives[at + open.len()..];
4266            let args = args.split_once(')').map_or(args, |(args, _)| args);
4267            args.split(',')
4268                .map(|arg| unquote_r_name(arg.trim()))
4269                .filter(|name| !name.is_empty())
4270        })
4271}
4272
4273/// `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| {
4279            name.strip_prefix(*quote)
4280                .and_then(|inner| inner.strip_suffix(*quote))
4281        })
4282        .unwrap_or(name)
4283}
4284
4285/// 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> {
4293    if !(line.starts_with("if (!base::exists(\"")
4294        || line.starts_with("if (!exists(\"")
4295        || line.starts_with("if (!base::inherits(base::get0(\""))
4296    {
4297        return None;
4298    }
4299    // S4 emits its complete guard and setGeneric call on one line.
4300    if let Some((_, call)) = line.split_once(")) methods::setGeneric(\"") {
4301        return call.split_once("\", ").map(|(name, _)| name);
4302    }
4303    if !line.ends_with(" {") {
4304        return 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.
4309    let body = following.find(|line| {
4310        let line = line.trim();
4311        !line.is_empty() && !line.starts_with('#')
4312    })?;
4313    let (name, rhs) = parse_top_level_assignment(body.strip_prefix("  ")?)?;
4314    // The wrapper's shadowed-formal pass can qualify UseMethod with base::.
4315    let s3 = rhs.strip_prefix("function(x, ...) ").is_some_and(|body| {
4316        body.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}
4321
4322/// 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> {
4326    let (name, rhs) = parse_top_level_assignment(line)?;
4327    rhs.starts_with("function").then_some(name)
4328}
4329
4330/// 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)> {
4338    if line.starts_with([' ', '\t']) {
4339        return None;
4340    }
4341    if let Some(quoted) = line.strip_prefix('`') {
4342        let mut escaped = false;
4343        for (index, ch) in quoted.char_indices() {
4344            if escaped {
4345                escaped = false;
4346            } else if ch == '\\' {
4347                escaped = true;
4348            } else if ch == '`' {
4349                let rhs = quoted[index + 1..].trim_start().strip_prefix("<-")?;
4350                return (!quoted[..index].is_empty())
4351                    .then_some((&quoted[..index], rhs.trim_start()));
4352            }
4353        }
4354        return None;
4355    }
4356    let (lhs, rhs) = line.split_once("<-")?;
4357    let name = lhs.trim();
4358    let first = name.chars().next()?;
4359    if !(first.is_ascii_alphabetic() || first == '.') {
4360        return None;
4361    }
4362    name.chars()
4363        .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_')
4364        .then_some((name, rhs.trim_start()))
4365}
4366// endregion
4367
4368// 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).
4374
4375/// 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 {
4392    crate::unwind_protect::with_r_unwind_protect_or_raise(
4393        || unsafe {
4394            use crate::{SEXP, SexpExt};
4395
4396            let char_sexp = path_sexp.string_elt_unchecked(0);
4397            let c_str = std::ffi::CStr::from_ptr(char_sexp.r_char_unchecked());
4398            let path = c_str
4399                .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}"));
4401
4402            write_r_wrappers_to_file(path);
4403
4404            SEXP::nil()
4405        },
4406        None,
4407    )
4408}
4409
4410/// 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 {
4424    crate::unwind_protect::with_r_unwind_protect_or_raise(
4425        || unsafe {
4426            use crate::{SEXP, SexpExt};
4427
4428            let char_sexp = path_sexp.string_elt_unchecked(0);
4429            let c_str = std::ffi::CStr::from_ptr(char_sexp.r_char_unchecked());
4430            let path = c_str
4431                .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}"));
4433
4434            crate::wasm_registry_writer::write_wasm_registry_to_file(path);
4435
4436            SEXP::nil()
4437        },
4438        None,
4439    )
4440}
4441// endregion
4442
4443// region: Unit tests for class-ref placeholder resolution
4444
4445#[cfg(test)]
4446mod tests {
4447    use super::*;
4448    use crate::match_arg::MatchArgAlias;
4449
4450    #[test]
4451    #[cfg(not(target_arch = "wasm32"))]
4452    fn optional_preamble_helpers_follow_fragment_dependencies() {
4453        use std::borrow::Cow;
4454        let render = |fragment: &'static str| {
4455            let mut text = String::new();
4456            super::append_used_wrapper_helpers(&mut text, &[Cow::Borrowed(fragment)]);
4457            text
4458        };
4459        assert!(render("f <- function() .Call(C_f)").is_empty());
4460
4461        let caller = render("f <- function() .miniextendr_caller_call()");
4462        assert!(caller.contains(".miniextendr_caller_call <- function"));
4463        assert!(caller.contains(".miniextendr_arg_error <- function"));
4464        assert!(!caller.contains(".miniextendr_field_at <- function"));
4465        assert!(!caller.contains(".miniextendr_env_method <- function"));
4466        assert!(!caller.contains(".miniextendr_match_arg"));
4467
4468        let check = render("f <- function(x) .miniextendr_arg_error('x', 'must be positive')");
4469        assert!(check.contains(".miniextendr_arg_error <- function"));
4470        assert!(!check.contains(".miniextendr_match_arg"));
4471
4472        let field = render("f <- function(x) .miniextendr_field_at(fields, x, environment())");
4473        assert!(field.contains(".miniextendr_field_at <- function"));
4474        assert!(!field.contains(".miniextendr_arg_error <- function"));
4475        let index = render("f <- function(x) .miniextendr_field_index_error('cls', environment())");
4476        assert!(index.contains(".miniextendr_field_index_error <- function"));
4477
4478        let choices = render("f <- function(x) .miniextendr_match_arg_several(x, choices, 'x')");
4479        assert!(choices.contains(".miniextendr_arg_error <- function"));
4480        assert!(choices.contains(".miniextendr_match_arg_several <- function"));
4481        assert!(!choices.contains(".miniextendr_match_arg <- function"));
4482
4483        let scalar = render("f <- function(x) .miniextendr_match_arg(x, choices, 'x')");
4484        assert!(scalar.contains(".miniextendr_arg_error <- function"));
4485        assert!(scalar.contains(".miniextendr_match_arg <- function"));
4486        assert!(!scalar.contains(".miniextendr_match_arg_several <- function"));
4487
4488        // The `...` check builds its own condition (#2157).
4489        let dots = render(
4490            "f <- function(x, ...) {\n  if (...length()) .miniextendr_check_dots(...names(), ...length(), \"x\")\n}",
4491        );
4492        assert!(dots.contains(".miniextendr_check_dots <- function"));
4493        assert!(!dots.contains(".miniextendr_arg_error <- function"));
4494        assert!(!scalar.contains(".miniextendr_check_dots <- function"));
4495    }
4496
4497    /// 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.
4501    fn resolve_class_refs(content: &str, entries: &[ClassNameEntry]) -> String {
4502        let class_index = build_class_name_index(entries.iter());
4503
4504        const OR_ANY_PREFIX: &str = ".__MX_CLASS_REF_OR_ANY_";
4505        const PREFIX: &str = ".__MX_CLASS_REF_";
4506        const SUFFIX: &str = "__";
4507
4508        let mut result = String::with_capacity(content.len());
4509        let mut remaining = content;
4510        while let Some(start) = remaining.find(PREFIX) {
4511            result.push_str(&remaining[..start]);
4512            let rest_from_prefix = &remaining[start..];
4513            let (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..];
4519            if let Some(end) = remaining.find(SUFFIX) {
4520                let rust_name = &remaining[..end];
4521                remaining = &remaining[end + SUFFIX.len()..];
4522                match class_index.get(rust_name) {
4523                    Some(entry) if quiet_fallback => {
4524                        if entry.class_system == "s7" {
4525                            result.push_str(entry.r_class_name);
4526                        } else {
4527                            result.push_str("S7::class_any");
4528                        }
4529                    }
4530                    Some(entry) => result.push_str(entry.r_class_name),
4531                    None if quiet_fallback => result.push_str("S7::class_any"),
4532                    None => result.push_str(rust_name),
4533                }
4534            } else {
4535                result.push_str(PREFIX);
4536                break;
4537            }
4538        }
4539        result.push_str(remaining);
4540        result
4541    }
4542
4543    /// 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]
4546    fn class_name_index_dedups_identical_entries() {
4547        let 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        ];
4561        let index = build_class_name_index(entries.iter());
4562        assert_eq!(index.len(), 1);
4563        assert_eq!(index["Foo"].r_class_name, "Foo");
4564    }
4565
4566    /// 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`")]
4571    fn class_name_index_panics_on_conflicting_override() {
4572        let 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        ];
4586        let _ = build_class_name_index(entries.iter());
4587    }
4588
4589    /// R class names belong to one Rust type, regardless of the selected
4590    /// class systems or the order linkme supplies the registrations.
4591    #[test]
4592    fn class_name_index_rejects_shared_r_class_names() {
4593        for class_system in ["env", "r6", "s3", "s4", "s7", "vctrs"] {
4594            let 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            ];
4608            for ordered in [[&entries[0], &entries[1]], [&entries[1], &entries[0]]] {
4609                let 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");
4612                let message = error.downcast_ref::<String>().unwrap();
4613                assert!(message.contains("R class `Shared`"), "{message}");
4614                assert!(message.contains("`First` (env)"), "{message}");
4615                assert!(
4616                    message.contains(&format!("`Second` ({class_system})")),
4617                    "{message}"
4618                );
4619                assert!(message.contains("class = \"...\""), "{message}");
4620            }
4621        }
4622    }
4623
4624    #[test]
4625    fn class_name_index_preserves_case_distinct_r_class_names() {
4626        let 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        ];
4640        let index = build_class_name_index(entries.iter());
4641        assert_eq!(index["First"].r_class_name, "Widget");
4642        assert_eq!(index["Second"].r_class_name, "widget");
4643    }
4644
4645    /// 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]
4648    fn sidecar_accessors_follow_the_field_syntax() {
4649        let 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        ];
4665        let class = |rust_type, field_syntax| ClassNameEntry {
4666            rust_type,
4667            r_class_name: rust_type,
4668            class_system: "s3",
4669            field_syntax,
4670        };
4671        let classes = [
4672            class("Fields", false),
4673            class("Fields", true),
4674            class("Plain", false),
4675        ];
4676        let fragments = sidecar_accessor_fragments(&accessors, &classes);
4677        let contents: Vec<&str> = fragments.iter().map(|f| f.content.as_ref()).collect();
4678        assert_eq!(contents, ["internal Fields", "exported Plain"]);
4679        assert!(
4680            fragments
4681                .iter()
4682                .all(|f| f.priority == RWrapperPriority::Sidecar)
4683        );
4684        // A type no impl block registers keeps its exported accessors.
4685        assert_eq!(
4686            sidecar_accessor_fragments(&accessors, &[])[0].content,
4687            "exported Fields"
4688        );
4689    }
4690
4691    /// 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]
4695    fn all_equal_methods_follow_the_class() {
4696        let 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        };
4704        let class = |rust_type, r_class_name, class_system| ClassNameEntry {
4705            rust_type,
4706            r_class_name,
4707            class_system,
4708            field_syntax: false,
4709        };
4710        let entries = [entry("Fit"), entry("Shape")];
4711        let classes = [class("Fit", "fit_rec", "r6"), class("Shape", "Shape", "s7")];
4712        let fragments = all_equal_fragments(&entries, &classes).unwrap();
4713        assert!(
4714            fragments
4715                .iter()
4716                .all(|f| f.priority == RWrapperPriority::TraitImpl)
4717        );
4718        assert_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        );
4728        assert!(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        ));
4733
4734        // A class name R can't read as a name is quoted.
4735        let quoted = all_equal_fragments(&[entry("Fit")], &[class("Fit", "fit-rec", "s3")]);
4736        assert!(
4737            quoted.unwrap()[0]
4738                .content
4739                .contains("`all.equal.fit-rec` <- function")
4740        );
4741    }
4742
4743    /// A type without a class, and a vctrs class, are refused together.
4744    #[test]
4745    fn all_equal_methods_need_a_pointer_class() {
4746        let 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        };
4754        let classes = [ClassNameEntry {
4755            rust_type: "Rec",
4756            r_class_name: "Rec",
4757            class_system: "vctrs",
4758            field_syntax: false,
4759        }];
4760        let Err(msg) = all_equal_fragments(&[entry("Loose", 1), entry("Rec", 2)], &classes) else {
4761            panic!("expected the derives to be refused");
4762        };
4763        assert_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    }
4771
4772    fn 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    }
4786
4787    /// One block per `pkg::name` whatever the number of sites, sorted by
4788    /// package then name, a non-syntactic name quoted (#1910).
4789    #[test]
4790    fn reexport_blocks_are_one_per_item_and_sorted() {
4791        assert!(reexport_fragment(&[]).is_none());
4792        let 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        ];
4800        let fragment = reexport_fragment(&entries).expect("a fragment");
4801        assert!(fragment.priority == RWrapperPriority::Reexport);
4802        assert_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.
4825        let moved = [reexport("generics", "tidy", "models.rs", 80)];
4826        assert_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    }
4835
4836    /// The blocks bind nothing, so they pass both scans of the file.
4837    #[test]
4838    fn reexport_blocks_pass_the_wrapper_scans() {
4839        let entries = [
4840            reexport("generics", "tidy", "fits.rs", 3),
4841            reexport("magrittr", "%>%", "fits.rs", 4),
4842        ];
4843        let 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        );
4849        let names: Vec<&str> = wrapper_names(&content).iter().map(|n| n.name).collect();
4850        assert_eq!(names, ["fit"]);
4851        assert!(detect_duplicate_wrapper_defs(&content, &std::collections::HashMap::new()).is_ok());
4852        assert_eq!(reexport_conflicts(&content, &entries), Ok(()));
4853    }
4854
4855    /// A name from two packages, and names the wrappers bind or export, are
4856    /// refused in one message naming every site (#1910).
4857    #[test]
4858    fn reexport_conflicts_are_refused_together() {
4859        let 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        );
4873        let 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        ];
4881        assert_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    }
4902
4903    #[test]
4904    fn raw_namespace_exports_are_read_by_name() {
4905        let names: Vec<&str> = raw_namespace_exports(
4906            "export(a, \"[[\") exportPattern(\"^x\") S3method(print, foo) if (TRUE) export(`b`)",
4907        )
4908        .collect();
4909        assert_eq!(names, ["a", "[[", "b"]);
4910    }
4911
4912    /// Run the production scalar cross-class return wrapper resolver over
4913    /// `content` using `entries` as the class registry.
4914    fn resolve_return_wrappers(content: &str, entries: &[ClassNameEntry]) -> String {
4915        let class_index = build_class_name_index(entries.iter());
4916        resolve_scalar_return_wrappers(content, &class_index)
4917    }
4918
4919    /// Run the production list cross-class return wrapper resolver (#1284)
4920    /// over `content` using `entries` as the class registry.
4921    fn resolve_list_wrappers(content: &str, entries: &[ClassNameEntry]) -> String {
4922        let class_index = build_class_name_index(entries.iter());
4923        resolve_list_return_wrappers(content, &class_index)
4924    }
4925
4926    #[test]
4927    fn test_class_ref_resolver_with_override() {
4928        // S7Shape is registered with r_class_name = "Shape" (override)
4929        let 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        ];
4943
4944        let input = r#"S7Circle <- S7::new_class("S7Circle", parent = .__MX_CLASS_REF_S7Shape__, properties = list())"#;
4945        let output = resolve_class_refs(input, &entries);
4946        assert_eq!(
4947            output,
4948            r#"S7Circle <- S7::new_class("S7Circle", parent = Shape, properties = list())"#
4949        );
4950    }
4951
4952    #[test]
4953    fn test_class_ref_resolver_multiple_placeholders() {
4954        let 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        ];
4968
4969        let input = "S7::method(convert, list(.__MX_CLASS_REF_Point2D__, Point3D)) <- function(from, to) {}";
4970        let output = resolve_class_refs(input, &entries);
4971        assert_eq!(
4972            output,
4973            "S7::method(convert, list(Point2D, Point3D)) <- function(from, to) {}"
4974        );
4975    }
4976
4977    #[test]
4978    fn test_class_ref_resolver_unresolved_falls_back_to_rust_name() {
4979        let entries: [ClassNameEntry; 0] = [];
4980        let input = "parent = .__MX_CLASS_REF_UnknownClass__";
4981        let output = resolve_class_refs(input, &entries);
4982        // Falls back to the bare Rust name
4983        assert_eq!(output, "parent = UnknownClass");
4984    }
4985
4986    #[test]
4987    fn 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.
4990        let entries: [ClassNameEntry; 0] = [];
4991        let input = "parent = S7::class_any";
4992        let output = resolve_class_refs(input, &entries);
4993        assert_eq!(output, "parent = S7::class_any");
4994    }
4995
4996    #[test]
4997    fn test_is_bare_identifier_via_resolver() {
4998        // A placeholder for a bare identifier should be resolved.
4999        let entries = [ClassNameEntry {
5000            rust_type: "MyClass",
5001            r_class_name: "MyClass",
5002            class_system: "r6",
5003            field_syntax: false,
5004        }];
5005        let input = "inherit = .__MX_CLASS_REF_MyClass__";
5006        let output = resolve_class_refs(input, &entries);
5007        assert_eq!(output, "inherit = MyClass");
5008    }
5009
5010    // #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.
5012
5013    #[test]
5014    fn or_any_resolves_registered_s7_class() {
5015        let entries = [ClassNameEntry {
5016            rust_type: "S7PropInner",
5017            r_class_name: "S7PropInner",
5018            class_system: "s7",
5019            field_syntax: false,
5020        }];
5021        let input = "class = .__MX_CLASS_REF_OR_ANY_S7PropInner__";
5022        let output = resolve_class_refs(input, &entries);
5023        assert_eq!(output, "class = S7PropInner");
5024    }
5025
5026    #[test]
5027    fn or_any_unregistered_type_falls_back_to_class_any_silently() {
5028        // No entry for SEXP, PathBuf, Json, etc.
5029        let entries: [ClassNameEntry; 0] = [];
5030        let input = "class = .__MX_CLASS_REF_OR_ANY_SEXP__";
5031        let output = resolve_class_refs(input, &entries);
5032        assert_eq!(output, "class = S7::class_any");
5033    }
5034
5035    #[test]
5036    fn 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.
5040        let entries = [ClassNameEntry {
5041            rust_type: "R6Counter",
5042            r_class_name: "R6Counter",
5043            class_system: "r6",
5044            field_syntax: false,
5045        }];
5046        let input = "class = .__MX_CLASS_REF_OR_ANY_R6Counter__";
5047        let output = resolve_class_refs(input, &entries);
5048        assert_eq!(output, "class = S7::class_any");
5049    }
5050
5051    #[test]
5052    fn 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.
5056        let entries: [ClassNameEntry; 0] = [];
5057        let input = "parent = .__MX_CLASS_REF_UnknownClass__";
5058        let output = resolve_class_refs(input, &entries);
5059        assert_eq!(output, "parent = UnknownClass");
5060    }
5061
5062    #[test]
5063    fn 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.
5066        let 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        ];
5075        let input =
5076            "inherit = .__MX_CLASS_REF_Parent__, class = .__MX_CLASS_REF_OR_ANY_PropInner__";
5077        let output = resolve_class_refs(input, &entries);
5078        assert_eq!(output, "inherit = Parent, class = S7::class_any");
5079    }
5080
5081    #[test]
5082    fn return_wrapper_resolves_r6_constructor() {
5083        let entries = [ClassNameEntry {
5084            rust_type: "R6Board",
5085            r_class_name: "R6Board",
5086            class_system: "r6",
5087            field_syntax: false,
5088        }];
5089        let input = "return(.__MX_WRAP_RETURN_R6Board__(.val))";
5090        let output = resolve_return_wrappers(input, &entries);
5091        assert_eq!(output, "return(R6Board$new(.ptr = .val))");
5092    }
5093
5094    #[test]
5095    fn return_wrapper_resolves_s7_override_constructor() {
5096        let entries = [ClassNameEntry {
5097            rust_type: "S7Board",
5098            r_class_name: "PrettyBoard",
5099            class_system: "s7",
5100            field_syntax: false,
5101        }];
5102        let input = ".__MX_WRAP_RETURN_S7Board__(.val)";
5103        let output = resolve_return_wrappers(input, &entries);
5104        assert_eq!(output, "PrettyBoard(.ptr = .val)");
5105    }
5106
5107    #[test]
5108    fn return_wrapper_resolves_s4_constructor() {
5109        let entries = [ClassNameEntry {
5110            rust_type: "S4Board",
5111            r_class_name: "S4Board",
5112            class_system: "s4",
5113            field_syntax: false,
5114        }];
5115        let input = ".__MX_WRAP_RETURN_S4Board__(.val)";
5116        let output = resolve_return_wrappers(input, &entries);
5117        assert_eq!(output, "methods::new(\"S4Board\", ptr = .val)");
5118    }
5119
5120    #[test]
5121    fn return_wrapper_resolves_attribute_classes() {
5122        let 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        ];
5136        let input =
5137            "a <- .__MX_WRAP_RETURN_S3Board__(.val)\nb <- .__MX_WRAP_RETURN_EnvBoard__(.ptr)";
5138        let output = resolve_return_wrappers(input, &entries);
5139        assert_eq!(
5140            output,
5141            "a <- structure(.val, class = \"S3Board\")\nb <- structure(.ptr, class = \"EnvBoard\")"
5142        );
5143    }
5144
5145    #[test]
5146    fn return_wrapper_unregistered_type_falls_back_to_value() {
5147        let entries: [ClassNameEntry; 0] = [];
5148        let input = ".__MX_WRAP_RETURN_JsonValue__(.val)";
5149        let output = resolve_return_wrappers(input, &entries);
5150        assert_eq!(output, ".val");
5151    }
5152
5153    // #1284 — list-shaped cross-class return markers (`Vec<Class>` returns).
5154
5155    /// 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]
5162    fn scalar_resolver_leaves_list_markers_untouched() {
5163        let entries = [ClassNameEntry {
5164            rust_type: "R6Board",
5165            r_class_name: "R6Board",
5166            class_system: "r6",
5167            field_syntax: false,
5168        }];
5169        let input = "a <- .__MX_WRAP_LIST_RETURN_R6Board__(.val)";
5170        let output = resolve_return_wrappers(input, &entries);
5171        assert_eq!(output, input, "scalar pass must not consume list markers");
5172    }
5173
5174    #[test]
5175    fn list_resolver_leaves_scalar_markers_untouched() {
5176        let entries = [ClassNameEntry {
5177            rust_type: "R6Board",
5178            r_class_name: "R6Board",
5179            class_system: "r6",
5180            field_syntax: false,
5181        }];
5182        let input = "a <- .__MX_WRAP_RETURN_R6Board__(.val)";
5183        let output = resolve_list_wrappers(input, &entries);
5184        assert_eq!(output, input, "list pass must not consume scalar markers");
5185    }
5186
5187    /// Both passes over content mixing the two families resolve each marker
5188    /// with its own resolver, in either pass order.
5189    #[test]
5190    fn scalar_and_list_markers_resolve_independently() {
5191        let entries = [ClassNameEntry {
5192            rust_type: "R6Board",
5193            r_class_name: "R6Board",
5194            class_system: "r6",
5195            field_syntax: false,
5196        }];
5197        let input =
5198            "a <- .__MX_WRAP_RETURN_R6Board__(.val)\nb <- .__MX_WRAP_LIST_RETURN_R6Board__(.val)";
5199        let expected = "a <- R6Board$new(.ptr = .val)\nb <- lapply(.val, function(.el) R6Board$new(.ptr = .el))";
5200
5201        let scalar_first =
5202            resolve_list_wrappers(&resolve_return_wrappers(input, &entries), &entries);
5203        let list_first = resolve_return_wrappers(&resolve_list_wrappers(input, &entries), &entries);
5204        assert_eq!(scalar_first, expected);
5205        assert_eq!(list_first, expected);
5206    }
5207
5208    #[test]
5209    fn list_return_wrapper_resolves_r6_lapply() {
5210        let entries = [ClassNameEntry {
5211            rust_type: "R6Board",
5212            r_class_name: "R6Board",
5213            class_system: "r6",
5214            field_syntax: false,
5215        }];
5216        let input = "return(.__MX_WRAP_LIST_RETURN_R6Board__(.val))";
5217        let output = resolve_list_wrappers(input, &entries);
5218        assert_eq!(
5219            output,
5220            "return(lapply(.val, function(.el) R6Board$new(.ptr = .el)))"
5221        );
5222    }
5223
5224    #[test]
5225    fn list_return_wrapper_resolves_s7_override_lapply() {
5226        let entries = [ClassNameEntry {
5227            rust_type: "S7Board",
5228            r_class_name: "PrettyBoard",
5229            class_system: "s7",
5230            field_syntax: false,
5231        }];
5232        let input = ".__MX_WRAP_LIST_RETURN_S7Board__(.val)";
5233        let output = resolve_list_wrappers(input, &entries);
5234        assert_eq!(
5235            output,
5236            "lapply(.val, function(.el) PrettyBoard(.ptr = .el))"
5237        );
5238    }
5239
5240    #[test]
5241    fn list_return_wrapper_resolves_s4_lapply() {
5242        let entries = [ClassNameEntry {
5243            rust_type: "S4Board",
5244            r_class_name: "S4Board",
5245            class_system: "s4",
5246            field_syntax: false,
5247        }];
5248        let input = ".__MX_WRAP_LIST_RETURN_S4Board__(.val)";
5249        let output = resolve_list_wrappers(input, &entries);
5250        assert_eq!(
5251            output,
5252            "lapply(.val, function(.el) methods::new(\"S4Board\", ptr = .el))"
5253        );
5254    }
5255
5256    #[test]
5257    fn list_return_wrapper_resolves_attribute_classes_lapply() {
5258        let 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        ];
5272        let input = "a <- .__MX_WRAP_LIST_RETURN_S3Board__(.val)\nb <- .__MX_WRAP_LIST_RETURN_EnvBoard__(.ptr)";
5273        let output = resolve_list_wrappers(input, &entries);
5274        assert_eq!(
5275            output,
5276            "a <- lapply(.val, function(.el) structure(.el, class = \"S3Board\"))\nb <- lapply(.ptr, function(.el) structure(.el, class = \"EnvBoard\"))"
5277        );
5278    }
5279
5280    #[test]
5281    fn list_return_wrapper_unregistered_type_falls_back_to_bare_list() {
5282        let entries: [ClassNameEntry; 0] = [];
5283        let input = ".__MX_WRAP_LIST_RETURN_JsonValue__(.val)";
5284        let output = resolve_list_wrappers(input, &entries);
5285        assert_eq!(output, ".val");
5286    }
5287
5288    // region: wrappers_file_content header (#1552)
5289
5290    #[test]
5291    fn wrappers_file_header_carries_version_and_body_digest() {
5292        // FNV-1a 64 reference vectors: "" and "a".
5293        assert_eq!(
5294            crate::abi::fnv1a_64(b"", crate::abi::FNV1A_64_OFFSET),
5295            0xcbf29ce484222325
5296        );
5297        assert_eq!(
5298            crate::abi::fnv1a_64(b"a", crate::abi::FNV1A_64_OFFSET),
5299            0xaf63dc4c8601ec8c
5300        );
5301
5302        let content = wrappers_file_content("a");
5303        let mut lines = content.lines();
5304        assert_eq!(lines.next(), Some(WRAPPERS_FILE_MARKER));
5305        assert_eq!(
5306            lines.next(),
5307            Some(concat!(
5308                "# miniextendr-api ",
5309                env!("CARGO_PKG_VERSION"),
5310                " | fnv1a-64 of the normalized lines below: af63dc4c8601ec8c"
5311            ))
5312        );
5313        assert_eq!(lines.next(), Some("a"));
5314        assert_eq!(lines.next(), None);
5315    }
5316
5317    #[test]
5318    fn wrappers_file_header_ignores_source_position_shifts() {
5319        let old = "# Generated from Rust fn `foo` (lib.rs:10:5)\nfoo <- function() 1\n";
5320        let new = "# Generated from Rust fn `foo` (lib.rs:42:8)\nfoo <- function() 1\n";
5321        let header = |s: &str| wrappers_file_content(s).lines().nth(1).unwrap().to_string();
5322        assert_eq!(header(old), header(new));
5323        assert!(wrappers_semantically_equal(
5324            &wrappers_file_content(old),
5325            &wrappers_file_content(new)
5326        ));
5327
5328        let changed = "# Generated from Rust fn `foo` (lib.rs:10:5)\nfoo <- function() 2\n";
5329        assert_ne!(header(old), header(changed));
5330    }
5331
5332    // endregion
5333
5334    // region: normalize_source_locs unit tests (#528)
5335
5336    #[test]
5337    fn normalize_source_locs_noop_on_plain_text() {
5338        // No `.rs:N:M` pattern — should return the input string unchanged (Borrowed).
5339        let input = "# just a comment\nfoo <- function() {}\n";
5340        let result = normalize_source_locs(input);
5341        assert_eq!(result.as_ref(), input);
5342        // Verify it's the zero-allocation borrowed path.
5343        assert!(matches!(result, std::borrow::Cow::Borrowed(_)));
5344    }
5345
5346    #[test]
5347    fn normalize_source_locs_single_attribution() {
5348        let input = "# Generated from Rust fn `foo` (lib.rs:42:8)";
5349        let result = normalize_source_locs(input);
5350        assert_eq!(
5351            result.as_ref(),
5352            "# Generated from Rust fn `foo` (lib.rs:_:_)"
5353        );
5354    }
5355
5356    #[test]
5357    fn normalize_source_locs_multiple_attributions() {
5358        let input = concat!(
5359            "# (conversions.rs:1:5)\n",
5360            "foo <- function() {}\n",
5361            "# (conversions.rs:158:8)\n",
5362            "bar <- function() {}\n",
5363        );
5364        let result = normalize_source_locs(input);
5365        assert_eq!(
5366            result.as_ref(),
5367            concat!(
5368                "# (conversions.rs:_:_)\n",
5369                "foo <- function() {}\n",
5370                "# (conversions.rs:_:_)\n",
5371                "bar <- function() {}\n",
5372            )
5373        );
5374    }
5375
5376    #[test]
5377    fn normalize_source_locs_does_not_match_extra_colon() {
5378        // `(lib.rs:1:5:6)` — four components — is NOT a valid attribution; leave alone.
5379        let input = "(lib.rs:1:5:6)";
5380        let 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.
5383        assert_eq!(result.as_ref(), input);
5384    }
5385
5386    #[test]
5387    fn normalize_source_locs_does_not_match_without_parens() {
5388        // Bare `lib.rs:1:5` without surrounding parens — leave untouched.
5389        let input = "lib.rs:1:5";
5390        let result = normalize_source_locs(input);
5391        assert_eq!(result.as_ref(), input);
5392        assert!(matches!(result, std::borrow::Cow::Borrowed(_)));
5393    }
5394
5395    #[test]
5396    fn wrappers_semantically_equal_position_only_diff() {
5397        // Two wrappers identical except for line:col are semantically equal.
5398        let old = "# Generated from Rust fn `foo` (lib.rs:42:8)\nfoo <- function() {}\n";
5399        let new = "# Generated from Rust fn `foo` (lib.rs:99:8)\nfoo <- function() {}\n";
5400        assert!(wrappers_semantically_equal(old, new));
5401    }
5402
5403    #[test]
5404    fn wrappers_semantically_equal_content_diff() {
5405        // A real semantic change (different function body) is NOT equal.
5406        let old = "# Generated from Rust fn `foo` (lib.rs:42:8)\nfoo <- function() { 1L }\n";
5407        let new = "# Generated from Rust fn `foo` (lib.rs:42:8)\nfoo <- function() { 2L }\n";
5408        assert!(!wrappers_semantically_equal(old, new));
5409    }
5410
5411    #[test]
5412    fn wrappers_semantically_equal_both_position_and_content_diff() {
5413        // Both positions and content differ — still NOT equal.
5414        let old = "# Generated from Rust fn `foo` (lib.rs:1:1)\nfoo <- function() { 1L }\n";
5415        let new = "# Generated from Rust fn `bar` (lib.rs:9:1)\nbar <- function() { 2L }\n";
5416        assert!(!wrappers_semantically_equal(old, new));
5417    }
5418
5419    // endregion
5420
5421    // region: generic doc marker tests
5422
5423    #[test]
5424    fn test_parse_generic_doc_marker_s7() {
5425        let marker = r#".__MX_GENERIC_DOC__(kind="S7", generic="get_value", class="MyClass", export=true, dispatch="x", no_dots=false)"#;
5426        let result = parse_generic_doc_marker(marker);
5427        assert!(result.is_some());
5428        let (kind, generic, class, export, dispatch, no_dots) = result.unwrap();
5429        assert_eq!(kind, "S7");
5430        assert_eq!(generic, "get_value");
5431        assert_eq!(class, "MyClass");
5432        assert!(export);
5433        assert_eq!(dispatch, "x");
5434        assert!(!no_dots);
5435    }
5436
5437    #[test]
5438    fn test_parse_generic_doc_marker_s4() {
5439        let marker =
5440            r#".__MX_GENERIC_DOC__(kind="S4", generic="s4_get", class="Counter", export=true)"#;
5441        let result = parse_generic_doc_marker(marker);
5442        assert!(result.is_some());
5443        let (kind, generic, class, export, dispatch, no_dots) = result.unwrap();
5444        assert_eq!(kind, "S4");
5445        assert_eq!(generic, "s4_get");
5446        assert_eq!(class, "Counter");
5447        assert!(export);
5448        assert_eq!(dispatch, "x"); // default
5449        assert!(!no_dots); // default
5450    }
5451
5452    #[test]
5453    fn test_resolve_generic_doc_markers_single_class() {
5454        let input = concat!(
5455            r#".__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        );
5461        let output = resolve_generic_doc_markers(input.to_string());
5462
5463        // Marker must be gone
5464        assert!(!output.contains(".__MX_GENERIC_DOC__"));
5465        // Standalone page must have bare @name
5466        assert!(output.contains("#' @name get_value"));
5467        // Must list the class
5468        assert!(output.contains("\\link{MyClass}"));
5469        // Must have NULL anchor
5470        assert!(output.contains("\nNULL\n"));
5471        // Generic guard must survive
5472        assert!(output.contains("if (!exists(\"get_value\""));
5473    }
5474
5475    #[test]
5476    fn test_resolve_generic_doc_markers_two_classes_one_generic() {
5477        // Two classes sharing one generic: both appear in the listing, only one doc page.
5478        let input = concat!(
5479            r#".__MX_GENERIC_DOC__(kind="S7", generic="get_value", class="ClassA", export=true, dispatch="x", no_dots=false)"#,
5480            "\n",
5481            "line_a\n",
5482            r#".__MX_GENERIC_DOC__(kind="S7", generic="get_value", class="ClassB", export=true, dispatch="x", no_dots=false)"#,
5483            "\n",
5484            "line_b\n",
5485        );
5486        let output = resolve_generic_doc_markers(input.to_string());
5487
5488        // No markers remain
5489        assert!(!output.contains(".__MX_GENERIC_DOC__"));
5490        // Only one @name line
5491        let name_count = output.matches("#' @name get_value").count();
5492        assert_eq!(
5493            name_count, 1,
5494            "expected exactly one @name get_value, got {name_count}"
5495        );
5496        // Both classes listed
5497        assert!(output.contains("\\link{ClassA}"));
5498        assert!(output.contains("\\link{ClassB}"));
5499        // Other lines survive
5500        assert!(output.contains("line_a"));
5501        assert!(output.contains("line_b"));
5502    }
5503
5504    #[test]
5505    fn 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.
5508        let input = "if (!exists(\"size\", mode = \"function\")) {\n  size <- S7::new_external_generic(\"vctrs\", \"size\")\n}\n";
5509        let output = resolve_generic_doc_markers(input.to_string());
5510        // Content should be identical (the line-by-line reconstruction adds \n)
5511        let output_trimmed: Vec<&str> = output.lines().collect();
5512        let input_trimmed: Vec<&str> = input.lines().collect();
5513        assert_eq!(output_trimmed, input_trimmed);
5514    }
5515
5516    #[test]
5517    fn test_resolve_generic_doc_markers_no_dots() {
5518        let input = concat!(
5519            r#".__MX_GENERIC_DOC__(kind="S7", generic="strict_fn", class="A", export=true, dispatch="x", no_dots=true)"#,
5520            "\n",
5521        );
5522        let output = resolve_generic_doc_markers(input.to_string());
5523        // no_dots=true → @param ... must NOT appear in the standalone block
5524        assert!(!output.contains("@param ..."));
5525        // @param x must appear
5526        assert!(output.contains("@param x"));
5527    }
5528
5529    #[test]
5530    fn test_resolve_generic_doc_markers_s4() {
5531        let input = concat!(
5532            r#".__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        );
5536        let output = resolve_generic_doc_markers(input.to_string());
5537        assert!(!output.contains(".__MX_GENERIC_DOC__"));
5538        assert!(output.contains("#' @name s4_compute"));
5539        assert!(output.contains("an S4 generic"));
5540        assert!(output.contains("\\link{S4Counter}"));
5541        // S4 export uses @exportMethod
5542        assert!(output.contains("#' @exportMethod s4_compute"));
5543        // setGeneric guard must survive
5544        assert!(output.contains("if (!methods::isGeneric(\"s4_compute\")"));
5545    }
5546
5547    #[test]
5548    fn test_resolve_generic_doc_markers_quotes_operator_exports() {
5549        for generic in ["[", "[[", "$", "%custom%"] {
5550            let input = format!(
5551                ".__MX_GENERIC_DOC__(kind=\"S7\", generic=\"{generic}\", class=\"Foo\", export=true, dispatch=\"x\", no_dots=false)\n"
5552            );
5553            let 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.
5556            assert!(
5557                output.contains(&format!("#' @rawNamespace export(\"{generic}\")")),
5558                "{output}"
5559            );
5560            assert!(
5561                output.contains(&format!("#' @name {generic}\n")),
5562                "{output}"
5563            );
5564            assert!(
5565                output.contains(&format!("#' The `{generic}` generic\n")),
5566                "{output}"
5567            );
5568        }
5569        let output = resolve_generic_doc_markers(
5570            r#".__MX_GENERIC_DOC__(kind="S7", generic="get", class="Foo", export=true, dispatch="x", no_dots=false)"#
5571                .to_string(),
5572        );
5573        assert!(
5574            output.contains("#' @rawNamespace export(get)\n"),
5575            "{output}"
5576        );
5577        assert!(output.contains("#' The `get()` generic\n"), "{output}");
5578    }
5579
5580    #[test]
5581    fn test_is_syntactic_r_name_matches_r() {
5582        for name in ["get", "var", ".hidden", "s7_trait_Ops_value", "a.b_c1"] {
5583            assert!(is_syntactic_r_name(name), "{name}");
5584        }
5585        for name in [
5586            "[", "[[", "$", "%custom%", "Foo_[[", "if", "NA", ".1x", "1a", "_a", "",
5587        ] {
5588            assert!(!is_syntactic_r_name(name), "{name}");
5589        }
5590        assert_eq!(r_namespace_name(r#"a"b\c"#), r#""a\"b\\c""#);
5591    }
5592
5593    // endregion
5594
5595    // region: duplicate wrapper-definition detection (#991)
5596
5597    /// Build an accessor-producer map the way `write_r_wrappers_to_file` does,
5598    /// from synthetic sidecar + class registries, for unit testing.
5599    fn accessor_map(
5600        sidecar: &[SidecarPropEntry],
5601        classes: &[ClassNameEntry],
5602    ) -> std::collections::HashMap<String, String> {
5603        let 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    }
5607
5608    #[test]
5609    fn parse_top_level_fn_def_name_accepts_bare_def() {
5610        assert_eq!(
5611            parse_top_level_fn_def_name("Foo_get_value <- function(x) .Call(C, x)"),
5612            Some("Foo_get_value")
5613        );
5614        // Dotted/internal helper names are valid R identifiers.
5615        assert_eq!(
5616            parse_top_level_fn_def_name(".miniextendr_raise_condition <- function(.val) {"),
5617            Some(".miniextendr_raise_condition")
5618        );
5619    }
5620
5621    #[test]
5622    fn 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.
5625        assert_eq!(
5626            parse_top_level_fn_def_name("`[.mx_bag` <- function(x, i, ...) {"),
5627            Some("[.mx_bag")
5628        );
5629        assert_eq!(
5630            parse_top_level_fn_def_name("`$.mx_bag` <- function(x, name) {"),
5631            Some("$.mx_bag")
5632        );
5633        // Unterminated or empty quoting is not a definition we own.
5634        assert_eq!(parse_top_level_fn_def_name("`[.foo <- function() {}"), None);
5635        assert_eq!(parse_top_level_fn_def_name("`` <- function() {}"), None);
5636    }
5637
5638    #[test]
5639    fn detect_duplicate_wrapper_defs_sees_through_backticks() {
5640        let content = "`[.foo` <- function(x, i) 1\n`[.foo` <- function(x, i) 2\n";
5641        let err = detect_duplicate_wrapper_defs(content, &std::collections::HashMap::new())
5642            .expect_err("duplicate backticked definition must be reported");
5643        assert!(err.contains("`[.foo`"), "{err}");
5644    }
5645
5646    #[test]
5647    fn has_page_assignment_tag_recognises_rdname_describein_and_name() {
5648        assert!(has_page_assignment_tag(
5649            "#' @rdname topic\nf <- function() 1"
5650        ));
5651        assert!(has_page_assignment_tag(
5652            "#' @describeIn topic Short text\n#' wrapped\nf <- function() 1"
5653        ));
5654        // `@name topic` documents `topic.Rd` in roxygen2.
5655        assert!(has_page_assignment_tag("#' @name topic\nf <- function() 1"));
5656        // A `@name` with no topic is malformed and does not count.
5657        assert!(!has_page_assignment_tag("#' @name\nf <- function() 1"));
5658        assert!(!has_page_assignment_tag(
5659            "#' @title T\n#' @export\nf <- function() 1"
5660        ));
5661        // Prefix-only matches do not count.
5662        assert!(!has_page_assignment_tag("#' @rdnamex\nf <- function() 1"));
5663    }
5664
5665    // region: standalone-function pages (#1590)
5666
5667    fn resolve_pages(fragments: &[(&str, Option<&str>)]) -> Vec<String> {
5668        let 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    }
5677
5678    /// A generated `@param` line as the macro emits it.
5679    fn filler(param: &str) -> String {
5680        format!("#' {PARAM_FILLER_MARKER}@param {param} (no documentation available)")
5681    }
5682
5683    fn filler_resolved(param: &str) -> String {
5684        format!("#' @param {param} (no documentation available)")
5685    }
5686
5687    /// The `.Rd` topic each fragment lands on, as `resolve_standalone_pages`
5688    /// reads it.
5689    fn 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    }
5701
5702    /// 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]
5707    fn free_functions_of_one_file_get_a_page_each() {
5708        let scale = format!(
5709            "#' @title stem_scale\n#' @param values Numbers.\n{}\n#' @export\nstem_scale <- function(values, factor) 1",
5710            filler("factor")
5711        );
5712        let floor = format!(
5713            "#' @title stem_floor\n{}\n{}\n#' @export\nstem_floor <- function(values, floor) 1",
5714            filler("values"),
5715            filler("floor")
5716        );
5717        let fragments = [
5718            (scale.as_str(), Some("stem_docs")),
5719            (&floor, Some("stem_docs")),
5720        ];
5721        assert_eq!(
5722            page_topics(&fragments),
5723            [
5724                Some("stem_scale".to_string()),
5725                Some("stem_floor".to_string())
5726            ]
5727        );
5728        let out = resolve_pages(&fragments);
5729        for page in &out {
5730            assert!(!page.contains("@rdname"), "{page}");
5731            assert!(!page.contains(PARAM_FILLER_MARKER), "{page}");
5732            assert!(!page.contains("@order"), "own page, no order: {page}");
5733        }
5734        assert!(out[0].contains(&filler_resolved("factor")), "{}", out[0]);
5735        assert!(out[1].contains(&filler_resolved("values")), "{}", out[1]);
5736        assert!(out[1].contains(&filler_resolved("floor")), "{}", out[1]);
5737    }
5738
5739    /// 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]
5745    fn explicit_stem_page_keeps_fillers_only_for_undocumented_arguments() {
5746        let 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        );
5750        let second = format!(
5751            "#' @rdname stem_docs\n#' @param offset Amount.\n{}\n#' @export\nstem_shift <- function(values, offset) 1",
5752            filler("values")
5753        );
5754        let 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        );
5759        let fragments = [
5760            (first.as_str(), Some("stem_docs")),
5761            (&second, Some("stem_docs")),
5762            (&third, Some("stem_docs")),
5763        ];
5764        assert!(
5765            page_topics(&fragments)
5766                .iter()
5767                .all(|page| page.as_deref() == Some("stem_docs"))
5768        );
5769        let out = resolve_pages(&fragments);
5770        for page in &out {
5771            assert!(!page.contains(PARAM_FILLER_MARKER), "{page}");
5772            assert!(!page.contains("@order"), "own page, no order: {page}");
5773            assert_eq!(page.matches("#' @rdname").count(), 1, "{page}");
5774        }
5775        assert!(out[0].contains(&filler_resolved("factor")), "{}", out[0]);
5776        assert!(!out[1].contains("@param values"), "{}", out[1]);
5777        assert!(!out[2].contains("@param values"), "{}", out[2]);
5778        assert!(out[2].contains(&filler_resolved("floor")), "{}", out[2]);
5779    }
5780
5781    /// 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]
5787    fn titled_blocks_inheriting_a_section_keep_separate_pages() {
5788        let first = "#' @title First setter\n#' @section Shared rules:\n#' Values are checked.\n#' @param x A value.\n#' @export\nfirst_setter <- function(x) 1";
5789        let second = "#' @title Second setter\n#' @inheritSection first_setter Shared rules\n#' @param x A value.\n#' @export\nsecond_setter <- function(x) 1";
5790        let third = "#' @title Third setter\n#' @inheritSection first_setter Shared rules\n#' @param x A value.\n#' @export\nthird_setter <- function(x) 1";
5791        let fragments = [
5792            (first, Some("setters")),
5793            (second, Some("setters")),
5794            (third, Some("setters")),
5795        ];
5796        assert_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.
5801        assert_eq!(resolve_pages(&fragments), [first, second, third]);
5802    }
5803
5804    /// 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]
5809    fn untitled_own_page_blocks_get_the_function_name_as_title() {
5810        let bare = "#' @export\nbare_fn <- function() 1";
5811        let internal = "#' @keywords internal\ninternal_fn <- function() 1";
5812        let undocumented = "plain_fn <- function() 1";
5813        let no_rd = "#' @noRd\nhidden <- function() 1";
5814        let out = resolve_pages(&[
5815            (bare, Some("helpers")),
5816            (internal, None),
5817            (undocumented, Some("helpers")),
5818            (no_rd, Some("helpers")),
5819        ]);
5820        assert_eq!(
5821            out[0],
5822            "#' @title bare_fn\n#' @export\nbare_fn <- function() 1"
5823        );
5824        assert_eq!(
5825            out[1],
5826            "#' @title internal_fn\n#' @keywords internal\ninternal_fn <- function() 1"
5827        );
5828        assert_eq!(out[2], undocumented);
5829        assert_eq!(out[3], no_rd);
5830    }
5831
5832    /// A grouped `@param lower,upper` documents both names.
5833    #[test]
5834    fn grouped_param_names_count_as_documented() {
5835        let a = "#' @rdname bounds\n#' @param lower,upper Bounds.\n#' @export\nf <- function(lower, upper) 1";
5836        let b = format!(
5837            "#' @rdname bounds\n{}\n{}\n#' @export\ng <- function(lower, upper) 1",
5838            filler("lower"),
5839            filler("upper")
5840        );
5841        let out = resolve_pages(&[(a, Some("bounds")), (&b, Some("bounds"))]);
5842        assert!(!out[1].contains("@param"), "{}", out[1]);
5843    }
5844
5845    /// 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]
5850    fn author_topic_blocks_drop_fillers_and_sort_last() {
5851        let joined = format!(
5852            "#' @title range_width\n#' @rdname range_summaries\n{}\n#' @export\nrange_width <- function(values) 1",
5853            filler("values")
5854        );
5855        let 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        );
5859        let own_name = format!(
5860            "#' @name bag_topic\n#' @rdname bag_topic\n#' @title Bags\n{}\nbag_len <- function(x) 1",
5861            filler("x")
5862        );
5863        let ordered = "#' @rdname range_summaries\n#' @order 3\nrange_mid <- function(values) 1";
5864        let 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        ]);
5870        assert!(!out[0].contains("@param"), "{}", out[0]);
5871        assert!(
5872            out[0].contains("#' @order NaN\n#' @export"),
5873            "order inserted at a tag boundary: {}",
5874            out[0]
5875        );
5876        for own in &out[1..3] {
5877            assert!(own.contains(&filler_resolved("x")), "{own}");
5878            assert!(!own.contains("@order"), "{own}");
5879        }
5880        // The author's own `@order` is kept, not doubled.
5881        assert_eq!(out[3].matches("@order").count(), 1, "{}", out[3]);
5882        assert!(out[3].contains("#' @order 3"), "{}", out[3]);
5883    }
5884
5885    /// `@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]
5888    fn describe_in_no_rd_and_stemless_blocks() {
5889        let described = "#' @describeIn range_summaries Midpoint.\n#' wrapped\nrange_midpoint <- function(values) 1";
5890        let no_rd = format!("{}\n#' @noRd\nhidden <- function(x) 1", filler("x"));
5891        let stemless = format!(
5892            "#' @title top\n{}\n#' @export\ntop <- function(x) 1",
5893            filler("x")
5894        );
5895        let out = resolve_pages(&[
5896            (described, Some("shared_param_docs")),
5897            (&no_rd, Some("shared_param_docs")),
5898            (&stemless, None),
5899        ]);
5900        assert_eq!(
5901            out[0],
5902            "#' @describeIn range_summaries Midpoint.\n#' wrapped\n#' @order NaN\nrange_midpoint <- function(values) 1"
5903        );
5904        assert_eq!(out[1], "#' @noRd\nhidden <- function(x) 1");
5905        assert!(out[2].contains(&filler_resolved("x")), "{}", out[2]);
5906        assert!(!out[2].contains("@rdname"), "{}", out[2]);
5907    }
5908
5909    // endregion
5910
5911    #[test]
5912    fn sort_wrapper_entries_is_priority_then_file_then_line() {
5913        fn 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        }
5926        let 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);
5934        let 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.
5937        assert_eq!(order, ["class", "a5", "a5-second", "a30", "b10"]);
5938    }
5939
5940    #[test]
5941    fn parse_top_level_fn_def_name_accepts_quoted_operators() {
5942        for name in [
5943            "[.Foo", "[[.Foo", "$.Foo", "==.Foo", "+.Foo", "[<-.Foo", r"a\`b\\c",
5944        ] {
5945            let line = format!("`{name}` <- function(x, ...) {{}}");
5946            assert_eq!(parse_top_level_fn_def_name(&line), Some(name));
5947        }
5948        for line in [
5949            "`obj`$method <- function() {}",
5950            "`obj`[[1]] <- function() {}",
5951            "`unclosed <- function() {}",
5952            "`value` <- 1L",
5953        ] {
5954            assert_eq!(parse_top_level_fn_def_name(line), None);
5955        }
5956    }
5957
5958    #[test]
5959    fn 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.
5964        for (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        ] {
5969            assert_eq!(parse_top_level_fn_def_name(line), Some(name), "{line}");
5970        }
5971        // The pre-escaping spelling of `trail\` never closed its backtick.
5972        assert_eq!(
5973            parse_top_level_fn_def_name(r"`trail\` <- function(x) {}"),
5974            None
5975        );
5976        let map = std::collections::HashMap::new();
5977        let twice = "`a\\\\b` <- function() 1\n`a\\\\b` <- function() 2\n";
5978        assert!(detect_duplicate_wrapper_defs(twice, &map).is_err());
5979    }
5980
5981    #[test]
5982    fn detect_duplicate_quoted_operator_defs() {
5983        let map = std::collections::HashMap::new();
5984        let content = "`[.Foo` <- function(x, ...) {}\n`[.Foo` <- function(x, ...) {}\n";
5985        let err = detect_duplicate_wrapper_defs(content, &map).unwrap_err();
5986        assert!(err.contains("`[.Foo` is defined more than once"), "{err}");
5987        let mixed = "foo <- function() {}\n`foo` <- function() {}\n";
5988        assert!(detect_duplicate_wrapper_defs(mixed, &map).is_err());
5989        let distinct = "`[.Foo` <- function(x, ...) {}\n`[[.Foo` <- function(x, ...) {}\n";
5990        assert!(detect_duplicate_wrapper_defs(distinct, &map).is_ok());
5991    }
5992
5993    #[test]
5994    fn parse_top_level_fn_def_name_rejects_non_defs() {
5995        // Indented closures (R6 methods, S7 getters) are not top-level wrappers.
5996        assert_eq!(
5997            parse_top_level_fn_def_name("    get_value = function(self) self$x"),
5998            None
5999        );
6000        // S7::method(...) <- function : LHS is a call, not a bare ident.
6001        assert_eq!(
6002            parse_top_level_fn_def_name("S7::method(get_value, Shape) <- function(x) .Call(C, x)"),
6003            None
6004        );
6005        // `$`-assignment target is not a bare ident.
6006        assert_eq!(
6007            parse_top_level_fn_def_name("obj$method <- function() {}"),
6008            None
6009        );
6010        // Not a function definition at all.
6011        assert_eq!(parse_top_level_fn_def_name("x <- 1L"), None);
6012        // Comment / arbitrary line.
6013        assert_eq!(parse_top_level_fn_def_name("# a comment"), None);
6014    }
6015
6016    /// 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]
6020    fn preamble_cites_no_issue_numbers() {
6021        let binding = format_conversion_error_class_binding(&["pkg_error"]);
6022        for (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        ] {
6039            for line in text.lines() {
6040                let cites = line
6041                    .as_bytes()
6042                    .windows(2)
6043                    .any(|w| w[0] == b'#' && w[1].is_ascii_digit());
6044                assert!(!cites, "{name} cites an issue number: {line}");
6045            }
6046        }
6047    }
6048
6049    /// 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]
6056    fn caller_call_helper_takes_the_call_formal() {
6057        let defs: Vec<&str> = CALLER_CALL_HELPER
6058            .lines()
6059            .filter_map(parse_top_level_fn_def_name)
6060            .collect();
6061        assert_eq!(defs, [".miniextendr_caller_call"]);
6062        assert!(
6063            CALLER_CALL_HELPER
6064                .contains(".miniextendr_caller_call <- function(call = NULL, own = FALSE) {")
6065        );
6066        // `own` skips the caller lookup.
6067        assert!(CALLER_CALL_HELPER.contains("  if (!own) {\n    parent <- sys.parent(2L)"));
6068        // The fallback call leaves the plumbing formal out.
6069        assert!(CALLER_CALL_HELPER.contains("call$.call <- NULL"));
6070        // Every call it returns is the call as written (`sys.call()`), never
6071        // a matched one.
6072        assert!(!CALLER_CALL_HELPER.contains("match.call"));
6073        assert!(CALLER_CALL_HELPER.ends_with("}\n"));
6074    }
6075
6076    /// 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]
6081    fn frame_call_helper_is_shared_by_the_raise_helper_and_the_checks() {
6082        let fn_text = frame_call_helper_fn!();
6083        let defs: Vec<&str> = FRAME_CALL_HELPER
6084            .lines()
6085            .filter_map(parse_top_level_fn_def_name)
6086            .collect();
6087        assert_eq!(defs, [".miniextendr_frame_call"]);
6088        assert!(FRAME_CALL_HELPER.ends_with(&format!(".miniextendr_frame_call <- {fn_text}\n")));
6089        assert!(RAISE_CONDITION_HELPER_FN.contains(&format!(
6090            "if (is.environment(.call)) .call <- ({fn_text})(.call)\n"
6091        )));
6092        assert!(
6093            WRAPPER_RAISE_CONDITION_HELPER_FN
6094                .contains("if (is.environment(.call)) .call <- .miniextendr_frame_call(.call)\n")
6095        );
6096        assert!(!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.
6099        assert!(!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`.
6102        assert!(fn_text.contains("generic <- env[[\".Generic\"]]"));
6103        assert!(fn_text.contains("call(\"<-\", call, quote(value))"));
6104        assert!(
6105            ARG_ERROR_HELPER
6106                .contains("  if (is.environment(call)) call <- .miniextendr_frame_call(call)\n")
6107        );
6108    }
6109
6110    /// 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]
6119    fn field_at_helper_picks_a_field_as_a_list_does() {
6120        let defs: Vec<&str> = FIELD_AT_HELPER
6121            .lines()
6122            .filter_map(parse_top_level_fn_def_name)
6123            .collect();
6124        assert_eq!(
6125            defs,
6126            [".miniextendr_field_at", ".miniextendr_field_index_error"]
6127        );
6128        assert!(FIELD_AT_HELPER.contains(
6129            ".miniextendr_field_index_error <- function(cls, env, class = NULL, strict = FALSE) {"
6130        ));
6131        assert!(FIELD_AT_HELPER.contains(
6132            "class = c(class, \"notSubsettableError\"), call = .miniextendr_frame_call(env)))"
6133        ));
6134        assert!(
6135            FIELD_AT_HELPER
6136                .contains(".miniextendr_field_at <- function(fields, i, env, class = NULL) {")
6137        );
6138        assert!(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        ));
6142        assert!(FIELD_AT_HELPER.contains("  fields[[k]]\n"));
6143        assert!(FIELD_AT_HELPER.starts_with("# Internal helper"));
6144        assert!(FIELD_AT_HELPER.ends_with("}\n"));
6145    }
6146
6147    /// 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]
6152    fn conversion_error_class_binding_quotes_or_nulls() {
6153        let last_line = |classes: &[&str]| {
6154            format_conversion_error_class_binding(classes)
6155                .lines()
6156                .last()
6157                .unwrap()
6158                .to_string()
6159        };
6160        assert_eq!(
6161            last_line(&[]),
6162            ".miniextendr_conversion_error_class <- NULL"
6163        );
6164        assert_eq!(
6165            last_line(&["pkg_error_argument", "pkg_error"]),
6166            ".miniextendr_conversion_error_class <- c(\"pkg_error_argument\", \"pkg_error\")"
6167        );
6168        assert_eq!(
6169            last_line(&["a\"b\\c"]),
6170            ".miniextendr_conversion_error_class <- c(\"a\\\"b\\\\c\")"
6171        );
6172        let binding = format_conversion_error_class_binding(&["pkg_error"]);
6173        assert!(
6174            binding
6175                .lines()
6176                .all(|l| parse_top_level_fn_def_name(l).is_none())
6177        );
6178    }
6179
6180    /// 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]
6185    fn raise_helper_resolves_the_conversion_class_marker() {
6186        let marker = crate::condition::CONVERSION_ERROR_CLASS_MARKER;
6187        assert_eq!(
6188            format!("{marker} <- NULL"),
6189            format_conversion_error_class_binding(&[])
6190                .lines()
6191                .last()
6192                .unwrap()
6193        );
6194        let lookup = format!(".at <- match(\"{marker}\", .class)");
6195        let resolve = format!("get0(\"{marker}\", ifnotfound = NULL)");
6196        let at = RAISE_CONDITION_HELPER_FN
6197            .find(&lookup)
6198            .expect("marker lookup");
6199        let at_resolve = RAISE_CONDITION_HELPER_FN
6200            .find(&resolve)
6201            .expect("resolution");
6202        let at_switch = RAISE_CONDITION_HELPER_FN.find("switch(.val$kind").unwrap();
6203        assert!(at < at_resolve && at_resolve < at_switch);
6204        // The bare binding name appears only as a string (no direct lookup).
6205        assert_eq!(
6206            RAISE_CONDITION_HELPER_FN.matches(marker).count(),
6207            RAISE_CONDITION_HELPER_FN
6208                .matches(&format!("\"{marker}\""))
6209                .count()
6210        );
6211    }
6212
6213    /// A `match_arg` check as the macro writes it, for `splice_match_arg_entry`.
6214    const 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}";
6217
6218    fn 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    }
6230
6231    /// 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]
6236    fn splice_match_arg_entry_writes_the_aliases_argument() {
6237        assert_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        );
6242        assert_eq!(
6243            splice_match_arg_entry(
6244                ALIASES_CHECK,
6245                &aliases_entry(&["red", "gray"], {
6246                    const 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        );
6258        assert_eq!(
6259            splice_match_arg_entry(
6260                ALIASES_CHECK,
6261                &aliases_entry(&["red", "gray"], {
6262                    const 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`.
6271        let caller = ALIASES_CHECK.replace("\"mode\", .__MX", "\"mode\", .mx_call, .__MX");
6272        assert!(
6273            splice_match_arg_entry(&caller, &aliases_entry(&["red", "gray"], &[]))
6274                .contains("c(\"red\", \"gray\"), \"mode\", .mx_call)\n")
6275        );
6276    }
6277
6278    /// 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]
6281    fn match_arg_aliases_arg_escapes() {
6282        assert_eq!(match_arg_aliases_arg(&["a"], &[], "p"), "");
6283        assert_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            ),
6292            r#", aliases = base::c("hello" = "say \"hi\"", "d:\\" = "c:\\path"), listed = " (or \"d:\\\" for \"c:\\path\")""#
6293        );
6294    }
6295
6296    /// 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]
6300    fn match_arg_aliases_arg_refuses_bad_aliases() {
6301        let 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        };
6310        const 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        ];
6319        let message = refused(BAD);
6320        assert!(message.contains(".__MX_ALIASES_p__"), "{message}");
6321        for 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        ] {
6329            assert!(message.contains(problem), "{problem:?} in {message}");
6330        }
6331        // A prefix of its own choice only is fine.
6332        assert_eq!(
6333            match_arg_aliases_arg(
6334                &["red", "gray"],
6335                &[MatchArgAlias::unlisted("gr", "gray")],
6336                "p"
6337            ),
6338            r#", aliases = base::c("gr" = "gray")"#
6339        );
6340    }
6341
6342    /// The argument-check helpers define exactly the three preamble functions
6343    /// and end on a closing brace.
6344    #[test]
6345    fn arg_check_helpers_define_the_three_functions() {
6346        let 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();
6351        assert_eq!(
6352            defs,
6353            [
6354                ".miniextendr_arg_error",
6355                ".miniextendr_match_arg_several",
6356                ".miniextendr_match_arg"
6357            ]
6358        );
6359        for helper in ARG_CHECK_HELPERS {
6360            assert!(helper.starts_with("# Internal helper"));
6361            assert!(helper.ends_with("}\n"));
6362        }
6363    }
6364
6365    #[test]
6366    fn detect_duplicate_clean_content_passes() {
6367        // A sidecar accessor and an unrelated S7 shortcut: distinct names, no clash.
6368        let 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        );
6373        let 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        );
6386        assert!(detect_duplicate_wrapper_defs(content, &map).is_ok());
6387    }
6388
6389    #[test]
6390    fn 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.
6393        let 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        );
6398        let 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        );
6411        let err = detect_duplicate_wrapper_defs(content, &map).unwrap_err();
6412        assert!(err.contains("Shape_get_area"), "msg: {err}");
6413        // Message must name the sidecar producer and the field.
6414        assert!(err.contains("sidecar field `area`"), "msg: {err}");
6415        assert!(err.contains("Shape"), "msg: {err}");
6416    }
6417
6418    #[test]
6419    fn 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.
6423        let 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        );
6427        let 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        );
6440        let err = detect_duplicate_wrapper_defs(content, &map).unwrap_err();
6441        assert!(err.contains("Shape_get_area"), "msg: {err}");
6442        assert!(err.contains("sidecar field `area`"), "msg: {err}");
6443    }
6444
6445    #[test]
6446    fn duplicate_class_and_function_bindings_fail_in_either_order() {
6447        for 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        ] {
6453            let class = format!("# Generated from Rust impl `Widget` (lib.rs:10:6)\n{class}\n");
6454            let function = "# Generated from Rust fn `widget` (lib.rs:30:8)\n\
6455                            Widget <- function() .Call(C_widget)\n";
6456            for content in [format!("{class}{function}"), format!("{function}{class}")] {
6457                let error =
6458                    detect_duplicate_wrapper_defs(&content, &std::collections::HashMap::new())
6459                        .expect_err("a function must not replace a class or namespace object");
6460                assert!(error.contains("`Widget`"), "{error}");
6461                assert!(error.contains("class/namespace object"), "{error}");
6462                assert!(error.contains("r_name"), "{error}");
6463                assert!(
6464                    error.contains("Rust impl `Widget` (lib.rs:10:6)"),
6465                    "{error}"
6466                );
6467                assert!(error.contains("Rust fn `widget` (lib.rs:30:8)"), "{error}");
6468            }
6469        }
6470    }
6471
6472    /// Two class objects of one name in the written file: the second would
6473    /// drop the first one's methods (#1677, #1776).
6474    #[test]
6475    fn duplicate_class_objects_name_both_producers() {
6476        for class in [
6477            "Widget <- new.env(parent = emptyenv())",
6478            "Widget <- R6::R6Class(\"Widget\",",
6479            "Widget <- S7::new_class(\"Widget\",",
6480        ] {
6481            let 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            );
6486            let error = detect_duplicate_wrapper_defs(&content, &std::collections::HashMap::new())
6487                .expect_err("a second class object drops the first one's methods");
6488            assert!(
6489                error.contains("class `Widget` is defined more than once"),
6490                "{error}"
6491            );
6492            assert!(error.contains("(lib.rs:10:6)"), "{error}");
6493            assert!(error.contains("(lib.rs:40:6)"), "{error}");
6494            assert!(error.contains("distinct `class = \"...\"`"), "{error}");
6495        }
6496    }
6497
6498    #[test]
6499    fn duplicate_scan_keeps_class_members_and_s4_constructors_separate() {
6500        let 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        );
6521        assert!(detect_duplicate_wrapper_defs(content, &std::collections::HashMap::new()).is_ok());
6522    }
6523
6524    #[test]
6525    fn guarded_generics_cannot_collide_with_plain_bindings() {
6526        for generic in [
6527            concat!(
6528                "if (!base::exists(\"probe\", mode = \"function\")) {\n",
6529                "  probe <- function(x, ...) UseMethod(\"probe\")\n",
6530                "}\n",
6531            ),
6532            concat!(
6533                "if (!base::exists(\"probe\", mode = \"function\")) {\n",
6534                "  probe <- function(x, ...) base::UseMethod(\"probe\")\n",
6535                "}\n",
6536            ),
6537            concat!(
6538                "if (!exists(\"probe\", where = topenv(environment()), inherits = FALSE)) ",
6539                "methods::setGeneric(\"probe\", function(x, ...) standardGeneric(\"probe\"))\n",
6540            ),
6541            concat!(
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            ),
6547            concat!(
6548                "if (!exists(\"probe\", mode = \"function\")) {\n",
6549                "  probe <- S7::new_generic(\"probe\", \"x\", function(x, ...) S7::S7_dispatch())\n",
6550                "}\n",
6551            ),
6552            concat!(
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.
6559            let repeated = format!("{generic}{generic}");
6560            assert!(
6561                detect_duplicate_wrapper_defs(&repeated, &std::collections::HashMap::new()).is_ok(),
6562                "{repeated}"
6563            );
6564            let generic = format!("# Generated from Rust impl `Shape` (shape.rs:3:6)\n{generic}");
6565            for 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.
6572                for content in [format!("{generic}{binding}"), format!("{binding}{generic}")] {
6573                    let error =
6574                        detect_duplicate_wrapper_defs(&content, &std::collections::HashMap::new())
6575                            .expect_err("a generated binding must not mask a generic");
6576                    assert!(error.contains("`probe`"), "{error}");
6577                    assert!(error.contains("guarded generic"), "{error}");
6578                    assert!(error.contains("r_name"), "{error}");
6579                    assert!(error.contains("`Shape` (shape.rs:3:6)"), "{error}");
6580                    assert!(error.contains("`probe` (probe.rs:7:8)"), "{error}");
6581                }
6582            }
6583        }
6584    }
6585
6586    /// 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]
6589    fn duplicate_scan_names_first_generic_and_sidecar_producers() {
6590        let generic = "if (!base::exists(\"Shape_get_area\", mode = \"function\")) {\n  \
6591                       Shape_get_area <- function(x, ...) UseMethod(\"Shape_get_area\")\n}\n";
6592        let 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        );
6599        let error = detect_duplicate_wrapper_defs(&content, &std::collections::HashMap::new())
6600            .expect_err("the generic would replace the sidecar accessor");
6601        assert!(
6602            error.contains(
6603                "function from sidecar field `area` on Rust type `Shape` (Rust source line 4:5)"
6604            ),
6605            "{error}"
6606        );
6607        assert!(
6608            error.contains("guarded generic from Rust impl `Circle` (circle.rs:9:6)"),
6609            "{error}"
6610        );
6611        assert!(!error.contains("Square"), "{error}");
6612
6613        // Reported with the first declaration when the plain binding follows.
6614        let 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        );
6620        let error = detect_duplicate_wrapper_defs(&content, &std::collections::HashMap::new())
6621            .expect_err("the function would replace the generic");
6622        assert!(error.contains("`Circle` (circle.rs:9:6)"), "{error}");
6623        assert!(
6624            error.contains("Rust fn `Shape_get_area` (lib.rs:1:8)"),
6625            "{error}"
6626        );
6627        assert!(!error.contains("Square"), "{error}");
6628    }
6629
6630    #[test]
6631    fn guarded_generics_recognize_quoted_operator_names() {
6632        let generic = concat!(
6633            "if (!base::exists(\"%probe%\", mode = \"function\")) {\n",
6634            "  `%probe%` <- function(x, ...) UseMethod(\"%probe%\")\n",
6635            "}\n",
6636        );
6637        let content = format!("{generic}`%probe%` <- function(x, y) .Call(C_probe, x, y)\n");
6638        let error = detect_duplicate_wrapper_defs(&content, &std::collections::HashMap::new())
6639            .expect_err("quoted assignment names must match the generic name");
6640        assert!(error.contains("`%probe%`"), "{error}");
6641    }
6642
6643    #[test]
6644    fn duplicate_scan_ignores_local_and_non_generic_guards() {
6645        let 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        );
6656        assert!(detect_duplicate_wrapper_defs(content, &std::collections::HashMap::new()).is_ok());
6657    }
6658
6659    #[test]
6660    fn 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.
6663        let 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        );
6667        let map = std::collections::HashMap::new();
6668        let err = detect_duplicate_wrapper_defs(content, &map).unwrap_err();
6669        assert!(err.contains("Counter_inc"), "msg: {err}");
6670        assert!(err.contains("defined more than once"), "msg: {err}");
6671    }
6672
6673    #[test]
6674    fn 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.
6677        let 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        );
6682        let map = std::collections::HashMap::new();
6683        assert!(detect_duplicate_wrapper_defs(content, &map).is_ok());
6684    }
6685
6686    // endregion
6687
6688    // region: impl blocks merged per type (#1776)
6689
6690    fn widget_path() -> &'static str {
6691        "pkg::widgets::Widget"
6692    }
6693
6694    fn other_widget_path() -> &'static str {
6695        "pkg::legacy::Widget"
6696    }
6697
6698    /// 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.
6701    fn 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    }
6723
6724    fn 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    }
6731
6732    /// The defining block's topic governs defaults from every block, while
6733    /// an explicit method page (including the original class page) wins.
6734    #[test]
6735    fn class_member_docs_inherit_the_defining_blocks_topic() {
6736        let 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"];
6739        let second = widget_block(
6740            20,
6741            "\n\n",
6742            concat!(
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        );
6759        let output = merged(&[second, first]).remove(0);
6760        assert!(
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        );
6768        assert!(
6769            output.contains("#' @rdname Widget\n#' @param n (undocumented)"),
6770            "{output}"
6771        );
6772        assert!(
6773            output.contains("#' @rdname own_page\n#' @order 2\nWidget_own"),
6774            "{output}"
6775        );
6776        assert!(
6777            output.contains("#' @describeIn widget_api A described member.\nWidget_described"),
6778            "{output}"
6779        );
6780        assert!(!output.contains(CLASS_MEMBER_DEFAULT_PREFIX), "{output}");
6781        assert!(!output.contains("Generated title"), "{output}");
6782        assert!(!output.contains("@param missing"), "{output}");
6783    }
6784
6785    #[test]
6786    fn class_member_docs_keep_default_page_and_author_order() {
6787        let 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        );
6792        assert_eq!(
6793            resolve_class_member_docs(member, "Widget", "Widget"),
6794            concat!(
6795                "#' @rdname Widget\n#' @param x An object.\n",
6796                "#' @order 3\nWidget_value <- function(x) x\n",
6797            )
6798        );
6799        assert_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    }
6804
6805    /// 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]
6809    fn docs_only_class_block_needs_something_to_say() {
6810        const 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";
6814        let with_docs = |first_docs: &'static str, second_docs: &'static str| {
6815            let mut first = widget_block(10, HEAD, "\n# first\n");
6816            first.class_docs = first_docs;
6817            let mut second = widget_block(20, "\n\n", "\n# second\n");
6818            second.class_docs = second_docs;
6819            merged(&[first, second]).remove(0)
6820        };
6821
6822        let silent = with_docs("\n\n", "\n\n");
6823        assert!(!silent.contains("widget_rec"), "{silent}");
6824        assert!(!silent.contains("NULL"), "{silent}");
6825        assert!(
6826            silent.contains("# first") && silent.contains("# second"),
6827            "{silent}"
6828        );
6829
6830        let page_only = with_docs("\n#' @rdname widget_rec\n", "\n\n");
6831        assert!(!page_only.contains("NULL"), "{page_only}");
6832
6833        let documented = with_docs("\n\n", "\n#' @description Records the package builds.\n");
6834        assert!(
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        );
6841
6842        let titled = with_docs(
6843            "\n\n",
6844            "\n#' @title Package records\n#' @description Records.\n",
6845        );
6846        assert!(!titled.contains("S3 Class"), "{titled}");
6847        assert!(titled.contains("#' @title Package records\n"), "{titled}");
6848    }
6849
6850    #[test]
6851    fn merge_defines_the_class_once_with_every_blocks_members() {
6852        let mut ctor = widget_block(
6853            10,
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";
6860        let mut more = widget_block(
6861            40,
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.
6868        let classes = merged(&[more, ctor]);
6869        assert_eq!(classes.len(), 1);
6870        assert_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    }
6887
6888    #[test]
6889    fn merge_defines_the_class_from_the_block_with_the_constructor() {
6890        let first = widget_block(10, "\nWidget <- new.env() # first\n", "\nWidget$a <- 1\n");
6891        let mut ctor = widget_block(40, "\nWidget <- new.env() # ctor\n", "\nWidget$b <- 2\n");
6892        ctor.defines = &["a constructor"];
6893        let classes = merged(&[first, ctor]);
6894        assert_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.
6906        let first = widget_block(10, "\nWidget <- new.env() # first\n", "\n\n");
6907        let second = widget_block(40, "\nWidget <- new.env() # second\n", "\n\n");
6908        assert_eq!(
6909            merged(&[second, first])[0],
6910            "# Generated from Rust impl `Widget` (lib.rs:10:6)\nWidget <- new.env() # first"
6911        );
6912    }
6913
6914    #[test]
6915    fn merge_keeps_one_block_as_written() {
6916        // The head's blank last line separates it from the members.
6917        let mut block = widget_block(
6918            10,
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";
6923        assert_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    }
6934
6935    #[test]
6936    fn 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.
6939        let mut hidden = widget_block(10, "\n\n", "\n\n");
6940        hidden.settings = "env, @noRd";
6941        let plain = widget_block(40, "\n\n", "\n\n");
6942        let msg = merge_class_blocks(&[hidden, plain])
6943            .err()
6944            .expect("settings differ");
6945        assert!(
6946            msg.contains("the impl blocks of `Widget` disagree on the class settings"),
6947            "{msg}"
6948        );
6949        assert!(
6950            msg.contains("`env, @noRd` on `impl Widget` (lib.rs:10)"),
6951            "{msg}"
6952        );
6953        assert!(msg.contains("`env` on `impl Widget` (lib.rs:40)"), "{msg}");
6954    }
6955
6956    #[test]
6957    fn merge_refuses_two_defining_blocks() {
6958        let mut ctor = widget_block(10, "\n\n", "\n\n");
6959        ctor.defines = &["a constructor"];
6960        let mut props = widget_block(40, "\n\n", "\n\n");
6961        props.defines = &["a constructor", "S7 properties"];
6962        let msg = merge_class_blocks(&[ctor, props])
6963            .err()
6964            .expect("two blocks define the class");
6965        assert!(
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        );
6973        assert!(
6974            msg.contains("move what `impl Widget` (lib.rs:40) has into `impl Widget` (lib.rs:10)"),
6975            "{msg}"
6976        );
6977    }
6978
6979    #[test]
6980    fn merge_refuses_a_member_bound_by_two_blocks() {
6981        let mut first = widget_block(10, "\n\n", "\n\n");
6982        first.member_names = &["`Widget$get`", "`Widget$set`"];
6983        let mut second = widget_block(40, "\n\n", "\n\n");
6984        second.member_names = &["`Widget$get`"];
6985        let msg = merge_class_blocks(&[first, second])
6986            .err()
6987            .expect("`$get` is bound twice");
6988        assert!(
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        );
6995        assert!(!msg.contains("Widget$set"), "{msg}");
6996    }
6997
6998    #[test]
6999    fn merge_refuses_two_types_with_one_class_name() {
7000        // Same identifier, different modules: two types, not two blocks of one.
7001        let mine = widget_block(10, "\nWidget <- new.env()\n", "\n\n");
7002        let mut theirs = widget_block(40, "\nWidget <- new.env()\n", "\n\n");
7003        theirs.type_path = other_widget_path;
7004        theirs.source_file = "legacy.rs";
7005        let msg = merge_class_blocks(&[mine, theirs])
7006            .err()
7007            .expect("one class, two types");
7008        assert!(
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    }
7017
7018    /// A class head as the class generators write it: generated defaults, the
7019    /// docs and doc-lines markers, then the class object.
7020    const 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";
7030
7031    #[test]
7032    fn merge_writes_every_blocks_docs_after_the_definers() {
7033        let 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";
7036        let 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";
7039        let mut last = widget_block(70, DOCUMENTED_HEAD, "\n\n");
7040        last.class_docs = "\n#' @details Two.\n#'\n#' }\n#' @param .ptr Pointer.\n";
7041        let 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.
7045        assert_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    }
7066
7067    #[test]
7068    fn merge_takes_the_title_from_any_block() {
7069        let mut ctor = widget_block(10, DOCUMENTED_HEAD, "\n\n");
7070        ctor.defines = &["a constructor"];
7071        let mut more = widget_block(40, DOCUMENTED_HEAD, "\n\n");
7072        more.class_docs = "\n#' @title Widgets\n#' @rdname Widget\n";
7073        let classes = merged(&[ctor, more]);
7074        assert!(
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    }
7086
7087    #[test]
7088    fn merge_drops_a_member_default_another_block_documents() {
7089        let 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";
7092        let 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";
7095        let class = &merged(&[ctor, more])[0];
7096        assert!(
7097            class.contains("#' @field doubled Twice the value.\n"),
7098            "{class}"
7099        );
7100        assert!(!class.contains("@field doubled Active binding."), "{class}");
7101        assert!(
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.
7107        assert!(
7108            class.contains("#' @param .ptr Internal pointer.\n"),
7109            "{class}"
7110        );
7111    }
7112
7113    #[test]
7114    fn merge_refuses_two_titles() {
7115        let mut ctor = widget_block(10, DOCUMENTED_HEAD, "\n\n");
7116        ctor.defines = &["a constructor"];
7117        ctor.class_docs = "\n#' @title Widgets\n";
7118        let mut more = widget_block(40, DOCUMENTED_HEAD, "\n\n");
7119        more.class_docs = "\n#' @title Gadgets\n";
7120        let same = widget_block(70, DOCUMENTED_HEAD, "\n\n");
7121        let msg = merge_class_blocks(&[ctor, more, same])
7122            .err()
7123            .expect("two titles");
7124        assert_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    }
7132
7133    #[test]
7134    fn merge_refuses_a_page_other_than_the_definers() {
7135        let mut ctor = widget_block(10, DOCUMENTED_HEAD, "\n\n");
7136        ctor.defines = &["a constructor"];
7137        let mut more = widget_block(40, DOCUMENTED_HEAD, "\n\n");
7138        more.class_docs = "\n#' @rdname widget_ops\n";
7139        let msg = merge_class_blocks(&[ctor, more]).err().expect("two pages");
7140        assert_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    }
7149
7150    #[test]
7151    fn merge_reports_every_problem_at_once() {
7152        let mut first = widget_block(10, "\n\n", "\n\n");
7153        first.defines = &["a constructor"];
7154        first.member_names = &["`Widget$get`"];
7155        let 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";
7159        let msg = merge_class_blocks(&[first, second])
7160            .err()
7161            .expect("three problems");
7162        assert_eq!(msg.lines().count(), 3, "{msg}");
7163    }
7164
7165    // endregion
7166
7167    // region: the constructor decided per type
7168
7169    /// 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.
7172    fn inferring_block(line: u32) -> ClassBlockEntry {
7173        let 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"];
7179        let 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    }
7191
7192    #[test]
7193    fn a_lone_candidate_is_the_constructor_whichever_block_has_it() {
7194        let methods = widget_block(10, "\nWidget <- new.env() # first\n", "\nWidget$get <- 2\n");
7195        let classes = merged(&[methods, inferring_block(40)]);
7196        assert_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    }
7206
7207    #[test]
7208    fn a_declared_constructor_keeps_the_candidate_a_static_method() {
7209        let 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"];
7213        let classes = merged(&[inferring_block(40), new]);
7214        assert_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    }
7223
7224    #[test]
7225    fn candidates_in_two_blocks_without_a_declared_constructor_are_refused() {
7226        let mut other = widget_block(10, "\n\n", "\n\n");
7227        other.constructor_candidates = &["empty", "from_file"];
7228        let msg = merge_class_blocks(&[other, inferring_block(40)])
7229            .err()
7230            .expect("three candidates");
7231        assert!(
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        );
7239        assert!(msg.contains("`#[miniextendr(env(constructor))]`"), "{msg}");
7240        // The candidates are the only problem reported.
7241        assert_eq!(msg.lines().count(), 1, "{msg}");
7242    }
7243
7244    // endregion
7245}
7246// endregion