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`.
67use syn::ext::IdentExt;
89// 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.
2122/// The user's identifier as a plain name: `r#where` → `"where"`, `x` → `"x"`.
23pub(crate) fn ident_name(ident: &syn::Ident) -> String {
24ident.unraw().to_string()
25}
2627/// 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 {
31ident.unraw()
32}
3334/// 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
4243/// 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 {
46let 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}
4950/// 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 {
54let 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}
5758/// 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}
6970/// 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}
7677/// 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}
8586/// 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}
9293/// 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 {
99use std::sync::atomic::{AtomicUsize, Ordering};
100static NEXT: AtomicUsize = AtomicUsize::new(0);
101let suffix = NEXT.fetch_add(1, Ordering::Relaxed);
102let 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}
105106// 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.
123124/// 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}
148149/// `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 {
153let prefix = crate_prefix();
154let 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}
157158/// `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 {
163let prefix = crate_prefix();
164let 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}
167168/// `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 {
177let prefix = crate_prefix();
178let 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}
181182/// 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 {
189match ::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}
194195/// `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 {
197let 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}
200201/// `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 {
203let 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}
206207/// `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 {
210let 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}
213214/// `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 {
217let 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}
220221/// `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 {
225let 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}
228229/// `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 {
232let 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}
235236/// 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.
241Fields,
242/// `.rdata_get_<Type>(x, name)`: the field's getter `.Call()`.
243Get,
244/// `.rdata_set_<Type>(x, name, value)`: the field's setter `.Call()`.
245Set,
246/// `.rdata_no_field_<Type>(name, env, cls)`: the `miniextendr_no_field`
247 /// error, naming the R class `cls` the method serves.
248NoField,
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.
252ErrorClass,
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`].
256EveryRead,
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.
261Live,
262}
263264/// `.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 {
268let 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}
279280/// `__mx_altrep_reg_<crate>_<Ident>` — ALTREP class registration fn.
281pub(crate) fn altrep_reg_fn_ident(ident: &syn::Ident) -> syn::Ident {
282let 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}
285286/// `__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 {
291let prefix = crate_prefix();
292let 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}
295296/// Trait tag, shared by the declaration and both impl expansion paths.
297pub(crate) fn trait_tag_ident(trait_name: &syn::Ident) -> syn::Ident {
298let 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}
301302/// 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 {
304let 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}
307308/// 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 {
313let trait_name = unraw(trait_name);
314let 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}
317318/// 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 {
320let 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}
323324/// 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 {
326let 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}
329330/// `__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 {
336let prefix = crate_prefix();
337let 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
341342/// 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 {
348let mut out = String::new();
349for (i, c) in s.char_indices() {
350if c.is_uppercase() && i > 0 {
351 out.push('_');
352 }
353 out.extend(c.to_lowercase());
354 }
355out356}
357358/// Convert a PascalCase string to kebab-case (`InProgress` → `in-progress`).
359pub(crate) fn to_kebab_case(s: &str) -> String {
360to_snake_case(s).replace('_', "-")
361}
362363/// 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 {
368match rename_all {
369Some("snake_case") => to_snake_case(name),
370Some("kebab-case") => to_kebab_case(name),
371Some("lower") => name.to_lowercase(),
372Some("upper") => name.to_uppercase(),
373_ => name.to_string(),
374 }
375}
376377// region: R definition names
378379/// 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];
401402/// Whether `name` is one of R's reserved words (`if`, `in`, `TRUE`, `NA`, ...).
403pub(crate) fn is_r_reserved_word(name: &str) -> bool {
404R_RESERVED_WORDS.contains(&name)
405}
406407/// 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 {
412let mut chars = name.chars();
413match chars.next() {
414Some(c) if c.is_ascii_alphabetic() => {}
415Some('.') => {
416if chars.next().is_some_and(|c| c.is_ascii_digit()) {
417return false;
418 }
419 }
420_ => return false,
421 }
422name.chars()
423 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_')
424 && !is_r_reserved_word(name)
425}
426427/// 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 {
431let bytes = name.as_bytes();
432bytes.len() >= 2
433&& bytes[0].is_ascii_alphabetic()
434 && bytes[bytes.len() - 1] != b'.'
435&& bytes436 .iter()
437 .all(|b| b.is_ascii_alphanumeric() || *b == b'.')
438}
439440/// 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 {
453if is_syntactic_r_name(name) {
454name.to_string()
455 } else {
456::alloc::__export::must_use({
::alloc::fmt::format(format_args!("`{0}`",
name.replace('\\', "\\\\").replace('`', "\\`")))
})format!("`{}`", name.replace('\\', "\\\\").replace('`', "\\`"))457 }
458}
459460/// 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 {
472if is_syntactic_r_name(name) {
473name.to_string()
474 } else {
475::alloc::__export::must_use({
::alloc::fmt::format(format_args!("\"{0}\"",
name.replace('\\', "\\\\").replace('"', "\\\"")))
})format!("\"{}\"", name.replace('\\', "\\\\").replace('"', "\\\""))476 }
477}
478479/// 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 {
491use std::fmt::Writeas _;
492let mut out = String::with_capacity(s.len());
493for c in s.chars() {
494match 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 => {
502let _ = out.write_fmt(format_args!("\\u{{{0:x}}}", u32::from(c)))write!(out, "\\u{{{:x}}}", u32::from(c));
503 }
504 c => {
505let _ = out.write_fmt(format_args!("\\U{{{0:x}}}", u32::from(c)))write!(out, "\\U{{{:x}}}", u32::from(c));
506 }
507 }
508 }
509out510}
511512// endregion
513514// region: package-qualified generics (#2154)
515516/// 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) {
521match generic.split_once("::") {
522Some((package, name)) => (Some(package), name),
523None => (None, generic),
524 }
525}
526527/// 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> {
532let generic = value.value();
533let error = |message: String| Err(syn::Error::new(value.span(), message));
534if generic.contains(":::") {
535return 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 }
540let Some((package, name)) = generic.split_once("::") else {
541return Ok(generic);
542 };
543if name.contains("::") {
544return 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 }
548if name.is_empty() {
549return 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 }
551if !is_r_package_name(package) {
552return 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 }
558Ok(generic)
559}
560561// endregion
562563/// 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> {
565let name = value.value();
566if !is_syntactic_r_name(&name) || !crate::r_class_formatter::is_bare_identifier(&name) {
567return Err(syn::Error::new(
568value.span(),
569"receiver must start with a letter and contain only letters, digits and underscores",
570 ));
571 }
572Ok(name)
573}
574575#[cfg(test)]
576mod tests {
577use super::*;
578579// 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]
586fn crate_prefix_fallback_is_normalized_pkg_name() {
587assert_eq!(crate_prefix(), "miniextendr_macros");
588 }
589590#[test]
591fn c_symbols_are_crate_prefixed() {
592let ty = quote::format_ident!("Counter");
593let tr = quote::format_ident!("Resettable");
594let m = quote::format_ident!("reset");
595assert_eq!(
596 bare_fn_c_wrapper_ident(&m).to_string(),
597"C_miniextendr_macros_reset"
598);
599assert_eq!(
600 impl_method_c_wrapper_ident(&ty, &m).to_string(),
601"C_miniextendr_macros_Counter__reset"
602);
603assert_eq!(
604 trait_member_c_wrapper_string(&ty, &tr, &m),
605"C_miniextendr_macros_Counter__Resettable__reset"
606);
607assert_eq!(
608 trait_member_c_wrapper_ident(&ty, &tr, &m).to_string(),
609 trait_member_c_wrapper_string(&ty, &tr, &m)
610 );
611assert_eq!(
612 sidecar_getter_c_name("Counter", "count"),
613"C_miniextendr_macros__mx_rdata_get_Counter_count"
614);
615assert_eq!(
616 sidecar_setter_c_name("Counter", "count"),
617"C_miniextendr_macros__mx_rdata_set_Counter_count"
618);
619assert_eq!(
620 altrep_reg_fn_ident(&ty).to_string(),
621"__mx_altrep_reg_miniextendr_macros_Counter"
622);
623assert_eq!(
624 vtable_static_ident(&tr, "Counter").to_string(),
625"__VTABLE_miniextendr_macros_Resettable_FOR_Counter"
626);
627assert_eq!(
628 vtshim_ident(&ty, &tr, &m).to_string(),
629"__vtshim_miniextendr_macros_Counter__Resettable__reset"
630);
631 }
632633#[test]
634fn trait_symbols_preserve_case_and_strip_raw_prefixes() {
635let upper = quote::format_ident!("Counter");
636let lower = quote::format_ident!("counter");
637let method = quote::format_ident!("value");
638assert_ne!(
639 vtable_static_ident(&upper, "Foo"),
640 vtable_static_ident(&lower, "Foo")
641 );
642assert_ne!(
643 vtable_static_ident(&upper, "Foo"),
644 vtable_static_ident(&upper, "foo")
645 );
646assert_ne!(trait_tag_ident(&upper), trait_tag_ident(&lower));
647assert_ne!(
648 trait_vtable_builder_ident(&upper),
649 trait_vtable_builder_ident(&lower)
650 );
651assert_ne!(
652 trait_method_shim_ident(&upper, &method),
653 trait_method_shim_ident(&lower, &method)
654 );
655assert_ne!(
656 trait_impl_wrappers_ident("Foo", &upper),
657 trait_impl_wrappers_ident("Foo", &lower)
658 );
659assert_ne!(
660 trait_dispatch_ident(&upper, "Foo"),
661 trait_dispatch_ident(&lower, "Foo")
662 );
663let raw: syn::Ident = syn::parse_quote!(r#match);
664assert_eq!(trait_tag_ident(&raw), "TAG_match");
665assert_eq!(trait_vtable_builder_ident(&raw), "__match_build_vtable");
666assert_eq!(
667 trait_method_shim_ident(&upper, &raw),
668"__Counter_match_shim"
669);
670 }
671672#[test]
673fn raw_identifiers_lose_their_prefix_in_names() {
674let raw: syn::Ident = syn::parse_quote!(r#where);
675let plain: syn::Ident = syn::parse_quote!(x);
676assert_eq!(raw.to_string(), "r#where", "Display keeps the prefix");
677assert_eq!(ident_name(&raw), "where");
678assert_eq!(ident_name(&plain), "x");
679assert_eq!(unraw(&raw).to_string(), "where");
680assert_eq!(
681quote::format_ident!("__storage_{}", unraw(&raw)),
682"__storage_where"
683);
684// The C symbol / const-name formatters unraw internally.
685assert_eq!(
686 bare_fn_c_wrapper_ident(&syn::parse_quote!(r#match)).to_string(),
687"C_miniextendr_macros_match"
688);
689assert_eq!(
690 r_wrapper_const_ident_for(&syn::parse_quote!(r#match)).to_string(),
691"R_WRAPPER_match"
692);
693let ty: syn::Ident = syn::parse_quote!(Row);
694assert_eq!(
695 impl_method_c_wrapper_ident(&ty, &syn::parse_quote!(r#type)).to_string(),
696"C_miniextendr_macros_Row__type"
697);
698 }
699700#[test]
701fn name_ident_makes_keywords_raw() {
702assert_eq!(name_ident("type").to_string(), "r#type");
703assert_eq!(name_ident("where").to_string(), "r#where");
704assert_eq!(name_ident("value").to_string(), "value");
705// Round-trips: a raw ident's name maps back to the same raw ident.
706let raw: syn::Ident = syn::parse_quote!(r#type);
707assert_eq!(name_ident(&ident_name(&raw)), raw);
708 }
709710#[test]
711fn snake_case() {
712assert_eq!(to_snake_case("HelloWorld"), "hello_world");
713assert_eq!(to_snake_case("InProgress"), "in_progress");
714assert_eq!(to_snake_case("ABC"), "a_b_c");
715assert_eq!(to_snake_case("Red"), "red");
716 }
717718#[test]
719fn kebab_case() {
720assert_eq!(to_kebab_case("HelloWorld"), "hello-world");
721assert_eq!(to_kebab_case("InProgress"), "in-progress");
722 }
723724#[test]
725fn rename_all() {
726assert_eq!(
727 apply_rename_all("HelloWorld", Some("snake_case")),
728"hello_world"
729);
730assert_eq!(
731 apply_rename_all("HelloWorld", Some("kebab-case")),
732"hello-world"
733);
734assert_eq!(apply_rename_all("HelloWorld", Some("lower")), "helloworld");
735assert_eq!(apply_rename_all("HelloWorld", Some("upper")), "HELLOWORLD");
736assert_eq!(apply_rename_all("HelloWorld", None), "HelloWorld");
737 }
738739#[test]
740fn namespace_names_quote_operators_only() {
741for name in ["get", "var", ".hidden", "s7_trait_Ops_value", "Foo_at"] {
742assert_eq!(r_namespace_name(name), name);
743assert_eq!(r_def_name(name), name);
744 }
745for (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 ] {
753assert_eq!(r_namespace_name(name), ns);
754assert_eq!(r_def_name(name), def);
755 }
756// The NAMESPACE form is an R string literal: escape `"` and `\`.
757assert_eq!(r_namespace_name(r#"a"b\c"#), r#""a\"b\\c""#);
758 }
759760#[test]
761fn 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.
765assert_eq!(r_def_name(r"a\b"), r"`a\\b`");
766assert_eq!(r_def_name(r"trail\"), r"`trail\\`");
767assert_eq!(r_def_name("a`b"), r"`a\`b`");
768// A backslash next to a backtick: each is escaped once, in order.
769assert_eq!(r_def_name(r"x\`y"), r"`x\\\`y`");
770assert_eq!(r_def_name(r"%\%"), r"`%\\%`");
771 }
772773#[test]
774fn package_qualified_generics() {
775let parse = |s: &str| parse_generic(&syn::LitStr::new(s, proc_macro2::Span::call_site()));
776for ok in [
777"print",
778"[[",
779"dplyr::dplyr_reconstruct",
780"data.table::as.data.table",
781 ] {
782assert_eq!(parse(ok).unwrap(), ok);
783 }
784assert_eq!(
785 split_generic("vctrs::vec_ptype2"),
786 (Some("vctrs"), "vec_ptype2")
787 );
788assert_eq!(split_generic("format"), (None, "format"));
789for (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 ] {
799let message = parse(bad).unwrap_err().to_string();
800assert!(message.contains(why), "{bad}: {message}");
801 }
802 }
803}