Skip to main content

miniextendr_api/
match_arg.rs

1//! `match.arg`-style enum conversion for R string arguments.
2//!
3//! This module provides the [`MatchArg`] trait for converting between Rust
4//! fieldless enums and R character strings with `match.arg` semantics
5//! (exact match or unique partial matching).
6//!
7//! # Usage
8//!
9//! ```ignore
10//! use miniextendr_api::MatchArg;
11//!
12//! #[derive(Copy, Clone, MatchArg)]
13//! #[match_arg(rename_all = "snake_case")]
14//! enum Mode {
15//!     Fast,
16//!     Safe,
17//!     Debug,
18//! }
19//!
20//! #[miniextendr]
21//! fn run(#[miniextendr(match_arg)] mode: Mode) -> String {
22//!     format!("{mode:?}")
23//! }
24//! ```
25//!
26//! The generated R wrapper uses `base::match.arg()` for validation before
27//! the main `.Call()`, giving users familiar R error messages and partial
28//! matching.
29//!
30//! An `Option<T>` parameter (`#[miniextendr(match_arg)] mode: Option<Mode>`)
31//! is the optional form: the R formal defaults to `NULL`, `NULL` converts to
32//! `None`, and any other value is matched as usual. The generated C wrapper
33//! decodes it with [`match_arg_option_from_sexp`] (there is no
34//! `TryFromSexp for Option<T>` blanket: that slot belongs to the newtype
35//! forwarding impls in [`crate::newtype`], and a downstream crate cannot add
36//! one for its own enum under the orphan rule), so a hand-written `MatchArg`
37//! impl needs nothing extra.
38//!
39//! A `Missing<..>` around the parameter type (`Missing<T>`,
40//! `Missing<Option<T>>`, `Missing<Vec<T>>` for `several_ok`) keeps the choice
41//! vector as the R formal and reports an omitted argument as
42//! `Missing::Absent` (#1551), as it does that formal passed whole for a
43//! single choice (#2152); the C wrapper decodes it with
44//! [`match_arg_missing_or`] around the decoder of the inner type.
45//!
46//! With the `either` feature, `Either<T, R>` takes a choice or a value of
47//! another kind: character or factor input is matched and becomes `Left(T)`,
48//! anything else converts to `R` (`match_arg_either_or`). A `several_ok` list
49//! takes the same split as `Either<Vec<T>, R>` or `Either<Box<[T]>, R>`
50//! (#1612): character or factor input is matched element by element and
51//! becomes `Left(Vec<T>)`, anything else converts to `R`.
52//!
53//! [`MatchArg::ALIASES`] (`#[match_arg(alias = "grey")]` on a variant) adds
54//! other spellings of a choice, matched only when typed in full, by every
55//! matcher on both sides. An error that lists the choices names them after
56//! the choices (`(or "grey" for "gray")`), unless an alias opts out with
57//! `listed = false`; the formal, the usage and the `@param` line list the
58//! choices only.
59//!
60//! `several_ok` parameters are validated strictly on the R side: every element
61//! has to match a choice, and `NULL` selects every choice (the same fallback
62//! [`match_arg_vec_from_sexp`] applies). Under `Either`, `NULL` is not a
63//! choice: it goes to `R` (or is `None` under `Option<Either<..>>`).
64
65use crate::condition::ArgError;
66use crate::from_r::{SexpError, TryFromSexp, charsxp_to_str};
67use crate::gc_protect::ProtectScope;
68use crate::{R_xlen_t, SEXP, SEXPTYPE, SexpExt};
69
70/// Trait for enum types that support `match.arg`-style string conversion.
71///
72/// Implementors provide a fixed set of choice strings and bidirectional
73/// conversion between enum variants and their string representations.
74///
75/// Use `#[derive(MatchArg)]` to auto-generate this implementation.
76pub trait MatchArg: Sized + Copy + 'static {
77    /// The canonical choice strings, in variant declaration order.
78    ///
79    /// The first choice is the default when the R argument is `NULL`.
80    const CHOICES: &'static [&'static str];
81
82    /// Other spellings of a choice: [`MatchArgAlias::new("grey",
83    /// "gray")`](MatchArgAlias::new) lets a user write `"grey"` for the choice
84    /// `"gray"` (#1843).
85    ///
86    /// An alias selects its choice only when typed in full: a prefix matches
87    /// the choices alone, so `"gr"` stays the prefix of `"gray"` it was. Every
88    /// matcher reads it, the generated wrapper's check and the Rust side
89    /// alike ([`match_arg_from_sexp`], [`match_arg_param`],
90    /// [`match_arg_vec_from_sexp`] element by element, the `Either` arm).
91    ///
92    /// Every error that lists the choices names the listed aliases after
93    /// them, `'color' should be one of "red", "gray", "blue" (or "grey" for
94    /// "gray")`, on both sides (#2148). An alias built with
95    /// [`MatchArgAlias::unlisted`] is matched the same way and named by no
96    /// error. The R formal, the usage and the `@param` line list `CHOICES`
97    /// only.
98    ///
99    /// `#[derive(MatchArg)]` fills it from `#[match_arg(alias = "...")]` on a
100    /// variant (`listed = false` for an unlisted one). A hand-written impl
101    /// lists its aliases here, not in [`from_choice`](Self::from_choice),
102    /// which the R-side check never reaches. The wrappers writer refuses, for
103    /// a type used as a `match_arg` parameter, an alias that is empty, `"NA"`,
104    /// a choice, repeated, or a prefix of a choice other than its own, and a
105    /// target that is not a choice.
106    const ALIASES: &'static [MatchArgAlias] = &[];
107
108    /// Convert a choice string to the corresponding enum variant.
109    ///
110    /// Returns `None` if the string doesn't match any choice exactly. An
111    /// alias is not a choice: [`ALIASES`](Self::ALIASES) maps it.
112    fn from_choice(choice: &str) -> Option<Self>;
113
114    /// Convert the enum variant to its canonical choice string.
115    fn to_choice(self) -> &'static str;
116}
117
118/// One entry of [`MatchArg::ALIASES`]: `alias`, another spelling of the
119/// choice `choice`, and whether the errors that list the choices name it.
120///
121/// ```ignore
122/// const ALIASES: &'static [MatchArgAlias] = &[
123///     MatchArgAlias::new("oral", "extravascular"),
124///     MatchArgAlias::unlisted("ev", "extravascular"),
125/// ];
126/// ```
127#[derive(#[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for MatchArgAlias { }
#[automatically_derived]
impl ::core::clone::Clone for MatchArgAlias {
    #[inline]
    fn clone(&self) -> Self {
        let _: ::core::clone::AssertParamIsClone<&'static str>;
        let _: ::core::clone::AssertParamIsClone<&'static str>;
        let _: ::core::clone::AssertParamIsClone<bool>;
        *self
    }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for MatchArgAlias { }Copy, #[automatically_derived]
impl ::core::fmt::Debug for MatchArgAlias {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field3_finish(f, "MatchArgAlias",
            "alias", &self.alias, "choice", &self.choice, "listed",
            &&self.listed)
    }
}Debug, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for MatchArgAlias { }
#[automatically_derived]
impl ::core::cmp::PartialEq for MatchArgAlias {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        self.listed == other.listed && self.alias == other.alias &&
            self.choice == other.choice
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for MatchArgAlias {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<&'static str>;
        let _: ::core::cmp::AssertParamIsEq<&'static str>;
        let _: ::core::cmp::AssertParamIsEq<bool>;
    }
}Eq)]
128pub struct MatchArgAlias {
129    /// The other spelling, matched only when typed in full.
130    pub alias: &'static str,
131    /// The choice it selects, one of [`MatchArg::CHOICES`].
132    pub choice: &'static str,
133    /// Whether an error that lists the choices names this alias after them
134    /// (`(or "oral" for "extravascular")`, #2148).
135    pub listed: bool,
136}
137
138impl MatchArgAlias {
139    /// A listed alias, what `#[match_arg(alias = "...")]` makes: the errors
140    /// that list the choices name it.
141    pub const fn new(alias: &'static str, choice: &'static str) -> Self {
142        Self {
143            alias,
144            choice,
145            listed: true,
146        }
147    }
148
149    /// An unlisted alias, what `#[match_arg(alias = "...", listed = false)]`
150    /// makes: matched like any alias, named by no error.
151    pub const fn unlisted(alias: &'static str, choice: &'static str) -> Self {
152        Self {
153            alias,
154            choice,
155            listed: false,
156        }
157    }
158}
159
160/// The words of a `match_arg` argument error, shared by the wrappers
161/// preamble's `.miniextendr_match_arg` / `.miniextendr_match_arg_several`
162/// ([`crate::registry::ARG_CHECK_HELPERS`], built with `concat!`) and
163/// [`MatchArgError::arg_error`], so R and Rust word these errors from one
164/// source (#1741). The generated conversion of a `match_arg` type words its
165/// error with `arg_error` too (#1767). `or` and `alias_for` are the words of
166/// the clause naming the listed aliases ([`listed_aliases_clause`], #2148),
167/// which the wrappers writer renders in Rust and passes to the R helpers as
168/// their `listed` argument. It expands to string literals.
169macro_rules! match_arg_wording {
170    (not_character) => {
171        "must be NULL or a character vector"
172    };
173    (not_scalar) => {
174        "must be of length 1"
175    };
176    (not_a_choice) => {
177        "should be one of"
178    };
179    (or) => {
180        "or"
181    };
182    (alias_for) => {
183        "for"
184    };
185}
186// The R helpers (host-only) read it from here; this module uses it in place.
187#[cfg(not(target_arch = "wasm32"))]
188pub(crate) use match_arg_wording;
189
190/// Error type for `MatchArg` conversion failures.
191///
192/// Every variant carries the type's choices and aliases, so an error can say
193/// what the value must be (`'mode' should be one of "fast", "slow" (or
194/// "quick" for "fast")`) whichever way it was wrong.
195#[derive(#[automatically_derived]
impl ::core::fmt::Debug for MatchArgError {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match self {
            Self::InvalidType {
                actual: __self_0, choices: __self_1, aliases: __self_2 } =>
                ::core::fmt::Formatter::debug_struct_field3_finish(f,
                    "InvalidType", "actual", __self_0, "choices", __self_1,
                    "aliases", &__self_2),
            Self::InvalidLength {
                actual: __self_0, choices: __self_1, aliases: __self_2 } =>
                ::core::fmt::Formatter::debug_struct_field3_finish(f,
                    "InvalidLength", "actual", __self_0, "choices", __self_1,
                    "aliases", &__self_2),
            Self::IsNa { choices: __self_0, aliases: __self_1 } =>
                ::core::fmt::Formatter::debug_struct_field2_finish(f, "IsNa",
                    "choices", __self_0, "aliases", &__self_1),
            Self::NoMatch {
                input: __self_0, choices: __self_1, aliases: __self_2 } =>
                ::core::fmt::Formatter::debug_struct_field3_finish(f,
                    "NoMatch", "input", __self_0, "choices", __self_1,
                    "aliases", &__self_2),
        }
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for MatchArgError {
    #[inline]
    fn clone(&self) -> Self {
        match self {
            Self::InvalidType {
                actual: __self_0, choices: __self_1, aliases: __self_2 } =>
                MatchArgError::InvalidType {
                    actual: ::core::clone::Clone::clone(__self_0),
                    choices: ::core::clone::Clone::clone(__self_1),
                    aliases: ::core::clone::Clone::clone(__self_2),
                },
            Self::InvalidLength {
                actual: __self_0, choices: __self_1, aliases: __self_2 } =>
                MatchArgError::InvalidLength {
                    actual: ::core::clone::Clone::clone(__self_0),
                    choices: ::core::clone::Clone::clone(__self_1),
                    aliases: ::core::clone::Clone::clone(__self_2),
                },
            Self::IsNa { choices: __self_0, aliases: __self_1 } =>
                MatchArgError::IsNa {
                    choices: ::core::clone::Clone::clone(__self_0),
                    aliases: ::core::clone::Clone::clone(__self_1),
                },
            Self::NoMatch {
                input: __self_0, choices: __self_1, aliases: __self_2 } =>
                MatchArgError::NoMatch {
                    input: ::core::clone::Clone::clone(__self_0),
                    choices: ::core::clone::Clone::clone(__self_1),
                    aliases: ::core::clone::Clone::clone(__self_2),
                },
        }
    }
}Clone)]
196pub enum MatchArgError {
197    /// The SEXP was not a character or factor type.
198    InvalidType {
199        /// The SEXPTYPE that was given.
200        actual: SEXPTYPE,
201        /// The valid choices.
202        choices: &'static [&'static str],
203        /// The type's aliases ([`MatchArg::ALIASES`]).
204        aliases: &'static [MatchArgAlias],
205    },
206    /// The input had length != 1.
207    InvalidLength {
208        /// The length that was given.
209        actual: usize,
210        /// The valid choices.
211        choices: &'static [&'static str],
212        /// The type's aliases ([`MatchArg::ALIASES`]).
213        aliases: &'static [MatchArgAlias],
214    },
215    /// The input was NA.
216    IsNa {
217        /// The valid choices.
218        choices: &'static [&'static str],
219        /// The type's aliases ([`MatchArg::ALIASES`]).
220        aliases: &'static [MatchArgAlias],
221    },
222    /// No choice matched the input.
223    NoMatch {
224        /// The input string that didn't match.
225        input: String,
226        /// The valid choices.
227        choices: &'static [&'static str],
228        /// The type's aliases ([`MatchArg::ALIASES`]).
229        aliases: &'static [MatchArgAlias],
230    },
231}
232
233impl MatchArgError {
234    /// [`InvalidType`](Self::InvalidType) for the choices of `T`.
235    fn invalid_type<T: MatchArg>(actual: SEXPTYPE) -> Self {
236        MatchArgError::InvalidType {
237            actual,
238            choices: T::CHOICES,
239            aliases: T::ALIASES,
240        }
241    }
242
243    /// [`InvalidLength`](Self::InvalidLength) for the choices of `T`.
244    fn invalid_length<T: MatchArg>(actual: usize) -> Self {
245        MatchArgError::InvalidLength {
246            actual,
247            choices: T::CHOICES,
248            aliases: T::ALIASES,
249        }
250    }
251
252    /// [`IsNa`](Self::IsNa) for the choices of `T`.
253    fn is_na<T: MatchArg>() -> Self {
254        MatchArgError::IsNa {
255            choices: T::CHOICES,
256            aliases: T::ALIASES,
257        }
258    }
259
260    /// [`NoMatch`](Self::NoMatch) of `input` for the choices of `T`.
261    fn no_match<T: MatchArg>(input: &str) -> Self {
262        MatchArgError::NoMatch {
263            input: input.to_string(),
264            choices: T::CHOICES,
265            aliases: T::ALIASES,
266        }
267    }
268
269    /// The choices the value should have been one of.
270    pub fn choices(&self) -> &'static [&'static str] {
271        match self {
272            MatchArgError::InvalidType { choices, .. }
273            | MatchArgError::InvalidLength { choices, .. }
274            | MatchArgError::IsNa { choices, .. }
275            | MatchArgError::NoMatch { choices, .. } => choices,
276        }
277    }
278
279    /// The aliases of the choices, listed and unlisted.
280    pub fn aliases(&self) -> &'static [MatchArgAlias] {
281        match self {
282            MatchArgError::InvalidType { aliases, .. }
283            | MatchArgError::InvalidLength { aliases, .. }
284            | MatchArgError::IsNa { aliases, .. }
285            | MatchArgError::NoMatch { aliases, .. } => aliases,
286        }
287    }
288
289    /// What the value must be, in R terms: `one of "fast", "slow"`, with the
290    /// listed aliases after the choices (`one of "fast", "slow" (or "quick"
291    /// for "fast")`). An `Either` whose arms both refused a value names a
292    /// `match_arg` arm with it (`'x' must be one of "oral", "bolus", or a
293    /// single double: got logical`, #1594).
294    pub fn expectation(&self) -> String {
295        one_of(self.choices(), self.aliases())
296    }
297
298    /// The argument error the generated wrapper's `match_arg` check
299    /// (`.miniextendr_match_arg`) raises for this value of parameter `param`
300    /// (its R name), word for word (#1741):
301    ///
302    /// - [`InvalidType`](Self::InvalidType): `'mode' must be NULL or a character vector`
303    /// - [`InvalidLength`](Self::InvalidLength): `'mode' must be of length 1`
304    /// - [`IsNa`](Self::IsNa), [`NoMatch`](Self::NoMatch): `'mode' should be
305    ///   one of "fast", "safe"`, then the listed aliases (`'mode' should be
306    ///   one of "fast", "safe" (or "quick" for "fast")`, #2148)
307    ///
308    /// The choices and the aliases are quoted as R's `dQuote(choices,
309    /// FALSE)` quotes them, without escapes. [`match_arg_param`] returns it;
310    /// [`ArgError::raise`] raises it as that condition. The generated conversion of a `match_arg`
311    /// type, with or without `#[miniextendr(match_arg)]`, raises its message
312    /// too (#1767).
313    pub fn arg_error(&self, param: &str) -> ArgError {
314        ArgError::new(param, self.arg_message(param, self.choices()))
315    }
316
317    /// The message of [`arg_error`](Self::arg_error), with the choices listed
318    /// in the order of `formal`, the choices of the parameter's R formal:
319    /// `T::CHOICES`, or those of a `default = "..."` parameter, which lists
320    /// its default first ([`match_arg_param_with_default`]). The listed
321    /// aliases follow in the order of `T::CHOICES` whatever the formal, as
322    /// the wrappers writer renders them for the R check.
323    pub(crate) fn arg_message(&self, param: &str, formal: &[&str]) -> String {
324        let what = match self {
325            MatchArgError::InvalidType { .. } => "must be NULL or a character vector"match_arg_wording!(not_character).to_string(),
326            MatchArgError::InvalidLength { .. } => "must be of length 1"match_arg_wording!(not_scalar).to_string(),
327            MatchArgError::IsNa { .. } | MatchArgError::NoMatch { .. } => {
328                let quoted: Vec<String> = formal.iter().map(|c| r_quote(c)).collect();
329                ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} {1}{2}", "should be one of",
                quoted.join(", "),
                listed_aliases_clause(self.choices(), self.aliases(),
                    r_quote)))
    })format!(
330                    "{} {}{}",
331                    match_arg_wording!(not_a_choice),
332                    quoted.join(", "),
333                    listed_aliases_clause(self.choices(), self.aliases(), r_quote)
334                )
335            }
336        };
337        ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("\'{0}\' {1}", param, what))
    })format!("'{param}' {what}")
