Skip to main content

miniextendr_macros/
r_preconditions.rs

1//! R-side precondition generation for type checking.
2//!
3//! Generates argument checks in R wrapper functions that run BEFORE the `.Call()` boundary.
4//! This gives users clear, idiomatic R error messages with proper stack traces instead of
5//! Rust panic messages.
6//!
7//! Each check tests ONE thing and names the requirement it enforces:
8//!
9//! ```r
10//! add <- function(a, b) {
11//!   if (!isTRUE(is.integer(a))) .miniextendr_arg_error("a", "must be integer")
12//!   if (!isTRUE(length(a) == 1L)) .miniextendr_arg_error("a", "must have length 1")
13//!   if (!isTRUE(is.integer(b))) .miniextendr_arg_error("b", "must be integer")
14//!   if (!isTRUE(length(b) == 1L)) .miniextendr_arg_error("b", "must have length 1")
15//!   .Call(C_add, .call = sys.call(), a, b)
16//! }
17//! ```
18//!
19//! A failing check calls the preamble helper `.miniextendr_arg_error`
20//! (`miniextendr-api/src/registry.rs`), which raises the same condition as a
21//! failed Rust conversion: the crate's `conversion_error_class`, `rust_error`,
22//! `kind = "conversion"` and `e$param`, with the message `'a' must be integer`
23//! (#1591). A passing check costs one `isTRUE()` test; the condition is only
24//! built on failure. `isTRUE()` keeps `stopifnot()`'s failure semantics (`NA`
25//! fails too), and the guards are cheaper than the `stopifnot()` call they
26//! replace. [`conversion_expectation`] gives the Rust conversion the same
27//! vocabulary for its messages.
28
29use std::collections::{HashMap, HashSet};
30
31use crate::naming::r_string_escape;
32
33/// A single R-side check on one parameter: a requirement and the R condition
34/// enforcing it.
35///
36/// Formatted as a guard (see [`RAssertion::to_guard`]):
37/// `if (!isTRUE(is.numeric(x))) .miniextendr_arg_error("x", "must be numeric")`.
38struct RAssertion {
39    /// R name of the checked parameter (`e$param` on failure).
40    param: String,
41    /// What the parameter must satisfy, without the parameter name
42    /// (`must be numeric`). The condition message is `'<param>' <requirement>`.
43    requirement: String,
44    /// R expression that must evaluate to `TRUE` for the check to pass.
45    condition: String,
46    /// The author's own condition message (`message = "..."` on `inherits` /
47    /// `not_inherits` / `no_na`; an `inherits` message also on the
48    /// parameter's type checks),
49    /// used verbatim instead of `'<param>' <requirement>`.
50    message: Option<String>,
51}
52
53impl RAssertion {
54    /// Create a new assertion on `param` with its requirement and R condition expression.
55    fn new(
56        param: impl Into<String>,
57        requirement: impl Into<String>,
58        condition: impl Into<String>,
59    ) -> Self {
60        Self {
61            param: param.into(),
62            requirement: requirement.into(),
63            condition: condition.into(),
64            message: None,
65        }
66    }
67
68    /// Use `message` verbatim as the condition message of a failure.
69    fn with_message(mut self, message: Option<&String>) -> Self {
70        self.message = message.cloned();
71        self
72    }
73
74    /// The condition message a failure raises: the author's message, else
75    /// `'<param>' <requirement>`.
76    #[cfg(test)]
77    fn message(&self) -> String {
78        match &self.message {
79            Some(message) => message.clone(),
80            None => format!("'{}' {}", self.param, self.requirement),
81        }
82    }
83
84    /// Format as a guard that raises an argument error on failure:
85    /// `if (!isTRUE(condition)) .miniextendr_arg_error("param", "requirement")`,
86    /// or with the author's message
87    /// `if (!isTRUE(condition)) .miniextendr_arg_error("param", message = "...")`.
88    ///
89    /// `call` is the call the error is attributed to. `None` leaves the
90    /// helper's default, the wrapper's own call (what `stopifnot()` reported);
91    /// a `call = caller` wrapper passes `.mx_call` (#1548). Next to a named
92    /// `message` the call is named too: positionally it would bind to `what`.
93    fn to_guard(&self, call: Option<&str>) -> String {
94        match &self.message {
95            None => {
96                let call_arg = call.map(|c| ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!(", {0}", c))
    })format!(", {c}")).unwrap_or_default();
97                ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("if (!isTRUE({0})) .miniextendr_arg_error(\"{1}\", \"{2}\"{3})",
                self.condition, self.param, self.requirement, call_arg))
    })format!(
98                    "if (!isTRUE({})) .miniextendr_arg_error(\"{}\", \"{}\"{})",
99                    self.condition, self.param, self.requirement, call_arg
100                )
101            }
102            Some(message) => {
103                let call_arg = call.map(|c| ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!(", call = {0}", c))
    })format!(", call = {c}")).unwrap_or_default();
104                ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("if (!isTRUE({0})) .miniextendr_arg_error(\"{1}\", message = \"{2}\"{3})",
                self.condition, self.param, r_string_escape(message),
                call_arg))
    })format!(
105                    "if (!isTRUE({})) .miniextendr_arg_error(\"{}\", message = \"{}\"{})",
106                    self.condition,
107                    self.param,
108                    r_string_escape(message),
109                    call_arg
110                )
111            }
112        }
113    }
114
115    /// Wrap for nullable: prepend `is.null(param) || ` to the condition,
116    /// and adjust the requirement to mention NULL.
117    fn nullable(self) -> Self {
118        let requirement = if let Some(rest) = self.requirement.strip_prefix("must be ") {
119            // "must be character" → "must be NULL or character"
120            ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("must be NULL or {0}", rest))
    })format!("must be NULL or {rest}")
121        } else if let Some(rest) = self.requirement.strip_prefix("must have ") {
122            // "must have length 1" → "must be NULL or have length 1"
123            ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("must be NULL or have {0}", rest))
    })format!("must be NULL or have {rest}")
124        } else {
125            ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} (or NULL)", self.requirement))
    })format!("{} (or NULL)", self.requirement)
126        };
127        Self {
128            condition: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("is.null({0}) || {1}", self.param,
                self.condition))
    })format!("is.null({}) || {}", self.param, self.condition),
129            param: self.param,
130            requirement,
131            message: self.message,
132        }
133    }
134}
135
136/// Per-function knobs that influence precondition codegen.
137///
138/// Coercion preserves numeric input types, extends `bool` / `Vec<bool>`
139/// checks to accept integers as well as logicals, and widens the native
140/// `i32` / `Vec<i32>` gates to whole-number doubles. Other types keep their checks.
141/// Strict input conversion remains enforced in Rust, where range and precision
142/// failures can carry contextual diagnostics.
143#[derive(#[automatically_derived]
impl ::core::clone::Clone for PreconditionOptions {
    #[inline]
    fn clone(&self) -> Self {
        PreconditionOptions {
            coerce_all: ::core::clone::Clone::clone(&self.coerce_all),
            coerce_params: ::core::clone::Clone::clone(&self.coerce_params),
            explicit: ::core::clone::Clone::clone(&self.explicit),
            unchecked: ::core::clone::Clone::clone(&self.unchecked),
            formals: ::core::clone::Clone::clone(&self.formals),
        }
    }
}Clone, #[automatically_derived]
impl ::core::default::Default for PreconditionOptions {
    #[inline]
    fn default() -> Self {
        PreconditionOptions {
            coerce_all: ::core::default::Default::default(),
            coerce_params: ::core::default::Default::default(),
            explicit: ::core::default::Default::default(),
            unchecked: ::core::default::Default::default(),
            formals: ::core::default::Default::default(),
        }
    }
}Default)]
144pub struct PreconditionOptions {
145    /// `coerce` knob is active for all parameters (`coerce_all`).
146    pub coerce_all: bool,
147    /// R-normalized names of parameters with a per-param `coerce` attribute.
148    pub coerce_params: HashSet<String>,
149    /// Checks the author named per parameter (`inherits`, `not_inherits`,
150    /// `no_na`), keyed by R-normalized parameter name.
151    pub explicit: HashMap<String, ExplicitChecks>,
152    /// R-normalized names of the parameters whose type-derived checks are
153    /// dropped (each one resolved by [`resolve_type_checks`]). Their
154    /// [`explicit`](Self::explicit) checks are still emitted.
155    pub unchecked: HashSet<String>,
156    /// The R formal of each parameter (#1842): the name every check, every
157    /// argument error and every map above uses. Without a declared formal,
158    /// the Rust name without leading underscores.
159    pub formals: crate::r_wrapper_builder::Formals,
160}
161
162impl PreconditionOptions {
163    /// Returns `true` if the parameter `r_name` is coerced (function-wide or
164    /// per-param).
165    fn is_coerced(&self, r_name: &str) -> bool {
166        self.coerce_all || self.coerce_params.contains(r_name)
167    }
168}
169
170// region: which parameters keep their type-derived checks (#1566 §2)
171
172/// Whether one parameter keeps its type-derived R-side checks, from every
173/// spelling that can decide it, most specific first; the first that speaks
174/// wins:
175///
176/// 1. `param`: a `Checked<T>` / `Unchecked<T>` marker, the per-parameter
177///    `preconditions` / `no_preconditions`, or a method's
178///    `preconditions(x)` / `no_preconditions(x)` list (merged at parse time,
179///    where a marker and a keyword that disagree are an error);
180/// 2. `item`: the function's or method's own `preconditions` /
181///    `no_preconditions`;
182/// 3. `impl_level`: the impl block's (`None` for a free function);
183/// 4. `crate_default`: `[package.metadata.miniextendr] preconditions`;
184/// 5. `no_preconditions_default`: the `no-preconditions-default` feature;
185/// 6. otherwise the checks are kept.
186///
187/// `Some(true)` everywhere means "keep".
188pub(crate) fn resolve_type_checks(
189    param: Option<bool>,
190    item: Option<bool>,
191    impl_level: Option<bool>,
192    crate_default: Option<bool>,
193    no_preconditions_default: bool,
194) -> bool {
195    param.unwrap_or_else(|| {
196        resolve_item_type_checks(item, impl_level, crate_default, no_preconditions_default)
197    })
198}
199
200/// [`resolve_type_checks`] without a per-parameter spelling: whether an item
201/// (function or method) keeps its type-derived checks by default, ranks 2 to
202/// 6. The condition-raise codegen keys on this item-level decision.
203pub(crate) fn resolve_item_type_checks(
204    item: Option<bool>,
205    impl_level: Option<bool>,
206    crate_default: Option<bool>,
207    no_preconditions_default: bool,
208) -> bool {
209    item.or(impl_level)
210        .or(crate_default)
211        .unwrap_or(!no_preconditions_default)
212}
213
214/// The R formals (`formals`) of the parameters of `inputs` whose
215/// type-derived checks are dropped, given each one's rank-1 decision
216/// (`param`, keyed by Rust name) and the item, impl and crate defaults
217/// ([`resolve_type_checks`]; the feature is this macro crate's
218/// `no-preconditions-default`).
219pub(crate) fn unchecked_params(
220    inputs: &syn::punctuated::Punctuated<syn::FnArg, ::syn::token::Commasyn::Token![,]>,
221    formals: &crate::r_wrapper_builder::Formals,
222    param: impl Fn(&str) -> Option<bool>,
223    item: Option<bool>,
224    impl_level: Option<bool>,
225    crate_default: Option<bool>,
226) -> HashSet<String> {
227    inputs
228        .iter()
229        .filter_map(|arg| {
230            let syn::FnArg::Typed(pt) = arg else {
231                return None;
232            };
233            let syn::Pat::Ident(pat_ident) = pt.pat.as_ref() else {
234                return None;
235            };
236            let name = crate::naming::ident_name(&pat_ident.ident);
237            let keep = resolve_type_checks(
238                param(&name),
239                item,
240                impl_level,
241                crate_default,
242                falsecfg!(feature = "no-preconditions-default"),
243            );
244            (!keep).then(|| formals.of(&name))
245        })
246        .collect()
247}
248
249/// Whether a parameter type has type-derived R-side checks at all, i.e.
250/// whether `Checked` / `Unchecked` and the per-parameter keywords have
251/// anything to keep or drop (`SEXP`, `Missing<T>`, `ExternalPtr<T>`,
252/// `&Dots` and unknown types have none).
253pub(crate) fn has_type_check(ty: &syn::Type) -> bool {
254    r_check_for_type(ty).is_some()
255}
256
257// endregion
258
259/// R-side checks the author asked for by name on one parameter, rather than
260/// ones derived from its Rust type.
261///
262/// Spelled `#[miniextendr(inherits = "cls", not_inherits = "bad", no_na)]` on
263/// a standalone fn parameter, or `inherits(x = "cls")` /
264/// `not_inherits(x = "bad")` / `no_na(x)` on an impl or trait method. They
265/// are the same kind of guards as the type checks, raising the same argument
266/// error. The class checks run before the parameter's type checks, so a value
267/// of the wrong class gets the class message whatever its type: `inherits`
268/// first, then `not_inherits` (#1815). That order matters for the classes a
269/// `not_inherits` typically refuses: `is.numeric()` is `FALSE` for a
270/// `difftime`, `Date` or `POSIXt`, so after an `AsNumeric` type check the
271/// refusal would never be reached. An `inherits` message also replaces the
272/// messages of the parameter's type checks; a `not_inherits` message does
273/// not, since a value that passes it is of none of the refused classes and is
274/// judged by its type. The NA check runs after the type checks, with its own
275/// message. All survive `no_preconditions`: the Rust conversion does not
276/// repeat them, so dropping them would change what the function accepts.
277///
278/// `no_na` also has a Rust half. A type can read more inputs as missing than
279/// `anyNA()` sees (the reading markers `AsNumeric*` / `AsCharacter*` read the
280/// text `"NA"`, blank strings, a factor `NA` level as `NA`), so the C wrapper
281/// checks the converted value too (`TryFromSexp::__mx_has_na`, see
282/// `RustConversionBuilder::with_no_na`) and raises the same condition with
283/// the same message ([`no_na_message`]). An `Either` parameter has only the
284/// Rust half: the arm the value converted to checks the input
285/// ([`no_na_checked_after_conversion`]).
286///
287/// Each check can carry the author's own condition message
288/// (`inherits(class = "cls", message = "...")`,
289/// `not_inherits(class = "bad", message = "...")`, `no_na(message = "...")`;
290/// method level `inherits(x(class = "cls", message = "..."))`,
291/// `not_inherits(x(class = "bad", message = "..."))`,
292/// `no_na(x(message = "..."))`), used verbatim in place of the generated
293/// `'x' must inherit from 'cls'`. The condition is otherwise the same.
294///
295/// An `inherits` check can also give a value that fails it a message chosen
296/// by the value's class (#1824): `inherits(class = "cls",
297/// when(class = "data.frame", message = "..."))`, method level
298/// `inherits(x(class = "cls", when(class = "data.frame", message = "...")))`
299/// ([`InheritsHint`]).
300#[derive(#[automatically_derived]
impl ::core::clone::Clone for ExplicitChecks {
    #[inline]
    fn clone(&self) -> Self {
        ExplicitChecks {
            inherits: ::core::clone::Clone::clone(&self.inherits),
            inherits_message: ::core::clone::Clone::clone(&self.inherits_message),
            inherits_hints: ::core::clone::Clone::clone(&self.inherits_hints),
            not_inherits: ::core::clone::Clone::clone(&self.not_inherits),
            not_inherits_message: ::core::clone::Clone::clone(&self.not_inherits_message),
            no_na: ::core::clone::Clone::clone(&self.no_na),
            no_na_message: ::core::clone::Clone::clone(&self.no_na_message),
        }
    }
}Clone, #[automatically_derived]
impl ::core::fmt::Debug for ExplicitChecks {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        let names: &'static _ =
            &["inherits", "inherits_message", "inherits_hints",
                        "not_inherits", "not_inherits_message", "no_na",
                        "no_na_message"];
        let values: &[&dyn ::core::fmt::Debug] =
            &[&self.inherits, &self.inherits_message, &self.inherits_hints,
                        &self.not_inherits, &self.not_inherits_message, &self.no_na,
                        &&self.no_na_message];
        ::core::fmt::Formatter::debug_struct_fields_finish(f,
            "ExplicitChecks", names, values)
    }
}Debug, #[automatically_derived]
impl ::core::default::Default for ExplicitChecks {
    #[inline]
    fn default() -> Self {
        ExplicitChecks {
            inherits: ::core::default::Default::default(),
            inherits_message: ::core::default::Default::default(),
            inherits_hints: ::core::default::Default::default(),
            not_inherits: ::core::default::Default::default(),
            not_inherits_message: ::core::default::Default::default(),
            no_na: ::core::default::Default::default(),
            no_na_message: ::core::default::Default::default(),
        }
    }
}Default, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for ExplicitChecks { }
#[automatically_derived]
impl ::core::cmp::PartialEq for ExplicitChecks {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        self.no_na == other.no_na && self.inherits == other.inherits &&
                            self.inherits_message == other.inherits_message &&
                        self.inherits_hints == other.inherits_hints &&
                    self.not_inherits == other.not_inherits &&
                self.not_inherits_message == other.not_inherits_message &&
            self.no_na_message == other.no_na_message
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for ExplicitChecks {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<Option<Vec<String>>>;
        let _: ::core::cmp::AssertParamIsEq<Option<String>>;
        let _: ::core::cmp::AssertParamIsEq<Vec<InheritsHint>>;
        let _: ::core::cmp::AssertParamIsEq<Option<Vec<String>>>;
        let _: ::core::cmp::AssertParamIsEq<Option<String>>;
        let _: ::core::cmp::AssertParamIsEq<bool>;
        let _: ::core::cmp::AssertParamIsEq<Option<String>>;
    }
}Eq)]
301pub struct ExplicitChecks {
302    /// `inherits = "cls"` / `inherits("a", "b")`: the argument must inherit
303    /// from at least one of these classes (`inherits(x, c(...))`).
304    pub inherits: Option<Vec<String>>,
305    /// `message = "..."` in `inherits(...)`: the message of a failed class
306    /// check, for all of its classes, and of that parameter's R type checks.
307    /// Only set together with `inherits`.
308    pub inherits_message: Option<String>,
309    /// `when(class = "...", message = "...")` in `inherits(...)`: the
310    /// messages a value that fails the `inherits` check gets for its class,
311    /// in the order written. Only set together with `inherits`.
312    pub inherits_hints: Vec<InheritsHint>,
313    /// `not_inherits = "cls"` / `not_inherits("a", "b")`: the argument must
314    /// inherit from none of these classes (`!inherits(x, c(...))`, #1815).
315    pub not_inherits: Option<Vec<String>>,
316    /// `message = "..."` in `not_inherits(...)`: the message of a failed
317    /// `not_inherits` check, for all of its classes. Unlike the `inherits`
318    /// message it leaves the type checks' messages alone. Only set together
319    /// with `not_inherits`.
320    pub not_inherits_message: Option<String>,
321    /// `no_na`: the argument must not contain `NA` (`!anyNA(x)`, so `NaN`
322    /// is refused too, as `is.na()` does), nor a value its type reads as `NA`
323    /// (checked in Rust after the conversion). For an `Either`, both checks
324    /// run in Rust, by the arm the value converted to.
325    pub no_na: bool,
326    /// `no_na(message = "...")`: the message of a failed NA check. Only set
327    /// together with `no_na`.
328    pub no_na_message: Option<String>,
329}
330
331impl ExplicitChecks {
332    /// Whether any check is requested.
333    pub fn is_empty(&self) -> bool {
334        self.inherits.is_none() && self.not_inherits.is_none() && !self.no_na
335    }
336
337    /// Merge `other` into `self` (a parameter may carry several attributes).
338    /// The `inherits` hints of `other` follow those of `self`, so the hints
339    /// keep the order they are written in.
340    ///
341    /// Fails when both give a message for the same check; the error names
342    /// the check.
343    pub fn merge(&mut self, other: ExplicitChecks) -> Result<(), String> {
344        merge_message(
345            &mut self.inherits_message,
346            other.inherits_message,
347            "inherits",
348        )?;
349        merge_message(
350            &mut self.not_inherits_message,
351            other.not_inherits_message,
352            "not_inherits",
353        )?;
354        merge_message(&mut self.no_na_message, other.no_na_message, "no_na")?;
355        if let Some(classes) = other.inherits {
356            self.inherits.get_or_insert_with(Vec::new).extend(classes);
357        }
358        self.inherits_hints.extend(other.inherits_hints);
359        if let Some(classes) = other.not_inherits {
360            self.not_inherits
361                .get_or_insert_with(Vec::new)
362                .extend(classes);
363        }
364        self.no_na |= other.no_na;
365        Ok(())
366    }
367
368    /// The error for a parameter whose checks contradict each other: a class
369    /// both in `inherits` and in `not_inherits`, which no argument could
370    /// satisfy through that class, or an `inherits` class in a `when(...)`
371    /// hint, whose message could never be shown (a value of that class
372    /// passes `inherits`). `None` when they agree.
373    ///
374    /// A hint class may also be a `not_inherits` class: a value of that
375    /// class alone gets the hint, and one that also inherits from a required
376    /// class passes the hint and `inherits` and is refused by `not_inherits`.
377    ///
378    /// Run once every attribute of the parameter is merged: the
379    /// `inherits = "cls"` spelling adds its class without a merge, and a
380    /// hint can name a class that another attribute requires.
381    pub fn class_conflict(&self, param: &str) -> Option<String> {
382        let required = self.inherits.as_deref().unwrap_or_default();
383        if let Some(class) = self
384            .not_inherits
385            .iter()
386            .flatten()
387            .find(|c| required.contains(c))
388        {
389            return Some(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("class `{0}` is in both `inherits` and `not_inherits` on parameter `{1}`; name it in only one of them",
                class, param))
    })format!(
390                "class `{class}` is in both `inherits` and `not_inherits` on parameter `{param}`; \
391                 name it in only one of them"
392            ));
393        }
394        let class = self
395            .inherits_hints
396            .iter()
397            .flat_map(|hint| &hint.classes)
398            .find(|c| required.contains(c))?;
399        Some(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("class `{0}` is in both `inherits` and a `when(...)` hint on parameter `{1}`; a value of that class passes `inherits`, so the hint would never be shown: drop it from `when(...)`",
                class, param))
    })format!(
400            "class `{class}` is in both `inherits` and a `when(...)` hint on parameter \
401             `{param}`; a value of that class passes `inherits`, so the hint would never be \
402             shown: drop it from `when(...)`"
403        ))
404    }
405
406    /// The class checks (`inherits`, then `not_inherits`) for parameter
407    /// `param` of type `ty`, which run before the parameter's type checks.
408    /// Empty without either.
409    ///
410    /// The `inherits` hints come first, one check each in the order written,
411    /// so a refused value gets the first hint naming one of its classes:
412    /// `inherits(p, <required>) || !inherits(p, <hint classes>)`, failing
413    /// with the hint's message. A value that passes `inherits` passes every
414    /// hint, so a hint changes the message of a refusal, never what is
415    /// accepted.
416    ///
417    /// An `Option<T>` / `Missing<T>` parameter passes `NULL` / an omitted
418    /// argument (see [`guarded`]).
419    fn class_assertions(&self, param: &str, ty: &syn::Type) -> Vec<RAssertion> {
420        let mut out = Vec::new();
421        if let Some(classes) = &self.inherits {
422            let (what, class_arg) = class_list_r(classes);
423            for hint in &self.inherits_hints {
424                let (_, hint_arg) = class_list_r(&hint.classes);
425                out.push(
426                    RAssertion::new(
427                        param,
428                        ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("must inherit from {0}", what))
    })format!("must inherit from {what}"),
429                        ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("inherits({0}, {1}) || !inherits({0}, {2})",
                param, class_arg, hint_arg))
    })format!("inherits({param}, {class_arg}) || !inherits({param}, {hint_arg})"),
