Skip to main content

miniextendr_macros/
naming.rs

1//! Naming helpers for the static symbols the `#[miniextendr]` attribute emits.
2//!
3//! These formatters live in one place so the attribute macro and any later
4//! consumer (e.g. a registration-chasing linter) can compute the exact same
5//! identifiers from a source `syn::Ident`.
6
7use syn::ext::IdentExt;
8
9// region: raw identifiers
10//
11// Rust keywords are ordinary R names (`type`, `where`, `match`, `mod`, ...),
12// so users spell them as raw identifiers: `fn r#match(r#where: &str)`. The
13// `r#` prefix is Rust surface syntax only — `Display` for `syn::Ident` keeps it
14// (`"r#where"`), which is neither a valid R name, nor a valid C symbol
15// fragment, nor accepted by `syn::Ident::new` / `format_ident!`. Every place
16// that turns a user identifier into a *name* (R formals, R function/method
17// names, list/data-frame column names, C symbols, HashMap keys, synthesized
18// Rust bindings) must go through `ident_name` / `unraw` instead of
19// `to_string()` / direct interpolation. Token positions (`quote! { #ident }`)
20// keep the raw ident — `r#where` is what the Rust side needs.
21
22/// The user's identifier as a plain name: `r#where` → `"where"`, `x` → `"x"`.
23pub(crate) fn ident_name(ident: &syn::Ident) -> String {
24    ident.unraw().to_string()
25}
26
27/// The user's identifier without its `r#` prefix, for `format_ident!` /
28/// `quote!` interpolation into *synthesized* identifiers (`__storage_where`).
29/// Not for emitting the identifier itself — `where` is not a valid binding.
30pub(crate) fn unraw(ident: &syn::Ident) -> syn::Ident {
31    ident.unraw()
32}
33
34/// A *name* (a column / list-element name, from a user field or a `rename`)
35/// as a Rust identifier for a generated struct field or binding. Keywords come
36/// back raw so `type` emits `r#type` instead of an unparsable token stream.
37pub(crate) fn name_ident(name: &str) -> syn::Ident {
38    syn::parse_str::<syn::Ident>(name)
39        .unwrap_or_else(|_| syn::Ident::new_raw(name, proc_macro2::Span::call_site()))
40}
41// endregion
42
43/// Local repr(C) type for a concrete trait vtable. Only the static beside it
44/// is exported to C; this type reproduces the declaration's supplied layout.
45pub(crate) fn vtable_layout_ident(trait_name: &syn::Ident, type_name: &str) -> syn::Ident {
46    let table = vtable_static_ident(trait_name, type_name);
47    ::quote::__private::mk_ident(&::alloc::__export::must_use({
                ::alloc::fmt::format(format_args!("{0}_Layout", table))
            }), ::quote::__private::Option::None)quote::format_ident!("{table}_Layout")
48}
49
50/// Identifier for the registration entry holding the R wrapper source.
51///
52/// Returns `R_WRAPPER_{rust_ident}`, preserving case.
53pub(crate) fn r_wrapper_const_ident_for(rust_ident: &syn::Ident) -> syn::Ident {
54    let name = unraw(rust_ident);
55    ::quote::__private::mk_ident(&::alloc::__export::must_use({
                ::alloc::fmt::format(format_args!("R_WRAPPER_{0}", name))
            }), ::quote::__private::Option::None)quote::format_ident!("R_WRAPPER_{name}")
56}
57
58/// The binder of a conditional bound, `for<'__mx_cond> FieldTy: Trait`.
59///
60/// A where clause on a concrete type that does not hold is an error where the
61/// impl is written (E0277). Behind a higher-ranked binder the same bound is
62/// checked where the impl is used, so the impl just does not apply to that
63/// type. Derives use it for impls that exist only when the user's field types
64/// allow them, such as a companion's `Debug`. The name stays clear of user
65/// lifetimes, which a binder must not shadow (E0496).
66pub(crate) fn conditional_bound_lifetime() -> syn::Lifetime {
67    syn::Lifetime::new("'__mx_cond", proc_macro2::Span::call_site())
68}
69
70/// The C wrapper's receiver slot, shared with method extraction and return
71/// handling. Its mixed-site span keeps a user parameter named `self_sexp`
72/// from replacing the original R handle after conversion.
73pub(crate) fn self_sexp_ident() -> syn::Ident {
74    syn::Ident::new("self_sexp", proc_macro2::Span::mixed_site())
75}
76
77/// The C wrapper's call slot, declared by `CWrapperContext` with `quote!`.
78/// A reference inside a `quote_spanned!` template would take the user type's
79/// span instead. That only resolves while the macro's call site is the
80/// user's, not when the wrapper is generated inside a `macro_rules!`
81/// expansion, as a trait impl's is (#1489). So references interpolate this.
82pub(crate) fn call_slot_ident() -> syn::Ident {
83    syn::Ident::new("__miniextendr_call", proc_macro2::Span::call_site())
84}
85
86/// The error a failed argument conversion binds, for the same reason as
87/// [`call_slot_ident`]: the binding and the expressions that read it are
88/// built in different templates.
89pub(crate) fn conversion_error_ident() -> syn::Ident {
90    syn::Ident::new("e", proc_macro2::Span::call_site())
91}
92
93/// A private export name for a trait's metadata macro. The public macro is
94/// re-exported beside the trait under the trait's own name, in Rust's separate
95/// macro namespace, so imports carry both. An expansion-local suffix prevents
96/// same-named traits in different modules from colliding at the crate root.
97/// This name never enters the trait tag or any C/wasm registration symbol.
98pub(crate) fn trait_metadata_macro_ident(trait_name: &syn::Ident) -> syn::Ident {
99    use std::sync::atomic::{AtomicUsize, Ordering};
100    static NEXT: AtomicUsize = AtomicUsize::new(0);
101    let suffix = NEXT.fetch_add(1, Ordering::Relaxed);
102    let name = unraw(trait_name);
103    ::quote::__private::mk_ident(&::alloc::__export::must_use({
                ::alloc::fmt::format(format_args!("__mx_trait_metadata_{0}_{1}",
                        name, suffix))
            }), ::quote::__private::Option::None)quote::format_ident!("__mx_trait_metadata_{name}_{suffix}")
104}
105
106// region: crate-prefixed C symbol naming (#1273)
107//
108// Every `#[miniextendr]`-emitted `#[no_mangle]` C symbol is prefixed with the
109// consuming crate's name so that two packages loaded into the same webR
110// session (which share one GOT under Emscripten's SIDE_MODULE model) can't
111// have the first-loaded package's definition of a name silently win for a
112// later package's identically-named export. Native `dyn.load` with
113// `RTLD_LOCAL` was never affected — this is purely a wasm cross-package
114// hygiene fix — but the prefix is applied unconditionally on every target so
115// there's a single code path and no target-dependent symbol shape.
116//
117// Registration name and linker symbol name are kept identical everywhere
118// (the invariant the wasm snapshot writer,
119// `miniextendr-api/src/wasm_registry_writer.rs`, depends on to reconstruct
120// `extern "C" { fn <name>(...); }` blocks from `R_CallMethodDef.name`) — so
121// every formatter below produces the *one* string used for both the
122// `#[no_mangle]` fn/static definition and its registration entry.
123
124/// The consuming crate's name, used as the prefix for every macro-emitted
125/// `#[no_mangle]` C symbol.
126///
127/// Reads `CARGO_CRATE_NAME`, which cargo sets (with hyphens already
128/// normalized to underscores) for the rustc invocation that expands this
129/// proc macro — the same precedent `miniextendr_init!()` relies on
130/// (`lib.rs`, `miniextendr_init` fn). Real macro expansion (i.e. compiling a
131/// downstream crate through cargo) always has this set.
132///
133/// Falls back to `CARGO_PKG_NAME` (normalized the same way, since it is
134/// *not* auto-normalized) for the one context where `CARGO_CRATE_NAME` is
135/// absent: this crate's own unit/insta tests call codegen functions directly
136/// at test *runtime* rather than through real macro expansion, and cargo
137/// does not forward `CARGO_CRATE_NAME` to a test binary's process
138/// environment (verified empirically — only `CARGO_PKG_*` vars are). Falls
139/// back further to the literal `"crate"` if even that is absent (e.g. a
140/// direct `rustc` invocation bypassing cargo entirely).
141pub(crate) fn crate_prefix() -> String {
142    std::env::var("CARGO_CRATE_NAME").unwrap_or_else(|_| {
143        std::env::var("CARGO_PKG_NAME")
144            .map(|n| n.replace('-', "_"))
145            .unwrap_or_else(|_| "crate".to_string())
146    })
147}
148
149/// `C_<crate>_<fn>` — bare-fn C wrapper identifier (internal-wrapper arm
150/// only; `extern`/`#[export_name]` fns pass through untouched and own their
151/// own cross-package uniqueness).
152pub(crate) fn bare_fn_c_wrapper_ident(rust_ident: &syn::Ident) -> syn::Ident {
153    let prefix = crate_prefix();
154    let rust_ident = unraw(rust_ident);
155    ::quote::__private::mk_ident(&::alloc::__export::must_use({
                ::alloc::fmt::format(format_args!("C_{0}_{1}", prefix,
                        rust_ident))
            }), ::quote::__private::Option::None)quote::format_ident!("C_{prefix}_{rust_ident}")
156}
157
158/// `C_<crate>_<Type>__<method>` — impl-method C wrapper identifier.
159pub(crate) fn impl_method_c_wrapper_ident(
160    type_ident: &syn::Ident,
161    method_ident: &syn::Ident,
162) -> syn::Ident {
163    let prefix = crate_prefix();
164    let method_ident = unraw(method_ident);
165    ::quote::__private::mk_ident(&::alloc::__export::must_use({
                ::alloc::fmt::format(format_args!("C_{0}_{1}__{2}", prefix,
                        type_ident, method_ident))
            }), ::quote::__private::Option::None)quote::format_ident!("C_{prefix}_{type_ident}__{method_ident}")
166}
167
168/// `C_<crate>_<Type>__<Trait>__<member>` as a `String` — shared by
169/// `TraitMethod` and `TraitConst`, and by both the ident and string forms
170/// each needs, so the format string can't drift across the four call sites
171/// that used to duplicate it.
172pub(crate) fn trait_member_c_wrapper_string(
173    type_ident: &syn::Ident,
174    trait_name: &syn::Ident,
175    member_ident: &syn::Ident,
176) -> String {
177    let prefix = crate_prefix();
178    let member_ident = unraw(member_ident);
179    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("C_{0}_{1}__{2}__{3}", prefix,
                type_ident, trait_name, member_ident))
    })format!("C_{prefix}_{type_ident}__{trait_name}__{member_ident}")