338    }
339
340    /// The reason of this error in R terms, after `<expected>: ` when an
341    /// `Either` names this arm in an argument error (#1591): `got "zzz"`,
342    /// `got numeric`, `got length 2`, `NA is not allowed`. With
343    /// `expected_known = false` (nothing before the reason names the choices)
344    /// it is the `Display` text, which says what was expected.
345    pub(crate) fn r_reason(&self, expected_known: bool) -> String {
346        if !expected_known {
347            return self.to_string();
348        }
349        match self {
350            MatchArgError::InvalidType { actual, .. } => {
351                ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("got {0}",
                crate::typed_list::sexptype_name(*actual)))
    })format!("got {}", crate::typed_list::sexptype_name(*actual))
352            }
353            MatchArgError::InvalidLength { actual, .. } => ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("got length {0}", actual))
    })format!("got length {actual}"),
354            MatchArgError::IsNa { .. } => "NA is not allowed".to_string(),
355            MatchArgError::NoMatch { input, .. } => ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("got {0:?}", input))
    })format!("got {input:?}"),
356        }
357    }
358}
359
360/// `one of "fast", "slow"`: what a `match_arg` / `choices` value must be, in
361/// the words of a conversion's argument error that names it beside other
362/// accepted values (`'x' must be one of "oral", "bolus", or a single double:
363/// got logical`, #1594). The listed `aliases` follow the choices (`one of
364/// "fast", "slow" (or "quick" for "fast")`).
365pub fn one_of(choices: &[&str], aliases: &[MatchArgAlias]) -> String {
366    choice_expectation(choices, aliases, false, "")
367}
368
369/// `one of "fast", "slow"<suffix>`, or `one or more of "fast", "slow"<suffix>`
370/// for a `several_ok` parameter (`several`), quoted as [`one_of`] quotes, the
371/// listed `aliases` between the choices and the suffix (`one of "fast",
372/// "slow" (or "quick" for "fast"), or a number`).
373/// The generated argument error of a `match_arg` / `choices` parameter on
374/// `Either<T, R>` passes the other accepted values as `suffix`
375/// (`, or a data frame`), in the words of its `@param` line; an `Either`
376/// whose arms both refused a value joins them with it too.
377#[doc(hidden)]
378pub fn choice_expectation(
379    choices: &[&str],
380    aliases: &[MatchArgAlias],
381    several: bool,
382    suffix: &str,
383) -> String {
384    let quoted: Vec<String> = choices.iter().map(|c| ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0:?}", c))
    })format!("{c:?}")).collect();
