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.
2829use std::collections::{HashMap, HashSet};
3031use crate::naming::r_string_escape;
3233/// 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).
40param: String,
41/// What the parameter must satisfy, without the parameter name
42 /// (`must be numeric`). The condition message is `'<param>' <requirement>`.
43requirement: String,
44/// R expression that must evaluate to `TRUE` for the check to pass.
45condition: 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>`.
50message: Option<String>,
51}
5253impl RAssertion {
54/// Create a new assertion on `param` with its requirement and R condition expression.
55fn new(
56 param: impl Into<String>,
57 requirement: impl Into<String>,
58 condition: impl Into<String>,
59 ) -> Self {
60Self {
61 param: param.into(),
62 requirement: requirement.into(),
63 condition: condition.into(),
64 message: None,
65 }
66 }
6768/// Use `message` verbatim as the condition message of a failure.
69fn with_message(mut self, message: Option<&String>) -> Self {
70self.message = message.cloned();
71self72 }
7374/// The condition message a failure raises: the author's message, else
75 /// `'<param>' <requirement>`.
76#[cfg(test)]
77fn message(&self) -> String {
78match &self.message {
79Some(message) => message.clone(),
80None => format!("'{}' {}", self.param, self.requirement),
81 }
82 }
8384/// 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`.
93fn to_guard(&self, call: Option<&str>) -> String {
94match &self.message {
95None => {
96let 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(\"{}\", \"{}\"{})",
99self.condition, self.param, self.requirement, call_arg
100 )101 }
102Some(message) => {
103let 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 = \"{}\"{})",
106self.condition,
107self.param,
108 r_string_escape(message),
109 call_arg
110 )111 }
112 }
113 }
114115/// Wrap for nullable: prepend `is.null(param) || ` to the condition,
116 /// and adjust the requirement to mention NULL.
117fn nullable(self) -> Self {
118let 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 };
127Self {
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,
130requirement,
131 message: self.message,
132 }
133 }
134}
135136/// 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`).
146pub coerce_all: bool,
147/// R-normalized names of parameters with a per-param `coerce` attribute.
148pub coerce_params: HashSet<String>,
149/// Checks the author named per parameter (`inherits`, `not_inherits`,
150 /// `no_na`), keyed by R-normalized parameter name.
151pub 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.
155pub 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.
159pub formals: crate::r_wrapper_builder::Formals,
160}
161162impl PreconditionOptions {
163/// Returns `true` if the parameter `r_name` is coerced (function-wide or
164 /// per-param).
165fn is_coerced(&self, r_name: &str) -> bool {
166self.coerce_all || self.coerce_params.contains(r_name)
167 }
168}
169170// region: which parameters keep their type-derived checks (#1566 §2)
171172/// 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 {
195param.unwrap_or_else(|| {
196resolve_item_type_checks(item, impl_level, crate_default, no_preconditions_default)
197 })
198}
199200/// [`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 {
209item.or(impl_level)
210 .or(crate_default)
211 .unwrap_or(!no_preconditions_default)
212}
213214/// 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> {
227inputs228 .iter()
229 .filter_map(|arg| {
230let syn::FnArg::Typed(pt) = argelse {
231return None;
232 };
233let syn::Pat::Ident(pat_ident) = pt.pat.as_ref() else {
234return None;
235 };
236let name = crate::naming::ident_name(&pat_ident.ident);
237let keep = resolve_type_checks(
238param(&name),
239item,
240impl_level,
241crate_default,
242falsecfg!(feature = "no-preconditions-default"),
243 );
244 (!keep).then(|| formals.of(&name))
245 })
246 .collect()
247}
248249/// 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 {
254r_check_for_type(ty).is_some()
255}
256257// endregion
258259/// 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(...))`).
304pub 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`.
308pub 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`.
312pub 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).
315pub 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`.
320pub 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.
325pub no_na: bool,
326/// `no_na(message = "...")`: the message of a failed NA check. Only set
327 /// together with `no_na`.
328pub no_na_message: Option<String>,
329}
330331impl ExplicitChecks {
332/// Whether any check is requested.
333pub fn is_empty(&self) -> bool {
334self.inherits.is_none() && self.not_inherits.is_none() && !self.no_na
335 }
336337/// 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.
343pub 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")?;
355if let Some(classes) = other.inherits {
356self.inherits.get_or_insert_with(Vec::new).extend(classes);
357 }
358self.inherits_hints.extend(other.inherits_hints);
359if let Some(classes) = other.not_inherits {
360self.not_inherits
361 .get_or_insert_with(Vec::new)
362 .extend(classes);
363 }
364self.no_na |= other.no_na;
365Ok(())
366 }
367368/// 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.
381pub fn class_conflict(&self, param: &str) -> Option<String> {
382let required = self.inherits.as_deref().unwrap_or_default();
383if let Some(class) = self384 .not_inherits
385 .iter()
386 .flatten()
387 .find(|c| required.contains(c))
388 {
389return 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 }
394let class = self
395.inherits_hints
396 .iter()
397 .flat_map(|hint| &hint.classes)
398 .find(|c| required.contains(c))?;
399Some(::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 }
405406/// 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`]).
419fn class_assertions(&self, param: &str, ty: &syn::Type) -> Vec<RAssertion> {
420let mut out = Vec::new();
421if let Some(classes) = &self.inherits {
422let (what, class_arg) = class_list_r(classes);
423for hint in &self.inherits_hints {
424let (_, 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 }
434out.push(
435RAssertion::new(
436param,
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 }
443if let Some(classes) = &self.not_inherits {
444let (what, class_arg) = class_list_r(classes);
445out.push(
446RAssertion::new(
447param,
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 }
454guarded(param, ty, out)
455 }
456457/// 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`]).
465fn value_assertions(&self, param: &str, ty: &syn::Type, choice: bool) -> Vec<RAssertion> {
466let mut out = Vec::new();
467if self.no_na && (choice || !no_na_checked_after_conversion(ty)) {
468out.push(
469RAssertion::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 }
473guarded(param, ty, out)
474 }
475476/// 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.
481fn type_check_message(&self) -> Option<&String> {
482self.inherits_message.as_ref()
483 }
484}
485486/// 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).
494pub classes: Vec<String>,
495/// The condition message, used verbatim.
496pub message: String,
497}
498499/// 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) {
503let quoted: Vec<String> = classes504 .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();
507let literals: Vec<String> = classes508 .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();
511let 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 };
516let 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}
522523/// 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> {
529let guard = if crate::miniextendr_fn::is_missing_type(ty) {
530Some(::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) {
532Some(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("is.null({0})", param))
})format!("is.null({param})"))
533 } else {
534None535 };
536if let Some(guard) = guard {
537for 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 }
541out542}
543544/// 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 {
562crate::type_inspect::choice_layers(ty)
563 .either_right
564 .is_some()
565}
566567/// 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 {
574let 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}
581582/// 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 {
588let ty = crate::miniextendr_fn::get_missing_inner_type(ty).unwrap_or(ty);
589let ty = crate::type_inspect::option_inner_type(ty).unwrap_or(ty);
590if let Some((left, right)) = crate::type_inspect::either_arms(ty) {
591return no_na_holds_several(left) || no_na_holds_several(right);
592 }
593if let syn::Type::Path(tp) = ty594 && let Some(seg) = tp.path.segments.last()
595 && seg.ident == "Result"
596&& let Some(ok) = crate::type_inspect::first_type_argument(seg)
597 {
598return no_na_holds_several(ok);
599 }
600is_vector_valued(ty)
601}
602603/// 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 {
610match custom {
611Some(message) => message.to_string(),
612None => ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("\'{1}\' {0}",
no_na_requirement(ty), param))
})format!("'{param}' {}", no_na_requirement(ty)),
613 }
614}
615616/// 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 {
626if let Some(check) = r_check_for_type(ty) {
627return check.is_vector();
628 }
629crate::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)
633if tp.path.segments.last().is_some_and(|s| s.ident.to_string().ends_with("Vec"))
634 )635}
636637/// 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> {
643match (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);
649Ok(())
650 }
651 (_, None) => Ok(()),
652 }
653}
654655/// 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"`.
667ScalarNumeric,
668/// Coerced bool: logical or integer, with length one.
669ScalarLogicalOrInteger,
670/// Coerced bool vector: logical or integer.
671VectorLogicalOrInteger,
672/// Coerced native `i32` scalar: integer, logical, raw, or a whole-number
673 /// double, with length one. The vector form is [`RTypeCheck::VectorIntegerWide`].
674ScalarIntegerWide,
675/// Non-negative numeric scalar: type + length-1 + `>= 0` (3 assertions).
676 /// Used for `u16`, `u32`, `u64`, `usize`.
677ScalarNonNeg,
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"`).
680Scalar(&'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.
686VectorNumeric,
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.
695VectorIntegerStrict,
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.
706VectorIntegerWide,
707/// Non-numeric vector: `is.<type>(x)` only (1 assertion).
708 /// The string is the R type predicate name.
709Vector(&'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.
713ScalarNumericOrText,
714/// `AsNumericVec`: numeric, logical, character, or factor (1 assertion).
715VectorNumericOrText,
716/// Nullable wrapper around an inner check: prepends `is.null(x) ||` to each assertion
717 /// and adjusts messages to mention NULL.
718Nullable(Box<RTypeCheck>),
719/// List check: `is.list(x)` (1 assertion).
720 /// Used for `HashMap`, `BTreeMap`, `NamedList`, `List`, `ListMut`.
721List,
722}
723724/// 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}
735736/// 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}
751752/// 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}
763764impl 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).
769fn assertions(&self, param: &str) -> Vec<RAssertion> {
770let length_one = || {
771RAssertion::new(
772param,
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 };
777match 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",
790format!("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",
797format!("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
819format!("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,
825format!("must be {r_type}"),
826format!("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",
838format!("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,
847format!("must be {r_type}"),
848format!("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) => inner864 .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",
871format!("is.list({param})"),
872 )],
873 }
874 }
875876/// 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.
879fn is_vector(&self) -> bool {
880match self {
881 RTypeCheck::VectorLogicalOrInteger882 | RTypeCheck::VectorNumeric883 | RTypeCheck::VectorIntegerStrict884 | RTypeCheck::VectorIntegerWide885 | RTypeCheck::Vector(_)
886 | RTypeCheck::VectorNumericOrText887 | RTypeCheck::List => true,
888 RTypeCheck::ScalarNumeric889 | RTypeCheck::ScalarLogicalOrInteger890 | RTypeCheck::ScalarIntegerWide891 | RTypeCheck::ScalarNonNeg892 | RTypeCheck::Scalar(_)
893 | RTypeCheck::ScalarNumericOrText => false,
894 RTypeCheck::Nullable(inner) => inner.is_vector(),
895 }
896 }
897898/// 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).
912fn expectation(&self) -> String {
913match 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}
940941/// 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> {
956let ty = crate::miniextendr_fn::get_missing_inner_type(ty).unwrap_or(ty);
957let Some(check) = r_check_for_type(ty) else {
958return unchecked_expectation(ty);
959 };
960let check = if coerced {
961coerce_widened(check, ty)
962 } else {
963check964 };
965Some(check.expectation())
966}
967968/// 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> {
979match ty {
980 syn::Type::Tuple(tuple) if !tuple.elems.is_empty() => {
981Some(::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) => {
984let segment = type_path.path.segments.last()?;
985match 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" => {
990let left = crate::type_inspect::first_type_argument(segment)?;
991let right = crate::type_inspect::second_type_argument(segment)?;
992Some(::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" => {
999let inner = extract_single_generic_arg(segment)?;
1000Some(::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}
10081009/// 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> {
1016let scalar = match ty {
1017 syn::Type::Path(tp) => tp.path.segments.last().map(|s| s.ident.to_string()),
1018_ => None,
1019 };
1020match scalar.as_deref() {
1021Some("i64" | "isize") => Some("a single whole number".into()),
1022Some("u64" | "usize") => Some("a single non-negative whole number".into()),
1023_ => conversion_expectation(ty, false),
1024 }
1025}
10261027/// 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 {
1036use crate::miniextendr_fn::CoercionMapping;
1037match CoercionMapping::from_type(ty) {
1038Some(CoercionMapping::Bool) => RTypeCheck::ScalarLogicalOrInteger,
1039Some(CoercionMapping::BoolVec) => RTypeCheck::VectorLogicalOrInteger,
1040Some(CoercionMapping::NativeInt) => RTypeCheck::ScalarIntegerWide,
1041Some(CoercionMapping::NativeIntVec) => RTypeCheck::VectorIntegerWide,
1042Some(CoercionMapping::NativeReal) => RTypeCheck::ScalarNumeric,
1043Some(CoercionMapping::NativeRealVec) => RTypeCheck::VectorNumeric,
1044_ => check,
1045 }
1046}
10471048/// 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> {
1052match 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}
10581059/// 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> {
1065let segment = type_path.path.segments.last()?;
1066let ident = segment.ident.to_string();
10671068match 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")),
10761077// 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),
10801081// Unsigned numeric scalars (non-negative constraint)
1082"u16" | "u32" | "u64" | "usize" => Some(RTypeCheck::ScalarNonNeg),
10831084// Logical scalar
1085"bool" | "Rbool" | "Rboolean" => Some(RTypeCheck::Scalar("logical")),
10861087// Character scalar
1088"String" | "char" | "PathBuf" => Some(RTypeCheck::Scalar("character")),
10891090// Raw scalar
1091"u8" => Some(RTypeCheck::Scalar("raw")),
10921093// Complex scalar
1094"Rcomplex" => Some(RTypeCheck::Scalar("complex")),
10951096// `as.numeric()`-style markers: numbers, text, and factor labels.
1097"AsNumeric" => Some(RTypeCheck::ScalarNumericOrText),
1098"AsNumericVec" => Some(RTypeCheck::VectorNumericOrText),
10991100// `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")),
11051106// Option<T> → Nullable
1107"Option" => {
1108let inner_ty = extract_single_generic_arg(segment)?;
1109r_check_for_type(inner_ty).map(|inner| RTypeCheck::Nullable(Box::new(inner)))
1110 }
11111112// Vec<T> → Vector (depends on element type)
1113"Vec" => {
1114let inner_ty = extract_single_generic_arg(segment)?;
1115r_check_for_vec_element(inner_ty)
1116 }
11171118// Map types and named list → List
1119"HashMap" | "BTreeMap" | "NamedList" => Some(RTypeCheck::List),
11201121// List (bare) → List
1122"List" | "ListMut" => Some(RTypeCheck::List),
11231124// 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,
11271128// Unknown type → skip (let Rust side validate)
1129_ => None,
1130 }
1131}
11321133/// 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> {
1138match type_ref.elem.as_ref() {
1139// &str → character scalar
1140syn::Type::Path(tp) => {
1141let seg = tp.path.segments.last()?;
1142match 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
1150syn::Type::Slice(slice) => r_check_for_vec_element(&slice.elem),
1151_ => None,
1152 }
1153}
11541155/// 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> {
1161let syn::Type::Path(tp) = elem_tyelse {
1162return None;
1163 };
1164let seg = tp.path.segments.last()?;
1165let ident = seg.ident.to_string();
11661167match 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),
11721173// `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),
11811182// 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" => {
1191Some(RTypeCheck::VectorIntegerWide)
1192 }
11931194// Logical vector
1195"bool" => Some(RTypeCheck::Vector("logical")),
11961197// Character vector
1198"String" => Some(RTypeCheck::Vector("character")),
11991200// Raw vector
1201"u8" => Some(RTypeCheck::Vector("raw")),
12021203// Complex vector
1204"Rcomplex" => Some(RTypeCheck::Vector("complex")),
12051206// Vec<Option<T>> — e.g., Vec<Option<String>> for nullable strings
1207"Option" => {
1208let inner = extract_single_generic_arg(seg)?;
1209// Vec<Option<String>> → character, Vec<Option<i32>> → numeric, etc.
1210r_check_for_vec_element(inner)
1211 }
12121213_ => None,
1214 }
1215}
12161217/// 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> {
1221if let syn::PathArguments::AngleBracketed(ref args) = segment.arguments
1222 && let Some(syn::GenericArgument::Type(ty)) = args.args.first()
1223 {
1224return Some(ty);
1225 }
1226None1227}
12281229/// 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`).
1236pub r_name: String,
1237}
12381239/// 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`].
1247assertions: Vec<RAssertion>,
1248/// Parameters with unknown custom types that were not prechecked.
1249#[allow(dead_code)] // Read in tests
1250pub fallback_params: Vec<FallbackParam>,
1251}
12521253impl 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).
1262pub fn guards(&self, call: Option<&str>) -> Vec<String> {
1263self.assertions.iter().map(|a| a.to_guard(call)).collect()
1264 }
1265}
12661267/// 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}
12781279/// 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 {
1285match ty {
1286 syn::Type::Path(tp) => {
1287let Some(seg) = tp.path.segments.last() else {
1288return false;
1289 };
1290 !is_skip_type(&seg.ident.to_string())
1291 }
1292// References (&str, &[T], &Dots) are handled by static table or skipped
1293syn::Type::Reference(_) => false,
1294_ => false,
1295 }
1296}
12971298/// 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 {
1322let mut assertions = Vec::new();
1323let mut fallback_params = Vec::new();
13241325for arg in inputs {
1326// Skip receiver (self/&self/&mut self)
1327let syn::FnArg::Typed(pt) = arg else {
1328continue;
1329 };
13301331// `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).
1334if crate::type_inspect::is_nargs_marker(&pt.ty)
1335 || crate::type_inspect::is_lazy_dots_marker(&pt.ty)
1336 {
1337continue;
1338 }
13391340// Extract parameter name
1341let syn::Pat::Ident(pat_ident) = pt.pat.as_ref() else {
1342continue;
1343 };
13441345// The parameter's R formal (#1842): what the checks read and name
1346let r_name = opts.formals.of_ident(&pat_ident.ident);
1347let explicit = opts.explicit.get(&r_name);
13481349// The class checks first, so a value of the wrong class gets their
1350 // message whatever its type.
1351if let Some(checks) = explicit {
1352 assertions.extend(checks.class_assertions(&r_name, pt.ty.as_ref()));
1353 }
13541355// Type-derived checks: skipped for match_arg params (already validated
1356 // by match.arg()) and for the parameters that drop them.
1357if !opts.unchecked.contains(&r_name) && !skip_params.contains(&r_name) {
1358// Preserve the ordinary input domain and include any coercion extensions.
1359if let Some(mut check) = r_check_for_type(pt.ty.as_ref()) {
1360if opts.is_coerced(&r_name) {
1361 check = coerce_widened(check, pt.ty.as_ref());
1362 }
1363let mut type_assertions = check.assertions(&r_name);
1364// An `inherits` message covers the type checks too.
1365if let Some(message) = explicit.and_then(ExplicitChecks::type_check_message) {
1366for 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
1373fallback_params.push(FallbackParam {
1374 r_name: r_name.clone(),
1375 });
1376 }
1377 }
13781379if 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 }
13871388PreconditionOutput {
1389assertions,
1390fallback_params,
1391 }
1392}
13931394#[cfg(test)]
1395mod tests {
1396use super::*;
13971398/// Helper to parse a type string into syn::Type
1399fn parse_type(s: &str) -> syn::Type {
1400 syn::parse_str(s).unwrap()
1401 }
14021403/// Helper to get assertions for a type.
1404fn assertions_for(ty_str: &str, param: &str) -> Vec<RAssertion> {
1405let ty = parse_type(ty_str);
1406 r_check_for_type(&ty).unwrap().assertions(param)
1407 }
14081409#[test]
1410fn scalar_numeric_produces_two_assertions() {
1411let asserts = assertions_for("i64", "x");
1412assert_eq!(asserts.len(), 2);
1413assert_eq!(asserts[0].message(), "'x' must be numeric, logical, or raw");
1414assert_eq!(
1415 asserts[0].condition,
1416"is.numeric(x) || is.logical(x) || is.raw(x)"
1417);
1418assert_eq!(asserts[1].message(), "'x' must have length 1");
1419assert_eq!(asserts[1].condition, "length(x) == 1L");
1420 }
14211422#[test]
1423fn native_scalars_use_their_storage_gate() {
1424// `i32` / `f64` convert from one SEXPTYPE, so the gate says so (#1112).
1425let asserts = assertions_for("i32", "x");
1426assert_eq!(asserts.len(), 2);
1427assert_eq!(asserts[0].message(), "'x' must be integer");
1428assert_eq!(asserts[0].condition, "is.integer(x)");
1429let asserts = assertions_for("f64", "x");
1430assert_eq!(asserts[0].message(), "'x' must be double");
1431assert_eq!(asserts[0].condition, "is.double(x)");
1432let asserts = assertions_for("Vec<f64>", "x");
1433assert_eq!(asserts.len(), 1);
1434assert_eq!(asserts[0].condition, "is.double(x)");
1435let asserts = assertions_for("&[f64]", "x");
1436assert_eq!(asserts[0].condition, "is.double(x)");
1437 }
14381439#[test]
1440fn all_signed_numeric_types_use_scalar_numeric() {
1441for ty_str in &["f32", "i8", "i16", "i64", "isize"] {
1442let asserts = assertions_for(ty_str, "x");
1443assert_eq!(asserts.len(), 2, "{} should produce 2 assertions", ty_str);
1444assert!(
1445 asserts[0].condition.contains("is.numeric(x)"),
1446"{} type check",
1447 ty_str
1448 );
1449assert!(
1450 asserts[0].condition.contains("is.logical(x)"),
1451"{} accepts logical",
1452 ty_str
1453 );
1454assert!(
1455 asserts[0].condition.contains("is.raw(x)"),
1456"{} accepts raw",
1457 ty_str
1458 );
1459 }
1460 }
14611462#[test]
1463fn scalar_non_neg_produces_three_assertions() {
1464let asserts = assertions_for("u32", "n");
1465assert_eq!(asserts.len(), 3);
1466assert_eq!(asserts[0].message(), "'n' must be numeric, logical, or raw");
1467assert_eq!(asserts[1].message(), "'n' must have length 1");
1468assert_eq!(asserts[2].message(), "'n' must be non-negative");
1469assert_eq!(asserts[2].condition, "is.raw(n) || n >= 0");
1470 }
14711472#[test]
1473fn all_unsigned_types_use_scalar_non_neg() {
1474for ty_str in &["u16", "u32", "u64", "usize"] {
1475let asserts = assertions_for(ty_str, "x");
1476assert_eq!(asserts.len(), 3, "{} should produce 3 assertions", ty_str);
1477assert!(
1478 asserts[2].condition.contains(">= 0"),
1479"{} non-neg check",
1480 ty_str
1481 );
1482 }
1483 }
14841485#[test]
1486fn scalar_logical() {
1487let asserts = assertions_for("bool", "x");
1488assert_eq!(asserts.len(), 2);
1489assert_eq!(asserts[0].message(), "'x' must be logical");
1490assert_eq!(asserts[0].condition, "is.logical(x)");
1491assert_eq!(asserts[1].condition, "length(x) == 1L");
1492 }
14931494#[test]
1495fn scalar_character() {
1496for ty_str in &["String", "char", "PathBuf"] {
1497let asserts = assertions_for(ty_str, "s");
1498assert_eq!(asserts.len(), 2);
1499assert_eq!(asserts[0].message(), "'s' must be character");
1500assert_eq!(asserts[0].condition, "is.character(s)");
1501 }
1502 }
15031504#[test]
1505fn ref_str() {
1506let ty: syn::Type = syn::parse_str("& str").unwrap();
1507let asserts = r_check_for_type(&ty).unwrap().assertions("s");
1508assert_eq!(asserts.len(), 2);
1509assert_eq!(asserts[0].condition, "is.character(s)");
1510 }
15111512#[test]
1513fn scalar_raw() {
1514let asserts = assertions_for("u8", "x");
1515assert_eq!(asserts.len(), 2);
1516assert_eq!(asserts[0].message(), "'x' must be raw");
1517assert_eq!(asserts[0].condition, "is.raw(x)");
1518 }
15191520#[test]
1521fn vector_float_stays_loose() {
1522// `Vec<f32>` coerces from every numeric source, so it keeps the loose predicate.
1523let asserts = assertions_for("Vec<f32>", "x");
1524assert_eq!(asserts.len(), 1);
1525assert_eq!(
1526 asserts[0].condition,
1527"is.numeric(x) || is.logical(x) || is.raw(x)"
1528);
1529 }
15301531#[test]
1532fn 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).
1536let asserts = assertions_for("Vec<i32>", "x");
1537assert_eq!(asserts.len(), 1);
1538assert_eq!(asserts[0].condition, "is.integer(x)");
1539assert_eq!(asserts[0].message(), "'x' must be an integer vector");
1540 }
15411542#[test]
1543fn 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.
1546for 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 ] {
1556let asserts = assertions_for(ty_str, "x");
1557assert_eq!(asserts.len(), 1, "{} should produce 1 assertion", ty_str);
1558assert_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 );
1565assert_eq!(
1566 asserts[0].message(),
1567"'x' must be integer or whole-number numeric"
1568);
1569 }
1570 }
15711572#[test]
1573fn slice_int_is_strict() {
1574let ty: syn::Type = syn::parse_str("& [i32]").unwrap();
1575let asserts = r_check_for_type(&ty).unwrap().assertions("x");
1576assert_eq!(asserts.len(), 1);
1577assert_eq!(asserts[0].condition, "is.integer(x)");
1578 }
15791580#[test]
1581fn coerced_native_int_widens_to_whole_number_gate() {
1582let ty = parse_type("i32");
1583let asserts = coerce_widened(r_check_for_type(&ty).unwrap(), &ty).assertions("x");
1584assert_eq!(asserts.len(), 2);
1585assert_eq!(
1586 asserts[0].message(),
1587"'x' must be integer or whole-number numeric"
1588);
1589assert!(asserts[0].condition.contains("x == trunc(x)"));
1590assert_eq!(asserts[1].condition, "length(x) == 1L");
15911592let ty = parse_type("Vec<i32>");
1593let asserts = coerce_widened(r_check_for_type(&ty).unwrap(), &ty).assertions("x");
1594assert_eq!(asserts.len(), 1);
1595assert!(asserts[0].condition.contains("x == trunc(x)"));
15961597// Native doubles move from `is.double` to the loose numeric gate;
1598 // borrowed slices have no mapping and stay strict.
1599for name in ["f64", "Vec<f64>"] {
1600let ty = parse_type(name);
1601let actual = coerce_widened(r_check_for_type(&ty).unwrap(), &ty).assertions("x");
1602assert_eq!(
1603 actual[0].condition, "is.numeric(x) || is.logical(x) || is.raw(x)",
1604"{name}"
1605);
1606 }
1607let ty = parse_type("&[i32]");
1608let actual = coerce_widened(r_check_for_type(&ty).unwrap(), &ty).assertions("x");
1609assert_eq!(actual[0].condition, "is.integer(x)");
1610 }
16111612#[test]
1613fn coerced_numeric_and_optional_checks_are_preserved() {
1614for name in ["Vec<u16>", "Vec<f32>", "Option<bool>"] {
1615let ty = parse_type(name);
1616let expected = r_check_for_type(&ty).unwrap().assertions("x");
1617let actual = coerce_widened(r_check_for_type(&ty).unwrap(), &ty).assertions("x");
1618assert_eq!(actual.len(), expected.len());
1619for (actual, expected) in actual.iter().zip(&expected) {
1620assert_eq!(actual.condition, expected.condition);
1621 }
1622 }
1623 }
16241625#[test]
1626fn coerced_bool_checks_accept_logical_and_integer() {
1627for name in ["bool", "Vec<bool>"] {
1628let ty = parse_type(name);
1629let actual = coerce_widened(r_check_for_type(&ty).unwrap(), &ty).assertions("x");
1630assert_eq!(actual[0].condition, "is.logical(x) || is.integer(x)");
1631assert_eq!(actual.len(), if name == "bool" { 2 } else { 1 });
1632 }
1633 }
16341635#[test]
1636fn build_checks_coerced_param_preserves_whole_number_check() {
1637let sig: syn::Signature = syn::parse_str("fn f(x: Vec<u16>)").unwrap();
1638let mut coerce_params = HashSet::new();
1639 coerce_params.insert("x".to_string());
1640let opts = PreconditionOptions {
1641 coerce_params,
1642 ..Default::default()
1643 };
1644let output = build_precondition_checks(&sig.inputs, &HashSet::new(), &opts);
1645let joined = output.guards(None).join("\n");
1646assert!(joined.contains("is.integer(x)"));
1647assert!(joined.contains("trunc(x)"));
1648 }
16491650/// 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]
1655fn guards_raise_the_argument_error_with_the_given_call() {
1656let sig: syn::Signature = syn::parse_str("fn f(n: i32, xs: Vec<f64>)").unwrap();
1657let output = build_precondition_checks(
1658&sig.inputs,
1659&HashSet::new(),
1660&PreconditionOptions::default(),
1661 );
1662assert_eq!(
1663 output.guards(None),
1664vec![
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 );
1670assert_eq!(
1671 output.guards(Some(".mx_call")),
1672vec![
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 );
1678let no_params: syn::Signature = syn::parse_str("fn g()").unwrap();
1679let empty = build_precondition_checks(
1680&no_params.inputs,
1681&HashSet::new(),
1682&PreconditionOptions::default(),
1683 );
1684assert!(empty.guards(None).is_empty());
1685assert!(empty.guards(Some(".mx_call")).is_empty());
1686 }
16871688/// 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]
1692fn conversion_expectation_follows_the_type_check() {
1693let exp = |ty: &str, coerced: bool| conversion_expectation(&parse_type(ty), coerced);
1694let some = |s: &str| Some(s.to_string());
1695assert_eq!(exp("AsNumericVec", false), some("numeric"));
1696assert_eq!(exp("AsNumeric", false), some("a single number"));
1697assert_eq!(
1698 exp("Option<AsNumeric>", false),
1699 some("NULL or a single number")
1700 );
1701assert_eq!(exp("i32", false), some("a single integer"));
1702assert_eq!(exp("i32", true), some("a single whole number"));
1703assert_eq!(exp("f64", false), some("a single double"));
1704assert_eq!(exp("f64", true), some("a single number"));
1705assert_eq!(exp("i64", false), some("a single number"));
1706assert_eq!(exp("u32", false), some("a single non-negative number"));
1707assert_eq!(exp("bool", false), some("TRUE or FALSE"));
1708assert_eq!(exp("Option<bool>", false), some("NULL, TRUE or FALSE"));
1709assert_eq!(exp("bool", true), some("a single logical or integer"));
1710assert_eq!(exp("String", false), some("a single string"));
1711assert_eq!(exp("&str", false), some("a single string"));
1712assert_eq!(exp("u8", false), some("a single raw value"));
1713assert_eq!(exp("Rcomplex", false), some("a single complex value"));
1714assert_eq!(exp("Vec<i32>", false), some("integer"));
1715assert_eq!(
1716 exp("Vec<i32>", true),
1717 some("integer or whole-number numeric")
1718 );
1719assert_eq!(
1720 exp("Vec<u16>", false),
1721 some("integer or whole-number numeric")
1722 );
1723assert_eq!(exp("Vec<f64>", false), some("double"));
1724assert_eq!(exp("&[f64]", false), some("double"));
1725assert_eq!(exp("Vec<Option<String>>", false), some("character"));
1726assert_eq!(exp("Vec<bool>", true), some("logical or integer"));
1727assert_eq!(exp("HashMap<String, i32>", false), some("a list"));
1728assert_eq!(exp("Missing<f64>", false), some("a single double"));
1729assert_eq!(
1730 exp("AsCharacter", false),
1731 some("coercible to a single string")
1732 );
1733assert_eq!(exp("AsCharacterVec", false), some("coercible to character"));
1734assert_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.
1739assert_eq!(exp("AsFromStr<IpAddr>", false), some("a single string"));
1740assert_eq!(exp("AsFromStrVec<i32>", false), some("character"));
1741assert_eq!(
1742 exp("Option<AsFromStr<IpAddr>>", false),
1743 some("NULL or a single string")
1744 );
1745assert_eq!(
1746 exp("Either<i32, String>", false),
1747 some("a single integer or a single string")
1748 );
1749assert_eq!(
1750 exp("Either<f64, Vec<i32>>", false),
1751 some("a single double or integer")
1752 );
1753assert_eq!(
1754 exp("Either<bool, Either<i32, String>>", false),
1755 some("TRUE or FALSE or a single integer or a single string")
1756 );
1757assert_eq!(
1758 exp("Option<Either<i32, String>>", false),
1759 some("NULL or a single integer or a single string")
1760 );
1761assert_eq!(
1762 exp("Option<Either<f64, String>>", false),
1763 some("NULL or a single double or a single string")
1764 );
1765assert_eq!(
1766 exp("Option<Either<AsNumericVec, DataFrame>>", false),
1767 some("NULL or numeric or a data frame")
1768 );
1769assert_eq!(exp("(i32, String)", false), some("a list of length 2"));
1770assert_eq!(exp("DataFrame", false), some("a data frame"));
1771assert_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.
1780for 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 ] {
1795assert_eq!(exp(unknown, false), None, "{unknown}");
1796 }
1797// A `choices(...)` `several_ok` string list with another kind of value
1798 // (#1612).
1799assert_eq!(
1800 exp("Either<Vec<String>, f64>", false),
1801 some("character or a single double")
1802 );
1803 }
18041805#[test]
1806fn strict_expectation_names_a_whole_number() {
1807let exp = |ty: &str| strict_conversion_expectation(&parse_type(ty));
1808let some = |s: &str| Some(s.to_string());
1809assert_eq!(exp("i64"), some("a single whole number"));
1810assert_eq!(exp("isize"), some("a single whole number"));
1811assert_eq!(exp("u64"), some("a single non-negative whole number"));
1812assert_eq!(exp("usize"), some("a single non-negative whole number"));
1813assert_eq!(exp("Vec<i64>"), some("integer or whole-number numeric"));
1814assert_eq!(
1815 exp("Vec<Option<u64>>"),
1816 some("integer or whole-number numeric")
1817 );
1818 }
18191820#[test]
1821fn vector_character() {
1822let asserts = assertions_for("Vec<String>", "x");
1823assert_eq!(asserts.len(), 1);
1824assert_eq!(asserts[0].condition, "is.character(x)");
1825 }
18261827#[test]
1828fn vector_optional_string() {
1829let asserts = assertions_for("Vec<Option<String>>", "x");
1830assert_eq!(asserts.len(), 1);
1831assert_eq!(asserts[0].condition, "is.character(x)");
1832 }
18331834#[test]
1835fn slice_u8() {
1836let ty: syn::Type = syn::parse_str("& [u8]").unwrap();
1837let asserts = r_check_for_type(&ty).unwrap().assertions("x");
1838assert_eq!(asserts.len(), 1);
1839assert_eq!(asserts[0].condition, "is.raw(x)");
1840 }
18411842#[test]
1843fn nullable_wraps_inner_assertions() {
1844let asserts = assertions_for("Option<i64>", "x");
1845assert_eq!(asserts.len(), 2);
1846assert_eq!(
1847 asserts[0].message(),
1848"'x' must be NULL or numeric, logical, or raw"
1849);
1850assert_eq!(
1851 asserts[0].condition,
1852"is.null(x) || is.numeric(x) || is.logical(x) || is.raw(x)"
1853);
1854let asserts = assertions_for("Option<i32>", "x");
1855assert_eq!(asserts[0].condition, "is.null(x) || is.integer(x)");
1856assert_eq!(asserts[1].message(), "'x' must be NULL or have length 1");
1857assert_eq!(asserts[1].condition, "is.null(x) || length(x) == 1L");
1858 }
18591860#[test]
1861fn nullable_character() {
1862let asserts = assertions_for("Option<String>", "s");
1863assert_eq!(asserts.len(), 2);
1864assert_eq!(asserts[0].message(), "'s' must be NULL or character");
1865assert_eq!(asserts[0].condition, "is.null(s) || is.character(s)");
1866assert_eq!(asserts[1].message(), "'s' must be NULL or have length 1");
1867 }
18681869#[test]
1870fn as_numeric_markers_admit_numbers_text_and_factors() {
1871let condition = "is.numeric(x) || is.logical(x) || is.character(x) || is.factor(x)";
1872let asserts = assertions_for("AsNumeric", "x");
1873assert_eq!(asserts.len(), 2);
1874assert_eq!(
1875 asserts[0].message(),
1876"'x' must be numeric, logical, character, or factor"
1877);
1878assert_eq!(asserts[0].condition, condition);
1879assert_eq!(asserts[1].message(), "'x' must have length 1");
1880assert_eq!(asserts[1].condition, "length(x) == 1L");
18811882// Path-qualified spellings resolve by their last segment.
1883let asserts = assertions_for("miniextendr_api::AsNumericVec", "x");
1884assert_eq!(asserts.len(), 1);
1885assert_eq!(
1886 asserts[0].message(),
1887"'x' must be numeric, logical, character, or factor"
1888);
1889assert_eq!(asserts[0].condition, condition);
1890 }
18911892#[test]
1893fn optional_as_numeric_is_nullable() {
1894let asserts = assertions_for("Option<AsNumeric>", "x");
1895assert_eq!(asserts.len(), 2);
1896assert_eq!(
1897 asserts[0].message(),
1898"'x' must be NULL or numeric, logical, character, or factor"
1899);
1900assert_eq!(
1901 asserts[0].condition,
1902"is.null(x) || is.numeric(x) || is.logical(x) || is.character(x) || is.factor(x)"
1903);
1904assert_eq!(asserts[1].message(), "'x' must be NULL or have length 1");
1905let asserts = assertions_for("Option<AsNumericVec>", "x");
1906assert_eq!(asserts.len(), 1);
1907assert!(asserts[0].condition.starts_with("is.null(x) || "));
1908 }
19091910#[test]
1911fn as_character_markers_admit_atomic_vectors() {
1912let asserts = assertions_for("AsCharacter", "x");
1913assert_eq!(asserts.len(), 2);
1914assert_eq!(asserts[0].message(), "'x' must be atomic");
1915assert_eq!(asserts[0].condition, "is.atomic(x)");
1916assert_eq!(asserts[1].message(), "'x' must have length 1");
1917assert_eq!(asserts[1].condition, "length(x) == 1L");
19181919// Path-qualified spellings resolve by their last segment.
1920let asserts = assertions_for("miniextendr_api::AsCharacterVec", "x");
1921assert_eq!(asserts.len(), 1);
1922assert_eq!(asserts[0].message(), "'x' must be atomic");
1923assert_eq!(asserts[0].condition, "is.atomic(x)");
1924 }
19251926#[test]
1927fn optional_as_character_is_nullable() {
1928let asserts = assertions_for("Option<AsCharacter>", "x");
1929assert_eq!(asserts.len(), 2);
1930assert_eq!(asserts[0].message(), "'x' must be NULL or atomic");
1931assert_eq!(asserts[0].condition, "is.null(x) || is.atomic(x)");
1932assert_eq!(asserts[1].message(), "'x' must be NULL or have length 1");
1933let asserts = assertions_for("Option<AsCharacterVec>", "x");
1934assert_eq!(asserts.len(), 1);
1935assert_eq!(asserts[0].message(), "'x' must be NULL or atomic");
1936assert_eq!(asserts[0].condition, "is.null(x) || is.atomic(x)");
1937 }
19381939#[test]
1940fn as_character_vec_with_no_na_checks_type_then_na() {
1941let out = explicit_output("fn f(x: AsCharacterVec)", &[("x", no_na())], false);
1942assert_eq!(
1943 out.guards(None),
1944vec![
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 }
19501951#[test]
1952fn coerce_keeps_the_as_numeric_gate() {
1953// `AsNumeric` has no coercion mapping: `coerce` leaves its gate alone.
1954let ty = parse_type("AsNumericVec");
1955let check = coerce_widened(r_check_for_type(&ty).unwrap(), &ty);
1956let asserts = check.assertions("x");
1957assert_eq!(asserts.len(), 1);
1958assert!(asserts[0].condition.contains("is.factor(x)"));
1959 }
19601961#[test]
1962fn map_types() {
1963for ty_str in &["HashMap<String, i32>", "BTreeMap<String, f64>"] {
1964let ty = parse_type(ty_str);
1965let asserts = r_check_for_type(&ty).unwrap().assertions("x");
1966assert_eq!(asserts.len(), 1);
1967assert_eq!(asserts[0].condition, "is.list(x)");
1968 }
1969 }
19701971#[test]
1972fn skip_types() {
1973for ty_str in &["SEXP", "ExternalPtr<MyType>"] {
1974let ty = parse_type(ty_str);
1975assert!(
1976 r_check_for_type(&ty).is_none(),
1977"{} should be skipped",
1978 ty_str
1979 );
1980 }
1981 }
19821983#[test]
1984fn scalar_param_produces_one_guard_per_check() {
1985let sig: syn::Signature = syn::parse_str("fn f(n: i32)").unwrap();
1986let output = build_precondition_checks(
1987&sig.inputs,
1988&HashSet::new(),
1989&PreconditionOptions::default(),
1990 );
1991let checks = output.guards(None);
1992assert_eq!(checks.len(), 2);
1993assert!(checks[0].contains("\"must be integer\""));
1994assert!(checks[1].contains("\"must have length 1\""));
1995assert!(output.fallback_params.is_empty());
1996 }
19971998#[test]
1999fn vector_param_single_guard() {
2000// Vec<f64> produces 1 check → one guard line
2001let sig: syn::Signature = syn::parse_str("fn f(x: Vec<f64>)").unwrap();
2002let output = build_precondition_checks(
2003&sig.inputs,
2004&HashSet::new(),
2005&PreconditionOptions::default(),
2006 );
2007assert_eq!(
2008 output.guards(None),
2009vec!["if (!isTRUE(is.double(x))) .miniextendr_arg_error(\"x\", \"must be double\")"]
2010 );
2011 }
20122013#[test]
2014fn two_scalar_params_produce_four_guards_in_order() {
2015let sig: syn::Signature = syn::parse_str("fn f(a: i32, b: f64)").unwrap();
2016let output = build_precondition_checks(
2017&sig.inputs,
2018&HashSet::new(),
2019&PreconditionOptions::default(),
2020 );
2021let checks = output.guards(None);
2022// 2 checks per param
2023assert_eq!(checks.len(), 4);
2024assert!(checks[0].contains("(\"a\", ") && checks[0].contains("integer"));
2025assert!(checks[1].contains("(\"a\", ") && checks[1].contains("length 1"));
2026assert!(checks[2].contains("(\"b\", ") && checks[2].contains("double"));
2027assert!(checks[3].contains("(\"b\", ") && checks[3].contains("length 1"));
2028 }
20292030#[test]
2031fn build_checks_skips_match_arg() {
2032let sig: syn::Signature = syn::parse_str("fn f(n: i32, mode: String)").unwrap();
2033let mut skip = HashSet::new();
2034 skip.insert("mode".to_string());
2035let output = build_precondition_checks(&sig.inputs, &skip, &PreconditionOptions::default());
2036// Only n's 2 checks remain
2037let joined = output.guards(None).join("\n");
2038assert!(joined.contains("(\"n\", "));
2039assert!(!joined.contains("mode"));
2040 }
20412042#[test]
2043fn unknown_type_produces_fallback() {
2044let sig: syn::Signature = syn::parse_str("fn f(x: MyCustomType)").unwrap();
2045let output = build_precondition_checks(
2046&sig.inputs,
2047&HashSet::new(),
2048&PreconditionOptions::default(),
2049 );
2050assert!(output.guards(None).is_empty());
2051assert_eq!(output.fallback_params.len(), 1);
2052assert_eq!(output.fallback_params[0].r_name, "x");
2053 }
20542055#[test]
2056fn mixed_known_and_unknown_types() {
2057let sig: syn::Signature = syn::parse_str("fn f(a: i32, b: MyType, c: String)").unwrap();
2058let output = build_precondition_checks(
2059&sig.inputs,
2060&HashSet::new(),
2061&PreconditionOptions::default(),
2062 );
2063// a (i32) and c (String) are known → checks
2064let joined = output.guards(None).join("\n");
2065assert!(joined.contains("(\"a\", "));
2066assert!(joined.contains("(\"c\", "));
2067assert!(!joined.contains("(\"b\", "));
2068// b (MyType) is unknown → fallback
2069assert_eq!(output.fallback_params.len(), 1);
2070assert_eq!(output.fallback_params[0].r_name, "b");
2071 }
20722073#[test]
2074fn sexp_not_fallback() {
2075let sig: syn::Signature = syn::parse_str("fn f(x: SEXP)").unwrap();
2076let output = build_precondition_checks(
2077&sig.inputs,
2078&HashSet::new(),
2079&PreconditionOptions::default(),
2080 );
2081assert!(output.guards(None).is_empty());
2082assert!(output.fallback_params.is_empty());
2083 }
20842085/// 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.
2088fn explicit_output(
2089 sig: &str,
2090 explicit: &[(&str, ExplicitChecks)],
2091 no_type_checks: bool,
2092 ) -> PreconditionOutput {
2093let sig: syn::Signature = syn::parse_str(sig).unwrap();
2094let unchecked = if no_type_checks {
2095 unchecked_params(
2096&sig.inputs,
2097&Default::default(),
2098 |_| Some(false),
2099None,
2100None,
2101None,
2102 )
2103 } else {
2104 HashSet::new()
2105 };
2106let 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 }
21162117/// 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]
2121fn resolve_type_checks_follows_the_precedence_table() {
2122/// `(param, item, impl, crate, feature, keep)`.
2123type Case = (
2124Option<bool>,
2125Option<bool>,
2126Option<bool>,
2127Option<bool>,
2128 bool,
2129 bool,
2130 );
2131let 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(
2147Some(true),
2148Some(false),
2149Some(false),
2150Some(false),
2151true,
2152true,
2153 ),
2154 (
2155Some(false),
2156Some(true),
2157Some(true),
2158Some(true),
2159false,
2160false,
2161 ),
2162 ];
2163for &(param, item, impl_level, crate_default, feature, keep) in cases {
2164assert_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.
2171assert!(!resolve_item_type_checks(None, None, Some(false), false));
2172assert!(resolve_item_type_checks(
2173Some(true),
2174Some(false),
2175None,
2176true
2177));
2178assert!(!resolve_item_type_checks(None, None, None, true));
2179 }
21802181/// `unchecked` drops one parameter's type-derived checks and keeps the
2182 /// others', and keeps that parameter's named checks.
2183#[test]
2184fn unchecked_drops_one_parameter_and_keeps_its_named_checks() {
2185let sig: syn::Signature = syn::parse_str("fn f(n: i32, xs: Vec<f64>)").unwrap();
2186let unchecked = unchecked_params(
2187&sig.inputs,
2188&Default::default(),
2189 |name| (name == "xs").then_some(false),
2190None,
2191None,
2192Some(true),
2193 );
2194assert_eq!(unchecked, ["xs".to_string()].into_iter().collect());
2195let opts = PreconditionOptions {
2196 explicit: [("xs".to_string(), no_na())].into_iter().collect(),
2197 unchecked,
2198 ..Default::default()
2199 };
2200let guards = build_precondition_checks(&sig.inputs, &HashSet::new(), &opts).guards(None);
2201assert_eq!(
2202 guards,
2203vec![
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`.
2210let sig: syn::Signature = syn::parse_str("fn f(_n: i32)").unwrap();
2211let unchecked = unchecked_params(
2212&sig.inputs,
2213&Default::default(),
2214 |_| None,
2215Some(false),
2216None,
2217None,
2218 );
2219assert_eq!(unchecked, ["n".to_string()].into_iter().collect());
2220 }
22212222/// 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]
2227fn item_no_preconditions_keeps_no_na_and_either_stays_rust_side() {
2228let sig: syn::Signature =
2229 syn::parse_str("fn f(x: Either<AsNumeric, DataFrame>, y: f64)").unwrap();
2230let 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,
2238Some(false),
2239None,
2240None,
2241 ),
2242 ..Default::default()
2243 };
2244let guards = build_precondition_checks(&sig.inputs, &HashSet::new(), &opts).guards(None);
2245assert_eq!(
2246 guards,
2247vec!["if (!isTRUE(!anyNA(y))) .miniextendr_arg_error(\"y\", \"must not be NA\")"]
2248 );
2249 }
22502251#[test]
2252fn has_type_check_is_false_for_unguarded_types() {
2253for guarded in [
2254"i32",
2255"Option<f64>",
2256"&str",
2257"&[f64]",
2258"Vec<String>",
2259"u16",
2260"List",
2261 ] {
2262assert!(has_type_check(&parse_type(guarded)), "{guarded}");
2263 }
2264for bare in [
2265"SEXP",
2266"Missing<i32>",
2267"ExternalPtr<T>",
2268"&Dots",
2269"MyType",
2270"Call",
2271"Either<AsNumeric, DataFrame>",
2272 ] {
2273assert!(!has_type_check(&parse_type(bare)), "{bare}");
2274 }
2275 }
22762277fn inherits(classes: &[&str]) -> ExplicitChecks {
2278 ExplicitChecks {
2279 inherits: Some(classes.iter().map(|c| c.to_string()).collect()),
2280 ..Default::default()
2281 }
2282 }
22832284fn no_na() -> ExplicitChecks {
2285 ExplicitChecks {
2286 no_na: true,
2287 ..Default::default()
2288 }
2289 }
22902291fn with_inherits_message(classes: &[&str], message: &str) -> ExplicitChecks {
2292 ExplicitChecks {
2293 inherits_message: Some(message.to_string()),
2294 ..inherits(classes)
2295 }
2296 }
22972298fn with_no_na_message(message: &str) -> ExplicitChecks {
2299 ExplicitChecks {
2300 no_na_message: Some(message.to_string()),
2301 ..no_na()
2302 }
2303 }
23042305#[test]
2306fn inherits_precedes_the_type_check() {
2307let out = explicit_output("fn f(x: List)", &[("x", inherits(&["pkg_obj"]))], false);
2308assert_eq!(
2309 out.guards(None),
2310vec![
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 }
23162317/// An `inherits` message is the message of every type check on the
2318 /// parameter too: storage and length alike.
2319#[test]
2320fn inherits_message_covers_the_type_checks() {
2321let out = explicit_output(
2322"fn f(x: f64)",
2323&[("x", with_inherits_message(&["pkg_unit"], "need a pkg_unit"))],
2324false,
2325 );
2326assert_eq!(
2327 out.guards(None),
2328vec![
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 }
23352336#[test]
2337fn inherits_message_on_option() {
2338let out = explicit_output(
2339"fn f(x: Option<List>)",
2340&[("x", with_inherits_message(&["pkg_obj"], "need a pkg_obj"))],
2341false,
2342 );
2343assert_eq!(
2344 out.guards(None),
2345vec![
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 }
23512352/// The `inherits` message does not cover the NA check, which keeps its
2353 /// generated message.
2354#[test]
2355fn no_na_keeps_its_own_message() {
2356let mut checks = with_inherits_message(&["pkg_num"], "need a pkg_num");
2357 checks.merge(no_na()).unwrap();
2358let out = explicit_output("fn f(x: Vec<f64>)", &[("x", checks)], false);
2359assert_eq!(
2360 out.guards(None),
2361vec![
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 }
23682369/// `coerce` widens the type checks; the widened checks carry the message.
2370#[test]
2371fn inherits_message_with_coerce() {
2372let sig: syn::Signature = syn::parse_str("fn f(x: i32)").unwrap();
2373let 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 };
2383let out = build_precondition_checks(&sig.inputs, &HashSet::new(), &opts);
2384let guards = out.guards(None);
2385assert_eq!(guards.len(), 3, "{guards:?}");
2386assert!(guards[0].starts_with("if (!isTRUE(inherits(x, \"pkg_count\")))"));
2387assert!(
2388 guards[1].contains("is.integer(x) || is.logical(x) || is.raw(x)"),
2389"{}",
2390 guards[1]
2391 );
2392assert!(guards[2].contains("length(x) == 1L"), "{}", guards[2]);
2393for guard in &guards {
2394assert!(
2395 guard.ends_with(".miniextendr_arg_error(\"x\", message = \"need a pkg_count\")"),
2396"{guard}"
2397);
2398 }
2399 }
24002401/// `Missing<T>` has no R type check, so the class check is its only guard.
2402#[test]
2403fn inherits_message_on_missing() {
2404let out = explicit_output(
2405"fn f(x: Missing<List>)",
2406&[("x", with_inherits_message(&["pkg_obj"], "need a pkg_obj"))],
2407false,
2408 );
2409assert_eq!(
2410 out.guards(None),
2411vec![
2412"if (!isTRUE(missing(x) || inherits(x, \"pkg_obj\"))) .miniextendr_arg_error(\"x\", message = \"need a pkg_obj\")"
2413]
2414 );
2415 }
24162417#[test]
2418fn inherits_any_of_several_classes() {
2419let out = explicit_output("fn f(x: SEXP)", &[("x", inherits(&["a", "b", "c"]))], false);
2420assert_eq!(
2421 out.guards(None),
2422vec![
2423"if (!isTRUE(inherits(x, c(\"a\", \"b\", \"c\")))) .miniextendr_arg_error(\"x\", \"must inherit from 'a', 'b' or 'c'\")"
2424]
2425 );
2426 }
24272428#[test]
2429fn inherits_escapes_quotes_and_backslashes() {
2430let out = explicit_output("fn f(x: SEXP)", &[("x", inherits(&["a\"b\\c"]))], false);
2431assert_eq!(
2432 out.guards(None),
2433vec![
2434"if (!isTRUE(inherits(x, \"a\\\"b\\\\c\"))) .miniextendr_arg_error(\"x\", \"must inherit from 'a\\\"b\\\\c'\")"
2435]
2436 );
2437 }
24382439fn not_inherits(classes: &[&str]) -> ExplicitChecks {
2440 ExplicitChecks {
2441 not_inherits: Some(classes.iter().map(|c| c.to_string()).collect()),
2442 ..Default::default()
2443 }
2444 }
24452446fn 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 }
24522453/// 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]
2459fn not_inherits_precedes_the_type_checks_and_keeps_their_messages() {
2460let mut checks = with_not_inherits_message(
2461&["difftime", "Date", "POSIXt"],
2462"`tau` must be a plain number",
2463 );
2464 checks.merge(no_na()).unwrap();
2465let out = explicit_output("fn f(tau: Option<AsNumeric>)", &[("tau", checks)], false);
2466assert_eq!(
2467 out.guards(None),
2468vec![
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 }
24762477/// Without a message, the refusal names the classes the way `inherits`
2478 /// names the required ones.
2479#[test]
2480fn not_inherits_generated_message() {
2481let one = explicit_output("fn f(x: SEXP)", &[("x", not_inherits(&["Date"]))], false);
2482assert_eq!(
2483 one.guards(None),
2484vec![
2485"if (!isTRUE(!inherits(x, \"Date\"))) .miniextendr_arg_error(\"x\", \"must not inherit from 'Date'\")"
2486]
2487 );
2488let several = explicit_output(
2489"fn f(x: SEXP)",
2490&[("x", not_inherits(&["difftime", "Date", "POSIXt"]))],
2491false,
2492 );
2493let guards = several.guards(None);
2494assert_eq!(
2495 guards,
2496vec![
2497"if (!isTRUE(!inherits(x, c(\"difftime\", \"Date\", \"POSIXt\")))) .miniextendr_arg_error(\"x\", \"must not inherit from 'difftime', 'Date' or 'POSIXt'\")"
2498]
2499 );
2500let asserts = not_inherits(&["a\"b"]).class_assertions("x", &parse_type("SEXP"));
2501assert_eq!(asserts[0].message(), "'x' must not inherit from 'a\\\"b'");
2502 }
25032504/// `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]
2508fn inherits_and_not_inherits_compose() {
2509let 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();
2516let out = explicit_output("fn f(x: List)", &[("x", checks)], false);
2517assert_eq!(
2518 out.guards(None),
2519vec![
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.
2526let out = explicit_output(
2527"fn f(x: List)",
2528&[("x", with_not_inherits_message(&["pkg_old"], "retired"))],
2529false,
2530 );
2531assert_eq!(
2532 out.guards(None)[1],
2533"if (!isTRUE(is.list(x))) .miniextendr_arg_error(\"x\", \"must be a list\")"
2534);
2535 }
25362537/// 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]
2543fn not_inherits_without_type_checks() {
2544let refused = || not_inherits(&["difftime"]);
2545let guards = |sig: &str, no_type_checks: bool| {
2546let mut checks = refused();
2547 checks.merge(no_na()).unwrap();
2548 explicit_output(sig, &[("x", checks)], no_type_checks).guards(None)
2549 };
2550let either = guards("fn f(x: Either<Option<AsNumeric>, DataFrame>)", false);
2551assert_eq!(
2552 either,
2553vec![
2554"if (!isTRUE(!inherits(x, \"difftime\"))) .miniextendr_arg_error(\"x\", \"must not inherit from 'difftime'\")"
2555]
2556 );
2557let optional_either = guards("fn f(x: Option<Either<AsNumeric, DataFrame>>)", false);
2558assert_eq!(optional_either.len(), 1);
2559assert!(
2560 optional_either[0]
2561 .starts_with("if (!isTRUE(is.null(x) || !inherits(x, \"difftime\")))"),
2562"{optional_either:?}"
2563);
2564let unchecked = guards("fn f(x: Option<AsNumeric>)", true);
2565assert_eq!(unchecked.len(), 2, "{unchecked:?}");
2566assert!(unchecked[0].contains("!inherits(x, \"difftime\")"));
2567assert!(unchecked[1].contains("!anyNA(x)"));
2568let missing = explicit_output("fn f(x: Missing<f64>)", &[("x", refused())], false);
2569assert_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 }
25742575/// Under `call = caller` the refusal names the caller's call: positional
2576 /// after the generated requirement, by name after a message.
2577#[test]
2578fn not_inherits_takes_the_caller_call() {
2579let plain = explicit_output("fn f(x: SEXP)", &[("x", not_inherits(&["Date"]))], false);
2580assert!(plain.guards(Some(".mx_call"))[0].ends_with(
2581".miniextendr_arg_error(\"x\", \"must not inherit from 'Date'\", .mx_call)"
2582));
2583let custom = explicit_output(
2584"fn f(x: SEXP)",
2585&[("x", with_not_inherits_message(&["Date"], "no dates"))],
2586false,
2587 );
2588assert!(
2589 custom.guards(Some(".mx_call"))[0].ends_with(
2590".miniextendr_arg_error(\"x\", message = \"no dates\", call = .mx_call)"
2591)
2592 );
2593 }
25942595/// 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]
2599fn not_inherits_merge_and_conflict() {
2600let mut checks = inherits(&["a", "b"]);
2601 checks.merge(not_inherits(&["c"])).unwrap();
2602assert_eq!(checks.class_conflict("x"), None);
2603 checks
2604 .merge(with_not_inherits_message(&["d"], "m"))
2605 .unwrap();
2606assert_eq!(checks.not_inherits, Some(vec!["c".into(), "d".into()]));
2607assert_eq!(checks.not_inherits_message.as_deref(), Some("m"));
2608assert!(checks.inherits_message.is_none());
2609let err = checks
2610 .merge(with_not_inherits_message(&["e"], "n"))
2611 .unwrap_err();
2612assert!(err.contains("`not_inherits`"), "{err}");
2613 checks.merge(not_inherits(&["b"])).unwrap();
2614let err = checks.class_conflict("x").expect("b is in both");
2615assert!(
2616 err.contains("class `b` is in both `inherits` and `not_inherits` on parameter `x`"),
2617"{err}"
2618);
2619assert!(!not_inherits(&["a"]).is_empty());
2620assert_eq!(not_inherits(&["a"]).class_conflict("x"), None);
2621 }
26222623// region: `when(...)` hints on `inherits` (#1824)
26242625/// `inherits(classes, message, when(...) ...)`: `hints` are
2626 /// `(classes, message)` pairs, in the order written.
2627fn 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 }
26442645/// 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]
2651fn inherits_hints_precede_the_check_in_written_order() {
2652let checks = with_hints(
2653&["pkg_model"],
2654Some("`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 );
2660let out = explicit_output("fn f(model: List)", &[("model", checks)], false);
2661assert_eq!(
2662 out.guards(None),
2663vec![
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.
2672let checks = with_hints(&["pkg_a", "pkg_b"], None, &[(&["data.frame"], "hint")]);
2673let out = explicit_output("fn f(x: SEXP)", &[("x", checks)], false);
2674assert_eq!(
2675 out.guards(None),
2676vec![
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 }
26822683/// 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]
2687fn inherits_hints_are_escaped() {
2688let checks = with_hints(
2689&["pkg_model"],
2690None,
2691&[(
2692&["caf\u{e9}", "a\"b\\c"],
2693"say \"hi\" \\ back\nline caf\u{e9} \u{1F600}",
2694 )],
2695 );
2696let out = explicit_output("fn f(x: SEXP)", &[("x", checks)], false);
2697assert_eq!(
2698 out.guards(None)[0],
2699r#"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 }
27022703/// Under `call = caller` a hint names the caller's call, by name after
2704 /// its message.
2705#[test]
2706fn inherits_hints_take_the_caller_call() {
2707let checks = with_hints(&["pkg_model"], None, &[(&["data.frame"], "hint")]);
2708let out = explicit_output("fn f(x: SEXP)", &[("x", checks)], false);
2709assert_eq!(
2710 out.guards(Some(".mx_call")),
2711vec![
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 }
27172718/// 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]
2722fn inherits_hints_pass_null_and_missing() {
2723let hinted = || with_hints(&["pkg_model"], None, &[(&["data.frame"], "hint")]);
2724let optional = explicit_output("fn f(x: Option<List>)", &[("x", hinted())], true);
2725assert_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);
2729let missing = explicit_output("fn f(x: Missing<List>)", &[("x", hinted())], true);
2730assert_eq!(
2731 missing.guards(None),
2732vec![
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 }
27382739/// Several `inherits` attributes on one parameter concatenate their
2740 /// hints in the order written; every hint then accepts every required
2741 /// class.
2742#[test]
2743fn inherits_hints_merge_in_order() {
2744let 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();
2749let hints: Vec<&str> = checks
2750 .inherits_hints
2751 .iter()
2752 .map(|h| h.message.as_str())
2753 .collect();
2754assert_eq!(hints, ["one", "two"]);
2755assert_eq!(checks.class_conflict("x"), None);
2756let out = explicit_output("fn f(x: SEXP)", &[("x", checks)], false);
2757let guards = out.guards(None);
2758assert_eq!(guards.len(), 3);
2759assert!(
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 );
2766assert!(guards[1].contains("!inherits(x, \"tbl\"))) "));
2767assert!(guards[1].ends_with("message = \"two\")"));
2768assert!(guards[2].ends_with("message = \"need an a\")"));
2769 }
27702771/// 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]
2776fn inherits_hint_class_conflicts() {
2777let checks = with_hints(&["pkg_model"], None, &[(&["df", "pkg_model"], "hint")]);
2778let err = checks
2779 .class_conflict("model")
2780 .expect("pkg_model is required");
2781assert!(
2782 err.contains(
2783"class `pkg_model` is in both `inherits` and a `when(...)` hint on parameter `model`"
2784),
2785"{err}"
2786);
2787let mut checks = with_hints(&["pkg_model"], None, &[(&["pkg_old"], "hint")]);
2788assert_eq!(checks.class_conflict("model"), None);
2789 checks.merge(inherits(&["pkg_old"])).unwrap();
2790assert!(checks.class_conflict("model").is_some());
27912792let mut checks = with_hints(&["pkg_model"], None, &[(&["pkg_old"], "hint")]);
2793 checks.merge(not_inherits(&["pkg_old"])).unwrap();
2794assert_eq!(checks.class_conflict("model"), None);
2795let out = explicit_output("fn f(x: SEXP)", &[("x", checks)], false);
2796assert_eq!(
2797 out.guards(None),
2798vec![
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 }
28052806// endregion
28072808#[test]
2809fn no_na_wording_follows_the_shape() {
2810let scalar = explicit_output("fn f(x: f64)", &[("x", no_na())], true);
2811assert_eq!(
2812 scalar.guards(None),
2813vec!["if (!isTRUE(!anyNA(x))) .miniextendr_arg_error(\"x\", \"must not be NA\")"]
2814 );
2815let vector = explicit_output("fn f(x: Vec<f64>)", &[("x", no_na())], true);
2816assert_eq!(
2817 vector.guards(None),
2818vec!["if (!isTRUE(!anyNA(x))) .miniextendr_arg_error(\"x\", \"must not contain NA\")"]
2819 );
2820let slice = explicit_output("fn f(x: &[f64])", &[("x", no_na())], true);
2821assert!(slice.guards(None)[0].contains("must not contain NA"));
2822// The vector markers are vectors too; their scalar forms are not.
2823for (ty, verb) in [
2824 ("AsNumericVec", "contain"),
2825 ("AsCharacterVec", "contain"),
2826 ("Option<AsCharacterVec>", "contain"),
2827 ("AsNumeric", "be"),
2828 ("AsCharacter", "be"),
2829 ] {
2830let out = explicit_output(&format!("fn f(x: {ty})"), &[("x", no_na())], true);
2831assert!(
2832 out.guards(None)[0].contains(&format!("must not {verb} NA")),
2833"{ty}: {:?}",
2834 out.guards(None)
2835 );
2836 }
2837 }
28382839/// 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]
2844fn no_na_wording_knows_the_vector_markers() {
2845let verb = |sig: &str| {
2846let out = explicit_output(sig, &[("x", no_na())], false);
2847let guards = out.guards(None);
2848let guard = guards.last().expect("the no_na guard");
2849if guard.contains("must not contain NA") {
2850"contain"
2851} else {
2852assert!(guard.contains("must not be NA"), "{guard}");
2853"be"
2854}
2855 };
2856for (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 ] {
2888let 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.
2894if !no_na_checked_after_conversion(&ty) {
2895assert_eq!(verb(sig), expected, "{sig}");
2896 }
2897assert_eq!(
2898 no_na_requirement(&ty),
2899format!("must not {expected} NA"),
2900"{sig}"
2901);
2902 }
2903 }
29042905/// `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]
2912fn no_na_on_an_either_is_checked_after_the_conversion() {
2913let guards = |sig: &str, skip: &[&str]| {
2914let sig: syn::Signature = syn::parse_str(sig).unwrap();
2915let opts = PreconditionOptions {
2916 explicit: [("x".to_string(), no_na())].into_iter().collect(),
2917 ..Default::default()
2918 };
2919let skip: HashSet<String> = skip.iter().map(|s| s.to_string()).collect();
2920 build_precondition_checks(&sig.inputs, &skip, &opts).guards(None)
2921 };
2922for 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 ] {
2930assert!(
2931 guards(sig, &[]).iter().all(|g| !g.contains("anyNA")),
2932"{sig}: {:?}",
2933 guards(sig, &[])
2934 );
2935 }
2936for (sig, skip) in [
2937 ("fn f(x: AsNumeric)", &[][..]),
2938 ("fn f(x: DataFrame)", &[]),
2939 ("fn f(x: Either<Mode, DataFrame>)", &["x"]),
2940 ] {
2941let guards = guards(sig, skip);
2942assert!(
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.
2952for (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 ] {
2965let guards = guards(sig, skip);
2966let expected = format!("!isTRUE({layer} || !anyNA(x))");
2967assert!(
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.
2974assert!(!no_na_checked_after_conversion(
2975&syn::parse_str("Result<Either<f64, String>, ()>").unwrap()
2976 ));
2977 }
29782979/// 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]
2982fn no_na_message_is_generated_or_custom() {
2983let ty = |s: &str| syn::parse_str::<syn::Type>(s).unwrap();
2984assert_eq!(
2985 no_na_message("x", &ty("AsNumeric"), None),
2986"'x' must not be NA"
2987);
2988assert_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.
2993assert_eq!(no_na_message("d", &ty("Doses"), None), "'d' must not be NA");
2994let custom = "`x` can't be NA: it's \"required\" \\ 100%\ncaf\u{e9}()";
2995assert_eq!(no_na_message("x", &ty("AsNumeric"), Some(custom)), custom);
2996 }
29972998#[test]
2999fn explicit_checks_pass_null_and_missing() {
3000let optional = explicit_output(
3001"fn f(x: Option<List>)",
3002&[("x", inherits(&["pkg_obj"]))],
3003true,
3004 );
3005assert_eq!(
3006 optional.guards(None),
3007vec![
3008"if (!isTRUE(is.null(x) || inherits(x, \"pkg_obj\"))) .miniextendr_arg_error(\"x\", \"must inherit from 'pkg_obj'\")"
3009]
3010 );
3011let missing = explicit_output("fn f(x: Missing<f64>)", &[("x", no_na())], true);
3012assert_eq!(
3013 missing.guards(None),
3014vec![
3015"if (!isTRUE(missing(x) || !anyNA(x))) .miniextendr_arg_error(\"x\", \"must not be NA\")"
3016]
3017 );
3018let optional_vec = explicit_output("fn f(x: Option<Vec<f64>>)", &[("x", no_na())], true);
3019assert!(optional_vec.guards(None)[0].contains("must not contain NA"));
3020 }
30213022#[test]
3023fn explicit_checks_survive_no_type_checks() {
3024let both = ExplicitChecks {
3025 inherits: Some(vec!["pkg_obj".into()]),
3026 no_na: true,
3027 ..Default::default()
3028 };
3029let 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`.
3033assert_eq!(
3034 out.guards(None),
3035vec![
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 );
3040let none = explicit_output("fn f(n: i32)", &[], true);
3041assert!(none.guards(None).is_empty());
3042 }
30433044#[test]
3045fn explicit_checks_apply_to_match_arg_params() {
3046let sig: syn::Signature = syn::parse_str("fn f(mode: String)").unwrap();
3047let opts = PreconditionOptions {
3048 explicit: [("mode".to_string(), no_na())].into_iter().collect(),
3049 ..Default::default()
3050 };
3051let skip: HashSet<String> = ["mode".to_string()].into_iter().collect();
3052let out = build_precondition_checks(&sig.inputs, &skip, &opts);
3053assert_eq!(
3054 out.guards(None),
3055vec!["if (!isTRUE(!anyNA(mode))) .miniextendr_arg_error(\"mode\", \"must not be NA\")"]
3056 );
3057 }
30583059#[test]
3060fn explicit_checks_take_the_caller_call() {
3061let out = explicit_output("fn f(x: f64)", &[("x", no_na())], true);
3062assert_eq!(
3063 out.guards(Some(".mx_call")),
3064vec![
3065"if (!isTRUE(!anyNA(x))) .miniextendr_arg_error(\"x\", \"must not be NA\", .mx_call)"
3066]
3067 );
3068 }
30693070#[test]
3071fn explicit_checks_merge() {
3072let mut a = inherits(&["a"]);
3073 a.merge(no_na()).unwrap();
3074 a.merge(inherits(&["b"])).unwrap();
3075assert_eq!(
3076 a,
3077 ExplicitChecks {
3078 inherits: Some(vec!["a".into(), "b".into()]),
3079 no_na: true,
3080 ..Default::default()
3081 }
3082 );
3083assert!(ExplicitChecks::default().is_empty());
30843085// One message per check: it may come from either attribute, not both.
3086let mut b = inherits(&["a"]);
3087 b.merge(with_inherits_message(&["b"], "one")).unwrap();
3088assert_eq!(b.inherits_message.as_deref(), Some("one"));
3089assert_eq!(b.inherits, Some(vec!["a".into(), "b".into()]));
3090 b.merge(with_no_na_message("two")).unwrap();
3091assert_eq!(b.no_na_message.as_deref(), Some("two"));
3092let err = b.merge(with_inherits_message(&["c"], "three")).unwrap_err();
3093assert!(
3094 err.contains("`inherits`") && err.contains("more than one `message`"),
3095"{err}"
3096);
3097let err = b.merge(with_no_na_message("four")).unwrap_err();
3098assert!(err.contains("`no_na`"), "{err}");
3099 }
31003101/// 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]
3107fn custom_messages_are_passed_verbatim_by_name() {
3108let 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 };
3115let out = explicit_output("fn f(model: List)", &[("model", checks)], false);
3116assert_eq!(
3117 out.guards(None),
3118vec![
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 );
3124let caller = out.guards(Some(".mx_call"));
3125assert!(
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.
3133assert!(
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 );
3140assert!(
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.
3148let plain = explicit_output("fn f(model: List)", &[("model", no_na())], false);
3149assert!(
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.
3156let optional = explicit_output(
3157"fn f(x: Option<List>)",
3158&[("x", with_inherits_message(&["pkg_obj"], "need a pkg_obj"))],
3159true,
3160 );
3161assert_eq!(
3162 optional.guards(None),
3163vec![
3164"if (!isTRUE(is.null(x) || inherits(x, \"pkg_obj\"))) .miniextendr_arg_error(\"x\", message = \"need a pkg_obj\")"
3165]
3166 );
3167let asserts = with_no_na_message("mine").value_assertions("x", &parse_type("f64"), false);
3168assert_eq!(asserts[0].message(), "mine");
3169 }
31703171/// 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]
3176fn custom_messages_are_escaped_as_r_string_literals() {
3177let message = "say \"hi\" \\ back\nline\ttab 100% caf\u{e9} \u{1F600} bell\u{7}";
3178let out = explicit_output("fn f(x: f64)", &[("x", with_no_na_message(message))], true);
3179assert_eq!(
3180 out.guards(None),
3181vec![
3182r#"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 );
3185assert_eq!(r_string_escape("a\rb"), r"a\rb");
3186assert_eq!(r_string_escape("plain 'text'"), "plain 'text'");
3187// Class names take the same escaping.
3188let out = explicit_output("fn f(x: SEXP)", &[("x", inherits(&["caf\u{e9}"]))], true);
3189assert_eq!(
3190 out.guards(None),
3191vec![
3192r#"if (!isTRUE(inherits(x, "caf\u{e9}"))) .miniextendr_arg_error("x", "must inherit from 'caf\u{e9}'")"#
3193]
3194 );
3195 }
3196}