180}
181
182/// Same as [`trait_member_c_wrapper_string`], as a `syn::Ident` for Rust
183/// token generation.
184pub(crate) fn trait_member_c_wrapper_ident(
185    type_ident: &syn::Ident,
186    trait_name: &syn::Ident,
187    member_ident: &syn::Ident,
188) -> syn::Ident {
189    match ::quote::__private::IdentFragmentAdapter(&trait_member_c_wrapper_string(type_ident,
                trait_name, member_ident)) {
    arg =>
        ::quote::__private::mk_ident(&::alloc::__export::must_use({
                        ::alloc::fmt::format(format_args!("{0}", arg))
                    }), ::quote::__private::Option::None.or(arg.span())),
}quote::format_ident!(
190        "{}",
191        trait_member_c_wrapper_string(type_ident, trait_name, member_ident)
192    )
193}
194
195/// `C_<crate>__mx_rdata_get_<Type>_<field>` — sidecar field getter C symbol.
196pub(crate) fn sidecar_getter_c_name(type_name: &str, field_name: &str) -> String {
197    let prefix = crate_prefix();
198    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("C_{0}__mx_rdata_get_{1}_{2}",
                prefix, type_name, field_name))
    })format!("C_{prefix}__mx_rdata_get_{type_name}_{field_name}")