385    let lead = if several { "one or more of" } else { "one of" };
386    let listed = listed_aliases_clause(choices, aliases, |s| ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0:?}", s))
    })format!("{s:?}"));
387    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{1} {0}{2}{3}", quoted.join(", "),
                lead, listed, suffix))
    })format!("{lead} {}{listed}{suffix}", quoted.join(", "))
388}
389
390/// `"x"`: a string quoted as R's `dQuote(x, FALSE)` quotes it, without
391/// escapes, as the `match_arg` check's message quotes the choices.
392pub(crate) fn r_quote(s: &str) -> String {
393    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("\"{0}\"", s))
    })format!("\"{s}\"")
394}
395
396/// The text that names the listed aliases after the choices of a message: a
397/// space, then `(or "oral" for "extravascular", "grey" for "gray")`; `""`
398/// when no alias is listed (#2148). The aliases follow the order of their
399/// choices in `choices` (`MatchArg::CHOICES`, the order of the variants),
400/// then their order in `aliases`. `quote` quotes each alias and choice, as
401/// the message quotes the choices before it.
402///
403/// Every error that lists the choices appends it: [`MatchArgError`]'s
404/// messages, [`choice_expectation`], and the wrappers preamble's checks, to
405/// which the wrappers writer passes it as their `listed` argument
406/// ([`crate::registry::match_arg_aliases_arg`]), so R and Rust word it alike.
407pub(crate) fn listed_aliases_clause(
408    choices: &[&str],
409    aliases: &[MatchArgAlias],
410    quote: impl Fn(&str) -> String,
411) -> String {
412    let mut listed: Vec<&MatchArgAlias> = aliases.iter().filter(|a| a.listed).collect();
413    if listed.is_empty() {
414        return String::new();
415    }
416    // Stable: aliases of one choice keep their order. A target that is no
417    // choice (a hand-written impl the writer refuses) goes last.
418    listed.sort_by_key(|a| {
419        choices
420            .iter()
421            .position(|choice| *choice == a.choice)
422            .unwrap_or(usize::MAX)
423    });
424    let pairs: Vec<String> = listed
425        .iter()
426        .map(|a| {
427            ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} {1} {2}", quote(a.alias),
                "for", quote(a.choice)))
    })format!(
428                "{} {} {}",
429                quote(a.alias),
430                match_arg_wording!(alias_for),
431                quote(a.choice)
432            )
433        })
434        .collect();
435    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!(" ({0} {1})", "or",
                pairs.join(", ")))
    })format!(" ({} {})", match_arg_wording!(or), pairs.join(", "))