430                    )
431                    .with_message(Some(&hint.message)),
432                );
433            }
434            out.push(
435                RAssertion::new(
436                    param,
437                    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("must inherit from {0}", what))
    })format!("must inherit from {what}"),
438                    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("inherits({0}, {1})", param,
                class_arg))
    })format!("inherits({param}, {class_arg})"),
439                )
440                .with_message(self.inherits_message.as_ref()),
441            );
442        }
443        if let Some(classes) = &self.not_inherits {
444            let (what, class_arg) = class_list_r(classes);
445            out.push(
446                RAssertion::new(
447                    param,
448                    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("must not inherit from {0}", what))
    })format!("must not inherit from {what}"),
449                    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("!inherits({0}, {1})", param,
                class_arg))
    })format!("!inherits({param}, {class_arg})"),
450                )
451                .with_message(self.not_inherits_message.as_ref()),
452            );
453        }
454        guarded(param, ty, out)
455    }
456
457    /// The NA check (`no_na`) for parameter `param` of type `ty`, which runs
458    /// after the parameter's type checks, with its own message. Empty without
459    /// `no_na`, and for an `Either` parameter that is not a choice parameter
460    /// (`choice`: `match_arg` / `choices`), whose NA check runs in Rust after
461    /// the conversion ([`no_na_checked_after_conversion`]).
462    ///
463    /// An `Option<T>` / `Missing<T>` parameter passes `NULL` / an omitted
464    /// argument (see [`guarded`]).
465    fn value_assertions(&self, param: &str, ty: &syn::Type, choice: bool) -> Vec<RAssertion> {
466        let mut out = Vec::new();
467        if self.no_na && (choice || !no_na_checked_after_conversion(ty)) {
468            out.push(
469                RAssertion::new(param, no_na_requirement(ty), ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("!anyNA({0})", param))
    })format!("!anyNA({param})"))
470                    .with_message(self.no_na_message.as_ref()),
471            );
472        }
473        guarded(param, ty, out)
474    }
475
476    /// The message of the parameter's R type checks: the `inherits` message,
477    /// when there is one. Without it the type checks keep their generated
478    /// messages. A `not_inherits` message is not one: a value that passed
479    /// `not_inherits` is of none of the refused classes, so its type check
480    /// keeps its own wording.
481    fn type_check_message(&self) -> Option<&String> {
482        self.inherits_message.as_ref()
483    }
484}
485
486/// One `when(class = "...", message = "...")` hint of an `inherits` check
487/// (#1824): the message a value gets when it fails the check and inherits
488/// from one of `classes`, in place of the check's own. A `data.frame` passed
489/// where a model object is expected can be told which function takes a data
490/// frame, while `NULL` or a number gets the plain refusal.
491#[derive(#[automatically_derived]
impl ::core::clone::Clone for InheritsHint {
    #[inline]
    fn clone(&self) -> Self {
        InheritsHint {
            classes: ::core::clone::Clone::clone(&self.classes),
            message: ::core::clone::Clone::clone(&self.message),
        }
    }
}Clone, #[automatically_derived]
impl ::core::fmt::Debug for InheritsHint {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field2_finish(f, "InheritsHint",
            "classes", &self.classes, "message", &&self.message)
    }
}Debug, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for InheritsHint { }
#[automatically_derived]
impl ::core::cmp::PartialEq for InheritsHint {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        self.classes == other.classes && self.message == other.message
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for InheritsHint {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<Vec<String>>;
        let _: ::core::cmp::AssertParamIsEq<String>;
    }
}Eq)]
492pub struct InheritsHint {
493    /// The classes the hint is for (any one of them).
494    pub classes: Vec<String>,
495    /// The condition message, used verbatim.
496    pub message: String,
497}
498
499/// A class check's classes in R: the quoted list for its message
500/// (`'a', 'b' or 'c'`) and the argument of `inherits()` (`"a"` or
501/// `c("a", "b", "c")`), each class escaped as an R string.
502fn class_list_r(classes: &[String]) -> (String, String) {
503    let quoted: Vec<String> = classes
504        .iter()
505        .map(|c| ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("\'{0}\'", r_string_escape(c)))
    })format!("'{}'", r_string_escape(c)))
506        .collect();
507    let literals: Vec<String> = classes
508        .iter()
509        .map(|c| ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("\"{0}\"", r_string_escape(c)))
    })format!("\"{}\"", r_string_escape(c)))
510        .collect();
511    let what = match quoted.as_slice() {
512        [one] => one.clone(),
513        [init @ .., last] => ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} or {1}", init.join(", "),
                last))
    })format!("{} or {last}", init.join(", ")),
514        [] => {
    ::core::panicking::panic_fmt(format_args!("internal error: entered unreachable code: {0}",
            format_args!("a class check is parsed as a non-empty class list")));
}unreachable!("a class check is parsed as a non-empty class list"),
515    };
516    let class_arg = match literals.as_slice() {
517        [one] => one.clone(),
518        _ => ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("c({0})", literals.join(", ")))
    })format!("c({})", literals.join(", ")),
519    };
520    (what, class_arg)
521}
522
523/// Let an `Option<T>` parameter pass `NULL`, and a `Missing<T>` parameter an
524/// omitted argument, in the named checks `out` on parameter `param` of type
525/// `ty`: each condition gains an `is.null(param) || ` / `missing(param) || `
526/// prefix. The failure message names only the failed requirement: it is shown
527/// only for a present, non-`NULL` value.
528fn guarded(param: &str, ty: &syn::Type, mut out: Vec<RAssertion>) -> Vec<RAssertion> {
529    let guard = if crate::miniextendr_fn::is_missing_type(ty) {
530        Some(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("missing({0})", param))
    })format!("missing({param})"))
531    } else if crate::type_inspect::is_option_type(ty) {
532        Some(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("is.null({0})", param))
    })format!("is.null({param})"))
533    } else {
534        None
535    };
536    if let Some(guard) = guard {
537        for a in &mut out {
538            a.condition = ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{1} || {0}", a.condition, guard))
    })format!("{guard} || {}", a.condition);
539        }
540    }
541    out
542}
543
544/// Whether a `no_na` parameter of Rust type `ty` is checked for `NA` only
545/// in Rust, after the conversion, with no R guard: an `Either<L, R>`, also
546/// under `Missing` and `Option`. The layers are peeled as for a choice
547/// parameter ([`crate::type_inspect::choice_layers`]). A plain
548/// `Option<Either<..>>` converts through `TryFromSexp`, which reads `NULL` as
549/// `None` (no `NA`) and asks the `Either` otherwise; a choice
550/// `Option<Either<Mode, X>>` keeps the guard (see below). The type is read
551/// as written: an alias, a newtype or a `Result<Either<..>, ()>` keeps the
552/// guard.
553///
554/// One `!anyNA(x)` on the whole argument is wrong for an arm with its own
555/// reading of `NA` (a data frame arm whose cells may be missing), and it
556/// scans the input a second time. So the C wrapper asks the arm the value
557/// converted to (`TryFromSexp::__mx_input_has_na`): by default the arm runs
558/// `anyNA()`'s check on the input in Rust, and a `DataFrame` arm refuses
559/// nothing. A choice parameter (`match_arg` / `choices`) keeps the R guard:
560/// its conversion does not go through `TryFromSexp`, so no Rust check follows.
561pub(crate) fn no_na_checked_after_conversion(ty: &syn::Type) -> bool {
562    crate::type_inspect::choice_layers(ty)
563        .either_right
564        .is_some()
565}
566
567/// What a `no_na` parameter of Rust type `ty` must satisfy, without the
568/// parameter name: `must not contain NA` for a value holding several values
569/// ([`no_na_holds_several`]), else `must not be NA`.
570///
571/// Shared by the R guard and the Rust check that the C wrapper runs after the
572/// conversion (see [`no_na_message`]), so both word a failure alike.
573pub(crate) fn no_na_requirement(ty: &syn::Type) -> String {
574    let verb = if no_na_holds_several(ty) {
575        "contain"
576    } else {
577        "be"
578    };
579    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("must not {0} NA", verb))
    })format!("must not {verb} NA")
580}
581
582/// Whether a `no_na` value of Rust type `ty` holds several values, looking
583/// through the layers `no_na` checks through: `Missing`, `Option`,
584/// `Result<T, _>` (its `T`) and `Either<L, R>`, which holds several when
585/// either arm does. The text is fixed at compile time, before the conversion
586/// picks an arm, and `contain` also reads right for one value.
587fn no_na_holds_several(ty: &syn::Type) -> bool {
588    let ty = crate::miniextendr_fn::get_missing_inner_type(ty).unwrap_or(ty);
589    let ty = crate::type_inspect::option_inner_type(ty).unwrap_or(ty);
590    if let Some((left, right)) = crate::type_inspect::either_arms(ty) {
591        return no_na_holds_several(left) || no_na_holds_several(right);
592    }
593    if let syn::Type::Path(tp) = ty
594        && let Some(seg) = tp.path.segments.last()
595        && seg.ident == "Result"
596        && let Some(ok) = crate::type_inspect::first_type_argument(seg)
597    {
598        return no_na_holds_several(ok);
599    }
600    is_vector_valued(ty)
601}
602
603/// The condition message of a failed `no_na` check on parameter `param` of
604/// type `ty`: the author's `no_na(message = "...")` verbatim (`custom`), else
605/// `'<param>' must not be NA` / `'<param>' must not contain NA`
606/// ([`no_na_requirement`]). The R guard builds the same text in R
607/// (`.miniextendr_arg_error`); the C wrapper's post-conversion check passes
608/// this one to `arg_check_condition_value`.
609pub(crate) fn no_na_message(param: &str, ty: &syn::Type, custom: Option<&str>) -> String {
610    match custom {
611        Some(message) => message.to_string(),
612        None => ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("\'{1}\' {0}",
                no_na_requirement(ty), param))
    })format!("'{param}' {}", no_na_requirement(ty)),