199}
200
201/// `C_<crate>__mx_rdata_set_<Type>_<field>` — sidecar field setter C symbol.
202pub(crate) fn sidecar_setter_c_name(type_name: &str, field_name: &str) -> String {
203    let prefix = crate_prefix();
204    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("C_{0}__mx_rdata_set_{1}_{2}",
                prefix, type_name, field_name))
    })format!("C_{prefix}__mx_rdata_set_{type_name}_{field_name}")
205}
206
207/// `C_<crate>__mx_all_equal_<Type>` — the field reader of the `all.equal()`
208/// method of `#[derive(AllEqual)]` (#2149).
209pub(crate) fn all_equal_c_name(type_name: &str) -> String {
210    let prefix = crate_prefix();
211    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("C_{0}__mx_all_equal_{1}", prefix,
                type_name))
    })format!("C_{prefix}__mx_all_equal_{type_name}")
212}
213
214/// `C_<crate>__mx_rdata_live_<Type>` — the liveness check of a type with
215/// `#[externalptr(restored(refuse = "every_read"))]` (#1901).
216pub(crate) fn sidecar_live_c_name(type_name: &str) -> String {
217    let prefix = crate_prefix();
218    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("C_{0}__mx_rdata_live_{1}", prefix,
                type_name))
    })format!("C_{prefix}__mx_rdata_live_{type_name}")
219}
220
221/// `C_<crate>__mx_rdata_fallback_<Type>` — the field fallback of an S3, S4
222/// or env class with `r_data_accessors` (`fallback = "Self::f"`, #2150),
223/// which the impl block that has it emits.
224pub(crate) fn sidecar_fallback_c_name(type_name: &str) -> String {
225    let prefix = crate_prefix();
226    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("C_{0}__mx_rdata_fallback_{1}",
                prefix, type_name))
    })format!("C_{prefix}__mx_rdata_fallback_{type_name}")
227}
228
229/// `C_<crate>__mx_rdata_names_<Type>` — the names the field fallback
230/// answers, for `.DollarNames()` (`fallback_names = "Self::g"`, #2150).
231pub(crate) fn sidecar_fallback_names_c_name(type_name: &str) -> String {
232    let prefix = crate_prefix();
233    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("C_{0}__mx_rdata_names_{1}", prefix,
                type_name))
    })format!("C_{prefix}__mx_rdata_names_{type_name}")
234}
235
236/// The R helpers `#[derive(ExternalPtr)]` emits for a type's sidecar fields
237/// and the `$` / `[[` field methods of `r_data_accessors` call (#1848).
238#[derive(#[automatically_derived]
impl ::core::fmt::Debug for RDataHelper {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::write_str(f,
            match self {
                RDataHelper::Fields => "Fields",
                RDataHelper::Get => "Get",
                RDataHelper::Set => "Set",
                RDataHelper::NoField => "NoField",
                RDataHelper::ErrorClass => "ErrorClass",
                RDataHelper::EveryRead => "EveryRead",
                RDataHelper::Live => "Live",
            })
    }
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for RDataHelper { }
#[automatically_derived]
impl ::core::clone::Clone for RDataHelper {
    #[inline]
    fn clone(&self) -> Self { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for RDataHelper { }Copy)]