436}
437
438/// The text is complete on its own, for a conversion outside an argument:
439/// `expected one of "fast", "slow", got "zzz"`, `expected a string or factor,
440/// got numeric`.
441impl std::fmt::Display for MatchArgError {
442    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
443        let expected = self.expectation();
444        match self {
445            MatchArgError::InvalidType { actual, .. } => f.write_fmt(format_args!("expected {1} (a string or factor), got {0}",
        crate::typed_list::sexptype_name(*actual), expected))write!(
446                f,
447                "expected {expected} (a string or factor), got {}",
448                crate::typed_list::sexptype_name(*actual)
449            ),
450            MatchArgError::InvalidLength { actual, .. } => {
451                f.write_fmt(format_args!("expected {0}, got length {1}", expected, actual))write!(f, "expected {expected}, got length {actual}")
452            }
453            MatchArgError::IsNa { .. } => f.write_fmt(format_args!("expected {0}, got NA", expected))write!(f, "expected {expected}, got NA"),
454            MatchArgError::NoMatch { input, .. } => {
455                f.write_fmt(format_args!("expected {0}, got {1:?}", expected, input))write!(f, "expected {expected}, got {input:?}")
456            }
457        }
458    }
459}
460
461impl std::error::Error for MatchArgError {}
462
463/// Kept whole, so a `TryFromSexp` whose error is `SexpError` (the one
464/// `#[derive(MatchArg)]` emits) still reports the choices.
465impl From<MatchArgError> for crate::from_r::SexpError {
466    fn from(e: MatchArgError) -> Self {
467        crate::from_r::SexpError::MatchArg(e)
468    }
469}
470
471/// Escape a Rust `&str` for embedding inside an R double-quoted string literal.
472///
473/// Handles `\`, `"`, newline, carriage return, and tab — the characters R
474/// recognises as escape sequences inside `"..."`. Used when formatting
475/// `MatchArg::CHOICES` into the default of a generated R wrapper formal, so
476/// that a choice like `say "hi"` or `c:\path` cannot produce syntactically
477/// invalid R code.
478pub fn escape_r_string(s: &str) -> String {
479    let mut out = String::with_capacity(s.len());
480    for ch in s.chars() {
481        match ch {
482            '\\' => out.push_str(r"\\"),
483            '"' => out.push_str(r#"\""#),
484            '\n' => out.push_str(r"\n"),
485            '\r' => out.push_str(r"\r"),
486            '\t' => out.push_str(r"\t"),
487            c => out.push(c),
488        }
489    }
490    out
491}
492
493/// What is wrong with the aliases of a type whose choices are `choices`, one
494/// sentence per bad alias, in order (empty when they are fine): an alias that
495/// is empty or `"NA"`, a choice, given twice, or a prefix of a choice other
496/// than its own (typing it would select something else than without the
497/// alias), and a target that is not a choice. `#[derive(MatchArg)]` refuses
498/// the same at compile time, with the same words; the wrappers writer checks
499/// a hand-written impl with this ([`crate::registry::match_arg_aliases_arg`]).
500#[cfg(not(target_arch = "wasm32"))]
501pub(crate) fn alias_problems(choices: &[&str], aliases: &[MatchArgAlias]) -> Vec<String> {
502    let mut problems = Vec::new();
503    let mut seen = std::collections::HashSet::new();
504    for &MatchArgAlias {
505        alias,
506        choice: target,
507        ..
508    } in aliases
509    {
510        if alias.is_empty() {
511            problems
512                .push("an alias can't be empty: the empty string matches no choice".to_string());
513        } else if alias == "NA" {
514            problems.push("alias \"NA\" would read as a missing value".to_string());
515        } else if choices.contains(&alias) {
516            problems.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("alias {0:?} is already a choice",
                alias))
    })format!("alias {alias:?} is already a choice"));
517        } else if !seen.insert(alias) {
518            problems.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("alias {0:?} is given more than once",
                alias))
    })format!("alias {alias:?} is given more than once"));