613    }
614}
615
616/// Whether an argument of Rust type `ty` holds several values, so a `no_na`
617/// failure reads `must not contain NA` rather than `must not be NA`.
618///
619/// The type table decides when it knows the type ([`RTypeCheck::is_vector`]):
620/// `Vec<T>` and slices, the vector markers (`AsNumericVec`, `AsCharacterVec`,
621/// any marker whose table entry is a vector check), maps and lists. For a type the
622/// table does not know, the shape decides: `Vec<T>`, `Box<[T]>`, slices and
623/// arrays, and a type named `...Vec` (the vector half of a marker family such
624/// as `AsFromStrVec` / `AsDisplayVec`, or a vector type like `BitVec`).
625fn is_vector_valued(ty: &syn::Type) -> bool {
626    if let Some(check) = r_check_for_type(ty) {
627        return check.is_vector();
628    }
629    crate::miniextendr_fn::is_vector_like_type(ty)
630        || #[allow(non_exhaustive_omitted_patterns)] match ty {
    syn::Type::Path(tp) if
        tp.path.segments.last().is_some_and(|s|
                s.ident.to_string().ends_with("Vec")) => true,
    _ => false,
}matches!(
631            ty,
632            syn::Type::Path(tp)
633                if tp.path.segments.last().is_some_and(|s| s.ident.to_string().ends_with("Vec"))
634        )
635}
636
637/// Record `from` as the message of `check`, which may have only one.
638fn merge_message(
639    into: &mut Option<String>,
640    from: Option<String>,
641    check: &str,
642) -> Result<(), String> {
643    match (into.is_some(), from) {
644        (true, Some(_)) => Err(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("`{0}` is given more than one `message` on this parameter; give it once",
                check))
    })format!(
645            "`{check}` is given more than one `message` on this parameter; give it once"
646        )),
647        (false, Some(message)) => {
648            *into = Some(message);
649            Ok(())
650        }
651        (_, None) => Ok(()),
652    }
653}
654
655/// Classification of an R-side type check for a function parameter.
656///
657/// Each variant maps to a specific set of R-side checks. Numeric checks
658/// use a broad predicate (`is.numeric || is.logical || is.raw`) because R coerces
659/// logical to numeric freely and raw to integer is valid for byte-sized types.
660/// Borderline cases (e.g., raw to i64 in strict mode) pass the precondition and
661/// reach Rust's strict checker, which produces better contextual error messages.
662enum RTypeCheck {
663    /// Numeric scalar: type check + length-1 check (2 assertions).
664    /// Used for the multi-source scalars `f32`, `i8`, `i16`, `i64`, `isize`, and
665    /// for a coerced native `f64`. Native `i32` / `f64` without coerce use
666    /// [`RTypeCheck::Scalar`] with `"integer"` / `"double"`.
667    ScalarNumeric,
668    /// Coerced bool: logical or integer, with length one.
669    ScalarLogicalOrInteger,
670    /// Coerced bool vector: logical or integer.
671    VectorLogicalOrInteger,
672    /// Coerced native `i32` scalar: integer, logical, raw, or a whole-number
673    /// double, with length one. The vector form is [`RTypeCheck::VectorIntegerWide`].
674    ScalarIntegerWide,
675    /// Non-negative numeric scalar: type + length-1 + `>= 0` (3 assertions).
676    /// Used for `u16`, `u32`, `u64`, `usize`.
677    ScalarNonNeg,
678    /// Non-numeric scalar: `is.<type>(x)` + length-1 check (2 assertions).
679    /// The string is the R type predicate name (e.g., `"logical"`, `"character"`).
680    Scalar(&'static str),
681    /// Floating-point numeric vector: loose `is.numeric || is.logical || is.raw`
682    /// (1 assertion). Used for `Vec<f32>` (multi-source converter) and for a
683    /// coerced native `Vec<f64>`; `Vec<f64>` / `&[f64]` without coerce use
684    /// [`RTypeCheck::Vector`] with `"double"` because the native path reads
685    /// REALSXP only.
686    VectorNumeric,
687    /// **INTSXP-only** integer vector: `is.integer(x)` (1 assertion). Used *only*
688    /// for `Vec<i32>` / `&[i32]` (issue #616). These use the native `RNativeType`
689    /// inbound path (`impl_vec_try_from_sexp_native!(i32)`), which requires
690    /// `INTSXP` and rejects `REALSXP` outright. The loose `is.numeric` predicate
691    /// previously let a `double` like `c(1, 2)` pass the R gate only to fail with
692    /// a cryptic "expected INTSXP, got REALSXP". `is.integer(x)` rejects every
693    /// `double` (whole or fractional) at the boundary with a clean message,
694    /// matching the actual Rust behaviour and closing the silent-truncation gap.
695    VectorIntegerStrict,
696    /// **Wide** integer vector accepting `REALSXP` whole-number values: the
697    /// lossless whole-number predicate (1 assertion). Used for every non-`i32`
698    /// integer element type — `Vec<i8>` / `Vec<i16>` / `Vec<u16>` / `Vec<u32>`
699    /// and the 64-bit family `Vec<i64>` / `Vec<u64>` / `Vec<isize>` / `Vec<usize>`
700    /// (issue #616). These use the coercing inbound path
701    /// (`impl_vec_try_from_sexp_numeric!` → `from_numeric_vec_with`) which accepts
702    /// INTSXP/REALSXP/RAWSXP/LGLSXP and rejects fractional doubles element-wise
703    /// (`f64: TryCoerce<T>` checks `self.fract() != 0`). We accept
704    /// integer/logical/raw **and** whole-number doubles, and reject genuinely
705    /// lossy fractional doubles (`1.5`) at the boundary.
706    VectorIntegerWide,
707    /// Non-numeric vector: `is.<type>(x)` only (1 assertion).
708    /// The string is the R type predicate name.
709    Vector(&'static str),
710    /// `AsNumeric` scalar: numeric, logical, character, or factor, with length
711    /// one (2 assertions). The marker parses character and factor labels the way
712    /// `as.numeric()` does, so the gate admits exactly the types it reads.
713    ScalarNumericOrText,
714    /// `AsNumericVec`: numeric, logical, character, or factor (1 assertion).
715    VectorNumericOrText,
716    /// Nullable wrapper around an inner check: prepends `is.null(x) ||` to each assertion
717    /// and adjusts messages to mention NULL.
718    Nullable(Box<RTypeCheck>),
719    /// List check: `is.list(x)` (1 assertion).
720    /// Used for `HashMap`, `BTreeMap`, `NamedList`, `List`, `ListMut`.
721    List,
722}
723
724/// Build the R expression for the numeric type predicate.
725///
726/// Returns `"is.numeric(p) || is.logical(p) || is.raw(p)"` for a given parameter `p`.
727/// This broad predicate matches R's coercion rules: logical coerces to numeric freely,
728/// and raw is accepted because it represents byte-level data.
729fn numeric_type_check(param: &str) -> String {
730    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("is.numeric({0}) || is.logical({0}) || is.raw({0})",
                param))
    })format!(
731        "is.numeric({p}) || is.logical({p}) || is.raw({p})",
732        p = param
733    )
734}
735
736/// Build the lossless whole-number predicate for a `REALSXP`-accepting integer
737/// vector (the `i64` / `u64` / `isize` / `usize` family).
738///
739/// Accepts integer/logical/raw and whole-number doubles, rejects fractional
740/// doubles. The whole-number test is NA-safe (`is.na(p) | p == trunc(p)`):
741/// `NA_real_` passes (maps to `NA_integer_`), `1.5` fails. `Inf`/`NaN` satisfy
742/// `x == trunc(x)` in R but are caught by the Rust-side range/NaN conversion
743/// check instead.
744fn integer_vector_wide_check(param: &str) -> String {
745    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("is.integer({0}) || is.logical({0}) || is.raw({0}) || (is.numeric({0}) && all(is.na({0}) | {0} == trunc({0})))",
                param))
    })format!(
746        "is.integer({p}) || is.logical({p}) || is.raw({p}) || \
747         (is.numeric({p}) && all(is.na({p}) | {p} == trunc({p})))",
748        p = param
749    )
750}
751
752/// Build the predicate for the `AsNumeric` / `AsNumericVec` markers.
753///
754/// `is.numeric()` is `FALSE` for a factor, so `is.factor()` is listed on its
755/// own: the marker reads a factor by its labels. Raw and complex stay out, as
756/// the marker refuses them.
757fn numeric_or_text_check(param: &str) -> String {
758    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("is.numeric({0}) || is.logical({0}) || is.character({0}) || is.factor({0})",
                param))
    })format!(
759        "is.numeric({p}) || is.logical({p}) || is.character({p}) || is.factor({p})",
760        p = param
761    )
762}
763
764impl RTypeCheck {
765    /// Produce the individual checks for this type check.
766    ///
767    /// Returns one or more `RAssertion` values, each one requirement on
768    /// `param` (the R parameter name used in the conditions and messages).
769    fn assertions(&self, param: &str) -> Vec<RAssertion> {
770        let length_one = || {
771            RAssertion::new(
772                param,
773                "must have length 1",
774                ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("length({0}) == 1L", param))
    })format!("length({param}) == 1L"),
775            )
776        };
777        match self {
778            RTypeCheck::ScalarNumeric => ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [RAssertion::new(param, "must be numeric, logical, or raw",
                    numeric_type_check(param)), length_one()]))vec![
779                RAssertion::new(
780                    param,
781                    "must be numeric, logical, or raw",
782                    numeric_type_check(param),
783                ),
784                length_one(),
785            ],
786            RTypeCheck::ScalarLogicalOrInteger => ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [RAssertion::new(param, "must be logical or integer",
                    ::alloc::__export::must_use({
                            ::alloc::fmt::format(format_args!("is.logical({0}) || is.integer({0})",
                                    param))
                        })), length_one()]))vec![
787                RAssertion::new(
788                    param,
789                    "must be logical or integer",
790                    format!("is.logical({p}) || is.integer({p})", p = param),
791                ),
792                length_one(),
793            ],
794            RTypeCheck::VectorLogicalOrInteger => ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [RAssertion::new(param, "must be logical or integer",
                    ::alloc::__export::must_use({
                            ::alloc::fmt::format(format_args!("is.logical({0}) || is.integer({0})",
                                    param))
                        }))]))vec![RAssertion::new(
795                param,
796                "must be logical or integer",
797                format!("is.logical({p}) || is.integer({p})", p = param),
798            )],
799            RTypeCheck::ScalarIntegerWide => ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [RAssertion::new(param, "must be integer or whole-number numeric",
                    integer_vector_wide_check(param)), length_one()]))vec![
800                RAssertion::new(
801                    param,
802                    "must be integer or whole-number numeric",
803                    integer_vector_wide_check(param),
804                ),
805                length_one(),
806            ],
807            RTypeCheck::ScalarNonNeg => ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [RAssertion::new(param, "must be numeric, logical, or raw",
                    numeric_type_check(param)), length_one(),
                RAssertion::new(param, "must be non-negative",
                    ::alloc::__export::must_use({
                            ::alloc::fmt::format(format_args!("is.raw({0}) || {0} >= 0",
                                    param))
                        }))]))vec![
808                RAssertion::new(
809                    param,
810                    "must be numeric, logical, or raw",
811                    numeric_type_check(param),
812                ),
813                length_one(),
814                RAssertion::new(
815                    param,
816                    "must be non-negative",
817                    // raw is always non-negative; guard with is.raw() to avoid
818                    // "comparison not implemented" error for raw values
819                    format!("is.raw({p}) || {p} >= 0", p = param),
820                ),
821            ],
822            RTypeCheck::Scalar(r_type) => ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [RAssertion::new(param,
                    ::alloc::__export::must_use({
                            ::alloc::fmt::format(format_args!("must be {0}", r_type))
                        }),
                    ::alloc::__export::must_use({
                            ::alloc::fmt::format(format_args!("is.{0}({1})", r_type,
                                    param))
                        })), length_one()]))vec![
823                RAssertion::new(
824                    param,
825                    format!("must be {r_type}"),
826                    format!("is.{r_type}({param})"),
827                ),
828                length_one(),
829            ],
830            RTypeCheck::VectorNumeric => ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [RAssertion::new(param, "must be numeric, logical, or raw",
                    numeric_type_check(param))]))vec![RAssertion::new(
831                param,
832                "must be numeric, logical, or raw",
833                numeric_type_check(param),
834            )],
835            RTypeCheck::VectorIntegerStrict => ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [RAssertion::new(param, "must be an integer vector",
                    ::alloc::__export::must_use({
                            ::alloc::fmt::format(format_args!("is.integer({0})", param))
                        }))]))vec![RAssertion::new(
836                param,
837                "must be an integer vector",
838                format!("is.integer({param})"),
839            )],
840            RTypeCheck::VectorIntegerWide => ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [RAssertion::new(param, "must be integer or whole-number numeric",
                    integer_vector_wide_check(param))]))vec![RAssertion::new(
841                param,
842                "must be integer or whole-number numeric",
843                integer_vector_wide_check(param),
844            )],
845            RTypeCheck::Vector(r_type) => ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [RAssertion::new(param,
                    ::alloc::__export::must_use({
                            ::alloc::fmt::format(format_args!("must be {0}", r_type))
                        }),
                    ::alloc::__export::must_use({
                            ::alloc::fmt::format(format_args!("is.{0}({1})", r_type,
                                    param))
                        }))]))vec![RAssertion::new(
846                param,
847                format!("must be {r_type}"),
848                format!("is.{r_type}({param})"),
849            )],
850            RTypeCheck::ScalarNumericOrText => ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [RAssertion::new(param,
                    "must be numeric, logical, character, or factor",
                    numeric_or_text_check(param)), length_one()]))vec![
851                RAssertion::new(
852                    param,
853                    "must be numeric, logical, character, or factor",
854                    numeric_or_text_check(param),
855                ),
856                length_one(),
857            ],
858            RTypeCheck::VectorNumericOrText => ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [RAssertion::new(param,
                    "must be numeric, logical, character, or factor",
                    numeric_or_text_check(param))]))vec![RAssertion::new(
859                param,
860                "must be numeric, logical, character, or factor",
861                numeric_or_text_check(param),
862            )],
863            RTypeCheck::Nullable(inner) => inner
864                .assertions(param)
865                .into_iter()
866                .map(RAssertion::nullable)
867                .collect(),
868            RTypeCheck::List => ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [RAssertion::new(param, "must be a list",
                    ::alloc::__export::must_use({
                            ::alloc::fmt::format(format_args!("is.list({0})", param))
                        }))]))vec![RAssertion::new(
869                param,
870                "must be a list",
871                format!("is.list({param})"),
872            )],
873        }
874    }
875
876    /// Whether the check admits several values: the vector checks and
877    /// [`RTypeCheck::List`], through [`RTypeCheck::Nullable`]. Matched
878    /// exhaustively, so a new check has to say which it is.
879    fn is_vector(&self) -> bool {
880        match self {
881            RTypeCheck::VectorLogicalOrInteger
882            | RTypeCheck::VectorNumeric
883            | RTypeCheck::VectorIntegerStrict
884            | RTypeCheck::VectorIntegerWide
885            | RTypeCheck::Vector(_)
886            | RTypeCheck::VectorNumericOrText
887            | RTypeCheck::List => true,
888            RTypeCheck::ScalarNumeric
889            | RTypeCheck::ScalarLogicalOrInteger
890            | RTypeCheck::ScalarIntegerWide
891            | RTypeCheck::ScalarNonNeg
892            | RTypeCheck::Scalar(_)
893            | RTypeCheck::ScalarNumericOrText => false,
894            RTypeCheck::Nullable(inner) => inner.is_vector(),
895        }
896    }
897
898    /// What an argument of this type must be, in R terms: the `<expected>` of
899    /// a Rust conversion failure's message, `'<p>' must be <expected>: <reason>`
900    /// (#1591).
901    ///
902    /// The R-side checks test one requirement at a time (type, then length);
903    /// a conversion failure names the whole expectation once. Scalars read
904    /// `a single <noun>` (`a single integer`, `a single string`, `TRUE or
905    /// FALSE`), vectors name the R type (`numeric`, `character`, `a list`).
906    /// `AsNumeric` / `AsNumericVec` say `a single number` / `numeric`, the
907    /// value they produce, rather than the inputs they read. `AsCharacter` /
908    /// `AsCharacterVec` likewise say `coercible to a single string` /
909    /// `coercible to character`: their R-side check has already required an
910    /// atomic argument, so what is left to fail is the `as.character()` step
911    /// (a failing method, a non-character or wrong-length result).
912    fn expectation(&self) -> String {
913        match self {
914            RTypeCheck::ScalarNumeric | RTypeCheck::ScalarNumericOrText => "a single number".into(),
915            RTypeCheck::ScalarLogicalOrInteger => "a single logical or integer".into(),
916            RTypeCheck::VectorLogicalOrInteger => "logical or integer".into(),
917            RTypeCheck::ScalarIntegerWide => "a single whole number".into(),
918            RTypeCheck::ScalarNonNeg => "a single non-negative number".into(),
919            RTypeCheck::Scalar(r_type) => match *r_type {
920                "logical" => "TRUE or FALSE".into(),
921                "character" => "a single string".into(),
922                "integer" | "double" => ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("a single {0}", r_type))
    })format!("a single {r_type}"),
923                "atomic" => "coercible to a single string".into(),
924                // `raw`, `complex`.
925                other => ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("a single {0} value", other))
    })format!("a single {other} value"),
926            },
927            RTypeCheck::VectorNumeric | RTypeCheck::VectorNumericOrText => "numeric".into(),
928            RTypeCheck::VectorIntegerStrict => "integer".into(),
929            RTypeCheck::VectorIntegerWide => "integer or whole-number numeric".into(),
930            RTypeCheck::Vector("atomic") => "coercible to character".into(),
931            RTypeCheck::Vector(r_type) => (*r_type).into(),
932            RTypeCheck::Nullable(inner) => match inner.expectation().as_str() {
933                "TRUE or FALSE" => "NULL, TRUE or FALSE".into(),
934                inner => ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("NULL or {0}", inner))
    })format!("NULL or {inner}"),
935            },
936            RTypeCheck::List => "a list".into(),
937        }
938    }
939}
940
941/// The R-facing expectation for a parameter of Rust type `ty`, for the
942/// message of its Rust conversion failure: `'<p>' must be <expected>: ...`
943/// (see [`RTypeCheck::expectation`]).
944///
945/// Derived from the same type classification as the R-side checks, including
946/// the `coerce` widening (`coerced`) and the `Missing<T>` / `Option<T>`
947/// wrappers, so the two paths describe an argument the same way. A few types
948/// without an R-side check still have an R-facing expectation
949/// ([`unchecked_expectation`]): `Either<L, R>`, the `AsFromStr` markers and
950/// tuples. `None` only for an opaque type (a custom `TryFromSexp` type, or an
951/// `Either` with such an arm): the wrapper then asks the type at run time
952/// (`rust_conversion_builder::declared_expectation`: a derived newtype's
953/// declared expectation, an `Either` side by side), else reports
954/// `invalid '<p>' argument: <reason>`.
955pub(crate) fn conversion_expectation(ty: &syn::Type, coerced: bool) -> Option<String> {
956    let ty = crate::miniextendr_fn::get_missing_inner_type(ty).unwrap_or(ty);
957    let Some(check) = r_check_for_type(ty) else {
958        return unchecked_expectation(ty);
959    };
960    let check = if coerced {
961        coerce_widened(check, ty)
962    } else {
963        check
964    };
965    Some(check.expectation())
966}
967
968/// The R-facing expectation of a type that has no R-side check, so its
969/// conversion alone says what it accepts (#1594):
970///
971/// | type | `<expected>` |
972/// |------|--------------|
973/// | `AsFromStr<T>` / `AsFromStrVec<T>` | `a single string` / `character` (the input `T` is parsed from) |
974/// | `Either<L, R>` | `<L> or <R>`, when both sides have one (`a single integer or a single string`) |
975/// | `(A, B, ...)` | `a list of length N` |
976/// | `DataFrame` | `a data frame` |
977/// | `Option<T>` of the above | `NULL or <T>` |
978fn unchecked_expectation(ty: &syn::Type) -> Option<String> {
979    match ty {
980        syn::Type::Tuple(tuple) if !tuple.elems.is_empty() => {
981            Some(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("a list of length {0}",
                tuple.elems.len()))
    })format!("a list of length {}", tuple.elems.len()))
982        }
983        syn::Type::Path(type_path) => {
984            let segment = type_path.path.segments.last()?;
985            match segment.ident.to_string().as_str() {
986                "AsFromStr" => Some("a single string".into()),
987                "AsFromStrVec" => Some("character".into()),
988                "DataFrame" => Some("a data frame".into()),
989                "Either" => {
990                    let left = crate::type_inspect::first_type_argument(segment)?;
991                    let right = crate::type_inspect::second_type_argument(segment)?;
992                    Some(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} or {1}",
                conversion_expectation(left, false)?,
                conversion_expectation(right, false)?))
    })format!(
993                        "{} or {}",
994                        conversion_expectation(left, false)?,
995                        conversion_expectation(right, false)?
996                    ))
997                }
998                "Option" => {
999                    let inner = extract_single_generic_arg(segment)?;
1000                    Some(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("NULL or {0}",
                unchecked_expectation(inner)?))
    })format!("NULL or {}", unchecked_expectation(inner)?))
