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