519        } else if !choices.contains(&target) {
520            problems.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("alias {0:?} names {1:?}, which is not a choice",
                alias, target))
    })format!(
521                "alias {alias:?} names {target:?}, which is not a choice"
522            ));
523        } else if let Some(other) = choices
524            .iter()
525            .find(|choice| **choice != target && choice.starts_with(alias))
526        {
527            problems.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("alias {0:?} for {1:?} is a prefix of the choice {2:?}, so it would change what typing {0:?} selects",
                alias, target, other))
    })format!(
528                "alias {alias:?} for {target:?} is a prefix of the choice {other:?}, \
529                 so it would change what typing {alias:?} selects"
530            ));
531        }
532    }
533    problems
534}
535
536/// Build an R character vector (STRSXP) from the choices of a `MatchArg` type.
537///
538/// This is called by generated choices-helper C wrappers to provide the
539/// choice list to `base::match.arg()` in the R wrapper.
540pub fn choices_sexp<T: MatchArg>() -> SEXP {
541    let choices = <T as MatchArg>::CHOICES;
542    unsafe {
543        let scope = ProtectScope::new();
544        // Preserve the R_BlankString short-circuit: skips hash-lookup on the
545        // interned empty CHARSXP for any empty choice strings.
546        let vec = scope.alloc_strsxp(choices.len()).into_raw();
547        for (i, s) in choices.iter().enumerate() {
548            let charsxp = if s.is_empty() {
549                SEXP::blank_string()
550            } else {
551                SEXP::charsxp(s)
552            };
553            vec.set_string_elt(i as R_xlen_t, charsxp);
554        }
555        vec
556    }
557}
558
559/// Map a `SexpError` from a string `TryFromSexp` conversion into a `MatchArgError`.
560///
561/// Only `Type` and `Length` are produced by the `&str`/`Option<&str>`/
562/// `Vec<Option<&str>>` conversions we delegate to — other variants are
563/// unreachable in this context.
564fn sexp_err_to_match_arg_err<T: MatchArg>(e: SexpError) -> MatchArgError {
565    match e {
566        SexpError::Type(t) => MatchArgError::invalid_type::<T>(t.actual),
567        SexpError::Length(l) => MatchArgError::invalid_length::<T>(l.actual),
568        other => {
    ::core::panicking::panic_fmt(format_args!("internal error: entered unreachable code: {0}",
            format_args!("unexpected SexpError from string conversion: {0}",
                other)));
}unreachable!("unexpected SexpError from string conversion: {other}"),
569    }
570}
571
572/// The choices of a `match_arg` parameter's R formal: `choices` with the
573/// default moved to the front and the others after it in their order
574/// (`"Safe"` in `"Fast", "Safe", "Debug"` gives `"Safe", "Fast", "Debug"`),
575/// or `None` when `is_default` holds for no choice.
576///
577/// `.miniextendr_match_arg()` takes the first element of the formal for
578/// `NULL` and for the formal itself, so this order is what makes a
579/// `default = "..."` the default, and the order its message lists. The
580/// wrappers writer builds such a formal with it
581/// ([`crate::registry::match_arg_formal`]) and
582/// [`match_arg_param_with_default`] matches against it, so the two cannot
583/// disagree (#1767).
584pub(crate) fn default_first<'a>(
585    choices: &[&'a str],
586    is_default: impl Fn(&str) -> bool,
587) -> Option<Vec<&'a str>> {
588    let at = choices.iter().position(|choice| is_default(choice))?;
589    let mut formal = Vec::with_capacity(choices.len());
590    formal.push(choices[at]);
591    formal.extend(
592        choices
593            .iter()
594            .enumerate()
595            .filter(|&(i, _)| i != at)
596            .map(|(_, choice)| *choice),
597    );
598    Some(formal)
599}
600
601/// The first choice of the formal: what `NULL` and the formal itself select.
602fn first_choice<T: MatchArg>(formal: &[&str]) -> Result<T, MatchArgError> {
603    formal
604        .first()
605        .and_then(|choice| T::from_choice(choice))
606        .ok_or_else(|| MatchArgError::no_match::<T>(""))
607}
608
609/// `NA` as a choice: `pmatch()`, and so the wrapper's check, matches it as
610/// the string `"NA"`, so a choice that is `"NA"` or the only one starting
611/// with it is selected. Anything else is [`MatchArgError::IsNa`]. No alias
612/// maps it: the wrapper looks aliases up with `match()`, which never matches
613/// `NA` to `"NA"`.
614fn match_na<T: MatchArg>() -> Result<T, MatchArgError> {
615    match_string::<T>("NA", &[]).map_err(|_| MatchArgError::is_na::<T>())
616}
617
618/// Whether the `len` strings `label(i)` (`None` for `NA`) are `formal`, in
619/// order: the formal default of a choice parameter, which an omitted argument
620/// evaluates to.
621fn is_formal<'a>(formal: &[&str], len: usize, label: impl Fn(isize) -> Option<&'a str>) -> bool {
622    len == formal.len()
623        && formal
624            .iter()
625            .zip(0..)
626            .all(|(choice, i)| label(i) == Some(*choice))
627}
628
629/// The label of element `i` of a factor (`None` for a missing code or one
630/// outside the levels).
631fn factor_label(codes: SEXP, levels: SEXP, i: isize) -> Option<&'static str> {
632    let code = codes.integer_elt(i);
633    // R factor codes are 1-based; NA_integer_ is i32::MIN.
634    let level = isize::try_from(code).ok()?.checked_sub(1)?;
635    if level < 0 || level >= isize::try_from(levels.len()).ok()? {
636        return None;
637    }
638    let charsxp = levels.string_elt(level);
639    if charsxp.is_na_string() {
640        return None;
641    }
642    // UTF-8 locale asserted at package init — charsxp_to_str is safe.
643    Some(unsafe { charsxp_to_str(charsxp) })
644}
645
646/// A factor as a choice: read as its labels, like `as.character()`.
647fn factor_to_choice<T: MatchArg>(sexp: SEXP, formal: &[&str]) -> Result<T, MatchArgError> {
648    let levels = sexp.get_levels();
649    let len = sexp.len();
650    if is_formal(formal, len, |i| factor_label(sexp, levels, i)) {
651        return first_choice::<T>(formal);
652    }
653    if len != 1 {
654        return Err(MatchArgError::invalid_length::<T>(len));
655    }
656    match factor_label(sexp, levels, 0) {
657        Some(label) => match_choice::<T>(label),
658        None => match_na::<T>(),
659    }
660}
661
662/// Match one choice argument with the semantics of the generated wrapper's
663/// `match_arg` check (`.miniextendr_match_arg`), which are those of
664/// `base::match.arg(arg, choices)`:
665///
666/// - `NULL` is the first choice;
667/// - a factor is read as its labels;
668/// - any other value that is not a character vector is
669///   [`MatchArgError::InvalidType`];
670/// - the full choices vector, `T::CHOICES` in order and without attributes
671///   (what an omitted argument evaluates to when the choices are its formal
672///   default), is the first choice;
673/// - otherwise the value must have length 1 ([`MatchArgError::InvalidLength`])
674///   and match a choice exactly, an alias exactly ([`MatchArg::ALIASES`]),
675///   or a choice as a unique prefix ([`MatchArgError::NoMatch`]). The empty
676///   string matches nothing, and `NA` is matched as the string `"NA"`, as
677///   `pmatch()` does ([`MatchArgError::IsNa`] when that matches nothing).
678///
679/// Used by the generated `TryFromSexp for T` implementation, and by the
680/// generated C wrapper of a `#[miniextendr(match_arg)]` parameter, which only
681/// sees the single choice the R wrapper already matched. A body matching a raw
682/// `SEXP` argument wants [`match_arg_param`], which words its errors as the
683/// wrapper does.
684pub fn match_arg_from_sexp<T: MatchArg>(sexp: SEXP) -> Result<T, MatchArgError> {
685    match_formal::<T>(sexp, <T as MatchArg>::CHOICES)
686}
687
688/// [`match_arg_from_sexp`] for a parameter whose R formal is `formal`, a
689/// permutation of `T::CHOICES`: its first element is what `NULL` and the
690/// formal itself select. Matching a string does not depend on the order.
691fn match_formal<T: MatchArg>(sexp: SEXP, formal: &[&str]) -> Result<T, MatchArgError> {
692    let sexptype = sexp.type_of();
693    if sexptype == SEXPTYPE::NILSXP {
694        return first_choice::<T>(formal);
695    }
696    if sexp.is_factor() {
697        return factor_to_choice::<T>(sexp, formal);
698    }
699    if sexptype != SEXPTYPE::STRSXP {
700        return Err(MatchArgError::invalid_type::<T>(sexptype));
701    }
702    let len = sexp.len();
703    // `identical(arg, choices)` compares attributes too: a named vector of
704    // the choices is not the formal default.
705    let label = |i: isize| {
706        let charsxp = sexp.string_elt(i);
707        // UTF-8 locale asserted at package init — charsxp_to_str is safe.
708        (!charsxp.is_na_string()).then(|| unsafe { charsxp_to_str(charsxp) })
709    };
710    if is_formal(formal, len, label) && !sexp.has_attributes() {
711        return first_choice::<T>(formal);
712    }
713    if len != 1 {
714        return Err(MatchArgError::invalid_length::<T>(len));
715    }
716    match label(0) {
717        Some(input) => match_choice::<T>(input),
718        None => match_na::<T>(),
719    }
720}
721
722/// Match a choice argument a body received as a raw `SEXP` exactly as the
723/// generated wrapper matches a `#[miniextendr(match_arg)]` parameter, and
724/// with the same error.
725///
726/// The result is [`match_arg_from_sexp`]'s (`NULL` and the full choices
727/// vector select the first choice, a factor is read as its labels, one string
728/// matches a choice exactly, an alias exactly, or a choice as a unique
729/// prefix). The error is the argument error
730/// the wrapper raises for parameter `param` (its R name), word for word:
731///
732/// - `'mode' must be NULL or a character vector`
733/// - `'mode' must be of length 1`
734/// - `'mode' should be one of "fast", "safe"`
735///
736/// [`ArgError::raise`] raises it as that condition (`kind = "conversion"`,
737/// the crate's `conversion_error_class`, `e$param`; see
738/// [`crate::arg_error!`]):
739///
740/// ```ignore
741/// use miniextendr_api::{SEXP, match_arg_param, miniextendr};
742///
743/// #[miniextendr]
744/// pub fn run(data: SEXP, mode: SEXP) -> String {
745///     if is_data_frame(data) {
746///         // Checked on this path only.
747///         let mode: Mode = match_arg_param(mode, "mode").unwrap_or_else(|e| e.raise());
748///         return format!("{mode:?}");
749///     }
750///     String::new()
751/// }
752/// ```
753///
754/// The choices are `T::CHOICES` in declaration order, the formal of a
755/// `#[miniextendr(match_arg)]` parameter. A value forwarded from a parameter
756/// with `default = "..."` wants [`match_arg_param_with_default`].
757pub fn match_arg_param<T: MatchArg>(sexp: SEXP, param: &str) -> Result<T, ArgError> {
758    match_arg_from_sexp::<T>(sexp).map_err(|e| e.arg_error(param))
759}
760
761/// [`match_arg_param`] for a choice argument whose R formal lists `default`
762/// first: the formal of a `#[miniextendr(match_arg, default = "...")]`
763/// parameter, which moves its default to the front and keeps the other
764/// choices in their order (#1767).
765///
766/// The formal's order decides three things, and here they follow the formal
767/// rather than `T::CHOICES`: the choice `NULL` selects, the vector that counts
768/// as the formal itself (what an omitted argument evaluates to), and the
769/// order of the choices in the message. For
770/// `#[miniextendr(match_arg, default = "\"Safe\"")] mode: Mode` with
771/// `Mode::CHOICES` `"Fast", "Safe", "Debug"`, the formal is
772/// `c("Safe", "Fast", "Debug")`:
773///
774/// | Input | `match_arg_param` | `match_arg_param_with_default(.., Mode::Safe)` |
775/// |---|---|---|
776/// | `NULL` | `Fast` | `Safe` |
777/// | `c("Safe", "Fast", "Debug")` | `'mode' must be of length 1` | `Safe` |
778/// | `c("Fast", "Safe", "Debug")` | `Fast` | `'mode' must be of length 1` |
779/// | `"zzz"` | `'mode' should be one of "Fast", "Safe", "Debug"` | `'mode' should be one of "Safe", "Fast", "Debug"` |
780///
781/// Matching a string is the same either way. The wrappers writer orders the
782/// formal with the same code, so the two agree on every input.
783///
784/// `default` is a `T`, not its string or its position: it is always one of
785/// the choices, a misspelled default does not compile, and a renamed or
786/// reordered choice moves with it.
787///
788/// ```ignore
789/// #[miniextendr]
790/// pub fn run(mode: SEXP) -> String {
791///     let mode: Mode = match_arg_param_with_default(mode, "mode", Mode::Safe)
792///         .unwrap_or_else(|e| e.raise());
793///     format!("{mode:?}")
794/// }
795/// ```
796pub fn match_arg_param_with_default<T: MatchArg>(
797    sexp: SEXP,
798    param: &str,
799    default: T,
800) -> Result<T, ArgError> {
801    let default = default.to_choice();
802    let formal = default_first(<T as MatchArg>::CHOICES, |choice| choice == default)
803        .expect("MatchArg::to_choice returns one of MatchArg::CHOICES");
804    match_formal::<T>(sexp, &formal)
805        .map_err(|e| ArgError::new(param, e.arg_message(param, &formal)))
806}
807
808/// Optional form of [`match_arg_from_sexp`] for `Option<T>` parameters (#1473).
809///
810/// `NULL` is `None` (the R formal of an `Option<T>` choice parameter defaults
811/// to `NULL`, meaning no choice was made); anything else goes through the same
812/// exact-or-partial matching as the plain type and becomes `Some`.
813///
814/// Used by the generated C wrapper for `#[miniextendr(match_arg)] x: Option<T>`
815/// parameters instead of `TryFromSexp`, because `Option<T>` cannot carry a
816/// `TryFromSexp` impl for a downstream enum (orphan rule) and a `T: MatchArg`
817/// blanket would collide with the newtype blanket in [`crate::newtype`].
818pub fn match_arg_option_from_sexp<T: MatchArg>(sexp: SEXP) -> Result<Option<T>, MatchArgError> {
819    match_arg_null_or(sexp, match_arg_from_sexp)
820}
821
822/// The `Option<_>` layer of a choice parameter: `NULL` is `None`, anything
823/// else is decoded by `inner` and becomes `Some`.
824///
825/// Generated C wrappers compose this with the other layer helpers
826/// ([`match_arg_missing_or`]) around the decoder of the choice itself;
827/// [`match_arg_option_from_sexp`] is the scalar case.
828pub fn match_arg_null_or<U, E>(
829    sexp: SEXP,
830    inner: impl FnOnce(SEXP) -> Result<U, E>,
831) -> Result<Option<U>, E> {
832    if sexp.type_of() == SEXPTYPE::NILSXP {
833        Ok(None)
834    } else {
835        inner(sexp).map(Some)
836    }
837}
838
839/// The `Missing<_>` layer of a choice parameter (#1551): the missing-argument
840/// sentinel is [`Missing::Absent`](crate::Missing::Absent), anything else
841/// (`NULL` included) is decoded by `inner` and becomes `Missing::Present`.
842///
843/// The generated R wrapper keeps the choice vector as the formal default,
844/// skips the `match.arg()` check for an omitted argument and forwards R's
845/// missing-argument sentinel (`if (missing(x)) quote(expr=) else x`), so
846/// `#[miniextendr(match_arg)] mode: Missing<Option<Mode>>` reaches Rust as
847/// `Absent` when omitted, `Present(None)` for `NULL`, and
848/// `Present(Some(mode))` for a matched choice. A single-choice parameter
849/// treats its formal passed whole the same way (#2152): for a value
850/// `identical()` to the choice vector the wrapper skips the check and forwards
851/// the sentinel (`if (missing(x) || identical(x, c(...))) quote(expr=) else
852/// x`), so it is `Absent` too; a `several_ok` list reads that vector as every
853/// choice. The C wrapper decodes it with
854/// `match_arg_missing_or(sexp, match_arg_option_from_sexp::<Mode>)`.
855pub fn match_arg_missing_or<U, E>(
856    sexp: SEXP,
857    inner: impl FnOnce(SEXP) -> Result<U, E>,
858) -> Result<crate::Missing<U>, E> {
859    if crate::missing::is_missing_arg(sexp) {
860        Ok(crate::Missing::Absent)
861    } else {
862        inner(sexp).map(crate::Missing::Present)
863    }
864}
865
866/// The `Either<_, R>` layer of a choice parameter: a choice or a value of
867/// another kind. Character and factor input (the forms `match.arg()` reads)
868/// is decoded by `left` and becomes `Left`; anything else, `NULL` included, is
869/// converted to `R` and becomes `Right`.
870///
871/// The generated R wrapper applies the same split: its prelude matches the
872/// argument against the choices only when it is character or factor, so a
873/// misspelled choice fails there and never reaches `R`, and a data frame is
874/// never tried as a choice. For `#[miniextendr(match_arg)] route:
875/// Either<Route, DataFrame>` the C wrapper decodes with
876/// `match_arg_either_or::<_, DataFrame, _>(sexp, match_arg_from_sexp::<Route>)`;
877/// for a `several_ok` list, `Either<Vec<Route>, DataFrame>` (#1612), `left` is
878/// [`match_arg_vec_from_sexp::<Route>`](match_arg_vec_from_sexp); a
879/// `choices(...)` parameter on `Either<String, R>` (or `Either<Vec<String>, R>`
880/// with `several_ok`) passes the string type's `TryFromSexp` as `left`.
881///
882/// A value that `R` refuses is reported against the whole parameter:
883/// `'route' must be one of "oral", "bolus", "infusion", or a data frame: got
884/// integer`.
885///
886/// So `R` must read something other than character or factor input: an `R`
887/// whose [`TryFromSexp::CHARACTER_ONLY`] is `true` could receive at most
888/// `NULL`, and `#[miniextendr]` rejects the parameter at compile time.
889///
890/// This differs from `TryFromSexp for Either<L, R>`, which tries `L` first on
891/// every input and would decode `NULL` as the first choice.
892#[cfg(feature = "either")]
893pub fn match_arg_either_or<L, R, E>(
894    sexp: SEXP,
895    left: impl FnOnce(SEXP) -> Result<L, E>,
896) -> Result<either::Either<L, R>, SexpError>
897where
898    E: Into<SexpError>,
899    R: TryFromSexp,
900    R::Error: Into<SexpError>,
901{
902    if sexp.type_of() == SEXPTYPE::STRSXP || sexp.is_factor() {
903        left(sexp).map(either::Either::Left).map_err(Into::into)
904    } else {
905        R::try_from_sexp(sexp)
906            .map(either::Either::Right)
907            .map_err(Into::into)
908    }
909}
910
911/// Reads [`TryFromSexp::CHARACTER_ONLY`] of the `R` arm of an `Either<T, R>`
912/// choice parameter without requiring `R: TryFromSexp`.
913///
914/// `#[miniextendr]` emits, for each such arm (inside the generated C
915/// wrapper),
916///
917/// ```ignore
918/// {
919///     use ::miniextendr_api::match_arg::EitherArmProbeFallback as _;
920///     const _: () = ::core::assert!(
921///         !::miniextendr_api::match_arg::EitherArmProbe::<R>::CHARACTER_ONLY,
922///         "...",
923///     );
924/// }
925/// ```
926///
927/// The path resolves to the inherent const below when `R: TryFromSexp`
928/// holds. When it does not, path resolution rejects the inherent candidate
929/// (its impl bound fails) and falls back to the const of
930/// [`EitherArmProbeFallback`], which is `false`. So the check adds no error
931/// of its own for an `R` without a conversion: the decode's single "`R:
932/// TryFromSexp` is not satisfied" is the whole diagnostic. Naming
933/// `<R as TryFromSexp>::CHARACTER_ONLY` directly would report that missing
934/// impl a second time.
935#[doc(hidden)]
936pub struct EitherArmProbe<R>(core::marker::PhantomData<fn() -> R>);
937
938impl<R: TryFromSexp> EitherArmProbe<R> {
939    /// `R`'s [`TryFromSexp::CHARACTER_ONLY`].
940    pub const CHARACTER_ONLY: bool = R::CHARACTER_ONLY;
941}
942
943/// The fallback of [`EitherArmProbe`] for an `R` without `TryFromSexp`: the
944/// check passes, and the decode reports the missing impl.
945#[doc(hidden)]
946pub trait EitherArmProbeFallback {
947    /// Always `false`.
948    const CHARACTER_ONLY: bool = false;
949}
950
951impl<R> EitherArmProbeFallback for EitherArmProbe<R> {}
952
953/// Match a string against the choices of a `MatchArg` type: a choice typed
954/// exactly, then an alias typed exactly ([`MatchArg::ALIASES`]), then a
955/// unique prefix of a choice.
956fn match_choice<T: MatchArg>(input: &str) -> Result<T, MatchArgError> {
957    match_string::<T>(input, <T as MatchArg>::ALIASES)
958}
959
960/// [`match_choice`] with the aliases `aliases`.
961fn match_string<T: MatchArg>(input: &str, aliases: &[MatchArgAlias]) -> Result<T, MatchArgError> {
962    let no_match = || MatchArgError::no_match::<T>(input);
963    // `pmatch()` matches the empty string to nothing, not even to an empty
964    // choice; without this every choice would be a partial match.
965    if input.is_empty() {
966        return Err(no_match());
967    }
968
969    // Exact match
970    if let Some(val) = T::from_choice(input) {
971        return Ok(val);
972    }
973
974    // An alias, typed in full only: a prefix of an alias is not one, so an
975    // alias never makes a prefix of the choices ambiguous.
976    if let Some(alias) = aliases.iter().find(|alias| alias.alias == input) {
977        return T::from_choice(alias.choice).ok_or_else(no_match);
978    }
979
980    // Unique partial match (like R's match.arg)
981    let mut matches = <T as MatchArg>::CHOICES
982        .iter()
983        .filter(|choice| choice.starts_with(input));
984    match (matches.next(), matches.next()) {
985        (Some(choice), None) => T::from_choice(choice).ok_or_else(no_match),
986        _ => Err(no_match()),
987    }
988}
989
990/// Convert a `Vec<T: MatchArg>` to an R character vector (STRSXP).
991///
992/// Each element is written as its canonical choice string via [`MatchArg::to_choice`].
993/// Empty choice strings are stored as `R_BlankString` (parity with [`choices_sexp`]).
994///
995/// Called by the [`MatchArg`]→[`IntoRVecElement`](crate::newtype::IntoRVecElement)
996/// bridge below, which backs `IntoR for Vec<MyEnum>`.
997pub fn match_arg_vec_into_sexp<T: MatchArg>(values: Vec<T>) -> SEXP {
998    unsafe {
999        let scope = ProtectScope::new();
1000        // Preserve the R_BlankString short-circuit: skips hash-lookup on the
1001        // interned empty CHARSXP for any empty choice strings.
1002        let vec = scope.alloc_strsxp(values.len()).into_raw();
1003        for (i, v) in values.into_iter().enumerate() {
1004            let s = v.to_choice();
1005            let charsxp = if s.is_empty() {
1006                SEXP::blank_string()
1007            } else {
1008                SEXP::charsxp(s)
1009            };
1010            vec.set_string_elt(i as R_xlen_t, charsxp);
1011        }
1012        vec
1013    }
1014}
1015
1016/// Bridge: every [`MatchArg`] type is an [`IntoRVecElement`](crate::newtype::IntoRVecElement),
1017/// so `Vec<MyEnum>` converts to an R character vector (STRSXP) via
1018/// [`match_arg_vec_into_sexp`].
1019///
1020/// `IntoR for Vec<T>` has a single blanket slot (see [`crate::newtype`]). Routing
1021/// `MatchArg` through `IntoRVecElement` lets `#[derive(IntoR)]` newtypes share
1022/// that slot — a newtype implements `IntoRVecElement` concretely in its own crate,
1023/// which coexists with this bridge because a local newtype is provably not
1024/// `MatchArg` (deriving both `MatchArg` and `IntoR` on one type is an E0119
1025/// coherence error, by design). Stable Rust has no negative trait bounds, so a
1026/// *second* `impl<T: …> IntoR for Vec<T>` blanket would conflict directly; this
1027/// indirection is what avoids that.
1028impl<T: MatchArg> crate::newtype::IntoRVecElement for T {
1029    fn elements_into_sexp(values: Vec<Self>) -> SEXP {
1030        match_arg_vec_into_sexp(values)
1031    }
1032}
1033
1034/// Extract multiple strings from an R SEXP (STRSXP) and match each against
1035/// the choices of a `MatchArg` type.
1036///
1037/// Used by the generated C wrapper for `match_arg + several_ok` parameters
1038/// (`match.arg` with `several.ok = TRUE`).
1039///
1040/// NULL input returns all variants. The R prelude's strict `several_ok`
1041/// helper maps `NULL` the same way and rejects any element that matches no
1042/// choice before the value reaches this function, so the per-element check
1043/// here is the second line of defence, not the only one (#1472). Under an
1044/// `Either<Vec<T>, R>` parameter (#1612), `match_arg_either_or` calls it
1045/// only for character or factor input, so `NULL` never reaches it there.
1046///
1047/// Note: factors (INTSXP) are not handled here — the R wrapper coerces factors
1048/// to character before the `.Call()` boundary, on the `Either` path too.
1049pub fn match_arg_vec_from_sexp<T: MatchArg>(sexp: SEXP) -> Result<Vec<T>, MatchArgError> {
1050    // NIL → all choices (match.arg default with several.ok = TRUE).
1051    if sexp.type_of() == SEXPTYPE::NILSXP {
1052        return <T as MatchArg>::CHOICES
1053            .iter()
1054            .map(|c| T::from_choice(c).ok_or_else(|| MatchArgError::no_match::<T>(c)))
1055            .collect();
1056    }
1057
1058    // STRSXP path — delegate type check + per-element NA handling
1059    // to Vec<Option<&str>>. `None` means NA_character_ (IsNa error).
1060    <Vec<Option<&'static str>> as TryFromSexp>::try_from_sexp(sexp)
1061        .map_err(sexp_err_to_match_arg_err::<T>)?
1062        .into_iter()
1063        .map(|opt| {
1064            opt.ok_or_else(MatchArgError::is_na::<T>)
1065                .and_then(match_choice::<T>)
1066        })
1067        .collect()
1068}
1069
1070#[cfg(test)]
1071mod tests {
1072    use super::escape_r_string;
1073
1074    #[test]
1075    fn escapes_backslash_and_quote() {
1076        assert_eq!(escape_r_string(r#"say "hi""#), r#"say \"hi\""#);
1077        assert_eq!(escape_r_string(r"c:\path"), r"c:\\path");
1078    }
1079
1080    #[test]
1081    fn escapes_control_characters() {
1082        assert_eq!(escape_r_string("line1\nline2"), r"line1\nline2");
1083        assert_eq!(escape_r_string("tab\there"), r"tab\there");
1084        assert_eq!(escape_r_string("cr\rlf"), r"cr\rlf");
1085    }
1086
1087    #[test]
1088    fn passes_through_plain_strings() {
1089        assert_eq!(escape_r_string("Fast"), "Fast");
1090        assert_eq!(escape_r_string("it's"), "it's");
1091        assert_eq!(escape_r_string(""), "");
1092    }
1093
1094    /// The expectation of a choice parameter: one or several choices, the
1095    /// other accepted values after them, quoted as `one_of` quotes.
1096    #[test]
1097    fn choice_expectation_names_the_choices_and_the_rest() {
1098        use super::{choice_expectation, one_of};
1099        assert_eq!(
1100            choice_expectation(&["fast"], &[], false, ""),
1101            r#"one of "fast""#
1102        );
1103        assert_eq!(
1104            choice_expectation(&["fast", "slow"], &[], false, ", or a data frame"),
1105            r#"one of "fast", "slow", or a data frame"#
1106        );
1107        assert_eq!(
1108            choice_expectation(&["fast", "slow"], &[], true, ", or a number"),
1109            r#"one or more of "fast", "slow", or a number"#
1110        );
1111        assert_eq!(
1112            choice_expectation(&[r#"say "hi""#, "bye"], &[], false, ""),
1113            r#"one of "say \"hi\"", "bye""#
1114        );
1115        assert_eq!(one_of(&["fast", "slow"], &[]), r#"one of "fast", "slow""#);
1116    }
1117
1118    /// The listed aliases go between the choices and the other accepted
1119    /// values, in the order of their choices, then their own (#2148); an
1120    /// unlisted alias is left out, and a type whose aliases are all unlisted
1121    /// reads as one without aliases.
1122    #[test]
1123    fn choice_expectation_lists_the_listed_aliases() {
1124        use super::{MatchArgAlias, choice_expectation, one_of};
1125        let choices = ["extravascular", "iv_bolus", "gray"];
1126        let aliases = [
1127            MatchArgAlias::new("grey", "gray"),
1128            MatchArgAlias::unlisted("ev", "extravascular"),
1129            MatchArgAlias::new("oral", "extravascular"),
1130            MatchArgAlias::new("Grau", "gray"),
1131        ];
1132        assert_eq!(
1133            one_of(&choices, &aliases),
1134            r#"one of "extravascular", "iv_bolus", "gray" (or "oral" for "extravascular", "grey" for "gray", "Grau" for "gray")"#
1135        );
1136        assert_eq!(
1137            choice_expectation(&choices, &aliases[..2], true, ", or a number"),
1138            r#"one or more of "extravascular", "iv_bolus", "gray" (or "grey" for "gray"), or a number"#
1139        );
1140        assert_eq!(
1141            one_of(&choices, &[MatchArgAlias::unlisted("ev", "extravascular")]),
1142            one_of(&choices, &[])
1143        );
1144        // Quoted as the choices are.
1145        assert_eq!(
1146            one_of(
1147                &[r#"say "hi""#],
1148                &[MatchArgAlias::new("hello", r#"say "hi""#)]
1149            ),
1150            r#"one of "say \"hi\"" (or "hello" for "say \"hi\"")"#
1151        );
1152    }
1153
1154    /// The `match_arg` check's message quotes the aliases as it quotes the
1155    /// choices, without escapes, and keeps them in the order of `CHOICES`
1156    /// when a `default = "..."` formal moves a choice to the front.
1157    #[test]
1158    fn arg_message_lists_the_listed_aliases() {
1159        use super::{MatchArgAlias, MatchArgError};
1160        static ALIASES: [MatchArgAlias; 3] = [
1161            MatchArgAlias::new("grey", "gray"),
1162            MatchArgAlias::new("rouge", "red"),
1163            MatchArgAlias::unlisted("hello", r#"say "hi""#),
1164        ];
1165        let e = MatchArgError::NoMatch {
1166            input: "zzz".to_string(),
1167            choices: &["red", "gray", r#"say "hi""#],
1168            aliases: &ALIASES,
1169        };
1170        assert_eq!(
1171            e.arg_message("color", &["gray", "red", r#"say "hi""#]),
1172            r#"'color' should be one of "gray", "red", "say "hi"" (or "rouge" for "red", "grey" for "gray")"#
1173        );
1174        assert_eq!(
1175            e.to_string(),
1176            r#"expected one of "red", "gray", "say \"hi\"" (or "rouge" for "red", "grey" for "gray"), got "zzz""#
1177        );
1178        let long = MatchArgError::InvalidLength {
1179            actual: 2,
1180            choices: &["red"],
1181            aliases: &ALIASES,
1182        };
1183        assert_eq!(
1184            long.arg_message("color", &["red"]),
1185            "'color' must be of length 1"
1186        );
1187    }
1188
1189    /// A `default = "..."` formal: the default first, the rest in their
1190    /// order (not a rotation of the list), and `None` for a default that is
1191    /// no choice (#1767).
1192    #[test]
1193    fn default_first_moves_the_default_to_the_front() {
1194        use super::default_first;
1195        let choices = ["Fast", "Safe", "Debug"];
1196        let is = |default: &'static str| move |c: &str| c == default;
1197        assert_eq!(
1198            default_first(&choices, is("Safe")).unwrap(),
1199            ["Safe", "Fast", "Debug"]
1200        );
1201        assert_eq!(
1202            default_first(&choices, is("Debug")).unwrap(),
1203            ["Debug", "Fast", "Safe"]
1204        );
1205        assert_eq!(default_first(&choices, is("Fast")).unwrap(), choices);
1206        assert_eq!(default_first(&choices, is("Slow")), None);
1207        assert_eq!(default_first(&["only"], is("only")).unwrap(), ["only"]);
1208    }
1209}