Skip to main content

miniextendr_macros/
match_arg_keys.rs

1//! Write-time placeholder & symbol-name formatting for the match_arg pipeline.
2//!
3//! Tradeoff signpost: this module is the **wiring** between the
4//! `#[derive(MatchArg)]` proc macro (see `match_arg_derive`) and the
5//! cdylib's write-time substitution pass. The `match.arg` codegen path
6//! deliberately splits responsibilities — the proc macro emits placeholders
7//! into the R wrapper, and the cdylib resolves them to a concrete `c("a",
8//! "b", ...)` literal at write time using `MX_MATCH_ARG_CHOICES`. That
9//! split lets the variant list change without re-running `cargo expand` on
10//! every consumer. Compared to hand-rolling `match.arg` in a wrapper body,
11//! the user gets partial matching, R-visible defaults, and `@param` doc
12//! lines for free, without ever stringifying the variant list in source.
13//!
14//! All four shapes (`choices_placeholder`, `param_doc_placeholder`,
15//! `choices_helper_c_name`, `choices_helper_def_ident`) share the same
16//! `{c_ident_without_prefix}_{r_param}` stem so the cdylib's write-time pass
17//! can correlate them. Keep them together so the shape can't drift.
18//!
19//! The `c_ident_without_prefix` input has `C_` already stripped (or is a stem
20//! already, e.g. `MyType__method`). Callers that have a full `c_ident` should
21//! pass `c_ident.trim_start_matches("C_")`.
22//!
23//! All four helpers call `c_stem(...)` internally so callers may also pass a
24//! full `c_ident` (e.g. `"C_my_fn"`) — the `C_` prefix is normalized away.
25
26fn c_stem(c_ident: &str) -> &str {
27    c_ident.trim_start_matches("C_")
28}
29
30/// R-side placeholder that the cdylib resolves to a `c("a", "b", ...)` literal
31/// at write time. Substituted by `MX_MATCH_ARG_CHOICES` entries.
32pub(crate) fn choices_placeholder(c_ident: &str, r_param: &str) -> String {
33    format!(".__MX_MATCH_ARG_CHOICES_{}_{}__", c_stem(c_ident), r_param)
34}
35
36/// R-side placeholder for the `@param` doc line, substituted by
37/// `MX_MATCH_ARG_PARAM_DOCS` entries at write time. See #210.
38pub(crate) fn param_doc_placeholder(c_ident: &str, r_param: &str) -> String {
39    format!(
40        ".__MX_MATCH_ARG_PARAM_DOC_{}_{}__",
41        c_stem(c_ident),
42        r_param
43    )
44}
45
46/// C symbol name for the helper fn that returns the enum's choices SEXP,
47/// called from the R wrapper's match.arg prelude.
48pub(crate) fn choices_helper_c_name(c_ident: &str, r_param: &str) -> String {
49    format!("C_{}__match_arg_choices__{}", c_stem(c_ident), r_param)
50}
51
52/// Rust ident holding the `R_CallMethodDef` for the match_arg choices helper.
53pub(crate) fn choices_helper_def_ident(c_ident: &str, r_param: &str) -> syn::Ident {
54    quote::format_ident!(
55        "call_method_def_{}",
56        choices_helper_c_name(c_ident, r_param)
57    )
58}
59
60/// Extract the unquoted form of a user-supplied `default = "..."` literal
61/// for a `match_arg` parameter.
62///
63/// The user writes the value as it appears in R source, so a string default
64/// is `default = "\"zstd\""` — the `String` we receive is `"zstd"` (with the
65/// quote chars). Strip the outer quotes if present; otherwise pass the raw
66/// value through unchanged. The write-time pass validates the result against
67/// the enum's `CHOICES` and panics on miss, so a malformed literal (e.g.
68/// `default = "1L"`) still surfaces as a clear runtime error at cdylib load.
69pub(crate) fn extract_match_arg_default(raw: &str) -> String {
70    raw.strip_prefix('"')
71        .and_then(|s| s.strip_suffix('"'))
72        .unwrap_or(raw)
73        .to_string()
74}
75
76/// Derive a safe Rust ident from a write-time placeholder string.
77///
78/// Strips surrounding underscores and turns every `.` into `_`, so a placeholder
79/// like `.__MX_MATCH_ARG_CHOICES_foo_bar__` becomes an ident suffix that quotes
80/// cleanly in emitted code.
81pub(crate) fn placeholder_ident_suffix(placeholder: &str) -> String {
82    placeholder.trim_matches('_').replace('.', "_")
83}
84
85/// Emit the `MX_MATCH_ARG_CHOICES` static + its linkme registration.
86///
87/// Factored so lib.rs (standalone fns) and miniextendr_impl.rs (impl methods)
88/// can't drift apart — both previously open-coded the same quote! block.
89///
90/// `preferred_default` is the unquoted form of the user's `default = "..."`
91/// (e.g. `"zstd"`). Pass `""` when the user supplied no default — the write
92/// pass then keeps the natural enum order.
93pub(crate) fn choices_entry_tokens(
94    cfg_attrs: &[syn::Attribute],
95    entry_ident: &syn::Ident,
96    placeholder: &str,
97    choices_ty: &syn::Type,
98    preferred_default: &str,
99) -> proc_macro2::TokenStream {
100    quote::quote! {
101        #(#cfg_attrs)*
102        #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_MATCH_ARG_CHOICES), linkme(crate = ::miniextendr_api::linkme))]
103        #[allow(non_upper_case_globals)]
104        #[allow(non_snake_case)]
105        static #entry_ident: ::miniextendr_api::registry::MatchArgChoicesEntry =
106            ::miniextendr_api::registry::MatchArgChoicesEntry {
107                placeholder: #placeholder,
108                choices_str: || {
109                    <#choices_ty as ::miniextendr_api::match_arg::MatchArg>::CHOICES
110                        .iter()
111                        .map(|c| format!(
112                            "\"{}\"",
113                            ::miniextendr_api::match_arg::escape_r_string(c)
114                        ))
115                        .collect::<Vec<_>>()
116                        .join(", ")
117                },
118                preferred_default: #preferred_default,
119            };
120    }
121}
122
123/// Emit the `MX_MATCH_ARG_PARAM_DOCS` static + its linkme registration.
124pub(crate) fn param_doc_entry_tokens(
125    cfg_attrs: &[syn::Attribute],
126    entry_ident: &syn::Ident,
127    placeholder: &str,
128    several_ok: bool,
129    choices_ty: &syn::Type,
130) -> proc_macro2::TokenStream {
131    quote::quote! {
132        #(#cfg_attrs)*
133        #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_MATCH_ARG_PARAM_DOCS), linkme(crate = ::miniextendr_api::linkme))]
134        #[allow(non_upper_case_globals)]
135        #[allow(non_snake_case)]
136        static #entry_ident: ::miniextendr_api::registry::MatchArgParamDocEntry =
137            ::miniextendr_api::registry::MatchArgParamDocEntry {
138                placeholder: #placeholder,
139                several_ok: #several_ok,
140                choices_str: || {
141                    <#choices_ty as ::miniextendr_api::match_arg::MatchArg>::CHOICES
142                        .iter()
143                        .map(|c| format!("\"{}\"", c))
144                        .collect::<Vec<_>>()
145                        .join(", ")
146                },
147            };
148    }
149}