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}