239pub(crate) enum RDataHelper {
240    /// `.rdata_fields_<Type>`: the field names, a character vector.
241    Fields,
242    /// `.rdata_get_<Type>(x, name)`: the field's getter `.Call()`.
243    Get,
244    /// `.rdata_set_<Type>(x, name, value)`: the field's setter `.Call()`.
245    Set,
246    /// `.rdata_no_field_<Type>(name, env, cls)`: the `miniextendr_no_field`
247    /// error, naming the R class `cls` the method serves.
248    NoField,
249    /// `.rdata_error_class_<Type>`: the classes the field methods put in
250    /// front of their own errors, `#[externalptr(field_error(class = ...))]`
251    /// (#1899); `NULL` without them.
252    ErrorClass,
253    /// `.rdata_every_read_<Type>`: `TRUE` for a type with
254    /// `restored(refuse = "every_read")` (#1901), else `FALSE`; it gates the
255    /// field methods' calls of [`RDataHelper::Live`].
256    EveryRead,
257    /// `.rdata_live_<Type>(x, raise)`: the liveness check of a type with
258    /// `restored(refuse = "every_read")` (#1901). Other types get a stub that
259    /// is never called (gated by [`RDataHelper::EveryRead`]), so a binding
260    /// the methods call is always a function, as `R CMD check` expects.
261    Live,
262}
263
264/// `.rdata_<helper>_<Type>`: the R name of a sidecar field helper, named by
265/// the Rust type (not an impl's `class = "..."`), since the derive only knows
266/// the type.
267pub(crate) fn rdata_helper_name(helper: RDataHelper, type_name: &str) -> String {
268    let helper = match helper {
269        RDataHelper::Fields => "fields",
270        RDataHelper::Get => "get",
271        RDataHelper::Set => "set",
272        RDataHelper::NoField => "no_field",
273        RDataHelper::ErrorClass => "error_class",
274        RDataHelper::EveryRead => "every_read",
275        RDataHelper::Live => "live",
276    };
277    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!(".rdata_{0}_{1}", helper,
                type_name))
    })format!(".rdata_{helper}_{type_name}")