1001                }
1002                _ => None,
1003            }
1004        }
1005        _ => None,
1006    }
1007}
1008
1009/// The R-facing expectation of a `strict` lossy-integer parameter (`i64`,
1010/// `u64`, `isize`, `usize` and their `Vec` / `Vec<Option<_>>` forms), whose
1011/// Rust conversion accepts only integer and whole-number double input
1012/// (#1594): `a single whole number` / `a single non-negative whole number`
1013/// for a scalar, the vector's usual `integer or whole-number numeric`
1014/// otherwise.
1015pub(crate) fn strict_conversion_expectation(ty: &syn::Type) -> Option<String> {
1016    let scalar = match ty {
1017        syn::Type::Path(tp) => tp.path.segments.last().map(|s| s.ident.to_string()),
1018        _ => None,
1019    };
1020    match scalar.as_deref() {
1021        Some("i64" | "isize") => Some("a single whole number".into()),
1022        Some("u64" | "usize") => Some("a single non-negative whole number".into()),
1023        _ => conversion_expectation(ty, false),
1024    }
1025}
1026
1027/// Keep the R gate in sync with the conversion actually selected for this type.
1028///
1029/// Native `i32` widens to whole-number doubles (plus logical/raw), so its gate
1030/// becomes the lossless whole-number predicate; `Vec<i32>` moves from the
1031/// INTSXP-only gate (#616) to the same wide predicate the other integer vectors
1032/// use. Native `f64` / `Vec<f64>` move from the `is.double` gate to the loose
1033/// numeric predicate. `Option<bool>` has no coercion mapping and stays nullable
1034/// logical.
1035fn coerce_widened(check: RTypeCheck, ty: &syn::Type) -> RTypeCheck {
1036    use crate::miniextendr_fn::CoercionMapping;
1037    match CoercionMapping::from_type(ty) {
1038        Some(CoercionMapping::Bool) => RTypeCheck::ScalarLogicalOrInteger,
1039        Some(CoercionMapping::BoolVec) => RTypeCheck::VectorLogicalOrInteger,
1040        Some(CoercionMapping::NativeInt) => RTypeCheck::ScalarIntegerWide,
1041        Some(CoercionMapping::NativeIntVec) => RTypeCheck::VectorIntegerWide,
1042        Some(CoercionMapping::NativeReal) => RTypeCheck::ScalarNumeric,
1043        Some(CoercionMapping::NativeRealVec) => RTypeCheck::VectorNumeric,
1044        _ => check,
1045    }
1046}
1047
1048/// Map a Rust type to its R-side type check, if applicable.
1049///
1050/// Returns `None` for types that should skip precondition checks (SEXP, Dots, ExternalPtr, etc.).
1051fn r_check_for_type(ty: &syn::Type) -> Option<RTypeCheck> {
1052    match ty {
1053        syn::Type::Path(type_path) => r_check_for_type_path(type_path),
1054        syn::Type::Reference(type_ref) => r_check_for_reference(type_ref),
1055        _ => None,
1056    }
1057}
1058
1059/// Map a `syn::TypePath` to its R-side type check.
1060///
1061/// Handles the most common case: simple types (`i32`, `String`, `bool`),
1062/// generic wrappers (`Vec<T>`, `Option<T>`), map types, and skip types.
1063/// Returns `None` for types that cannot be prechecked from R.
1064fn r_check_for_type_path(type_path: &syn::TypePath) -> Option<RTypeCheck> {
1065    let segment = type_path.path.segments.last()?;
1066    let ident = segment.ident.to_string();
1067
1068    match ident.as_str() {
1069        // Native scalars convert from exactly one SEXPTYPE (`i32::try_from_sexp`
1070        // is INTSXP-only, `f64` REALSXP-only), so the R gate names that storage
1071        // instead of letting a double reach Rust and fail with "expected INTSXP,
1072        // got REALSXP" (#1112). Under `coerce` the gate widens with the conversion
1073        // (see `coerce_widened`).
1074        "i32" => Some(RTypeCheck::Scalar("integer")),
1075        "f64" => Some(RTypeCheck::Scalar("double")),
1076
1077        // Non-native numeric scalars use the multi-source converters (integer,
1078        // double, logical, raw), so the loose predicate matches what Rust accepts.
1079        "f32" | "i8" | "i16" | "i64" | "isize" => Some(RTypeCheck::ScalarNumeric),
1080
1081        // Unsigned numeric scalars (non-negative constraint)
1082        "u16" | "u32" | "u64" | "usize" => Some(RTypeCheck::ScalarNonNeg),
1083
1084        // Logical scalar
1085        "bool" | "Rbool" | "Rboolean" => Some(RTypeCheck::Scalar("logical")),
1086
1087        // Character scalar
1088        "String" | "char" | "PathBuf" => Some(RTypeCheck::Scalar("character")),
1089
1090        // Raw scalar
1091        "u8" => Some(RTypeCheck::Scalar("raw")),
1092
1093        // Complex scalar
1094        "Rcomplex" => Some(RTypeCheck::Scalar("complex")),
1095
1096        // `as.numeric()`-style markers: numbers, text, and factor labels.
1097        "AsNumeric" => Some(RTypeCheck::ScalarNumericOrText),
1098        "AsNumericVec" => Some(RTypeCheck::VectorNumericOrText),
1099
1100        // `as.character()`-style markers: any atomic vector (a list or data
1101        // frame is refused, as `as.character()` would flatten it), with length
1102        // one for the scalar. "'x' must be atomic" is base R's own wording.
1103        "AsCharacter" => Some(RTypeCheck::Scalar("atomic")),
1104        "AsCharacterVec" => Some(RTypeCheck::Vector("atomic")),
1105
1106        // Option<T> → Nullable
1107        "Option" => {
1108            let inner_ty = extract_single_generic_arg(segment)?;
1109            r_check_for_type(inner_ty).map(|inner| RTypeCheck::Nullable(Box::new(inner)))
1110        }
1111
1112        // Vec<T> → Vector (depends on element type)
1113        "Vec" => {
1114            let inner_ty = extract_single_generic_arg(segment)?;
1115            r_check_for_vec_element(inner_ty)
1116        }
1117
1118        // Map types and named list → List
1119        "HashMap" | "BTreeMap" | "NamedList" => Some(RTypeCheck::List),
1120
1121        // List (bare) → List
1122        "List" | "ListMut" => Some(RTypeCheck::List),
1123
1124        // Skip types: SEXP, Dots, Missing, ExternalPtr, RLogical, etc.
1125        // `LazyDots` is `...`, passed as the wrapper's frame (#1892).
1126        "SEXP" | "Dots" | "LazyDots" | "Missing" | "ExternalPtr" | "OwnedProtect" => None,
1127
1128        // Unknown type → skip (let Rust side validate)
1129        _ => None,
1130    }
1131}
1132
1133/// Map a reference type to its R-side type check.
1134///
1135/// Handles `&str` and `&Path` (character scalar), `&[T]` (vector based on element type),
1136/// and `&Dots` (skipped). Returns `None` for unrecognized reference types.
1137fn r_check_for_reference(type_ref: &syn::TypeReference) -> Option<RTypeCheck> {
1138    match type_ref.elem.as_ref() {
1139        // &str → character scalar
1140        syn::Type::Path(tp) => {
1141            let seg = tp.path.segments.last()?;
1142            match seg.ident.to_string().as_str() {
1143                "str" => Some(RTypeCheck::Scalar("character")),
1144                "Path" => Some(RTypeCheck::Scalar("character")),
1145                "Dots" => None,
1146                _ => None,
1147            }
1148        }
1149        // &[T] → vector check based on element type
1150        syn::Type::Slice(slice) => r_check_for_vec_element(&slice.elem),
1151        _ => None,
1152    }
1153}
1154
1155/// Map a `Vec<T>` or `&[T]` element type to the appropriate vector type check.
1156///
1157/// Numeric elements produce `VectorNumeric`, `bool` produces `Vector("logical")`,
1158/// `String` produces `Vector("character")`, etc. Handles nested `Option<T>` for
1159/// nullable element types (e.g., `Vec<Option<String>>` becomes character vector).
1160fn r_check_for_vec_element(elem_ty: &syn::Type) -> Option<RTypeCheck> {
1161    let syn::Type::Path(tp) = elem_ty else {
1162        return None;
1163    };
1164    let seg = tp.path.segments.last()?;
1165    let ident = seg.ident.to_string();
1166
1167    match ident.as_str() {
1168        // `Vec<f64>` / `&[f64]` read the REALSXP payload directly (native path),
1169        // so the gate is `is.double`; `Vec<f32>` coerces from every numeric source.
1170        "f64" => Some(RTypeCheck::Vector("double")),
1171        "f32" => Some(RTypeCheck::VectorNumeric),
1172
1173        // `Vec<i32>` / `&[i32]` is the *only* INTSXP-only integer vector (issue
1174        // #616): its inbound conversion uses the native `RNativeType` path
1175        // (`impl_vec_try_from_sexp_native!(i32)` in `from_r/collections.rs`),
1176        // which rejects REALSXP outright. So the R gate is `is.integer` — a
1177        // `double` like `c(1, 2)` fails cleanly at the boundary instead of
1178        // producing a cryptic "expected INTSXP, got REALSXP". `u8` is handled
1179        // below as a raw vector.
1180        "i32" => Some(RTypeCheck::VectorIntegerStrict),
1181
1182        // Every other integer-element vector (`i8`/`i16`/`u16`/`u32` and the
1183        // 64-bit family `i64`/`u64`/`isize`/`usize`) uses the coercing inbound
1184        // path (`impl_vec_try_from_sexp_numeric!` → `from_numeric_vec_with`),
1185        // which accepts INTSXP/REALSXP/RAWSXP/LGLSXP and rejects fractional
1186        // doubles element-wise via `f64: TryCoerce<T>` (the `self.fract() != 0`
1187        // check in `coerce.rs`). The R gate mirrors that: accept integer/logical/
1188        // raw + whole-number doubles, reject fractional. (64-bit ints also arrive
1189        // as REALSXP since R has no native 64-bit integer type.)
1190        "i8" | "i16" | "u16" | "u32" | "i64" | "u64" | "isize" | "usize" => {
1191            Some(RTypeCheck::VectorIntegerWide)
1192        }
1193
1194        // Logical vector
1195        "bool" => Some(RTypeCheck::Vector("logical")),
1196
1197        // Character vector
1198        "String" => Some(RTypeCheck::Vector("character")),
1199
1200        // Raw vector
1201        "u8" => Some(RTypeCheck::Vector("raw")),
1202
1203        // Complex vector
1204        "Rcomplex" => Some(RTypeCheck::Vector("complex")),
1205
1206        // Vec<Option<T>> — e.g., Vec<Option<String>> for nullable strings
1207        "Option" => {
1208            let inner = extract_single_generic_arg(seg)?;
1209            // Vec<Option<String>> → character, Vec<Option<i32>> → numeric, etc.
1210            r_check_for_vec_element(inner)
1211        }
1212
1213        _ => None,
1214    }
1215}
1216
1217/// Extract the single generic type argument from a path segment.
1218///
1219/// e.g., `Option<String>` → `String`, `Vec<i32>` → `i32`
1220fn extract_single_generic_arg(segment: &syn::PathSegment) -> Option<&syn::Type> {
1221    if let syn::PathArguments::AngleBracketed(ref args) = segment.arguments
1222        && let Some(syn::GenericArgument::Type(ty)) = args.args.first()
1223    {
1224        return Some(ty);
1225    }
1226    None
1227}
1228
1229/// A parameter whose Rust type is not in the static type table.
1230///
1231/// Currently, fallback params are recorded but no R-side validation is generated
1232/// for them -- the Rust-side conversion handles type errors with its own messages.
1233#[allow(dead_code)] // Read in tests
1234pub struct FallbackParam {
1235    /// R-normalized parameter name (e.g., `_dots` becomes `dots`).
1236    pub r_name: String,
1237}
1238
1239/// Output of precondition analysis for a function's parameters.
1240///
1241/// Holds the R-side checks for known types and a list of parameters with
1242/// unknown types that were not statically prechecked.
1243pub struct PreconditionOutput {
1244    /// The checks, in parameter order: each parameter's class checks, its
1245    /// type checks, then its NA check ([`ExplicitChecks`]). Rendered by
1246    /// [`PreconditionOutput::guards`].
1247    assertions: Vec<RAssertion>,
1248    /// Parameters with unknown custom types that were not prechecked.
1249    #[allow(dead_code)] // Read in tests
1250    pub fallback_params: Vec<FallbackParam>,
1251}
1252
1253impl PreconditionOutput {
1254    /// One guard line per check, raising an argument error through
1255    /// `.miniextendr_arg_error` when it fails (#1591); empty when there is
1256    /// nothing to check.
1257    ///
1258    /// `call` is the call every failure is attributed to: `None` for the
1259    /// wrapper's own call (the helper's default, the call `stopifnot()`
1260    /// reported), or `.mx_call` for a `call = caller` wrapper, which binds the
1261    /// caller's matched call before the checks run (#1548).
1262    pub fn guards(&self, call: Option<&str>) -> Vec<String> {
1263        self.assertions.iter().map(|a| a.to_guard(call)).collect()
1264    }
1265}
1266
1267/// Returns `true` for types that should never get a fallback precheck.
1268///
1269/// These types are either handled specially by the FFI layer (`SEXP`),
1270/// consumed by the macro infrastructure (`Dots`, `LazyDots`, `Missing`), or
1271/// managed internally (`ExternalPtr`, `OwnedProtect`).
1272fn is_skip_type(ident: &str) -> bool {
1273    #[allow(non_exhaustive_omitted_patterns)] match ident {
    "SEXP" | "Dots" | "LazyDots" | "Missing" | "ExternalPtr" | "OwnedProtect"
        => true,
    _ => false,
}matches!(
1274        ident,
1275        "SEXP" | "Dots" | "LazyDots" | "Missing" | "ExternalPtr" | "OwnedProtect"
1276    )
1277}
1278
1279/// Returns `true` if a type is unknown to the static type table and should
1280/// be recorded as a fallback parameter.
1281///
1282/// Returns `false` for skip types (SEXP, Dots, etc.) and reference types
1283/// (which are handled by the static table or skipped).
1284fn needs_fallback(ty: &syn::Type) -> bool {
1285    match ty {
1286        syn::Type::Path(tp) => {
1287            let Some(seg) = tp.path.segments.last() else {
1288                return false;
1289            };
1290            !is_skip_type(&seg.ident.to_string())
1291        }
1292        // References (&str, &[T], &Dots) are handled by static table or skipped
1293        syn::Type::Reference(_) => false,
1294        _ => false,
1295    }
1296}
1297
1298/// Build precondition checks for a function's parameters.
1299///
1300/// Returns the checks for known types (render them with
1301/// [`PreconditionOutput::guards`]) and the parameters with unknown custom
1302/// types (`fallback_params`). The guards read:
1303/// ```r
1304/// if (!isTRUE(is.integer(a))) .miniextendr_arg_error("a", "must be integer")
1305/// if (!isTRUE(length(a) == 1L)) .miniextendr_arg_error("a", "must have length 1")
1306/// ```
1307///
1308/// Skips:
1309/// - `self`/`&self`/`&mut self` (receiver args)
1310/// - Parameters in `skip_params` (e.g., match_arg params already validated)
1311/// - Skip types (SEXP, Dots, ExternalPtr, etc.)
1312/// - The type-derived checks of the parameters in `opts.unchecked`
1313///
1314/// A parameter's [`ExplicitChecks`] are never skipped: its class checks
1315/// (`inherits`, then `not_inherits`) come before its type checks, which take
1316/// the `inherits` message when there is one, and its NA check after them.
1317pub fn build_precondition_checks(
1318    inputs: &syn::punctuated::Punctuated<syn::FnArg, ::syn::token::Commasyn::Token![,]>,
1319    skip_params: &HashSet<String>,
1320    opts: &PreconditionOptions,
1321) -> PreconditionOutput {
1322    let mut assertions = Vec::new();
1323    let mut fallback_params = Vec::new();
1324
1325    for arg in inputs {
1326        // Skip receiver (self/&self/&mut self)
1327        let syn::FnArg::Typed(pt) = arg else {
1328            continue;
1329        };
1330
1331        // `NArgs` is no R formal: the wrapper passes `nargs()`, which no
1332        // check needs to look at (#1860). Nor is a `LazyDots` parameter's name:
1333        // its formal is `...`, passed unforced as the wrapper's frame (#1892).
1334        if crate::type_inspect::is_nargs_marker(&pt.ty)
1335            || crate::type_inspect::is_lazy_dots_marker(&pt.ty)
1336        {
1337            continue;
1338        }
1339
1340        // Extract parameter name
1341        let syn::Pat::Ident(pat_ident) = pt.pat.as_ref() else {
1342            continue;
1343        };
1344
1345        // The parameter's R formal (#1842): what the checks read and name
1346        let r_name = opts.formals.of_ident(&pat_ident.ident);
1347        let explicit = opts.explicit.get(&r_name);
1348
1349        // The class checks first, so a value of the wrong class gets their
1350        // message whatever its type.
1351        if let Some(checks) = explicit {
1352            assertions.extend(checks.class_assertions(&r_name, pt.ty.as_ref()));
1353        }
1354
1355        // Type-derived checks: skipped for match_arg params (already validated
1356        // by match.arg()) and for the parameters that drop them.
1357        if !opts.unchecked.contains(&r_name) && !skip_params.contains(&r_name) {
1358            // Preserve the ordinary input domain and include any coercion extensions.
1359            if let Some(mut check) = r_check_for_type(pt.ty.as_ref()) {
1360                if opts.is_coerced(&r_name) {
1361                    check = coerce_widened(check, pt.ty.as_ref());
1362                }
1363                let mut type_assertions = check.assertions(&r_name);
1364                // An `inherits` message covers the type checks too.
1365                if let Some(message) = explicit.and_then(ExplicitChecks::type_check_message) {
1366                    for a in &mut type_assertions {
1367                        a.message = Some(message.clone());
1368                    }
1369                }
1370                assertions.extend(type_assertions);
1371            } else if needs_fallback(pt.ty.as_ref()) {
1372                // Unknown type → record for potential future validation
1373                fallback_params.push(FallbackParam {
1374                    r_name: r_name.clone(),
1375                });
1376            }
1377        }
1378
1379        if let Some(checks) = explicit {
1380            assertions.extend(checks.value_assertions(
1381                &r_name,
1382                pt.ty.as_ref(),
1383                skip_params.contains(&r_name),
1384            ));
1385        }
1386    }
1387
1388    PreconditionOutput {
1389        assertions,
1390        fallback_params,
1391    }
1392}
1393
1394#[cfg(test)]
1395mod tests {
1396    use super::*;
1397
1398    /// Helper to parse a type string into syn::Type
1399    fn parse_type(s: &str) -> syn::Type {
1400        syn::parse_str(s).unwrap()
1401    }
1402
1403    /// Helper to get assertions for a type.
1404    fn assertions_for(ty_str: &str, param: &str) -> Vec<RAssertion> {
1405        let ty = parse_type(ty_str);
1406        r_check_for_type(&ty).unwrap().assertions(param)
1407    }
1408
1409    #[test]
1410    fn scalar_numeric_produces_two_assertions() {
1411        let asserts = assertions_for("i64", "x");
1412        assert_eq!(asserts.len(), 2);
1413        assert_eq!(asserts[0].message(), "'x' must be numeric, logical, or raw");
1414        assert_eq!(
1415            asserts[0].condition,
1416            "is.numeric(x) || is.logical(x) || is.raw(x)"
1417        );
1418        assert_eq!(asserts[1].message(), "'x' must have length 1");
1419        assert_eq!(asserts[1].condition, "length(x) == 1L");
1420    }
1421
1422    #[test]
1423    fn native_scalars_use_their_storage_gate() {
1424        // `i32` / `f64` convert from one SEXPTYPE, so the gate says so (#1112).
1425        let asserts = assertions_for("i32", "x");
1426        assert_eq!(asserts.len(), 2);
1427        assert_eq!(asserts[0].message(), "'x' must be integer");
1428        assert_eq!(asserts[0].condition, "is.integer(x)");
1429        let asserts = assertions_for("f64", "x");
1430        assert_eq!(asserts[0].message(), "'x' must be double");
1431        assert_eq!(asserts[0].condition, "is.double(x)");
1432        let asserts = assertions_for("Vec<f64>", "x");
1433        assert_eq!(asserts.len(), 1);
1434        assert_eq!(asserts[0].condition, "is.double(x)");
1435        let asserts = assertions_for("&[f64]", "x");
1436        assert_eq!(asserts[0].condition, "is.double(x)");
1437    }
1438
1439    #[test]
1440    fn all_signed_numeric_types_use_scalar_numeric() {
1441        for ty_str in &["f32", "i8", "i16", "i64", "isize"] {
1442            let asserts = assertions_for(ty_str, "x");
1443            assert_eq!(asserts.len(), 2, "{} should produce 2 assertions", ty_str);
1444            assert!(
1445                asserts[0].condition.contains("is.numeric(x)"),
1446                "{} type check",
1447                ty_str
1448            );
1449            assert!(
1450                asserts[0].condition.contains("is.logical(x)"),
1451                "{} accepts logical",
1452                ty_str
1453            );
1454            assert!(
1455                asserts[0].condition.contains("is.raw(x)"),
1456                "{} accepts raw",
1457                ty_str
1458            );
1459        }
1460    }
1461
1462    #[test]
1463    fn scalar_non_neg_produces_three_assertions() {
1464        let asserts = assertions_for("u32", "n");
1465        assert_eq!(asserts.len(), 3);
1466        assert_eq!(asserts[0].message(), "'n' must be numeric, logical, or raw");
1467        assert_eq!(asserts[1].message(), "'n' must have length 1");
1468        assert_eq!(asserts[2].message(), "'n' must be non-negative");
1469        assert_eq!(asserts[2].condition, "is.raw(n) || n >= 0");
1470    }
1471
1472    #[test]
1473    fn all_unsigned_types_use_scalar_non_neg() {
1474        for ty_str in &["u16", "u32", "u64", "usize"] {
1475            let asserts = assertions_for(ty_str, "x");
1476            assert_eq!(asserts.len(), 3, "{} should produce 3 assertions", ty_str);
1477            assert!(
1478                asserts[2].condition.contains(">= 0"),
1479                "{} non-neg check",
1480                ty_str
1481            );
1482        }
1483    }
1484
1485    #[test]
1486    fn scalar_logical() {
1487        let asserts = assertions_for("bool", "x");
1488        assert_eq!(asserts.len(), 2);
1489        assert_eq!(asserts[0].message(), "'x' must be logical");
1490        assert_eq!(asserts[0].condition, "is.logical(x)");
1491        assert_eq!(asserts[1].condition, "length(x) == 1L");
1492    }
1493
1494    #[test]
1495    fn scalar_character() {
1496        for ty_str in &["String", "char", "PathBuf"] {
1497            let asserts = assertions_for(ty_str, "s");
1498            assert_eq!(asserts.len(), 2);
1499            assert_eq!(asserts[0].message(), "'s' must be character");
1500            assert_eq!(asserts[0].condition, "is.character(s)");
1501        }
1502    }
1503
1504    #[test]
1505    fn ref_str() {
1506        let ty: syn::Type = syn::parse_str("& str").unwrap();
1507        let asserts = r_check_for_type(&ty).unwrap().assertions("s");
1508        assert_eq!(asserts.len(), 2);
1509        assert_eq!(asserts[0].condition, "is.character(s)");
1510    }
1511
1512    #[test]
1513    fn scalar_raw() {
1514        let asserts = assertions_for("u8", "x");
1515        assert_eq!(asserts.len(), 2);
1516        assert_eq!(asserts[0].message(), "'x' must be raw");
1517        assert_eq!(asserts[0].condition, "is.raw(x)");
1518    }
1519
1520    #[test]
1521    fn vector_float_stays_loose() {
1522        // `Vec<f32>` coerces from every numeric source, so it keeps the loose predicate.
1523        let asserts = assertions_for("Vec<f32>", "x");
1524        assert_eq!(asserts.len(), 1);
1525        assert_eq!(
1526            asserts[0].condition,
1527            "is.numeric(x) || is.logical(x) || is.raw(x)"
1528        );
1529    }
1530
1531    #[test]
1532    fn vector_i32_is_intsxp_strict() {
1533        // `Vec<i32>` is the only INTSXP-only integer vector: the native inbound
1534        // conversion rejects REALSXP, so the R gate is `is.integer(x)` — a clean
1535        // boundary rejection for any double (issue #616).
1536        let asserts = assertions_for("Vec<i32>", "x");
1537        assert_eq!(asserts.len(), 1);
1538        assert_eq!(asserts[0].condition, "is.integer(x)");
1539        assert_eq!(asserts[0].message(), "'x' must be an integer vector");
1540    }
1541
1542    #[test]
1543    fn vector_integer_wide_is_lossless() {
1544        // Every non-i32 integer element type uses the coercing inbound path that
1545        // accepts whole REALSXP and rejects fractional → lossless whole-number gate.
1546        for ty_str in &[
1547            "Vec<i8>",
1548            "Vec<i16>",
1549            "Vec<u16>",
1550            "Vec<u32>",
1551            "Vec<i64>",
1552            "Vec<u64>",
1553            "Vec<isize>",
1554            "Vec<usize>",
1555        ] {
1556            let asserts = assertions_for(ty_str, "x");
1557            assert_eq!(asserts.len(), 1, "{} should produce 1 assertion", ty_str);
1558            assert_eq!(
1559                asserts[0].condition,
1560                "is.integer(x) || is.logical(x) || is.raw(x) || \
1561                 (is.numeric(x) && all(is.na(x) | x == trunc(x)))",
1562                "{}",
1563                ty_str
1564            );
1565            assert_eq!(
1566                asserts[0].message(),
1567                "'x' must be integer or whole-number numeric"
1568            );
1569        }
1570    }
1571
1572    #[test]
1573    fn slice_int_is_strict() {
1574        let ty: syn::Type = syn::parse_str("& [i32]").unwrap();
1575        let asserts = r_check_for_type(&ty).unwrap().assertions("x");
1576        assert_eq!(asserts.len(), 1);
1577        assert_eq!(asserts[0].condition, "is.integer(x)");
1578    }
1579
1580    #[test]
1581    fn coerced_native_int_widens_to_whole_number_gate() {
1582        let ty = parse_type("i32");
1583        let asserts = coerce_widened(r_check_for_type(&ty).unwrap(), &ty).assertions("x");
1584        assert_eq!(asserts.len(), 2);
1585        assert_eq!(
1586            asserts[0].message(),
1587            "'x' must be integer or whole-number numeric"
1588        );
1589        assert!(asserts[0].condition.contains("x == trunc(x)"));
1590        assert_eq!(asserts[1].condition, "length(x) == 1L");
1591
1592        let ty = parse_type("Vec<i32>");
1593        let asserts = coerce_widened(r_check_for_type(&ty).unwrap(), &ty).assertions("x");
1594        assert_eq!(asserts.len(), 1);
1595        assert!(asserts[0].condition.contains("x == trunc(x)"));
1596
1597        // Native doubles move from `is.double` to the loose numeric gate;
1598        // borrowed slices have no mapping and stay strict.
1599        for name in ["f64", "Vec<f64>"] {
1600            let ty = parse_type(name);
1601            let actual = coerce_widened(r_check_for_type(&ty).unwrap(), &ty).assertions("x");
1602            assert_eq!(
1603                actual[0].condition, "is.numeric(x) || is.logical(x) || is.raw(x)",
1604                "{name}"
1605            );
1606        }
1607        let ty = parse_type("&[i32]");
1608        let actual = coerce_widened(r_check_for_type(&ty).unwrap(), &ty).assertions("x");
1609        assert_eq!(actual[0].condition, "is.integer(x)");
1610    }
1611
1612    #[test]
1613    fn coerced_numeric_and_optional_checks_are_preserved() {
1614        for name in ["Vec<u16>", "Vec<f32>", "Option<bool>"] {
1615            let ty = parse_type(name);
1616            let expected = r_check_for_type(&ty).unwrap().assertions("x");
1617            let actual = coerce_widened(r_check_for_type(&ty).unwrap(), &ty).assertions("x");
1618            assert_eq!(actual.len(), expected.len());
1619            for (actual, expected) in actual.iter().zip(&expected) {
1620                assert_eq!(actual.condition, expected.condition);
1621            }
1622        }
1623    }
1624
1625    #[test]
1626    fn coerced_bool_checks_accept_logical_and_integer() {
1627        for name in ["bool", "Vec<bool>"] {
1628            let ty = parse_type(name);
1629            let actual = coerce_widened(r_check_for_type(&ty).unwrap(), &ty).assertions("x");
1630            assert_eq!(actual[0].condition, "is.logical(x) || is.integer(x)");
1631            assert_eq!(actual.len(), if name == "bool" { 2 } else { 1 });
1632        }
1633    }
1634
1635    #[test]
1636    fn build_checks_coerced_param_preserves_whole_number_check() {
1637        let sig: syn::Signature = syn::parse_str("fn f(x: Vec<u16>)").unwrap();
1638        let mut coerce_params = HashSet::new();
1639        coerce_params.insert("x".to_string());
1640        let opts = PreconditionOptions {
1641            coerce_params,
1642            ..Default::default()
1643        };
1644        let output = build_precondition_checks(&sig.inputs, &HashSet::new(), &opts);
1645        let joined = output.guards(None).join("\n");
1646        assert!(joined.contains("is.integer(x)"));
1647        assert!(joined.contains("trunc(x)"));
1648    }
1649
1650    /// Every check is one guard raising the shared argument error (#1591):
1651    /// the wrapper's own call by default, the captured `.mx_call` under
1652    /// `call = caller` (#1548). The requirement leaves out the parameter name,
1653    /// which the helper puts in front.
1654    #[test]
1655    fn guards_raise_the_argument_error_with_the_given_call() {
1656        let sig: syn::Signature = syn::parse_str("fn f(n: i32, xs: Vec<f64>)").unwrap();
1657        let output = build_precondition_checks(
1658            &sig.inputs,
1659            &HashSet::new(),
1660            &PreconditionOptions::default(),
1661        );
1662        assert_eq!(
1663            output.guards(None),
1664            vec![
1665                "if (!isTRUE(is.integer(n))) .miniextendr_arg_error(\"n\", \"must be integer\")",
1666                "if (!isTRUE(length(n) == 1L)) .miniextendr_arg_error(\"n\", \"must have length 1\")",
1667                "if (!isTRUE(is.double(xs))) .miniextendr_arg_error(\"xs\", \"must be double\")",
1668            ]
1669        );
1670        assert_eq!(
1671            output.guards(Some(".mx_call")),
1672            vec![
1673                "if (!isTRUE(is.integer(n))) .miniextendr_arg_error(\"n\", \"must be integer\", .mx_call)",
1674                "if (!isTRUE(length(n) == 1L)) .miniextendr_arg_error(\"n\", \"must have length 1\", .mx_call)",
1675                "if (!isTRUE(is.double(xs))) .miniextendr_arg_error(\"xs\", \"must be double\", .mx_call)",
1676            ]
1677        );
1678        let no_params: syn::Signature = syn::parse_str("fn g()").unwrap();
1679        let empty = build_precondition_checks(
1680            &no_params.inputs,
1681            &HashSet::new(),
1682            &PreconditionOptions::default(),
1683        );
1684        assert!(empty.guards(None).is_empty());
1685        assert!(empty.guards(Some(".mx_call")).is_empty());
1686    }
1687
1688    /// The conversion-side expectation (#1591) reads the same classification:
1689    /// scalars name one value, vectors the R type, `Option` adds `NULL`,
1690    /// `coerce` widens with the gate, and types without a check have none.
1691    #[test]
1692    fn conversion_expectation_follows_the_type_check() {
1693        let exp = |ty: &str, coerced: bool| conversion_expectation(&parse_type(ty), coerced);
1694        let some = |s: &str| Some(s.to_string());
1695        assert_eq!(exp("AsNumericVec", false), some("numeric"));
1696        assert_eq!(exp("AsNumeric", false), some("a single number"));
1697        assert_eq!(
1698            exp("Option<AsNumeric>", false),
1699            some("NULL or a single number")
1700        );
1701        assert_eq!(exp("i32", false), some("a single integer"));
1702        assert_eq!(exp("i32", true), some("a single whole number"));
1703        assert_eq!(exp("f64", false), some("a single double"));
1704        assert_eq!(exp("f64", true), some("a single number"));
1705        assert_eq!(exp("i64", false), some("a single number"));
1706        assert_eq!(exp("u32", false), some("a single non-negative number"));
1707        assert_eq!(exp("bool", false), some("TRUE or FALSE"));
1708        assert_eq!(exp("Option<bool>", false), some("NULL, TRUE or FALSE"));
1709        assert_eq!(exp("bool", true), some("a single logical or integer"));
1710        assert_eq!(exp("String", false), some("a single string"));
1711        assert_eq!(exp("&str", false), some("a single string"));
1712        assert_eq!(exp("u8", false), some("a single raw value"));
1713        assert_eq!(exp("Rcomplex", false), some("a single complex value"));
1714        assert_eq!(exp("Vec<i32>", false), some("integer"));
1715        assert_eq!(
1716            exp("Vec<i32>", true),
1717            some("integer or whole-number numeric")
1718        );
1719        assert_eq!(
1720            exp("Vec<u16>", false),
1721            some("integer or whole-number numeric")
1722        );
1723        assert_eq!(exp("Vec<f64>", false), some("double"));
1724        assert_eq!(exp("&[f64]", false), some("double"));
1725        assert_eq!(exp("Vec<Option<String>>", false), some("character"));
1726        assert_eq!(exp("Vec<bool>", true), some("logical or integer"));
1727        assert_eq!(exp("HashMap<String, i32>", false), some("a list"));
1728        assert_eq!(exp("Missing<f64>", false), some("a single double"));
1729        assert_eq!(
1730            exp("AsCharacter", false),
1731            some("coercible to a single string")
1732        );
1733        assert_eq!(exp("AsCharacterVec", false), some("coercible to character"));
1734        assert_eq!(
1735            exp("Option<AsCharacter>", false),
1736            some("NULL or coercible to a single string")
1737        );
1738        // Types without an R-side check that still say what they accept.
1739        assert_eq!(exp("AsFromStr<IpAddr>", false), some("a single string"));
1740        assert_eq!(exp("AsFromStrVec<i32>", false), some("character"));
1741        assert_eq!(
1742            exp("Option<AsFromStr<IpAddr>>", false),
1743            some("NULL or a single string")
1744        );
1745        assert_eq!(
1746            exp("Either<i32, String>", false),
1747            some("a single integer or a single string")
1748        );
1749        assert_eq!(
1750            exp("Either<f64, Vec<i32>>", false),
1751            some("a single double or integer")
1752        );
1753        assert_eq!(
1754            exp("Either<bool, Either<i32, String>>", false),
1755            some("TRUE or FALSE or a single integer or a single string")
1756        );
1757        assert_eq!(
1758            exp("Option<Either<i32, String>>", false),
1759            some("NULL or a single integer or a single string")
1760        );
1761        assert_eq!(
1762            exp("Option<Either<f64, String>>", false),
1763            some("NULL or a single double or a single string")
1764        );
1765        assert_eq!(
1766            exp("Option<Either<AsNumericVec, DataFrame>>", false),
1767            some("NULL or numeric or a data frame")
1768        );
1769        assert_eq!(exp("(i32, String)", false), some("a list of length 2"));
1770        assert_eq!(exp("DataFrame", false), some("a data frame"));
1771        assert_eq!(
1772            exp("Option<DataFrame>", false),
1773            some("NULL or a data frame")
1774        );
1775        // Choice parameters, including the `Missing` / `Option` / `Either`
1776        // layers of #1551, have no static expectation: their conversion error
1777        // supplies it at run time (`one of "a", "b"`). An opaque side leaves
1778        // the whole `Either` without a static one; the wrapper words it side
1779        // by side at run time.
1780        for unknown in [
1781            "Hyperparams",
1782            "Either<i32, Hyperparams>",
1783            "SEXP",
1784            "()",
1785            "Mode",
1786            "Option<Mode>",
1787            "Missing<Option<Mode>>",
1788            "Missing<Vec<Mode>>",
1789            "Either<Route, DataFrame>",
1790            "Option<Either<Route, DataFrame>>",
1791            "Missing<Option<Either<Mode, List>>>",
1792            "Either<Vec<Mode>, DataFrame>",
1793            "Option<Either<Vec<Mode>, DataFrame>>",
1794        ] {
1795            assert_eq!(exp(unknown, false), None, "{unknown}");
1796        }
1797        // A `choices(...)` `several_ok` string list with another kind of value
1798        // (#1612).
1799        assert_eq!(
1800            exp("Either<Vec<String>, f64>", false),
1801            some("character or a single double")
1802        );
1803    }
1804
1805    #[test]
1806    fn strict_expectation_names_a_whole_number() {
1807        let exp = |ty: &str| strict_conversion_expectation(&parse_type(ty));
1808        let some = |s: &str| Some(s.to_string());
1809        assert_eq!(exp("i64"), some("a single whole number"));
1810        assert_eq!(exp("isize"), some("a single whole number"));
1811        assert_eq!(exp("u64"), some("a single non-negative whole number"));
1812        assert_eq!(exp("usize"), some("a single non-negative whole number"));
1813        assert_eq!(exp("Vec<i64>"), some("integer or whole-number numeric"));
1814        assert_eq!(
1815            exp("Vec<Option<u64>>"),
1816            some("integer or whole-number numeric")
1817        );
1818    }
1819
1820    #[test]
1821    fn vector_character() {
1822        let asserts = assertions_for("Vec<String>", "x");
1823        assert_eq!(asserts.len(), 1);
1824        assert_eq!(asserts[0].condition, "is.character(x)");
1825    }
1826
1827    #[test]
1828    fn vector_optional_string() {
1829        let asserts = assertions_for("Vec<Option<String>>", "x");
1830        assert_eq!(asserts.len(), 1);
1831        assert_eq!(asserts[0].condition, "is.character(x)");
1832    }
1833
1834    #[test]
1835    fn slice_u8() {
1836        let ty: syn::Type = syn::parse_str("& [u8]").unwrap();
1837        let asserts = r_check_for_type(&ty).unwrap().assertions("x");
1838        assert_eq!(asserts.len(), 1);
1839        assert_eq!(asserts[0].condition, "is.raw(x)");
1840    }
1841
1842    #[test]
1843    fn nullable_wraps_inner_assertions() {
1844        let asserts = assertions_for("Option<i64>", "x");
1845        assert_eq!(asserts.len(), 2);
1846        assert_eq!(
1847            asserts[0].message(),
1848            "'x' must be NULL or numeric, logical, or raw"
1849        );
1850        assert_eq!(
1851            asserts[0].condition,
1852            "is.null(x) || is.numeric(x) || is.logical(x) || is.raw(x)"
1853        );
1854        let asserts = assertions_for("Option<i32>", "x");
1855        assert_eq!(asserts[0].condition, "is.null(x) || is.integer(x)");
1856        assert_eq!(asserts[1].message(), "'x' must be NULL or have length 1");
1857        assert_eq!(asserts[1].condition, "is.null(x) || length(x) == 1L");
1858    }
1859
1860    #[test]
1861    fn nullable_character() {
1862        let asserts = assertions_for("Option<String>", "s");
1863        assert_eq!(asserts.len(), 2);
1864        assert_eq!(asserts[0].message(), "'s' must be NULL or character");
1865        assert_eq!(asserts[0].condition, "is.null(s) || is.character(s)");
1866        assert_eq!(asserts[1].message(), "'s' must be NULL or have length 1");
1867    }
1868
1869    #[test]
1870    fn as_numeric_markers_admit_numbers_text_and_factors() {
1871        let condition = "is.numeric(x) || is.logical(x) || is.character(x) || is.factor(x)";
1872        let asserts = assertions_for("AsNumeric", "x");
1873        assert_eq!(asserts.len(), 2);
1874        assert_eq!(
1875            asserts[0].message(),
1876            "'x' must be numeric, logical, character, or factor"
1877        );
1878        assert_eq!(asserts[0].condition, condition);
1879        assert_eq!(asserts[1].message(), "'x' must have length 1");
1880        assert_eq!(asserts[1].condition, "length(x) == 1L");
1881
1882        // Path-qualified spellings resolve by their last segment.
1883        let asserts = assertions_for("miniextendr_api::AsNumericVec", "x");
1884        assert_eq!(asserts.len(), 1);
1885        assert_eq!(
1886            asserts[0].message(),
1887            "'x' must be numeric, logical, character, or factor"
1888        );
1889        assert_eq!(asserts[0].condition, condition);
1890    }
1891
1892    #[test]
1893    fn optional_as_numeric_is_nullable() {
1894        let asserts = assertions_for("Option<AsNumeric>", "x");
1895        assert_eq!(asserts.len(), 2);
1896        assert_eq!(
1897            asserts[0].message(),
1898            "'x' must be NULL or numeric, logical, character, or factor"
1899        );
1900        assert_eq!(
1901            asserts[0].condition,
1902            "is.null(x) || is.numeric(x) || is.logical(x) || is.character(x) || is.factor(x)"
1903        );
1904        assert_eq!(asserts[1].message(), "'x' must be NULL or have length 1");
1905        let asserts = assertions_for("Option<AsNumericVec>", "x");
1906        assert_eq!(asserts.len(), 1);
1907        assert!(asserts[0].condition.starts_with("is.null(x) || "));
1908    }
1909
1910    #[test]
1911    fn as_character_markers_admit_atomic_vectors() {
1912        let asserts = assertions_for("AsCharacter", "x");
1913        assert_eq!(asserts.len(), 2);
1914        assert_eq!(asserts[0].message(), "'x' must be atomic");
1915        assert_eq!(asserts[0].condition, "is.atomic(x)");
1916        assert_eq!(asserts[1].message(), "'x' must have length 1");
1917        assert_eq!(asserts[1].condition, "length(x) == 1L");
1918
1919        // Path-qualified spellings resolve by their last segment.
1920        let asserts = assertions_for("miniextendr_api::AsCharacterVec", "x");
1921        assert_eq!(asserts.len(), 1);
1922        assert_eq!(asserts[0].message(), "'x' must be atomic");
1923        assert_eq!(asserts[0].condition, "is.atomic(x)");
1924    }
1925
1926    #[test]
1927    fn optional_as_character_is_nullable() {
1928        let asserts = assertions_for("Option<AsCharacter>", "x");
1929        assert_eq!(asserts.len(), 2);
1930        assert_eq!(asserts[0].message(), "'x' must be NULL or atomic");
1931        assert_eq!(asserts[0].condition, "is.null(x) || is.atomic(x)");
1932        assert_eq!(asserts[1].message(), "'x' must be NULL or have length 1");
1933        let asserts = assertions_for("Option<AsCharacterVec>", "x");
1934        assert_eq!(asserts.len(), 1);
1935        assert_eq!(asserts[0].message(), "'x' must be NULL or atomic");
1936        assert_eq!(asserts[0].condition, "is.null(x) || is.atomic(x)");
1937    }
1938
1939    #[test]
1940    fn as_character_vec_with_no_na_checks_type_then_na() {
1941        let out = explicit_output("fn f(x: AsCharacterVec)", &[("x", no_na())], false);
1942        assert_eq!(
1943            out.guards(None),
1944            vec![
1945                "if (!isTRUE(is.atomic(x))) .miniextendr_arg_error(\"x\", \"must be atomic\")",
1946                "if (!isTRUE(!anyNA(x))) .miniextendr_arg_error(\"x\", \"must not contain NA\")",
1947            ]
1948        );
1949    }
1950
1951    #[test]
1952    fn coerce_keeps_the_as_numeric_gate() {
1953        // `AsNumeric` has no coercion mapping: `coerce` leaves its gate alone.
1954        let ty = parse_type("AsNumericVec");
1955        let check = coerce_widened(r_check_for_type(&ty).unwrap(), &ty);
1956        let asserts = check.assertions("x");
1957        assert_eq!(asserts.len(), 1);
1958        assert!(asserts[0].condition.contains("is.factor(x)"));
1959    }
1960
1961    #[test]
1962    fn map_types() {
1963        for ty_str in &["HashMap<String, i32>", "BTreeMap<String, f64>"] {
1964            let ty = parse_type(ty_str);
1965            let asserts = r_check_for_type(&ty).unwrap().assertions("x");
1966            assert_eq!(asserts.len(), 1);
1967            assert_eq!(asserts[0].condition, "is.list(x)");
1968        }
1969    }
1970
1971    #[test]
1972    fn skip_types() {
1973        for ty_str in &["SEXP", "ExternalPtr<MyType>"] {
1974            let ty = parse_type(ty_str);
1975            assert!(
1976                r_check_for_type(&ty).is_none(),
1977                "{} should be skipped",
1978                ty_str
1979            );
1980        }
1981    }
1982
1983    #[test]
1984    fn scalar_param_produces_one_guard_per_check() {
1985        let sig: syn::Signature = syn::parse_str("fn f(n: i32)").unwrap();
1986        let output = build_precondition_checks(
1987            &sig.inputs,
1988            &HashSet::new(),
1989            &PreconditionOptions::default(),
1990        );
1991        let checks = output.guards(None);
1992        assert_eq!(checks.len(), 2);
1993        assert!(checks[0].contains("\"must be integer\""));
1994        assert!(checks[1].contains("\"must have length 1\""));
1995        assert!(output.fallback_params.is_empty());
1996    }
1997
1998    #[test]
1999    fn vector_param_single_guard() {
2000        // Vec<f64> produces 1 check → one guard line
2001        let sig: syn::Signature = syn::parse_str("fn f(x: Vec<f64>)").unwrap();
2002        let output = build_precondition_checks(
2003            &sig.inputs,
2004            &HashSet::new(),
2005            &PreconditionOptions::default(),
2006        );
2007        assert_eq!(
2008            output.guards(None),
2009            vec!["if (!isTRUE(is.double(x))) .miniextendr_arg_error(\"x\", \"must be double\")"]
2010        );
2011    }
2012
2013    #[test]
2014    fn two_scalar_params_produce_four_guards_in_order() {
2015        let sig: syn::Signature = syn::parse_str("fn f(a: i32, b: f64)").unwrap();
2016        let output = build_precondition_checks(
2017            &sig.inputs,
2018            &HashSet::new(),
2019            &PreconditionOptions::default(),
2020        );
2021        let checks = output.guards(None);
2022        // 2 checks per param
2023        assert_eq!(checks.len(), 4);
2024        assert!(checks[0].contains("(\"a\", ") && checks[0].contains("integer"));
2025        assert!(checks[1].contains("(\"a\", ") && checks[1].contains("length 1"));
2026        assert!(checks[2].contains("(\"b\", ") && checks[2].contains("double"));
2027        assert!(checks[3].contains("(\"b\", ") && checks[3].contains("length 1"));
2028    }
2029
2030    #[test]
2031    fn build_checks_skips_match_arg() {
2032        let sig: syn::Signature = syn::parse_str("fn f(n: i32, mode: String)").unwrap();
2033        let mut skip = HashSet::new();
2034        skip.insert("mode".to_string());
2035        let output = build_precondition_checks(&sig.inputs, &skip, &PreconditionOptions::default());
2036        // Only n's 2 checks remain
2037        let joined = output.guards(None).join("\n");
2038        assert!(joined.contains("(\"n\", "));
2039        assert!(!joined.contains("mode"));
2040    }
2041
2042    #[test]
2043    fn unknown_type_produces_fallback() {
2044        let sig: syn::Signature = syn::parse_str("fn f(x: MyCustomType)").unwrap();
2045        let output = build_precondition_checks(
2046            &sig.inputs,
2047            &HashSet::new(),
2048            &PreconditionOptions::default(),
2049        );
2050        assert!(output.guards(None).is_empty());
2051        assert_eq!(output.fallback_params.len(), 1);
2052        assert_eq!(output.fallback_params[0].r_name, "x");
2053    }
2054
2055    #[test]
2056    fn mixed_known_and_unknown_types() {
2057        let sig: syn::Signature = syn::parse_str("fn f(a: i32, b: MyType, c: String)").unwrap();
2058        let output = build_precondition_checks(
2059            &sig.inputs,
2060            &HashSet::new(),
2061            &PreconditionOptions::default(),
2062        );
2063        // a (i32) and c (String) are known → checks
2064        let joined = output.guards(None).join("\n");
2065        assert!(joined.contains("(\"a\", "));
2066        assert!(joined.contains("(\"c\", "));
2067        assert!(!joined.contains("(\"b\", "));
2068        // b (MyType) is unknown → fallback
2069        assert_eq!(output.fallback_params.len(), 1);
2070        assert_eq!(output.fallback_params[0].r_name, "b");
2071    }
2072
2073    #[test]
2074    fn sexp_not_fallback() {
2075        let sig: syn::Signature = syn::parse_str("fn f(x: SEXP)").unwrap();
2076        let output = build_precondition_checks(
2077            &sig.inputs,
2078            &HashSet::new(),
2079            &PreconditionOptions::default(),
2080        );
2081        assert!(output.guards(None).is_empty());
2082        assert!(output.fallback_params.is_empty());
2083    }
2084
2085    /// Build checks for `sig` with the given explicit checks keyed by R name.
2086    /// The checks of `sig` with the named `explicit` checks; `no_type_checks`
2087    /// drops every parameter's type-derived checks.
2088    fn explicit_output(
2089        sig: &str,
2090        explicit: &[(&str, ExplicitChecks)],
2091        no_type_checks: bool,
2092    ) -> PreconditionOutput {
2093        let sig: syn::Signature = syn::parse_str(sig).unwrap();
2094        let unchecked = if no_type_checks {
2095            unchecked_params(
2096                &sig.inputs,
2097                &Default::default(),
2098                |_| Some(false),
2099                None,
2100                None,
2101                None,
2102            )
2103        } else {
2104            HashSet::new()
2105        };
2106        let opts = PreconditionOptions {
2107            explicit: explicit
2108                .iter()
2109                .map(|(name, checks)| (name.to_string(), checks.clone()))
2110                .collect(),
2111            unchecked,
2112            ..Default::default()
2113        };
2114        build_precondition_checks(&sig.inputs, &HashSet::new(), &opts)
2115    }
2116
2117    /// The precedence table of `resolve_type_checks`: the first spelling that
2118    /// speaks wins, from the parameter down to the feature; the framework
2119    /// default keeps the checks. `(param, item, impl, crate, feature) → keep`.
2120    #[test]
2121    fn resolve_type_checks_follows_the_precedence_table() {
2122        /// `(param, item, impl, crate, feature, keep)`.
2123        type Case = (
2124            Option<bool>,
2125            Option<bool>,
2126            Option<bool>,
2127            Option<bool>,
2128            bool,
2129            bool,
2130        );
2131        let cases: &[Case] = &[
2132            // Rank 6: nothing speaks.
2133            (None, None, None, None, false, true),
2134            // Rank 5: the feature drops them.
2135            (None, None, None, None, true, false),
2136            // Rank 4 over rank 5: the crate default beats the feature.
2137            (None, None, None, Some(true), true, true),
2138            (None, None, None, Some(false), false, false),
2139            // Rank 3 over 4 and 5: the impl block.
2140            (None, None, Some(true), Some(false), true, true),
2141            (None, None, Some(false), Some(true), false, false),
2142            // Rank 2 over 3: the fn / method keyword.
2143            (None, Some(true), Some(false), Some(false), true, true),
2144            (None, Some(false), Some(true), Some(true), false, false),
2145            // Rank 1 over everything: marker, per-param keyword, method list.
2146            (
2147                Some(true),
2148                Some(false),
2149                Some(false),
2150                Some(false),
2151                true,
2152                true,
2153            ),
2154            (
2155                Some(false),
2156                Some(true),
2157                Some(true),
2158                Some(true),
2159                false,
2160                false,
2161            ),
2162        ];
2163        for &(param, item, impl_level, crate_default, feature, keep) in cases {
2164            assert_eq!(
2165                resolve_type_checks(param, item, impl_level, crate_default, feature),
2166                keep,
2167                "{param:?} {item:?} {impl_level:?} {crate_default:?} {feature}"
2168            );
2169        }
2170        // The item-level decision is ranks 2 to 6.
2171        assert!(!resolve_item_type_checks(None, None, Some(false), false));
2172        assert!(resolve_item_type_checks(
2173            Some(true),
2174            Some(false),
2175            None,
2176            true
2177        ));
2178        assert!(!resolve_item_type_checks(None, None, None, true));
2179    }
2180
2181    /// `unchecked` drops one parameter's type-derived checks and keeps the
2182    /// others', and keeps that parameter's named checks.
2183    #[test]
2184    fn unchecked_drops_one_parameter_and_keeps_its_named_checks() {
2185        let sig: syn::Signature = syn::parse_str("fn f(n: i32, xs: Vec<f64>)").unwrap();
2186        let unchecked = unchecked_params(
2187            &sig.inputs,
2188            &Default::default(),
2189            |name| (name == "xs").then_some(false),
2190            None,
2191            None,
2192            Some(true),
2193        );
2194        assert_eq!(unchecked, ["xs".to_string()].into_iter().collect());
2195        let opts = PreconditionOptions {
2196            explicit: [("xs".to_string(), no_na())].into_iter().collect(),
2197            unchecked,
2198            ..Default::default()
2199        };
2200        let guards = build_precondition_checks(&sig.inputs, &HashSet::new(), &opts).guards(None);
2201        assert_eq!(
2202            guards,
2203            vec![
2204                "if (!isTRUE(is.integer(n))) .miniextendr_arg_error(\"n\", \"must be integer\")",
2205                "if (!isTRUE(length(n) == 1L)) .miniextendr_arg_error(\"n\", \"must have length 1\")",
2206                "if (!isTRUE(!anyNA(xs))) .miniextendr_arg_error(\"xs\", \"must not contain NA\")",
2207            ]
2208        );
2209        // The set holds R names: `_n` is the R formal `n`.
2210        let sig: syn::Signature = syn::parse_str("fn f(_n: i32)").unwrap();
2211        let unchecked = unchecked_params(
2212            &sig.inputs,
2213            &Default::default(),
2214            |_| None,
2215            Some(false),
2216            None,
2217            None,
2218        );
2219        assert_eq!(unchecked, ["n".to_string()].into_iter().collect());
2220    }
2221
2222    /// An item-level `no_preconditions` leaves `no_na` alone: a plain
2223    /// parameter keeps its R guard, and an `Either` (which has no type-derived
2224    /// check, so no rank-1 spelling) still gets none, its NA check running in
2225    /// Rust after the conversion.
2226    #[test]
2227    fn item_no_preconditions_keeps_no_na_and_either_stays_rust_side() {
2228        let sig: syn::Signature =
2229            syn::parse_str("fn f(x: Either<AsNumeric, DataFrame>, y: f64)").unwrap();
2230        let opts = PreconditionOptions {
2231            explicit: [("x".to_string(), no_na()), ("y".to_string(), no_na())]
2232                .into_iter()
2233                .collect(),
2234            unchecked: unchecked_params(
2235                &sig.inputs,
2236                &Default::default(),
2237                |_| None,
2238                Some(false),
2239                None,
2240                None,
2241            ),
2242            ..Default::default()
2243        };
2244        let guards = build_precondition_checks(&sig.inputs, &HashSet::new(), &opts).guards(None);
2245        assert_eq!(
2246            guards,
2247            vec!["if (!isTRUE(!anyNA(y))) .miniextendr_arg_error(\"y\", \"must not be NA\")"]
2248        );
2249    }
2250
2251    #[test]
2252    fn has_type_check_is_false_for_unguarded_types() {
2253        for guarded in [
2254            "i32",
2255            "Option<f64>",
2256            "&str",
2257            "&[f64]",
2258            "Vec<String>",
2259            "u16",
2260            "List",
2261        ] {
2262            assert!(has_type_check(&parse_type(guarded)), "{guarded}");
2263        }
2264        for bare in [
2265            "SEXP",
2266            "Missing<i32>",
2267            "ExternalPtr<T>",
2268            "&Dots",
2269            "MyType",
2270            "Call",
2271            "Either<AsNumeric, DataFrame>",
2272        ] {
2273            assert!(!has_type_check(&parse_type(bare)), "{bare}");
2274        }
2275    }
2276
2277    fn inherits(classes: &[&str]) -> ExplicitChecks {
2278        ExplicitChecks {
2279            inherits: Some(classes.iter().map(|c| c.to_string()).collect()),
2280            ..Default::default()
2281        }
2282    }
2283
2284    fn no_na() -> ExplicitChecks {
2285        ExplicitChecks {
2286            no_na: true,
2287            ..Default::default()
2288        }
2289    }
2290
2291    fn with_inherits_message(classes: &[&str], message: &str) -> ExplicitChecks {
2292        ExplicitChecks {
2293            inherits_message: Some(message.to_string()),
2294            ..inherits(classes)
2295        }
2296    }
2297
2298    fn with_no_na_message(message: &str) -> ExplicitChecks {
2299        ExplicitChecks {
2300            no_na_message: Some(message.to_string()),
2301            ..no_na()
2302        }
2303    }
2304
2305    #[test]
2306    fn inherits_precedes_the_type_check() {
2307        let out = explicit_output("fn f(x: List)", &[("x", inherits(&["pkg_obj"]))], false);
2308        assert_eq!(
2309            out.guards(None),
2310            vec![
2311                "if (!isTRUE(inherits(x, \"pkg_obj\"))) .miniextendr_arg_error(\"x\", \"must inherit from 'pkg_obj'\")",
2312                "if (!isTRUE(is.list(x))) .miniextendr_arg_error(\"x\", \"must be a list\")",
2313            ]
2314        );
2315    }
2316
2317    /// An `inherits` message is the message of every type check on the
2318    /// parameter too: storage and length alike.
2319    #[test]
2320    fn inherits_message_covers_the_type_checks() {
2321        let out = explicit_output(
2322            "fn f(x: f64)",
2323            &[("x", with_inherits_message(&["pkg_unit"], "need a pkg_unit"))],
2324            false,
2325        );
2326        assert_eq!(
2327            out.guards(None),
2328            vec![
2329                "if (!isTRUE(inherits(x, \"pkg_unit\"))) .miniextendr_arg_error(\"x\", message = \"need a pkg_unit\")",
2330                "if (!isTRUE(is.double(x))) .miniextendr_arg_error(\"x\", message = \"need a pkg_unit\")",
2331                "if (!isTRUE(length(x) == 1L)) .miniextendr_arg_error(\"x\", message = \"need a pkg_unit\")",
2332            ]
2333        );
2334    }
2335
2336    #[test]
2337    fn inherits_message_on_option() {
2338        let out = explicit_output(
2339            "fn f(x: Option<List>)",
2340            &[("x", with_inherits_message(&["pkg_obj"], "need a pkg_obj"))],
2341            false,
2342        );
2343        assert_eq!(
2344            out.guards(None),
2345            vec![
2346                "if (!isTRUE(is.null(x) || inherits(x, \"pkg_obj\"))) .miniextendr_arg_error(\"x\", message = \"need a pkg_obj\")",
2347                "if (!isTRUE(is.null(x) || is.list(x))) .miniextendr_arg_error(\"x\", message = \"need a pkg_obj\")",
2348            ]
2349        );
2350    }
2351
2352    /// The `inherits` message does not cover the NA check, which keeps its
2353    /// generated message.
2354    #[test]
2355    fn no_na_keeps_its_own_message() {
2356        let mut checks = with_inherits_message(&["pkg_num"], "need a pkg_num");
2357        checks.merge(no_na()).unwrap();
2358        let out = explicit_output("fn f(x: Vec<f64>)", &[("x", checks)], false);
2359        assert_eq!(
2360            out.guards(None),
2361            vec![
2362                "if (!isTRUE(inherits(x, \"pkg_num\"))) .miniextendr_arg_error(\"x\", message = \"need a pkg_num\")",
2363                "if (!isTRUE(is.double(x))) .miniextendr_arg_error(\"x\", message = \"need a pkg_num\")",
2364                "if (!isTRUE(!anyNA(x))) .miniextendr_arg_error(\"x\", \"must not contain NA\")",
2365            ]
2366        );
2367    }
2368
2369    /// `coerce` widens the type checks; the widened checks carry the message.
2370    #[test]
2371    fn inherits_message_with_coerce() {
2372        let sig: syn::Signature = syn::parse_str("fn f(x: i32)").unwrap();
2373        let opts = PreconditionOptions {
2374            coerce_all: true,
2375            explicit: [(
2376                "x".to_string(),
2377                with_inherits_message(&["pkg_count"], "need a pkg_count"),
2378            )]
2379            .into_iter()
2380            .collect(),
2381            ..Default::default()
2382        };
2383        let out = build_precondition_checks(&sig.inputs, &HashSet::new(), &opts);
2384        let guards = out.guards(None);
2385        assert_eq!(guards.len(), 3, "{guards:?}");
2386        assert!(guards[0].starts_with("if (!isTRUE(inherits(x, \"pkg_count\")))"));
2387        assert!(
2388            guards[1].contains("is.integer(x) || is.logical(x) || is.raw(x)"),
2389            "{}",
2390            guards[1]
2391        );
2392        assert!(guards[2].contains("length(x) == 1L"), "{}", guards[2]);
2393        for guard in &guards {
2394            assert!(
2395                guard.ends_with(".miniextendr_arg_error(\"x\", message = \"need a pkg_count\")"),
2396                "{guard}"
2397            );
2398        }
2399    }
2400
2401    /// `Missing<T>` has no R type check, so the class check is its only guard.
2402    #[test]
2403    fn inherits_message_on_missing() {
2404        let out = explicit_output(
2405            "fn f(x: Missing<List>)",
2406            &[("x", with_inherits_message(&["pkg_obj"], "need a pkg_obj"))],
2407            false,
2408        );
2409        assert_eq!(
2410            out.guards(None),
2411            vec![
2412                "if (!isTRUE(missing(x) || inherits(x, \"pkg_obj\"))) .miniextendr_arg_error(\"x\", message = \"need a pkg_obj\")"
2413            ]
2414        );
2415    }
2416
2417    #[test]
2418    fn inherits_any_of_several_classes() {
2419        let out = explicit_output("fn f(x: SEXP)", &[("x", inherits(&["a", "b", "c"]))], false);
2420        assert_eq!(
2421            out.guards(None),
2422            vec![
2423                "if (!isTRUE(inherits(x, c(\"a\", \"b\", \"c\")))) .miniextendr_arg_error(\"x\", \"must inherit from 'a', 'b' or 'c'\")"
2424            ]
2425        );
2426    }
2427
2428    #[test]
2429    fn inherits_escapes_quotes_and_backslashes() {
2430        let out = explicit_output("fn f(x: SEXP)", &[("x", inherits(&["a\"b\\c"]))], false);
2431        assert_eq!(
2432            out.guards(None),
2433            vec![
2434                "if (!isTRUE(inherits(x, \"a\\\"b\\\\c\"))) .miniextendr_arg_error(\"x\", \"must inherit from 'a\\\"b\\\\c'\")"
2435            ]
2436        );
2437    }
2438
2439    fn not_inherits(classes: &[&str]) -> ExplicitChecks {
2440        ExplicitChecks {
2441            not_inherits: Some(classes.iter().map(|c| c.to_string()).collect()),
2442            ..Default::default()
2443        }
2444    }
2445
2446    fn with_not_inherits_message(classes: &[&str], message: &str) -> ExplicitChecks {
2447        ExplicitChecks {
2448            not_inherits_message: Some(message.to_string()),
2449            ..not_inherits(classes)
2450        }
2451    }
2452
2453    /// The #1815 case: `not_inherits` with a message on an `Option<AsNumeric>`
2454    /// with `no_na`. The refusal runs before the marker's type checks
2455    /// (`is.numeric()` is `FALSE` for these classes, so after them it would
2456    /// never be reached), passes `NULL`, and leaves the type and NA checks
2457    /// their generated messages.
2458    #[test]
2459    fn not_inherits_precedes_the_type_checks_and_keeps_their_messages() {
2460        let mut checks = with_not_inherits_message(
2461            &["difftime", "Date", "POSIXt"],
2462            "`tau` must be a plain number",
2463        );
2464        checks.merge(no_na()).unwrap();
2465        let out = explicit_output("fn f(tau: Option<AsNumeric>)", &[("tau", checks)], false);
2466        assert_eq!(
2467            out.guards(None),
2468            vec![
2469                "if (!isTRUE(is.null(tau) || !inherits(tau, c(\"difftime\", \"Date\", \"POSIXt\")))) .miniextendr_arg_error(\"tau\", message = \"`tau` must be a plain number\")",
2470                "if (!isTRUE(is.null(tau) || is.numeric(tau) || is.logical(tau) || is.character(tau) || is.factor(tau))) .miniextendr_arg_error(\"tau\", \"must be NULL or numeric, logical, character, or factor\")",
2471                "if (!isTRUE(is.null(tau) || length(tau) == 1L)) .miniextendr_arg_error(\"tau\", \"must be NULL or have length 1\")",
2472                "if (!isTRUE(is.null(tau) || !anyNA(tau))) .miniextendr_arg_error(\"tau\", \"must not be NA\")",
2473            ]
2474        );
2475    }
2476
2477    /// Without a message, the refusal names the classes the way `inherits`
2478    /// names the required ones.
2479    #[test]
2480    fn not_inherits_generated_message() {
2481        let one = explicit_output("fn f(x: SEXP)", &[("x", not_inherits(&["Date"]))], false);
2482        assert_eq!(
2483            one.guards(None),
2484            vec![
2485                "if (!isTRUE(!inherits(x, \"Date\"))) .miniextendr_arg_error(\"x\", \"must not inherit from 'Date'\")"
2486            ]
2487        );
2488        let several = explicit_output(
2489            "fn f(x: SEXP)",
2490            &[("x", not_inherits(&["difftime", "Date", "POSIXt"]))],
2491            false,
2492        );
2493        let guards = several.guards(None);
2494        assert_eq!(
2495            guards,
2496            vec![
2497                "if (!isTRUE(!inherits(x, c(\"difftime\", \"Date\", \"POSIXt\")))) .miniextendr_arg_error(\"x\", \"must not inherit from 'difftime', 'Date' or 'POSIXt'\")"
2498            ]
2499        );
2500        let asserts = not_inherits(&["a\"b"]).class_assertions("x", &parse_type("SEXP"));
2501        assert_eq!(asserts[0].message(), "'x' must not inherit from 'a\\\"b'");
2502    }
2503
2504    /// `inherits` and `not_inherits` on one parameter: the required classes
2505    /// first, then the refused ones, then the type checks, which take the
2506    /// `inherits` message only.
2507    #[test]
2508    fn inherits_and_not_inherits_compose() {
2509        let mut checks = with_inherits_message(&["pkg_obj"], "need a pkg_obj");
2510        checks
2511            .merge(with_not_inherits_message(
2512                &["pkg_old"],
2513                "pkg_old is retired",
2514            ))
2515            .unwrap();
2516        let out = explicit_output("fn f(x: List)", &[("x", checks)], false);
2517        assert_eq!(
2518            out.guards(None),
2519            vec![
2520                "if (!isTRUE(inherits(x, \"pkg_obj\"))) .miniextendr_arg_error(\"x\", message = \"need a pkg_obj\")",
2521                "if (!isTRUE(!inherits(x, \"pkg_old\"))) .miniextendr_arg_error(\"x\", message = \"pkg_old is retired\")",
2522                "if (!isTRUE(is.list(x))) .miniextendr_arg_error(\"x\", message = \"need a pkg_obj\")",
2523            ]
2524        );
2525        // A `not_inherits` message alone leaves the type check's wording.
2526        let out = explicit_output(
2527            "fn f(x: List)",
2528            &[("x", with_not_inherits_message(&["pkg_old"], "retired"))],
2529            false,
2530        );
2531        assert_eq!(
2532            out.guards(None)[1],
2533            "if (!isTRUE(is.list(x))) .miniextendr_arg_error(\"x\", \"must be a list\")"
2534        );
2535    }
2536
2537    /// The check is plain R on the formal, so it needs no type check: an
2538    /// `Either` (no R guard at all, its `no_na` checked in Rust) gets it, and
2539    /// so does a parameter under `no_preconditions`. `Missing<T>` passes an
2540    /// omitted argument, `Option<Either<..>>` `NULL`. A bare `Either` arm
2541    /// that reads `NULL` needs no prefix: `!inherits(NULL, cls)` is `TRUE`.
2542    #[test]
2543    fn not_inherits_without_type_checks() {
2544        let refused = || not_inherits(&["difftime"]);
2545        let guards = |sig: &str, no_type_checks: bool| {
2546            let mut checks = refused();
2547            checks.merge(no_na()).unwrap();
2548            explicit_output(sig, &[("x", checks)], no_type_checks).guards(None)
2549        };
2550        let either = guards("fn f(x: Either<Option<AsNumeric>, DataFrame>)", false);
2551        assert_eq!(
2552            either,
2553            vec![
2554                "if (!isTRUE(!inherits(x, \"difftime\"))) .miniextendr_arg_error(\"x\", \"must not inherit from 'difftime'\")"
2555            ]
2556        );
2557        let optional_either = guards("fn f(x: Option<Either<AsNumeric, DataFrame>>)", false);
2558        assert_eq!(optional_either.len(), 1);
2559        assert!(
2560            optional_either[0]
2561                .starts_with("if (!isTRUE(is.null(x) || !inherits(x, \"difftime\")))"),
2562            "{optional_either:?}"
2563        );
2564        let unchecked = guards("fn f(x: Option<AsNumeric>)", true);
2565        assert_eq!(unchecked.len(), 2, "{unchecked:?}");
2566        assert!(unchecked[0].contains("!inherits(x, \"difftime\")"));
2567        assert!(unchecked[1].contains("!anyNA(x)"));
2568        let missing = explicit_output("fn f(x: Missing<f64>)", &[("x", refused())], false);
2569        assert_eq!(
2570            missing.guards(None)[0],
2571            "if (!isTRUE(missing(x) || !inherits(x, \"difftime\"))) .miniextendr_arg_error(\"x\", \"must not inherit from 'difftime'\")"
2572        );
2573    }
2574
2575    /// Under `call = caller` the refusal names the caller's call: positional
2576    /// after the generated requirement, by name after a message.
2577    #[test]
2578    fn not_inherits_takes_the_caller_call() {
2579        let plain = explicit_output("fn f(x: SEXP)", &[("x", not_inherits(&["Date"]))], false);
2580        assert!(plain.guards(Some(".mx_call"))[0].ends_with(
2581            ".miniextendr_arg_error(\"x\", \"must not inherit from 'Date'\", .mx_call)"
2582        ));
2583        let custom = explicit_output(
2584            "fn f(x: SEXP)",
2585            &[("x", with_not_inherits_message(&["Date"], "no dates"))],
2586            false,
2587        );
2588        assert!(
2589            custom.guards(Some(".mx_call"))[0].ends_with(
2590                ".miniextendr_arg_error(\"x\", message = \"no dates\", call = .mx_call)"
2591            )
2592        );
2593    }
2594
2595    /// A class in both lists is a contradiction the parsers reject; any
2596    /// other pairing, or one list alone, is fine. Merging keeps the two
2597    /// lists and their messages apart.
2598    #[test]
2599    fn not_inherits_merge_and_conflict() {
2600        let mut checks = inherits(&["a", "b"]);
2601        checks.merge(not_inherits(&["c"])).unwrap();
2602        assert_eq!(checks.class_conflict("x"), None);
2603        checks
2604            .merge(with_not_inherits_message(&["d"], "m"))
2605            .unwrap();
2606        assert_eq!(checks.not_inherits, Some(vec!["c".into(), "d".into()]));
2607        assert_eq!(checks.not_inherits_message.as_deref(), Some("m"));
2608        assert!(checks.inherits_message.is_none());
2609        let err = checks
2610            .merge(with_not_inherits_message(&["e"], "n"))
2611            .unwrap_err();
2612        assert!(err.contains("`not_inherits`"), "{err}");
2613        checks.merge(not_inherits(&["b"])).unwrap();
2614        let err = checks.class_conflict("x").expect("b is in both");
2615        assert!(
2616            err.contains("class `b` is in both `inherits` and `not_inherits` on parameter `x`"),
2617            "{err}"
2618        );
2619        assert!(!not_inherits(&["a"]).is_empty());
2620        assert_eq!(not_inherits(&["a"]).class_conflict("x"), None);
2621    }
2622
2623    // region: `when(...)` hints on `inherits` (#1824)
2624
2625    /// `inherits(classes, message, when(...) ...)`: `hints` are
2626    /// `(classes, message)` pairs, in the order written.
2627    fn with_hints(
2628        classes: &[&str],
2629        message: Option<&str>,
2630        hints: &[(&[&str], &str)],
2631    ) -> ExplicitChecks {
2632        ExplicitChecks {
2633            inherits_message: message.map(str::to_string),
2634            inherits_hints: hints
2635                .iter()
2636                .map(|(classes, message)| InheritsHint {
2637                    classes: classes.iter().map(|c| c.to_string()).collect(),
2638                    message: message.to_string(),
2639                })
2640                .collect(),
2641            ..inherits(classes)
2642        }
2643    }
2644
2645    /// Each hint is one check ahead of the `inherits` check, in the order
2646    /// written, so the first hint naming one of a refused value's classes is
2647    /// raised. A value that passes `inherits` passes every hint
2648    /// (`inherits(p, <required>) || ...`), so a `pkg_model` that is also a
2649    /// `data.frame` is accepted. The type checks keep the `inherits` message.
2650    #[test]
2651    fn inherits_hints_precede_the_check_in_written_order() {
2652        let checks = with_hints(
2653            &["pkg_model"],
2654            Some("`model` must be a `pkg_model` object"),
2655            &[
2656                (&["data.frame"], "a data frame: use `col_dose()`"),
2657                (&["pkg_fit", "pkg_results"], "a fit: use `fit$model`"),
2658            ],
2659        );
2660        let out = explicit_output("fn f(model: List)", &[("model", checks)], false);
2661        assert_eq!(
2662            out.guards(None),
2663            vec![
2664                "if (!isTRUE(inherits(model, \"pkg_model\") || !inherits(model, \"data.frame\"))) .miniextendr_arg_error(\"model\", message = \"a data frame: use `col_dose()`\")",
2665                "if (!isTRUE(inherits(model, \"pkg_model\") || !inherits(model, c(\"pkg_fit\", \"pkg_results\")))) .miniextendr_arg_error(\"model\", message = \"a fit: use `fit$model`\")",
2666                "if (!isTRUE(inherits(model, \"pkg_model\"))) .miniextendr_arg_error(\"model\", message = \"`model` must be a `pkg_model` object\")",
2667                "if (!isTRUE(is.list(model))) .miniextendr_arg_error(\"model\", message = \"`model` must be a `pkg_model` object\")",
2668            ]
2669        );
2670        // Without its own message the check keeps the generated one; several
2671        // required classes are all accepted by every hint.
2672        let checks = with_hints(&["pkg_a", "pkg_b"], None, &[(&["data.frame"], "hint")]);
2673        let out = explicit_output("fn f(x: SEXP)", &[("x", checks)], false);
2674        assert_eq!(
2675            out.guards(None),
2676            vec![
2677                "if (!isTRUE(inherits(x, c(\"pkg_a\", \"pkg_b\")) || !inherits(x, \"data.frame\"))) .miniextendr_arg_error(\"x\", message = \"hint\")",
2678                "if (!isTRUE(inherits(x, c(\"pkg_a\", \"pkg_b\")))) .miniextendr_arg_error(\"x\", \"must inherit from 'pkg_a' or 'pkg_b'\")",
2679            ]
2680        );
2681    }
2682
2683    /// A hint is an R string literal like any message: quotes, backslashes,
2684    /// control characters and non-ASCII text are escaped, and so are its
2685    /// class names.
2686    #[test]
2687    fn inherits_hints_are_escaped() {
2688        let checks = with_hints(
2689            &["pkg_model"],
2690            None,
2691            &[(
2692                &["caf\u{e9}", "a\"b\\c"],
2693                "say \"hi\" \\ back\nline caf\u{e9} \u{1F600}",
2694            )],
2695        );
2696        let out = explicit_output("fn f(x: SEXP)", &[("x", checks)], false);
2697        assert_eq!(
2698            out.guards(None)[0],
2699            r#"if (!isTRUE(inherits(x, "pkg_model") || !inherits(x, c("caf\u{e9}", "a\"b\\c")))) .miniextendr_arg_error("x", message = "say \"hi\" \\ back\nline caf\u{e9} \U{1f600}")"#
2700        );
2701    }
2702
2703    /// Under `call = caller` a hint names the caller's call, by name after
2704    /// its message.
2705    #[test]
2706    fn inherits_hints_take_the_caller_call() {
2707        let checks = with_hints(&["pkg_model"], None, &[(&["data.frame"], "hint")]);
2708        let out = explicit_output("fn f(x: SEXP)", &[("x", checks)], false);
2709        assert_eq!(
2710            out.guards(Some(".mx_call")),
2711            vec![
2712                "if (!isTRUE(inherits(x, \"pkg_model\") || !inherits(x, \"data.frame\"))) .miniextendr_arg_error(\"x\", message = \"hint\", call = .mx_call)",
2713                "if (!isTRUE(inherits(x, \"pkg_model\"))) .miniextendr_arg_error(\"x\", \"must inherit from 'pkg_model'\", .mx_call)",
2714            ]
2715        );
2716    }
2717
2718    /// An `Option<T>` parameter passes `NULL`, and a `Missing<T>` parameter
2719    /// an omitted argument, through the hints as through the check. Under
2720    /// `no_preconditions` the hints stay with the check.
2721    #[test]
2722    fn inherits_hints_pass_null_and_missing() {
2723        let hinted = || with_hints(&["pkg_model"], None, &[(&["data.frame"], "hint")]);
2724        let optional = explicit_output("fn f(x: Option<List>)", &[("x", hinted())], true);
2725        assert_eq!(
2726            optional.guards(None)[0],
2727            "if (!isTRUE(is.null(x) || inherits(x, \"pkg_model\") || !inherits(x, \"data.frame\"))) .miniextendr_arg_error(\"x\", message = \"hint\")"
2728        );
2729        let missing = explicit_output("fn f(x: Missing<List>)", &[("x", hinted())], true);
2730        assert_eq!(
2731            missing.guards(None),
2732            vec![
2733                "if (!isTRUE(missing(x) || inherits(x, \"pkg_model\") || !inherits(x, \"data.frame\"))) .miniextendr_arg_error(\"x\", message = \"hint\")",
2734                "if (!isTRUE(missing(x) || inherits(x, \"pkg_model\"))) .miniextendr_arg_error(\"x\", \"must inherit from 'pkg_model'\")",
2735            ]
2736        );
2737    }
2738
2739    /// Several `inherits` attributes on one parameter concatenate their
2740    /// hints in the order written; every hint then accepts every required
2741    /// class.
2742    #[test]
2743    fn inherits_hints_merge_in_order() {
2744        let mut checks = with_hints(&["pkg_a"], Some("need an a"), &[(&["df"], "one")]);
2745        checks
2746            .merge(with_hints(&["pkg_b"], None, &[(&["tbl"], "two")]))
2747            .unwrap();
2748        checks.merge(inherits(&["pkg_c"])).unwrap();
2749        let hints: Vec<&str> = checks
2750            .inherits_hints
2751            .iter()
2752            .map(|h| h.message.as_str())
2753            .collect();
2754        assert_eq!(hints, ["one", "two"]);
2755        assert_eq!(checks.class_conflict("x"), None);
2756        let out = explicit_output("fn f(x: SEXP)", &[("x", checks)], false);
2757        let guards = out.guards(None);
2758        assert_eq!(guards.len(), 3);
2759        assert!(
2760            guards[0].starts_with(
2761                "if (!isTRUE(inherits(x, c(\"pkg_a\", \"pkg_b\", \"pkg_c\")) || !inherits(x, \"df\")))"
2762            ),
2763            "{}",
2764            guards[0]
2765        );
2766        assert!(guards[1].contains("!inherits(x, \"tbl\"))) "));
2767        assert!(guards[1].ends_with("message = \"two\")"));
2768        assert!(guards[2].ends_with("message = \"need an a\")"));
2769    }
2770
2771    /// A hint class that `inherits` requires could never be shown: an error,
2772    /// also when another attribute requires it. A hint class that
2773    /// `not_inherits` refuses is fine: a value of that class alone gets the
2774    /// hint, one that also has a required class gets the refusal.
2775    #[test]
2776    fn inherits_hint_class_conflicts() {
2777        let checks = with_hints(&["pkg_model"], None, &[(&["df", "pkg_model"], "hint")]);
2778        let err = checks
2779            .class_conflict("model")
2780            .expect("pkg_model is required");
2781        assert!(
2782            err.contains(
2783                "class `pkg_model` is in both `inherits` and a `when(...)` hint on parameter `model`"
2784            ),
2785            "{err}"
2786        );
2787        let mut checks = with_hints(&["pkg_model"], None, &[(&["pkg_old"], "hint")]);
2788        assert_eq!(checks.class_conflict("model"), None);
2789        checks.merge(inherits(&["pkg_old"])).unwrap();
2790        assert!(checks.class_conflict("model").is_some());
2791
2792        let mut checks = with_hints(&["pkg_model"], None, &[(&["pkg_old"], "hint")]);
2793        checks.merge(not_inherits(&["pkg_old"])).unwrap();
2794        assert_eq!(checks.class_conflict("model"), None);
2795        let out = explicit_output("fn f(x: SEXP)", &[("x", checks)], false);
2796        assert_eq!(
2797            out.guards(None),
2798            vec![
2799                "if (!isTRUE(inherits(x, \"pkg_model\") || !inherits(x, \"pkg_old\"))) .miniextendr_arg_error(\"x\", message = \"hint\")",
2800                "if (!isTRUE(inherits(x, \"pkg_model\"))) .miniextendr_arg_error(\"x\", \"must inherit from 'pkg_model'\")",
2801                "if (!isTRUE(!inherits(x, \"pkg_old\"))) .miniextendr_arg_error(\"x\", \"must not inherit from 'pkg_old'\")",
2802            ]
2803        );
2804    }
2805
2806    // endregion
2807
2808    #[test]
2809    fn no_na_wording_follows_the_shape() {
2810        let scalar = explicit_output("fn f(x: f64)", &[("x", no_na())], true);
2811        assert_eq!(
2812            scalar.guards(None),
2813            vec!["if (!isTRUE(!anyNA(x))) .miniextendr_arg_error(\"x\", \"must not be NA\")"]
2814        );
2815        let vector = explicit_output("fn f(x: Vec<f64>)", &[("x", no_na())], true);
2816        assert_eq!(
2817            vector.guards(None),
2818            vec!["if (!isTRUE(!anyNA(x))) .miniextendr_arg_error(\"x\", \"must not contain NA\")"]
2819        );
2820        let slice = explicit_output("fn f(x: &[f64])", &[("x", no_na())], true);
2821        assert!(slice.guards(None)[0].contains("must not contain NA"));
2822        // The vector markers are vectors too; their scalar forms are not.
2823        for (ty, verb) in [
2824            ("AsNumericVec", "contain"),
2825            ("AsCharacterVec", "contain"),
2826            ("Option<AsCharacterVec>", "contain"),
2827            ("AsNumeric", "be"),
2828            ("AsCharacter", "be"),
2829        ] {
2830            let out = explicit_output(&format!("fn f(x: {ty})"), &[("x", no_na())], true);
2831            assert!(
2832                out.guards(None)[0].contains(&format!("must not {verb} NA")),
2833                "{ty}: {:?}",
2834                out.guards(None)
2835            );
2836        }
2837    }
2838
2839    /// Beyond the type table's vector checks (above): a `Missing<_>` marker,
2840    /// maps, arrays, and a marker the table does not know, by its `...Vec`
2841    /// name (`AsFromStrVec<T>`, `AsDisplayVec<T>`), hold several values too;
2842    /// their scalar halves and opaque types do not.
2843    #[test]
2844    fn no_na_wording_knows_the_vector_markers() {
2845        let verb = |sig: &str| {
2846            let out = explicit_output(sig, &[("x", no_na())], false);
2847            let guards = out.guards(None);
2848            let guard = guards.last().expect("the no_na guard");
2849            if guard.contains("must not contain NA") {
2850                "contain"
2851            } else {
2852                assert!(guard.contains("must not be NA"), "{guard}");
2853                "be"
2854            }
2855        };
2856        for (sig, expected) in [
2857            ("fn f(x: Option<AsNumericVec>)", "contain"),
2858            ("fn f(x: Missing<AsNumericVec>)", "contain"),
2859            ("fn f(x: AsFromStrVec<std::net::IpAddr>)", "contain"),
2860            ("fn f(x: AsDisplayVec<u8>)", "contain"),
2861            ("fn f(x: HashMap<String, f64>)", "contain"),
2862            ("fn f(x: [f64; 3])", "contain"),
2863            // An optional data frame is worded as a bare one.
2864            ("fn f(x: Option<DataFrame>)", "be"),
2865            ("fn f(x: Option<AsNumeric>)", "be"),
2866            ("fn f(x: AsFromStr<std::net::IpAddr>)", "be"),
2867            ("fn f(x: String)", "be"),
2868            ("fn f(x: MyCustomType)", "be"),
2869            // Through `Result<T, _>` and `Either`, the layers `no_na` checks
2870            // through: an `Either` holds several when either arm does.
2871            ("fn f(x: Result<AsNumericVec, ()>)", "contain"),
2872            ("fn f(x: Result<AsNumeric, ()>)", "be"),
2873            ("fn f(x: Either<AsNumericVec, Vec<u8>>)", "contain"),
2874            ("fn f(x: Either<AsNumericVec, i32>)", "contain"),
2875            ("fn f(x: Either<i32, AsCharacterVec>)", "contain"),
2876            ("fn f(x: Either<AsNumeric, AsCharacter>)", "be"),
2877            // An `Option` arm is read by its value.
2878            ("fn f(x: Either<Option<AsNumeric>, DataFrame>)", "be"),
2879            (
2880                "fn f(x: Either<Option<AsNumericVec>, DataFrame>)",
2881                "contain",
2882            ),
2883            (
2884                "fn f(x: Option<Either<i32, Either<String, Vec<f64>>>>)",
2885                "contain",
2886            ),
2887        ] {
2888            let ty = match syn::parse_str::<syn::Signature>(sig).unwrap().inputs[0].clone() {
2889                syn::FnArg::Typed(pt) => *pt.ty,
2890                syn::FnArg::Receiver(_) => unreachable!(),
2891            };
2892            // An `Either` has only the Rust check; the others have the R guard
2893            // too, and the Rust check words it the same way.
2894            if !no_na_checked_after_conversion(&ty) {
2895                assert_eq!(verb(sig), expected, "{sig}");
2896            }
2897            assert_eq!(
2898                no_na_requirement(&ty),
2899                format!("must not {expected} NA"),
2900                "{sig}"
2901            );
2902        }
2903    }
2904
2905    /// `no_na` on an `Either` (also under `Missing` or `Option`) emits no R
2906    /// guard: the C wrapper checks the input by the arm the value converted
2907    /// to. A plain parameter keeps the guard (`Option<DataFrame>` behind its
2908    /// `is.null(x)` pass), and so does a choice `Either` (`match_arg` /
2909    /// `choices`, also under `Option` / `Missing<Option<..>>`), whose
2910    /// conversion has no Rust check.
2911    #[test]
2912    fn no_na_on_an_either_is_checked_after_the_conversion() {
2913        let guards = |sig: &str, skip: &[&str]| {
2914            let sig: syn::Signature = syn::parse_str(sig).unwrap();
2915            let opts = PreconditionOptions {
2916                explicit: [("x".to_string(), no_na())].into_iter().collect(),
2917                ..Default::default()
2918            };
2919            let skip: HashSet<String> = skip.iter().map(|s| s.to_string()).collect();
2920            build_precondition_checks(&sig.inputs, &skip, &opts).guards(None)
2921        };
2922        for sig in [
2923            "fn f(x: Either<AsNumeric, DataFrame>)",
2924            "fn f(x: Either<Option<AsNumeric>, DataFrame>)",
2925            "fn f(x: either::Either<Vec<f64>, List>)",
2926            "fn f(x: Missing<Either<AsNumeric, DataFrame>>)",
2927            "fn f(x: Option<Either<AsNumericVec, DataFrame>>)",
2928            "fn f(x: Missing<Option<Either<f64, String>>>)",
2929        ] {
2930            assert!(
2931                guards(sig, &[]).iter().all(|g| !g.contains("anyNA")),
2932                "{sig}: {:?}",
2933                guards(sig, &[])
2934            );
2935        }
2936        for (sig, skip) in [
2937            ("fn f(x: AsNumeric)", &[][..]),
2938            ("fn f(x: DataFrame)", &[]),
2939            ("fn f(x: Either<Mode, DataFrame>)", &["x"]),
2940        ] {
2941            let guards = guards(sig, skip);
2942            assert!(
2943                guards
2944                    .last()
2945                    .is_some_and(|g| g.contains("!isTRUE(!anyNA(x))")),
2946                "{sig}: {guards:?}"
2947            );
2948        }
2949        // A choice `Either` under `Option` / `Missing<Option<..>>` keeps the
2950        // guard too, after the layer's `is.null(x)` / `missing(x)` pass, as
2951        // does an optional data frame.
2952        for (sig, layer, skip) in [
2953            (
2954                "fn f(x: Option<Either<Mode, DataFrame>>)",
2955                "is.null(x)",
2956                &["x"][..],
2957            ),
2958            (
2959                "fn f(x: Missing<Option<Either<Mode, List>>>)",
2960                "missing(x)",
2961                &["x"],
2962            ),
2963            ("fn f(x: Option<DataFrame>)", "is.null(x)", &[]),
2964        ] {
2965            let guards = guards(sig, skip);
2966            let expected = format!("!isTRUE({layer} || !anyNA(x))");
2967            assert!(
2968                guards.last().is_some_and(|g| g.contains(&expected)),
2969                "{sig}: {guards:?}"
2970            );
2971        }
2972        // `Result<Either<..>, ()>` is not a layer the predicate peels: it
2973        // keeps the guard, and the plain Rust check.
2974        assert!(!no_na_checked_after_conversion(
2975            &syn::parse_str("Result<Either<f64, String>, ()>").unwrap()
2976        ));
2977    }
2978
2979    /// The message the Rust `no_na` check raises: generated from the
2980    /// parameter's R name and type, or the author's message verbatim.
2981    #[test]
2982    fn no_na_message_is_generated_or_custom() {
2983        let ty = |s: &str| syn::parse_str::<syn::Type>(s).unwrap();
2984        assert_eq!(
2985            no_na_message("x", &ty("AsNumeric"), None),
2986            "'x' must not be NA"
2987        );
2988        assert_eq!(
2989            no_na_message("dv", &ty("Missing<Option<AsNumericVec>>"), None),
2990            "'dv' must not contain NA"
2991        );
2992        // A vector alias not named `...Vec` reads `be`, as its R guard does.
2993        assert_eq!(no_na_message("d", &ty("Doses"), None), "'d' must not be NA");
2994        let custom = "`x` can't be NA: it's \"required\" \\ 100%\ncaf\u{e9}()";
2995        assert_eq!(no_na_message("x", &ty("AsNumeric"), Some(custom)), custom);
2996    }
2997
2998    #[test]
2999    fn explicit_checks_pass_null_and_missing() {
3000        let optional = explicit_output(
3001            "fn f(x: Option<List>)",
3002            &[("x", inherits(&["pkg_obj"]))],
3003            true,
3004        );
3005        assert_eq!(
3006            optional.guards(None),
3007            vec![
3008                "if (!isTRUE(is.null(x) || inherits(x, \"pkg_obj\"))) .miniextendr_arg_error(\"x\", \"must inherit from 'pkg_obj'\")"
3009            ]
3010        );
3011        let missing = explicit_output("fn f(x: Missing<f64>)", &[("x", no_na())], true);
3012        assert_eq!(
3013            missing.guards(None),
3014            vec![
3015                "if (!isTRUE(missing(x) || !anyNA(x))) .miniextendr_arg_error(\"x\", \"must not be NA\")"
3016            ]
3017        );
3018        let optional_vec = explicit_output("fn f(x: Option<Vec<f64>>)", &[("x", no_na())], true);
3019        assert!(optional_vec.guards(None)[0].contains("must not contain NA"));
3020    }
3021
3022    #[test]
3023    fn explicit_checks_survive_no_type_checks() {
3024        let both = ExplicitChecks {
3025            inherits: Some(vec!["pkg_obj".into()]),
3026            no_na: true,
3027            ..Default::default()
3028        };
3029        let out = explicit_output("fn f(n: i32, x: List)", &[("x", both)], true);
3030        // `n`'s type checks and `x`'s `is.list()` are gone; the named checks stay,
3031        // the class first. A list holds several values, so its NA check says
3032        // `contain`.
3033        assert_eq!(
3034            out.guards(None),
3035            vec![
3036                "if (!isTRUE(inherits(x, \"pkg_obj\"))) .miniextendr_arg_error(\"x\", \"must inherit from 'pkg_obj'\")",
3037                "if (!isTRUE(!anyNA(x))) .miniextendr_arg_error(\"x\", \"must not contain NA\")",
3038            ]
3039        );
3040        let none = explicit_output("fn f(n: i32)", &[], true);
3041        assert!(none.guards(None).is_empty());
3042    }
3043
3044    #[test]
3045    fn explicit_checks_apply_to_match_arg_params() {
3046        let sig: syn::Signature = syn::parse_str("fn f(mode: String)").unwrap();
3047        let opts = PreconditionOptions {
3048            explicit: [("mode".to_string(), no_na())].into_iter().collect(),
3049            ..Default::default()
3050        };
3051        let skip: HashSet<String> = ["mode".to_string()].into_iter().collect();
3052        let out = build_precondition_checks(&sig.inputs, &skip, &opts);
3053        assert_eq!(
3054            out.guards(None),
3055            vec!["if (!isTRUE(!anyNA(mode))) .miniextendr_arg_error(\"mode\", \"must not be NA\")"]
3056        );
3057    }
3058
3059    #[test]
3060    fn explicit_checks_take_the_caller_call() {
3061        let out = explicit_output("fn f(x: f64)", &[("x", no_na())], true);
3062        assert_eq!(
3063            out.guards(Some(".mx_call")),
3064            vec![
3065                "if (!isTRUE(!anyNA(x))) .miniextendr_arg_error(\"x\", \"must not be NA\", .mx_call)"
3066            ]
3067        );
3068    }
3069
3070    #[test]
3071    fn explicit_checks_merge() {
3072        let mut a = inherits(&["a"]);
3073        a.merge(no_na()).unwrap();
3074        a.merge(inherits(&["b"])).unwrap();
3075        assert_eq!(
3076            a,
3077            ExplicitChecks {
3078                inherits: Some(vec!["a".into(), "b".into()]),
3079                no_na: true,
3080                ..Default::default()
3081            }
3082        );
3083        assert!(ExplicitChecks::default().is_empty());
3084
3085        // One message per check: it may come from either attribute, not both.
3086        let mut b = inherits(&["a"]);
3087        b.merge(with_inherits_message(&["b"], "one")).unwrap();
3088        assert_eq!(b.inherits_message.as_deref(), Some("one"));
3089        assert_eq!(b.inherits, Some(vec!["a".into(), "b".into()]));
3090        b.merge(with_no_na_message("two")).unwrap();
3091        assert_eq!(b.no_na_message.as_deref(), Some("two"));
3092        let err = b.merge(with_inherits_message(&["c"], "three")).unwrap_err();
3093        assert!(
3094            err.contains("`inherits`") && err.contains("more than one `message`"),
3095            "{err}"
3096        );
3097        let err = b.merge(with_no_na_message("four")).unwrap_err();
3098        assert!(err.contains("`no_na`"), "{err}");
3099    }
3100
3101    /// The author's message replaces `'<p>' <requirement>` verbatim: the
3102    /// helper gets it as `message = `, and the call, when there is one, by
3103    /// name too (after a named `message` a positional call would bind to
3104    /// `what`). One message covers every class of the check, and the
3105    /// parameter's type checks.
3106    #[test]
3107    fn custom_messages_are_passed_verbatim_by_name() {
3108        let checks = ExplicitChecks {
3109            inherits: Some(vec!["pkg_a".into(), "pkg_b".into()]),
3110            inherits_message: Some("`model` must be a `pkg_model`; see pkg_model().".into()),
3111            no_na: true,
3112            no_na_message: Some("no NA in `model`".into()),
3113            ..Default::default()
3114        };
3115        let out = explicit_output("fn f(model: List)", &[("model", checks)], false);
3116        assert_eq!(
3117            out.guards(None),
3118            vec![
3119                "if (!isTRUE(inherits(model, c(\"pkg_a\", \"pkg_b\")))) .miniextendr_arg_error(\"model\", message = \"`model` must be a `pkg_model`; see pkg_model().\")",
3120                "if (!isTRUE(is.list(model))) .miniextendr_arg_error(\"model\", message = \"`model` must be a `pkg_model`; see pkg_model().\")",
3121                "if (!isTRUE(!anyNA(model))) .miniextendr_arg_error(\"model\", message = \"no NA in `model`\")",
3122            ]
3123        );
3124        let caller = out.guards(Some(".mx_call"));
3125        assert!(
3126            caller[0].ends_with(
3127                ".miniextendr_arg_error(\"model\", message = \"`model` must be a `pkg_model`; see pkg_model().\", call = .mx_call)"
3128            ),
3129            "{}",
3130            caller[0]
3131        );
3132        // A type check with the class message names the call too.
3133        assert!(
3134            caller[1].ends_with(
3135                ".miniextendr_arg_error(\"model\", message = \"`model` must be a `pkg_model`; see pkg_model().\", call = .mx_call)"
3136            ),
3137            "{}",
3138            caller[1]
3139        );
3140        assert!(
3141            caller[2].ends_with(
3142                ".miniextendr_arg_error(\"model\", message = \"no NA in `model`\", call = .mx_call)"
3143            ),
3144            "{}",
3145            caller[2]
3146        );
3147        // A type check without a class message keeps the positional call.
3148        let plain = explicit_output("fn f(model: List)", &[("model", no_na())], false);
3149        assert!(
3150            plain.guards(Some(".mx_call"))[0]
3151                .ends_with(".miniextendr_arg_error(\"model\", \"must be a list\", .mx_call)"),
3152            "{:?}",
3153            plain.guards(Some(".mx_call"))
3154        );
3155        // The `Option` / `Missing` guards are unchanged.
3156        let optional = explicit_output(
3157            "fn f(x: Option<List>)",
3158            &[("x", with_inherits_message(&["pkg_obj"], "need a pkg_obj"))],
3159            true,
3160        );
3161        assert_eq!(
3162            optional.guards(None),
3163            vec![
3164                "if (!isTRUE(is.null(x) || inherits(x, \"pkg_obj\"))) .miniextendr_arg_error(\"x\", message = \"need a pkg_obj\")"
3165            ]
3166        );
3167        let asserts = with_no_na_message("mine").value_assertions("x", &parse_type("f64"), false);
3168        assert_eq!(asserts[0].message(), "mine");
3169    }
3170
3171    /// A message is an R string literal: quotes, backslashes, newlines, tabs
3172    /// and control characters are escaped, and non-ASCII text becomes
3173    /// `\u{..}` / `\U{..}` escapes (R code in a package must be ASCII). `%`
3174    /// needs nothing: the message never goes through `sprintf()`.
3175    #[test]
3176    fn custom_messages_are_escaped_as_r_string_literals() {
3177        let message = "say \"hi\" \\ back\nline\ttab 100% caf\u{e9} \u{1F600} bell\u{7}";
3178        let out = explicit_output("fn f(x: f64)", &[("x", with_no_na_message(message))], true);
3179        assert_eq!(
3180            out.guards(None),
3181            vec![
3182                r#"if (!isTRUE(!anyNA(x))) .miniextendr_arg_error("x", message = "say \"hi\" \\ back\nline\ttab 100% caf\u{e9} \U{1f600} bell\u{7}")"#
3183            ]
3184        );
3185        assert_eq!(r_string_escape("a\rb"), r"a\rb");
3186        assert_eq!(r_string_escape("plain 'text'"), "plain 'text'");
3187        // Class names take the same escaping.
3188        let out = explicit_output("fn f(x: SEXP)", &[("x", inherits(&["caf\u{e9}"]))], true);
3189        assert_eq!(
3190            out.guards(None),
3191            vec![
3192                r#"if (!isTRUE(inherits(x, "caf\u{e9}"))) .miniextendr_arg_error("x", "must inherit from 'caf\u{e9}'")"#
3193            ]
3194        );
3195    }
3196}