278}
279
280/// `__mx_altrep_reg_<crate>_<Ident>` — ALTREP class registration fn.
281pub(crate) fn altrep_reg_fn_ident(ident: &syn::Ident) -> syn::Ident {
282    let prefix = crate_prefix();
283    ::quote::__private::mk_ident(&::alloc::__export::must_use({
                ::alloc::fmt::format(format_args!("__mx_altrep_reg_{0}_{1}",
                        prefix, ident))
            }), ::quote::__private::Option::None)quote::format_ident!("__mx_altrep_reg_{prefix}_{ident}")
284}
285
286/// `__VTABLE_<crate>_<Trait>_FOR_<Type>` — trait-impl vtable static.
287/// Preserve spelling: Rust distinguishes `Counter` from `counter`, as must
288/// both the Rust identifier and the exported linker symbol. `type_name` may
289/// carry the existing generic-argument hash suffix.
290pub(crate) fn vtable_static_ident(trait_name: &syn::Ident, type_name: &str) -> syn::Ident {
291    let prefix = crate_prefix();
292    let trait_name = unraw(trait_name);
293    ::quote::__private::mk_ident(&::alloc::__export::must_use({
                ::alloc::fmt::format(format_args!("__VTABLE_{0}_{1}_FOR_{2}",
                        prefix, trait_name, type_name))
            }), ::quote::__private::Option::None)quote::format_ident!("__VTABLE_{prefix}_{trait_name}_FOR_{type_name}")
294}
295
296/// Trait tag, shared by the declaration and both impl expansion paths.
297pub(crate) fn trait_tag_ident(trait_name: &syn::Ident) -> syn::Ident {
298    let trait_name = unraw(trait_name);
299    ::quote::__private::mk_ident(&::alloc::__export::must_use({
                ::alloc::fmt::format(format_args!("TAG_{0}", trait_name))
            }), ::quote::__private::Option::None)quote::format_ident!("TAG_{trait_name}")
300}
301
302/// Trait vtable builder, defined beside the trait and called from its impls.
303pub(crate) fn trait_vtable_builder_ident(trait_name: &syn::Ident) -> syn::Ident {
304    let trait_name = unraw(trait_name);
305    ::quote::__private::mk_ident(&::alloc::__export::must_use({
                ::alloc::fmt::format(format_args!("__{0}_build_vtable",
                        trait_name))
            }), ::quote::__private::Option::None)quote::format_ident!("__{trait_name}_build_vtable")
306}
307
308/// Generic shim defined beside a non-generic trait.
309pub(crate) fn trait_method_shim_ident(
310    trait_name: &syn::Ident,
311    method_name: &syn::Ident,
312) -> syn::Ident {
313    let trait_name = unraw(trait_name);
314    let method_name = unraw(method_name);
315    ::quote::__private::mk_ident(&::alloc::__export::must_use({
                ::alloc::fmt::format(format_args!("__{0}_{1}_shim",
                        trait_name, method_name))
            }), ::quote::__private::Option::None)quote::format_ident!("__{trait_name}_{method_name}_shim")
316}
317
318/// Wrapper fragment registered by either trait impl expansion path.
319pub(crate) fn trait_impl_wrappers_ident(type_name: &str, trait_name: &syn::Ident) -> syn::Ident {
320    let trait_name = unraw(trait_name);
321    ::quote::__private::mk_ident(&::alloc::__export::must_use({
                ::alloc::fmt::format(format_args!("R_WRAPPERS_{0}_{1}_IMPL",
                        type_name, trait_name))
            }), ::quote::__private::Option::None)quote::format_ident!("R_WRAPPERS_{type_name}_{trait_name}_IMPL")
322}
323
324/// Trait dispatch registration; uses the same type suffix as its vtable.
325pub(crate) fn trait_dispatch_ident(trait_name: &syn::Ident, type_name: &str) -> syn::Ident {
326    let trait_name = unraw(trait_name);
327    ::quote::__private::mk_ident(&::alloc::__export::must_use({
                ::alloc::fmt::format(format_args!("__MX_DISPATCH_{0}_FOR_{1}",
                        trait_name, type_name))
            }), ::quote::__private::Option::None)quote::format_ident!("__MX_DISPATCH_{trait_name}_FOR_{type_name}")
328}
329
330/// `__vtshim_<crate>_<Type>__<Trait>__<method>` — trait-impl vtable shim fn.
331pub(crate) fn vtshim_ident(
332    type_ident: &syn::Ident,
333    trait_name: &syn::Ident,
334    method_ident: &syn::Ident,
335) -> syn::Ident {
336    let prefix = crate_prefix();
337    let method_ident = unraw(method_ident);
338    ::quote::__private::mk_ident(&::alloc::__export::must_use({
                ::alloc::fmt::format(format_args!("__vtshim_{0}_{1}__{2}__{3}",
                        prefix, type_ident, trait_name, method_ident))
            }), ::quote::__private::Option::None)quote::format_ident!("__vtshim_{prefix}_{type_ident}__{trait_name}__{method_ident}")
339}
340// endregion
341
342/// Convert a PascalCase string to snake_case.
343///
344/// Inserts an underscore before each uppercase letter (except the first),
345/// then lowercases the entire result. For example, `"InProgress"` becomes
346/// `"in_progress"`.
347pub(crate) fn to_snake_case(s: &str) -> String {
348    let mut out = String::new();
349    for (i, c) in s.char_indices() {
350        if c.is_uppercase() && i > 0 {
351            out.push('_');
352        }
353        out.extend(c.to_lowercase());
354    }
355    out
356}
357
358/// Convert a PascalCase string to kebab-case (`InProgress` → `in-progress`).
359pub(crate) fn to_kebab_case(s: &str) -> String {
360    to_snake_case(s).replace('_', "-")
361}
362
363/// Apply a `rename_all` transformation to a variant name.
364///
365/// Supports `"snake_case"`, `"kebab-case"`, `"lower"`, `"upper"`. Returns the
366/// name unchanged if `rename_all` is `None` or unrecognised.
367pub(crate) fn apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
368    match rename_all {
369        Some("snake_case") => to_snake_case(name),
370        Some("kebab-case") => to_kebab_case(name),
371        Some("lower") => name.to_lowercase(),
372        Some("upper") => name.to_uppercase(),
373        _ => name.to_string(),
374    }
375}
376
377// region: R definition names
378
379/// R's reserved words: never usable as a bare name on the left of `<-`.
380const R_RESERVED_WORDS: &[&str] = &[
381    "if",
382    "else",
383    "repeat",
384    "while",
385    "function",
386    "for",
387    "next",
388    "break",
389    "TRUE",
390    "FALSE",
391    "NULL",
392    "Inf",
393    "NaN",
394    "NA",
395    "NA_integer_",
396    "NA_real_",
397    "NA_character_",
398    "NA_complex_",
399    "in",
400];
401
402/// Whether `name` is one of R's reserved words (`if`, `in`, `TRUE`, `NA`, ...).
403pub(crate) fn is_r_reserved_word(name: &str) -> bool {
404    R_RESERVED_WORDS.contains(&name)
405}
406
407/// Whether `name` is a syntactic R name: ASCII letters, digits, `.` and `_`
408/// only; starts with a letter, or with a `.` that is not followed by a digit;
409/// and is not a reserved word. Anything else (`[.foo`, `$.foo`, `==.foo`,
410/// `%in%.foo`) has to be backtick-quoted in source.
411pub(crate) fn is_syntactic_r_name(name: &str) -> bool {
412    let mut chars = name.chars();
413    match chars.next() {
414        Some(c) if c.is_ascii_alphabetic() => {}
415        Some('.') => {
416            if chars.next().is_some_and(|c| c.is_ascii_digit()) {
417                return false;
418            }
419        }
420        _ => return false,
421    }
422    name.chars()
423        .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_')
424        && !is_r_reserved_word(name)
425}
426
427/// R's rule for a package name (Writing R Extensions, "The DESCRIPTION
428/// file"): ASCII letters, digits and `.`, at least two characters, starting
429/// with a letter and not ending in `.`.
430pub(crate) fn is_r_package_name(name: &str) -> bool {
431    let bytes = name.as_bytes();
432    bytes.len() >= 2
433        && bytes[0].is_ascii_alphabetic()
434        && bytes[bytes.len() - 1] != b'.'
435        && bytes
436            .iter()
437            .all(|b| b.is_ascii_alphanumeric() || *b == b'.')
438}
439
440/// The left-hand side of a generated `<name> <- function(...)` definition.
441///
442/// Syntactic names pass through unchanged so the common output stays readable;
443/// a non-syntactic one (an S3 method on `[`, `$`, `==`, ...) is wrapped in
444/// backticks, which R accepts for any name. Without this the wrapper file did
445/// not parse (#1475).
446///
447/// R processes backslash escapes inside backticks as it does in strings, so a
448/// `\` is doubled (first, so the escapes added for backticks stay single) and a
449/// backtick becomes `` \` ``: the name `a\b` is written `` `a\\b` ``.
450/// `miniextendr-api`'s duplicate-definition scan
451/// (`registry::parse_top_level_fn_def_name`) reads this escaped spelling.
452pub(crate) fn r_def_name(name: &str) -> String {
453    if is_syntactic_r_name(name) {
454        name.to_string()
455    } else {
456        ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("`{0}`",
                name.replace('\\', "\\\\").replace('`', "\\`")))
    })format!("`{}`", name.replace('\\', "\\\\").replace('`', "\\`"))
457    }
458}
459
460/// The argument of a raw NAMESPACE directive such as `export(<name>)`.
461///
462/// `@rawNamespace` lines reach NAMESPACE verbatim, so roxygen2 does not quote
463/// them the way it quotes `@export <name>`. Syntactic names stay bare; a
464/// non-syntactic one (an S7 generic on `[[` or `%op%`) becomes a double-quoted
465/// string, the form roxygen2 itself writes, because `export([[)` does not parse.
466/// `miniextendr-api`'s generic-doc pass mirrors this rule
467/// (`registry::r_namespace_name`); the two must stay in step so roxygen2 sees
468/// one directive, not two spellings of it. Only `\` and `"` are escaped, as
469/// the mirror does ([`r_string_escape`] would also escape control and
470/// non-ASCII characters).
471pub(crate) fn r_namespace_name(name: &str) -> String {
472    if is_syntactic_r_name(name) {
473        name.to_string()
474    } else {
475        ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("\"{0}\"",
                name.replace('\\', "\\\\").replace('"', "\\\"")))
    })format!("\"{}\"", name.replace('\\', "\\\\").replace('"', "\\\""))
476    }
477}
478
479/// Escape `s` for the inside of an R double-quoted string literal. Every
480/// generated R string literal built from user text goes through this (the
481/// precondition requirement text and the author's messages, the class names
482/// in `inherits()`, lifecycle arguments).
483///
484/// Besides `\` and `"`, a newline, carriage return or tab becomes its escape
485/// (a guard stays on one line of the wrappers file), any other control
486/// character and every non-ASCII character a `\u{..}` / `\U{..}` escape: R
487/// code in a package must be ASCII, and R reads these escapes back to the
488/// same UTF-8 text. A NUL cannot be written (R strings cannot hold one);
489/// the attribute parser rejects it in a message.
490pub(crate) fn r_string_escape(s: &str) -> String {
491    use std::fmt::Write as _;
492    let mut out = String::with_capacity(s.len());
493    for c in s.chars() {
494        match c {
495            '\\' => out.push_str("\\\\"),
496            '"' => out.push_str("\\\""),
497            '\n' => out.push_str("\\n"),
498            '\r' => out.push_str("\\r"),
499            '\t' => out.push_str("\\t"),
500            c if c.is_ascii() && !c.is_ascii_control() => out.push(c),
501            c if u32::from(c) <= 0xFFFF => {
502                let _ = out.write_fmt(format_args!("\\u{{{0:x}}}", u32::from(c)))write!(out, "\\u{{{:x}}}", u32::from(c));
503            }
504            c => {
505                let _ = out.write_fmt(format_args!("\\U{{{0:x}}}", u32::from(c)))write!(out, "\\U{{{:x}}}", u32::from(c));
506            }
507        }
508    }
509    out
510}
511
512// endregion
513
514// region: package-qualified generics (#2154)
515
516/// A `generic = "..."` value split into its package and the generic's own
517/// name: `"dplyr::dplyr_reconstruct"` is `(Some("dplyr"), "dplyr_reconstruct")`,
518/// `"print"` is `(None, "print")`. The value must have passed
519/// [`parse_generic`].
520pub(crate) fn split_generic(generic: &str) -> (Option<&str>, &str) {
521    match generic.split_once("::") {
522        Some((package, name)) => (Some(package), name),
523        None => (None, generic),
524    }
525}
526
527/// Parse a `generic = "..."` value. A generic may name its package,
528/// `pkg::generic`, with a valid R package name (ASCII letters, digits and
529/// `.`; at least two characters; starts with a letter; doesn't end in `.`)
530/// and one `::`.
531pub(crate) fn parse_generic(value: &syn::LitStr) -> syn::Result<String> {
532    let generic = value.value();
533    let error = |message: String| Err(syn::Error::new(value.span(), message));
534    if generic.contains(":::") {
535        return error(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("`{0}`: a method registers on an exported generic; name it `pkg::generic`, not `pkg:::generic`",
                generic))
    })format!(
536            "`{generic}`: a method registers on an exported generic; name it `pkg::generic`, \
537             not `pkg:::generic`"
538        ));
539    }
540    let Some((package, name)) = generic.split_once("::") else {
541        return Ok(generic);
542    };
543    if name.contains("::") {
544        return error(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("`{0}`: a generic takes one `pkg::` qualifier",
                generic))
    })format!(
545            "`{generic}`: a generic takes one `pkg::` qualifier"
546        ));
547    }
548    if name.is_empty() {
549        return error(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("`{0}`: no generic name after `{1}::`",
                generic, package))
    })format!("`{generic}`: no generic name after `{package}::`"));
550    }
551    if !is_r_package_name(package) {
552        return error(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("`{0}`: `{1}` is not an R package name; a package name has only ASCII letters, digits and `.`, at least two characters, starts with a letter and doesn\'t end in `.`",
                generic, package))
    })format!(
553            "`{generic}`: `{package}` is not an R package name; a package name has only ASCII \
554             letters, digits and `.`, at least two characters, starts with a letter and \
555             doesn't end in `.`"
556        ));
557    }
558    Ok(generic)
559}
560
561// endregion
562
563/// Parse a dispatch receiver that is safe to emit as a bare R symbol.
564pub(crate) fn parse_r_receiver(value: &syn::LitStr) -> syn::Result<String> {
565    let name = value.value();
566    if !is_syntactic_r_name(&name) || !crate::r_class_formatter::is_bare_identifier(&name) {
567        return Err(syn::Error::new(
568            value.span(),
569            "receiver must start with a letter and contain only letters, digits and underscores",
570        ));
571    }
572    Ok(name)
573}
574
575#[cfg(test)]
576mod tests {
577    use super::*;
578
579    // At unit-test runtime cargo does not forward CARGO_CRATE_NAME to the
580    // test binary's process env, so crate_prefix() takes the CARGO_PKG_NAME
581    // fallback: "miniextendr-macros" → "miniextendr_macros". Real macro
582    // expansion (inside rustc) always sees CARGO_CRATE_NAME. The insta
583    // snapshots pin the same value; if this test fails the snapshots are
584    // stale too.
585    #[test]
586    fn crate_prefix_fallback_is_normalized_pkg_name() {
587        assert_eq!(crate_prefix(), "miniextendr_macros");
588    }
589
590    #[test]
591    fn c_symbols_are_crate_prefixed() {
592        let ty = quote::format_ident!("Counter");
593        let tr = quote::format_ident!("Resettable");
594        let m = quote::format_ident!("reset");
595        assert_eq!(
596            bare_fn_c_wrapper_ident(&m).to_string(),
597            "C_miniextendr_macros_reset"
598        );
599        assert_eq!(
600            impl_method_c_wrapper_ident(&ty, &m).to_string(),
601            "C_miniextendr_macros_Counter__reset"
602        );
603        assert_eq!(
604            trait_member_c_wrapper_string(&ty, &tr, &m),
605            "C_miniextendr_macros_Counter__Resettable__reset"
606        );
607        assert_eq!(
608            trait_member_c_wrapper_ident(&ty, &tr, &m).to_string(),
609            trait_member_c_wrapper_string(&ty, &tr, &m)
610        );
611        assert_eq!(
612            sidecar_getter_c_name("Counter", "count"),
613            "C_miniextendr_macros__mx_rdata_get_Counter_count"
614        );
615        assert_eq!(
616            sidecar_setter_c_name("Counter", "count"),
617            "C_miniextendr_macros__mx_rdata_set_Counter_count"
618        );
619        assert_eq!(
620            altrep_reg_fn_ident(&ty).to_string(),
621            "__mx_altrep_reg_miniextendr_macros_Counter"
622        );
623        assert_eq!(
624            vtable_static_ident(&tr, "Counter").to_string(),
625            "__VTABLE_miniextendr_macros_Resettable_FOR_Counter"
626        );
627        assert_eq!(
628            vtshim_ident(&ty, &tr, &m).to_string(),
629            "__vtshim_miniextendr_macros_Counter__Resettable__reset"
630        );
631    }
632
633    #[test]
634    fn trait_symbols_preserve_case_and_strip_raw_prefixes() {
635        let upper = quote::format_ident!("Counter");
636        let lower = quote::format_ident!("counter");
637        let method = quote::format_ident!("value");
638        assert_ne!(
639            vtable_static_ident(&upper, "Foo"),
640            vtable_static_ident(&lower, "Foo")
641        );
642        assert_ne!(
643            vtable_static_ident(&upper, "Foo"),
644            vtable_static_ident(&upper, "foo")
645        );
646        assert_ne!(trait_tag_ident(&upper), trait_tag_ident(&lower));
647        assert_ne!(
648            trait_vtable_builder_ident(&upper),
649            trait_vtable_builder_ident(&lower)
650        );
651        assert_ne!(
652            trait_method_shim_ident(&upper, &method),
653            trait_method_shim_ident(&lower, &method)
654        );
655        assert_ne!(
656            trait_impl_wrappers_ident("Foo", &upper),
657            trait_impl_wrappers_ident("Foo", &lower)
658        );
659        assert_ne!(
660            trait_dispatch_ident(&upper, "Foo"),
661            trait_dispatch_ident(&lower, "Foo")
662        );
663        let raw: syn::Ident = syn::parse_quote!(r#match);
664        assert_eq!(trait_tag_ident(&raw), "TAG_match");
665        assert_eq!(trait_vtable_builder_ident(&raw), "__match_build_vtable");
666        assert_eq!(
667            trait_method_shim_ident(&upper, &raw),
668            "__Counter_match_shim"
669        );
670    }
671
672    #[test]
673    fn raw_identifiers_lose_their_prefix_in_names() {
674        let raw: syn::Ident = syn::parse_quote!(r#where);
675        let plain: syn::Ident = syn::parse_quote!(x);
676        assert_eq!(raw.to_string(), "r#where", "Display keeps the prefix");
677        assert_eq!(ident_name(&raw), "where");
678        assert_eq!(ident_name(&plain), "x");
679        assert_eq!(unraw(&raw).to_string(), "where");
680        assert_eq!(
681            quote::format_ident!("__storage_{}", unraw(&raw)),
682            "__storage_where"
683        );
684        // The C symbol / const-name formatters unraw internally.
685        assert_eq!(
686            bare_fn_c_wrapper_ident(&syn::parse_quote!(r#match)).to_string(),
687            "C_miniextendr_macros_match"
688        );
689        assert_eq!(
690            r_wrapper_const_ident_for(&syn::parse_quote!(r#match)).to_string(),
691            "R_WRAPPER_match"
692        );
693        let ty: syn::Ident = syn::parse_quote!(Row);
694        assert_eq!(
695            impl_method_c_wrapper_ident(&ty, &syn::parse_quote!(r#type)).to_string(),
696            "C_miniextendr_macros_Row__type"
697        );
698    }
699
700    #[test]
701    fn name_ident_makes_keywords_raw() {
702        assert_eq!(name_ident("type").to_string(), "r#type");
703        assert_eq!(name_ident("where").to_string(), "r#where");
704        assert_eq!(name_ident("value").to_string(), "value");
705        // Round-trips: a raw ident's name maps back to the same raw ident.
706        let raw: syn::Ident = syn::parse_quote!(r#type);
707        assert_eq!(name_ident(&ident_name(&raw)), raw);
708    }
709
710    #[test]
711    fn snake_case() {
712        assert_eq!(to_snake_case("HelloWorld"), "hello_world");
713        assert_eq!(to_snake_case("InProgress"), "in_progress");
714        assert_eq!(to_snake_case("ABC"), "a_b_c");
715        assert_eq!(to_snake_case("Red"), "red");
716    }
717
718    #[test]
719    fn kebab_case() {
720        assert_eq!(to_kebab_case("HelloWorld"), "hello-world");
721        assert_eq!(to_kebab_case("InProgress"), "in-progress");
722    }
723
724    #[test]
725    fn rename_all() {
726        assert_eq!(
727            apply_rename_all("HelloWorld", Some("snake_case")),
728            "hello_world"
729        );
730        assert_eq!(
731            apply_rename_all("HelloWorld", Some("kebab-case")),
732            "hello-world"
733        );
734        assert_eq!(apply_rename_all("HelloWorld", Some("lower")), "helloworld");
735        assert_eq!(apply_rename_all("HelloWorld", Some("upper")), "HELLOWORLD");
736        assert_eq!(apply_rename_all("HelloWorld", None), "HelloWorld");
737    }
738
739    #[test]
740    fn namespace_names_quote_operators_only() {
741        for name in ["get", "var", ".hidden", "s7_trait_Ops_value", "Foo_at"] {
742            assert_eq!(r_namespace_name(name), name);
743            assert_eq!(r_def_name(name), name);
744        }
745        for (name, ns, def) in [
746            ("[", r#""[""#, "`[`"),
747            ("[[", r#""[[""#, "`[[`"),
748            ("$", r#""$""#, "`$`"),
749            ("%custom%", r#""%custom%""#, "`%custom%`"),
750            ("Foo_[[", r#""Foo_[[""#, "`Foo_[[`"),
751            ("if", r#""if""#, "`if`"),
752        ] {
753            assert_eq!(r_namespace_name(name), ns);
754            assert_eq!(r_def_name(name), def);
755        }
756        // The NAMESPACE form is an R string literal: escape `"` and `\`.
757        assert_eq!(r_namespace_name(r#"a"b\c"#), r#""a\"b\\c""#);
758    }
759
760    #[test]
761    fn def_names_escape_backslashes_before_backticks() {
762        // R reads backslash escapes inside backticks, so `\` must be doubled
763        // (`` `a\\b` `` is the three-character name `a\b`) and a trailing one must
764        // not escape the closing backtick.
765        assert_eq!(r_def_name(r"a\b"), r"`a\\b`");
766        assert_eq!(r_def_name(r"trail\"), r"`trail\\`");
767        assert_eq!(r_def_name("a`b"), r"`a\`b`");
768        // A backslash next to a backtick: each is escaped once, in order.
769        assert_eq!(r_def_name(r"x\`y"), r"`x\\\`y`");
770        assert_eq!(r_def_name(r"%\%"), r"`%\\%`");
771    }
772
773    #[test]
774    fn package_qualified_generics() {
775        let parse = |s: &str| parse_generic(&syn::LitStr::new(s, proc_macro2::Span::call_site()));
776        for ok in [
777            "print",
778            "[[",
779            "dplyr::dplyr_reconstruct",
780            "data.table::as.data.table",
781        ] {
782            assert_eq!(parse(ok).unwrap(), ok);
783        }
784        assert_eq!(
785            split_generic("vctrs::vec_ptype2"),
786            (Some("vctrs"), "vec_ptype2")
787        );
788        assert_eq!(split_generic("format"), (None, "format"));
789        for (bad, why) in [
790            ("dplyr:::dplyr_reconstruct", "not `pkg:::generic`"),
791            ("a::b::c", "one `pkg::` qualifier"),
792            ("dplyr::", "no generic name"),
793            ("::print", "not an R package name"),
794            ("x::print", "not an R package name"),
795            ("2pkg::print", "not an R package name"),
796            ("pkg.::print", "not an R package name"),
797            ("my_pkg::print", "not an R package name"),
798        ] {
799            let message = parse(bad).unwrap_err().to_string();
800            assert!(message.contains(why), "{bad}: {message}");
801        }
802    }
803}