1//! Condition macros and signal enum for the Rust→R condition pipeline.
2//!
3//! This module provides two things:
4//!
5//! 1. **[`enum@RCondition`] enum** — the internal panic payload used by `error!()`,
6//! `warning!()`, `message!()`, and `condition!()` macros. Caught by
7//! [`crate::unwind_protect::with_r_unwind_protect`] before the generic
8//! panic→error path, then forwarded to R as a structured condition with
9//! `rust_*` class layering via
10//! [`crate::error_value::make_rust_condition_value`].
11//!
12//! 2. **[`AsRError`] struct** — wraps any `E: std::error::Error` and
13//! preserves the full error chain (cause/source) when converting to an R
14//! error message. Use as the `Err` type in `Result` returns.
15//!
16//! # When to reach for what
17//!
18//! There are three Rust→R error-emission paths and they are not
19//! interchangeable. The crate-level rationale (why tagged-SEXP at all, what
20//! `error_in_r` defaults imply, and the `with_r_unwind_protect` leak) lives
21//! on [`crate::error_value`]; the practical picking-one summary:
22//!
23//! - **`panic!`** — escape hatch. Becomes class `c("rust_error",
24//! "simpleError", "error", "condition")` with `kind = "panic"`. Use for
25//! genuine bugs or impossible states. Cheapest in source; coarsest in R
26//! (callers can only match `rust_error` / `error`, not a specific class).
27//! - **`error!` / `warning!` / `message!` / `condition!`** (this module) —
28//! typed conditions. Same transport, but allow an optional `class =
29//! "name"` so R-side `tryCatch` can route by class. `warning!` /
30//! `message!` / `condition!` are the only way to emit non-error
31//! conditions; `panic!` is always an error.
32//! - **`Result<T, E>` with [`AsRError<E>`]** — value-style propagation
33//! through Rust code. Converts at the boundary; `kind = "result_err"`.
34//! Best when the failure path is real-and-recoverable in Rust and the
35//! error chain (`std::error::Error::source`) is worth preserving.
36//!
37//! `Rf_error` is *not* on this list. Direct `Rf_error` skips Rust
38//! destructors unconditionally and is forbidden by lint MXL300; see
39//! [`crate::error_value`] for the full reasoning.
40//!
41//! # Macro-vs-module name collision
42//!
43//! `#[macro_export]` puts each macro at the *crate root*, where `error!` and
44//! `condition!` collide with the same-named modules `pub mod error` and `pub
45//! mod condition`. The practical implication: `use miniextendr_api::{error,
46//! condition}` imports the *modules*, not the macros, and a subsequent
47//! `error!(...)` call fails to resolve.
48//!
49//! Workarounds, in rough order of ergonomics:
50//!
51//! 1. Invoke via fully-qualified path: `miniextendr_api::error!("...")`.
52//! 2. `use miniextendr_api as mx;` then `mx::error!("...")`.
53//! 3. `warning!` and `message!` have no module conflict — `use
54//! miniextendr_api::{warning, message};` works directly.
55//!
56//! See the individual macro docs for the per-macro reminder.
57//!
58//! # Condition macros
59//!
60//! The four macros are the user-facing API for raising non-panic conditions from
61//! Rust. They ride the tagged-condition transport that every `#[miniextendr]`
62//! function uses:
63//!
64//! ```ignore
65//! use miniextendr_api::{error, warning, message, condition};
66//!
67//! #[miniextendr]
68//! fn demo_error() {
69//! error!("something went wrong: {}", 42);
70//! }
71//!
72//! #[miniextendr]
73//! fn demo_warning() {
74//! warning!("something looks suspicious");
75//! }
76//!
77//! #[miniextendr]
78//! fn demo_message() {
79//! message!("progress: {} of {}", 1, 10);
80//! }
81//!
82//! #[miniextendr]
83//! fn demo_condition() {
84//! condition!("a signallable condition");
85//! }
86//! ```
87//!
88//! Optional `class =` extension for programmatic catching:
89//!
90//! ```ignore
91//! #[miniextendr]
92//! fn typed_error(name: &str) {
93//! error!(class = "my_error", "missing field: {name}");
94//! }
95//! ```
96//!
97//! ```r
98//! tryCatch(typed_error("x"), my_error = function(e) "caught!")
99//! # [1] "caught!"
100//! ```
101//!
102//! Optional `data =` extension attaches structured fields readable as
103//! `e$<name>` in handlers (rlang-`abort()`-style):
104//!
105//! ```ignore
106//! #[miniextendr]
107//! fn validate(value: i32) {
108//! if !(0..=100).contains(&value) {
109//! miniextendr_api::error!(
110//! class = "validation_error",
111//! data = [("value", value), ("min", 0), ("max", 100)],
112//! "value {value} out of range"
113//! );
114//! }
115//! }
116//! ```
117//!
118//! ```r
119//! tryCatch(validate(150L), validation_error = function(e) c(e$value, e$min, e$max))
120//! # [1] 150 0 100
121//! ```
122//!
123//! Supported `data` value types (anything with `RValue: From<_>`): scalars and
124//! `Vec`s of `i32`, `f64`, `bool`, `String` / `&str`; their NA-aware `Option` /
125//! `Vec<Option<_>>` forms (`None` → R `NA`); the wide-integer ladder (`i64` /
126//! `u32`, narrowed to `integer(1)` when it fits, `double(1)` otherwise); and the
127//! [`RValue::debug`](crate::RValue::debug) escape hatch, which stringifies any
128//! `T: Debug`. For nested lists or complex/raw/NA-bearing values build an
129//! [`RValue`](crate::RValue) directly. The payload is built as a Send-safe owned
130//! value at the call site and materialised as R objects on the main thread — so
131//! `data =` works from worker-thread code too.
132//!
133//! Three `data =` grammars are accepted (see [`crate::error!`]):
134//! - single pair: `data = ("name", value)`
135//! - bracketed list: `data = [("a", v1), ("b", v2)]`
136//! - keyed builder sugar: `data = { value = 42, code = 7 }` (bare-ident keys)
137//!
138//! # `AsRError`
139//!
140//! ```ignore
141//! use miniextendr_api::condition::AsRError;
142//!
143//! #[miniextendr]
144//! fn parse_config(path: &str) -> Result<i32, AsRError<std::io::Error>> {
145//! let content = std::fs::read_to_string(path).map_err(AsRError)?;
146//! Ok(content.len() as i32)
147//! }
148//! ```
149150// region: ConditionData — Send-safe owned condition-data payload
151152/// Named condition-data payload: an ordered list of `(name, value)` pairs.
153///
154/// Produced by the macros' `data = ...` form and consumed by
155/// [`crate::error_value::make_rust_condition_value`]. Each value is an
156/// [`RValue`](crate::RValue) — an owned, `Send`, R-native value tree. Send-safe
157/// by construction (no live `SEXP`), so the payload can travel through
158/// `panic_any` and cross the worker→main thread boundary; the R objects are
159/// materialised on the main thread at the unwind boundary.
160///
161/// The macros accept any value with `RValue: From<_>` (scalars and `Vec`s of
162/// `i32` / `f64` / `bool` / `String` / `&str`; their NA-aware `Option` /
163/// `Vec<Option<_>>` forms; the `i64` / `u32` wide-integer ladder); a scalar
164/// `7i32` becomes `integer(1)` and a `Vec<i32>` becomes `integer(n)`. Any
165/// `T: Debug` rides along via [`RValue::debug`](crate::RValue::debug). For
166/// nested lists or complex/raw values build an [`RValue`](crate::RValue)
167/// directly.
168pub type ConditionData = Vec<(String, crate::RValue)>;
169170// endregion
171172// region: ConditionCall — whether a condition reports a call
173174/// Whether a condition reports a call (`conditionCall()`): the per-condition
175/// equivalent of R's `warning(..., call. = FALSE)` / `stop(..., call. = FALSE)`.
176///
177/// Every condition a `#[miniextendr]` function raises carries the call its
178/// transport captured: the wrapper's call as written (or the caller's, under
179/// `call = caller`), `NULL` in ALTREP and connection callbacks. A condition
180/// about the data rather than the call opts out with [`ConditionCall::None`];
181/// its siblings from the same function keep their call.
182///
183/// Spellings: `call = none` first in [`crate::warning!`], [`crate::error!`],
184/// [`crate::condition!`] and the `defer_*!` macros;
185/// [`RError::without_call`]; `#[condition(call = none)]` on a
186/// `#[derive(RCondition)]` type or variant; or an
187/// [`RConditionData::call`] impl.
188///
189/// On the tagged condition value the choice is the `call` slot itself:
190/// `FALSE` for [`ConditionCall::None`], read back by the R helper (and by the
191/// trait-ABI re-panic, [`RCondition::from_tagged_sexp`]) as "no call".
192#[derive(#[automatically_derived]
impl ::core::fmt::Debug for ConditionCall {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::write_str(f,
match self {
ConditionCall::Inherit => "Inherit",
ConditionCall::None => "None",
})
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for ConditionCall { }
#[automatically_derived]
impl ::core::clone::Clone for ConditionCall {
#[inline]
fn clone(&self) -> Self { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for ConditionCall { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for ConditionCall { }
#[automatically_derived]
impl ::core::cmp::PartialEq for ConditionCall {
#[inline]
fn eq(&self, other: &Self) -> bool {
::core::intrinsics::discriminant_value(self) ==
::core::intrinsics::discriminant_value(other)
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for ConditionCall { }Eq, #[automatically_derived]
impl ::core::default::Default for ConditionCall {
#[inline]
fn default() -> Self { Self::Inherit }
}Default)]
193pub enum ConditionCall {
194/// The call the transport captured (the default).
195#[default]
196Inherit,
197/// No call: `conditionCall()` is `NULL` and the printed condition has no
198 /// `In f(...) :` prefix.
199None,
200}
201202impl ConditionCall {
203/// `call` when the condition keeps its call, `None` otherwise: the call to
204 /// hand a transport that takes the R call object directly.
205#[inline]
206pub fn apply(self, call: Option<crate::SEXP>) -> Option<crate::SEXP> {
207match self {
208 ConditionCall::Inherit => call,
209 ConditionCall::None => Option::None,
210 }
211 }
212}
213214// endregion
215216// region: RCondition enum — structured condition payload
217218/// Owned payload for structured R conditions.
219///
220/// `#[derive(RCondition)]` gives a struct or enum a conversion into this
221/// payload. Its `#[condition(kind = warning)]` (or `error` / `message` /
222/// `condition`) chooses the signal; an enum variant can override the kind.
223/// Convert with `RCondition::from(value)`, then call [`defer`](Self::defer)
224/// to accompany a return value, or [`raise`](Self::raise) to leave Rust.
225///
226/// Raised by the `error!()`, `warning!()`, `message!()`, and `condition!()`
227/// macros via `std::panic::panic_any`. Caught by `with_r_unwind_protect`
228/// before the generic panic→string path and forwarded to R as a tagged SEXP
229/// with `rust_*` class layering. The `defer_*` family
230/// ([`crate::deferred_condition`]) queues the same payloads instead of
231/// panicking, so the call still returns its value.
232///
233/// Every variant carries the same four parts: the message, the user classes
234/// (prepended to the `rust_*` layering, empty for none), the optional `data`
235/// fields and the [`ConditionCall`] choice. All four are owned and `Send`, so
236/// the payload can be queued from a worker thread. Each variant is one kind
237/// of the tagged condition value ([`crate::error_value::kind`]).
238#[derive(#[automatically_derived]
impl ::core::fmt::Debug for RCondition {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
match self {
Self::Error {
message: __self_0,
class: __self_1,
data: __self_2,
call: __self_3 } =>
::core::fmt::Formatter::debug_struct_field4_finish(f, "Error",
"message", __self_0, "class", __self_1, "data", __self_2,
"call", &__self_3),
Self::Warning {
message: __self_0,
class: __self_1,
data: __self_2,
call: __self_3 } =>
::core::fmt::Formatter::debug_struct_field4_finish(f,
"Warning", "message", __self_0, "class", __self_1, "data",
__self_2, "call", &__self_3),
Self::Message {
message: __self_0,
class: __self_1,
data: __self_2,
call: __self_3 } =>
::core::fmt::Formatter::debug_struct_field4_finish(f,
"Message", "message", __self_0, "class", __self_1, "data",
__self_2, "call", &__self_3),
Self::Condition {
message: __self_0,
class: __self_1,
data: __self_2,
call: __self_3 } =>
::core::fmt::Formatter::debug_struct_field4_finish(f,
"Condition", "message", __self_0, "class", __self_1, "data",
__self_2, "call", &__self_3),
Self::Conversion {
message: __self_0,
class: __self_1,
data: __self_2,
call: __self_3 } =>
::core::fmt::Formatter::debug_struct_field4_finish(f,
"Conversion", "message", __self_0, "class", __self_1,
"data", __self_2, "call", &__self_3),
}
}
}Debug)]
239pub enum RCondition {
240/// Raised by `error!(...)` / `error!(class = "...", ...)`, and by the
241 /// `Result<T, E>` Err arm once reconstructed across a package boundary.
242Error {
243/// Human-readable condition message.
244message: String,
245/// User classes, most specific first, before the framework's classes.
246class: Vec<String>,
247/// Named fields attached to the R condition.
248data: Option<ConditionData>,
249/// Call attribution policy; messages always omit the call.
250call: ConditionCall,
251 },
252/// Raised by `warning!(...)` / `warning!(class = "...", ...)`.
253Warning {
254/// Human-readable condition message.
255message: String,
256/// User classes, most specific first, before the framework's classes.
257class: Vec<String>,
258/// Named fields attached to the R condition.
259data: Option<ConditionData>,
260/// Call attribution policy; messages always omit the call.
261call: ConditionCall,
262 },
263/// Raised by `message!(...)`, and queued by [`crate::defer_message()`]. Only
264 /// the typed-payload path fills `class` (the macro has no `class =` form);
265 /// the R helper layers it in front of `rust_message` like the other kinds.
266 /// A message never reports a call, whatever `call` says.
267Message {
268/// Human-readable condition message.
269message: String,
270/// User classes, most specific first, before the framework's classes.
271class: Vec<String>,
272/// Named fields attached to the R condition.
273data: Option<ConditionData>,
274/// Call attribution policy; messages always omit the call.
275call: ConditionCall,
276 },
277/// Raised by `condition!(...)` / `condition!(class = "...", ...)`.
278Condition {
279/// Human-readable condition message.
280message: String,
281/// User classes, most specific first, before the framework's classes.
282class: Vec<String>,
283/// Named fields attached to the R condition.
284data: Option<ConditionData>,
285/// Call attribution policy; messages always omit the call.
286call: ConditionCall,
287 },
288/// An argument error, `kind = "conversion"`: raised by
289 /// [`crate::arg_error!`] / [`ArgError::raise`] (built by
290 /// [`RCondition::arg_error`]), and a failed argument conversion once
291 /// reconstructed across a package boundary.
292Conversion {
293/// Human-readable condition message.
294message: String,
295/// User classes, most specific first, before the framework's classes.
296class: Vec<String>,
297/// Named fields attached to the R condition.
298data: Option<ConditionData>,
299/// Call attribution policy; messages always omit the call.
300call: ConditionCall,
301 },
302}
303304impl RCondition {
305/// Leave the current Rust body with this condition.
306 ///
307 /// The enclosing miniextendr guard forwards it to R with its kind and
308 /// metadata intact. Like `warning!`, raising a warning still exits Rust:
309 /// muffling it in R does not resume this Rust body. Use [`defer`](Self::defer)
310 /// to keep computing and return a value.
311pub fn raise(self) -> ! {
312 std::panic::panic_any(self)
313 }
314315/// Queue this condition until the current guarded call returns.
316 ///
317 /// Preserves its declared kind, classes, fields and call policy, including
318 /// for a mixed-kind enum. Errors queued here stop R at that boundary.
319pub fn defer(self) {
320crate::deferred_condition::defer(self);
321 }
322323/// Split into the kind (one of [`crate::error_value::kind`]) and the
324 /// [`ErrParts`] (message, class, data, call choice) that
325 /// [`crate::error_value::condition_parts_value`] builds the tagged value
326 /// from.
327#[doc(hidden)]
328pub fn into_parts(self) -> (&'static str, ErrParts) {
329use crate::error_value::kind;
330let (kind, message, class, data, call) = match self {
331 RCondition::Error {
332 message,
333 class,
334 data,
335 call,
336 } => (kind::ERROR, message, class, data, call),
337 RCondition::Warning {
338 message,
339 class,
340 data,
341 call,
342 } => (kind::WARNING, message, class, data, call),
343 RCondition::Message {
344 message,
345 class,
346 data,
347 call,
348 } => (kind::MESSAGE, message, class, data, call),
349 RCondition::Condition {
350 message,
351 class,
352 data,
353 call,
354 } => (kind::CONDITION, message, class, data, call),
355 RCondition::Conversion {
356 message,
357 class,
358 data,
359 call,
360 } => (kind::CONVERSION, message, class, data, call),
361 };
362 (
363kind,
364ErrParts {
365message,
366class,
367data,
368call,
369 },
370 )
371 }
372373/// The condition message, as `conditionMessage()` would read it in R.
374 ///
375 /// Use it on a condition [`catch_condition`] caught or
376 /// [`capture`](crate::deferred_condition::capture) collected, for example
377 /// to put a cause's text into a new message.
378pub fn message(&self) -> &str {
379match self {
380 RCondition::Error { message, .. }
381 | RCondition::Warning { message, .. }
382 | RCondition::Message { message, .. }
383 | RCondition::Condition { message, .. }
384 | RCondition::Conversion { message, .. } => message,
385 }
386 }
387388/// This condition with `parent` as its cause, as `rlang::abort(parent = )`
389 /// chains conditions.
390 ///
391 /// R gets the cause as the field `e$parent`: a condition object with its
392 /// own classes, message and fields (and its own parent, if it has one),
393 /// built like the condition itself but not signalled. The cause's classes
394 /// stay out of this condition's class vector, so a handler written for
395 /// the cause's class does not catch this condition; code that wants the
396 /// cause branches on it: `inherits(e$parent, "pkg_error_config")`.
397 ///
398 /// The cause reports the same call as this condition, or none when it was
399 /// raised with `call = none`. A second `with_parent` replaces the first,
400 /// and the cause replaces a `data` field this condition names `parent`.
401 ///
402 /// ```ignore
403 /// match catch_condition(|| rebuild(&saved)) {
404 /// Ok(value) => value,
405 /// Err(cause) => RCondition::Error {
406 /// message: format!("could not rebuild: {}", cause.message()),
407 /// class: vec!["pkg_error_rebuild".into()],
408 /// data: None,
409 /// call: ConditionCall::Inherit,
410 /// }
411 /// .with_parent(cause)
412 /// .raise(),
413 /// }
414 /// ```
415pub fn with_parent(mut self, parent: RCondition) -> Self {
416let encoded = parent.into_tagged_rvalue();
417let data = match &mut self {
418 RCondition::Error { data, .. }
419 | RCondition::Warning { data, .. }
420 | RCondition::Message { data, .. }
421 | RCondition::Condition { data, .. }
422 | RCondition::Conversion { data, .. } => data,
423 };
424let fields = data.get_or_insert_with(Vec::new);
425fields.retain(|(name, _)| name != PARENT_FIELD);
426fields.push((PARENT_FIELD.to_owned(), encoded));
427self428 }
429430/// The condition as the tagged value the R raise helper reads (`error`,
431 /// `kind`, `class`, `call`, `data`), in [`RValue`](crate::RValue) form so
432 /// it can travel inside another condition's data ([`Self::with_parent`]).
433fn into_tagged_rvalue(self) -> crate::RValue {
434use crate::RValue;
435let (kind, parts) = self.into_parts();
436let text = |value: String| RValue::Character(::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
[Some(value)]))vec![Some(value)]);
437let class = if parts.class.is_empty() {
438 RValue::Null439 } else {
440 RValue::Character(parts.class.into_iter().map(Some).collect())
441 };
442let call = match parts.call {
443 ConditionCall::Inherit => RValue::Null,
444 ConditionCall::None => RValue::Logical(::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
[Some(false)]))vec![Some(false)]),
445 };
446let data = parts.data.map_or(RValue::Null, |fields| {
447 RValue::List(
448fields449 .into_iter()
450 .map(|(name, value)| (Some(name), value))
451 .collect(),
452 )
453 });
454 RValue::List(::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
[(Some("error".to_owned()), text(parts.message)),
(Some("kind".to_owned()), text(kind.to_owned())),
(Some("class".to_owned()), class),
(Some("call".to_owned()), call),
(Some("data".to_owned()), data)]))vec![
455 (Some("error".to_owned()), text(parts.message)),
456 (Some("kind".to_owned()), text(kind.to_owned())),
457 (Some("class".to_owned()), class),
458 (Some("call".to_owned()), call),
459 (Some("data".to_owned()), data),
460 ])
461 }
462463/// Which call the condition reports when R raises it: see
464 /// [`ConditionCall`].
465pub fn call(&self) -> ConditionCall {
466match self {
467 RCondition::Error { call, .. }
468 | RCondition::Warning { call, .. }
469 | RCondition::Message { call, .. }
470 | RCondition::Condition { call, .. }
471 | RCondition::Conversion { call, .. } => *call,
472 }
473 }
474475/// The payload of [`crate::arg_error!`] and [`ArgError::raise`]: the
476 /// argument error of parameter `param` (its R name) with `message`.
477 ///
478 /// `kind = "conversion"`, `e$param` as the only data field, and the class
479 /// [`CONVERSION_ERROR_CLASS_MARKER`], which the generated R replaces with
480 /// the crate's `conversion_error_class`: the condition the wrapper's own
481 /// argument checks raise (`.miniextendr_arg_error`).
482#[doc(hidden)]
483pub fn arg_error(param: impl Into<String>, message: String, call: ConditionCall) -> Self {
484 RCondition::Conversion {
485message,
486 class: ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
[CONVERSION_ERROR_CLASS_MARKER.to_string()]))vec![CONVERSION_ERROR_CLASS_MARKER.to_string()],
487 data: Some(::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
[(CONVERSION_PARAM_FIELD.to_string(),
crate::RValue::from(param.into()))]))vec![(
488 CONVERSION_PARAM_FIELD.to_string(),
489crate::RValue::from(param.into()),
490 )]),
491call,
492 }
493 }
494}
495496/// The class that stands for the crate's `conversion_error_class` in a tagged
497/// condition value (#1740).
498///
499/// A `macro_rules!` macro cannot read `[package.metadata.miniextendr]`, so
500/// [`crate::arg_error!`] cannot name the crate's classes the way the
501/// generated conversion arms do. It writes this class instead, and the
502/// generated R wrapper's `.miniextendr_raise_condition` replaces it with
503/// `.miniextendr_conversion_error_class`, the binding its own argument checks
504/// read. The string is that binding's name. Where no generated wrapper raises
505/// the condition (a raising guard: an ALTREP `RUnwind` callback,
506/// [`crate::unwind_protect::with_r_unwind_protect_or_raise`]) Rust replaces it
507/// with the classes `miniextendr_init!` registered for the package
508/// ([`resolve_conversion_error_class_marker`]).
509#[doc(hidden)]
510pub const CONVERSION_ERROR_CLASS_MARKER: &str = ".miniextendr_conversion_error_class";
511512/// `class` with [`CONVERSION_ERROR_CLASS_MARKER`] replaced by `crate_classes`,
513/// in place, as the generated `.miniextendr_raise_condition` replaces it in R.
514/// A raising guard has no generated R to do it, so it calls this with
515/// [`crate_conversion_error_class`].
516pub(crate) fn resolve_conversion_error_class_marker(
517 class: Vec<String>,
518 crate_classes: &[&str],
519) -> Vec<String> {
520let mut resolved = Vec::with_capacity(class.len() + crate_classes.len());
521for c in class {
522if c == CONVERSION_ERROR_CLASS_MARKER {
523 resolved.extend(crate_classes.iter().map(|&k| k.to_string()));
524 } else {
525 resolved.push(c);
526 }
527 }
528resolved529}
530531/// The package crate's `conversion_error_class`, from the entry
532/// `miniextendr_init!` registers in
533/// [`MX_CONVERSION_ERROR_CLASS`](crate::registry::MX_CONVERSION_ERROR_CLASS)
534/// (each package links its own copy of this crate, so it is that package's),
535/// or on wasm32 from the `wasm_registry.rs` snapshot (#1768). Empty when the
536/// crate sets none.
537pub(crate) fn crate_conversion_error_class() -> &'static [&'static str] {
538crate::registry::conversion_error_class()
539}
540541/// Run `f`, returning a condition raised inside it as `Err` instead of
542/// letting it reach R; any other unwind carries on.
543///
544/// For a body that turns another function's failure into its own result: a
545/// refusal that names the reason, a fallback, a retry.
546///
547/// ```ignore
548/// use miniextendr_api::condition::catch_condition;
549///
550/// match catch_condition(|| rebuild(&snapshot)) {
551/// Ok(object) => object,
552/// Err(cause) => error!(
553/// class = "pkg_error_rebuild",
554/// data = ("cause", cause.message().to_string()),
555/// "cannot rebuild the object: {}", cause.message()
556/// ),
557/// }
558/// ```
559///
560/// **Caught:** every [`RCondition`](enum@RCondition) payload, with its kind, classes, data
561/// and call choice: [`error!`](crate::error!) / [`rust_error!`](crate::rust_error!),
562/// [`warning!`](crate::warning!), [`message!`](crate::message!),
563/// [`condition!`](crate::condition!), [`RCondition::raise`], the argument and
564/// conversion errors ([`RCondition::arg_error`]) and the `ExternalPtr` type
565/// errors. An `Err` can be raised again with [`RCondition::raise`] or queued
566/// with [`RCondition::defer`].
567///
568/// **Resumed** unchanged, with [`std::panic::resume_unwind`]:
569/// - an R error, interrupt or other R exit crossing the Rust frames (R code
570/// run through [`with_r_unwind_protect`](crate::unwind_protect::with_r_unwind_protect)
571/// or [`eval_with_handlers`](crate::expression::eval_with_handlers)), so
572/// R carries on to the context it was jumping to;
573/// - a plain `panic!()` or any other payload, which still becomes an R error
574/// at the boundary, with its source location.
575///
576/// Use [`try_eval_with_handlers`](crate::expression::try_eval_with_handlers)
577/// to get an R error back as a value.
578///
579/// Deferred conditions queued inside `f` stay queued (see
580/// [`deferred_condition::capture`](crate::deferred_condition::capture)). A
581/// caught raise still runs the panic hook: with `MINIEXTENDR_BACKTRACE=true`
582/// it prints the panic to stderr.
583///
584/// Generated `#[miniextendr(worker)]` wrappers run the body through it on the
585/// worker thread: the payload is `Send`, so it crosses back to the main
586/// thread as a value and is raised there with its class, data and call kept.
587pub fn catch_condition<T>(f: impl FnOnce() -> T) -> Result<T, RCondition> {
588match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
589Ok(value) => Ok(value),
590Err(payload) => match payload.downcast::<RCondition>() {
591Ok(condition) => Err(*condition),
592Err(payload) => std::panic::resume_unwind(payload),
593 },
594 }
595}
596597// endregion
598599// region: Macros
600601/// Internal: one condition-data field name, validated against the reserved
602/// slots. Not part of the public API.
603///
604/// - `lit "name"` / `ident name`: the name is a literal, so the reserved-name
605/// check runs at compile time (`const` assertion) and the field costs one
606/// `to_string()` at runtime.
607/// - `expr <e>`: a computed name; checked at runtime by
608/// [`crate::condition::check_condition_field_name`].
609#[doc(hidden)]
610#[macro_export]
611macro_rules!__mx_condition_field_name {
612 (lit $name:literal) => {{
613const _: () = ::core::assert!(
614 !$crate::condition::is_reserved_condition_field($name),
615"condition data field name is reserved: `message`, `call` and `kind` are the condition's own slots, and `.mx_parent` carries its parent; rename the field"
616);
617 ($name).to_string()
618 }};
619 (ident $name:ident) => {{
620const _: () = ::core::assert!(
621 !$crate::condition::is_reserved_condition_field(::core::stringify!($name)),
622"condition data field name is reserved: `message`, `call` and `kind` are the condition's own slots, and `.mx_parent` carries its parent; rename the field"
623);
624::core::stringify!($name).to_string()
625 }};
626 (expr $name:expr) => {
627$crate::condition::check_condition_field_name(($name).to_string())
628 };
629}
630631/// Internal: one `(name, value)` pair of a bracketed `data = [...]` list; the
632/// name goes through the reserved-slot check (compile time for literals).
633#[doc(hidden)]
634#[macro_export]
635macro_rules!__mx_condition_pair {
636 (($name:literal, $value:expr $(,)?)) => {
637 ($crate::__mx_condition_field_name!(lit $name), $crate::RValue::from($value))
638 };
639 (($name:expr, $value:expr $(,)?)) => {
640 ($crate::__mx_condition_field_name!(expr $name), $crate::RValue::from($value))
641 };
642}
643644/// Internal: normalise a macro `data = ...` argument into
645/// `Option<ConditionData>`. Not part of the public API.
646///
647/// Three forms are accepted:
648/// - a single pair: `("name", value)`
649/// - a bracketed list of pairs: `[("a", v1), ("b", v2)]`
650/// - keyed builder sugar: `{ name = value, other = value }` — the field name is
651/// a bare identifier (stringified by the macro), so `{ value = 42, code = 7 }`
652/// is shorthand for `[("value", 42), ("code", 7)]`.
653///
654/// Each `value` is converted via `RValue::from`, so any type with an `RValue`
655/// `From` impl (the scalar/vector/`Option`/wide-integer set, and tuples of
656/// those as unnamed lists) works without ceremony.
657///
658/// Field names are checked against the condition's own slots (`message` /
659/// `call` / `kind`): at compile time when the name is a literal or bare
660/// identifier, at runtime otherwise.
661#[doc(hidden)]
662#[macro_export]
663macro_rules!__mx_condition_data {
664 (($name:literal, $value:expr $(,)?)) => {
665 ::std::option::Option::Some(::std::vec![(
666$crate::__mx_condition_field_name!(lit $name),
667$crate::RValue::from($value),
668 )])
669 };
670 (($name:expr, $value:expr $(,)?)) => {
671 ::std::option::Option::Some(::std::vec![(
672$crate::__mx_condition_field_name!(expr $name),
673$crate::RValue::from($value),
674 )])
675 };
676 ([ $($pair:tt),* $(,)? ]) => {
677 ::std::option::Option::Some(::std::vec![
678 $( $crate::__mx_condition_pair!($pair), )*
679 ])
680 };
681 ({ $($name:ident = $value:expr),* $(,)? }) => {
682 ::std::option::Option::Some(::std::vec![
683 $(
684 (
685$crate::__mx_condition_field_name!(ident $name),
686$crate::RValue::from($value),
687 ),
688 )*
689 ])
690 };
691}
692693/// Internal: parse the shared option grid of `error!` / `warning!` /
694/// `condition!` and the `defer_*!` macros into `(call, class, data, message)`.
695/// Not part of the public API.
696///
697/// Accepted orders (every part optional except the message):
698/// `call = none`, then `class = ..`, then `data = ..`, then the `format!`
699/// arguments. `class` takes anything implementing
700/// [`crate::condition::ConditionClass`] (one string or several). A `call`
701/// other than `none`, or one after `class` / `data`, is a compile error: a
702/// `format!` string must start with a literal, so `call` as the first token is
703/// never a message.
704///
705/// ```compile_fail
706/// # use miniextendr_api as mx;
707/// # fn f() {
708/// mx::warning!(call = wrapper, "no such call option");
709/// # }
710/// ```
711///
712/// ```compile_fail
713/// # use miniextendr_api as mx;
714/// # fn f() {
715/// mx::warning!(class = "pkg_warning", call = none, "`call` goes first");
716/// # }
717/// ```
718///
719/// ```
720/// # use miniextendr_api as mx;
721/// # fn f() {
722/// mx::warning!(call = none, class = "pkg_warning", data = { n = 1 }, "plain");
723/// # }
724/// ```
725#[doc(hidden)]
726#[macro_export]
727macro_rules!__mx_condition_parts {
728// The option grid after the `call` choice: `(class, data, message)`.
729(@options class = $class:expr, call $($rest:tt)*) => {
730$crate::__mx_condition_parts!(@misplaced_call)
731 };
732 (@options class = $class:expr, data = $data:tt, call $($rest:tt)*) => {
733$crate::__mx_condition_parts!(@misplaced_call)
734 };
735 (@options data = $data:tt, call $($rest:tt)*) => {
736$crate::__mx_condition_parts!(@misplaced_call)
737 };
738 (@options class = $class:expr, data = $data:tt, $($arg:tt)*) => {
739 (
740$crate::condition::ConditionClass::into_condition_class($class),
741$crate::__mx_condition_data!($data),
742::std::format!($($arg)*),
743 )
744 };
745 (@options data = $data:tt, $($arg:tt)*) => {
746 (
747 ::std::vec::Vec::<::std::string::String>::new(),
748$crate::__mx_condition_data!($data),
749::std::format!($($arg)*),
750 )
751 };
752 (@options class = $class:expr, $($arg:tt)*) => {
753 (
754$crate::condition::ConditionClass::into_condition_class($class),
755 ::std::option::Option::<$crate::condition::ConditionData>::None,
756::std::format!($($arg)*),
757 )
758 };
759 (@options $($arg:tt)*) => {
760 (
761 ::std::vec::Vec::<::std::string::String>::new(),
762 ::std::option::Option::<$crate::condition::ConditionData>::None,
763::std::format!($($arg)*),
764 )
765 };
766 (@options call $($rest:tt)*) => {
767::core::compile_error!("condition macros: `call = none` is given twice")
768 };
769 (@misplaced_call) => {
770::core::compile_error!(
771"condition macros: `call = none` comes first, before `class = ...` and `data = ...`: \
772 `warning!(call = none, class = \"...\", \"...\")`"
773)
774 };
775// Entry: the optional `call = none`, then the option grid.
776(call = none, $($rest:tt)+) => {{
777let (__mx_class, __mx_data, __mx_message) =
778$crate::__mx_condition_parts!(@options $($rest)+);
779 (
780$crate::condition::ConditionCall::None,
781 __mx_class,
782 __mx_data,
783 __mx_message,
784 )
785 }};
786 (call $($rest:tt)*) => {
787::core::compile_error!(
788"condition macros: the only call option is `call = none` (no call, like R's \
789 `call. = FALSE`), and it comes first, followed by a comma: \
790 `warning!(call = none, \"...\")`"
791)
792 };
793 ($($rest:tt)*) => {{
794let (__mx_class, __mx_data, __mx_message) =
795$crate::__mx_condition_parts!(@options $($rest)*);
796 (
797$crate::condition::ConditionCall::Inherit,
798 __mx_class,
799 __mx_data,
800 __mx_message,
801 )
802 }};
803}
804805/// Raise an R error from Rust with `rust_error` class layering.
806///
807/// Rides the tagged-condition transport that every `#[miniextendr]` function uses.
808/// The raised condition has class `c("rust_error", "simpleError", "error", "condition")`.
809///
810/// An optional `class = "name"` form prepends a custom class for programmatic catching:
811/// `c("name", "rust_error", "simpleError", "error", "condition")`. `class` also
812/// takes a vector (`class = ["pkg_error_missing_field", "pkg_error"]`, or any
813/// [`ConditionClass`] value) so handlers can catch the family or the member.
814///
815/// The framework layers (`rust_error`, `simpleError`, `error`, `condition`)
816/// always follow the user classes; no option leaves them out. Handlers and
817/// `inherits()` see the user classes as on an R `errorCondition()`; a test
818/// that compares the whole class vector with an R implementation's compares
819/// the leading classes instead.
820///
821/// # Structured `data = ...` payloads
822///
823/// An optional `data = ...` form (after `class`, before the message) attaches
824/// named fields to the condition object, rlang-`abort()`-style. Handlers read
825/// them as `e$<name>` instead of parsing the message string:
826///
827/// ```ignore
828/// // Single field:
829/// mx::error!(class = "range_error", data = ("value", value), "value {value} out of range");
830///
831/// // Multiple fields (bracketed list of pairs):
832/// mx::error!(
833/// class = "validation_error",
834/// data = [("value", value), ("min", 0), ("max", 100)],
835/// "value {value} out of range"
836/// );
837///
838/// // Keyed builder sugar (bare-ident keys, stringified by the macro):
839/// mx::error!(
840/// class = "validation_error",
841/// data = { value = value, min = 0, max = 100 },
842/// "value {value} out of range"
843/// );
844/// ```
845///
846/// ```r
847/// tryCatch(validate(150L), validation_error = function(e) c(e$value, e$min, e$max))
848/// # [1] 150 0 100
849/// ```
850///
851/// Argument order is fixed: `call = none` (optional), then `class = ...`
852/// (optional), then `data = ...` (optional), then the format message.
853///
854/// Field names `message`, `call` and `kind` are the condition's own slots and
855/// are rejected (at compile time for literal / bare-identifier names, at
856/// runtime otherwise); give the field another name (`rule` instead of `kind`,
857/// say). A downstream error type with such a field renames it where the
858/// payload is built (`#[serde(rename)]`, a different key).
859///
860/// **Supported value types**: scalars and `Vec`s of `i32`, `f64`, `bool`, and
861/// `String` (plus `&str` / `Vec<&str>`, converted to owned); their NA-aware
862/// `Option` / `Vec<Option<_>>` forms (→ R `NA`); the wide-integer ladder (`i64`
863/// / `u32`); and the [`RValue::debug`](crate::RValue::debug) escape hatch for
864/// any `T: Debug`. The payload must be `Send` — it travels through `panic_any`
865/// and may cross the worker→main thread boundary, so live `SEXP`s cannot ride
866/// along; the R objects are materialised on the main thread at the unwind
867/// boundary. For nested lists or complex/raw values build an
868/// [`RValue`](crate::RValue) directly.
869///
870/// # Conditions without a call
871///
872/// A leading `call = none` raises the condition with no call, like R's
873/// `stop(..., call. = FALSE)`: `conditionCall(e)` is `NULL` and the error
874/// prints as `Error: <message>` instead of `Error in f(...) : <message>`. Use
875/// it for a condition about the data rather than about the call. Classes and
876/// `data` are unchanged:
877///
878/// ```ignore
879/// mx::error!(call = none, class = "pkg_bad_row", data = { row = row }, "row {row} is malformed");
880/// ```
881///
882/// The same marker exists as [`RError::without_call`] for `Result` errors and
883/// as `#[condition(call = none)]` on a `#[derive(RCondition)]` type or
884/// variant; see [`ConditionCall`].
885///
886/// # See also
887///
888/// - [`crate::warning!`] / [`crate::message!`] / [`crate::condition!`] — the
889/// non-error sibling kinds (warning continues execution; message is muffled
890/// by `suppressMessages`; condition is silent without a handler).
891/// - [`std::panic!`] — escape hatch with the same `rust_error` class layering
892/// but no custom-class slot. Use for true bugs / impossible states; reach for
893/// `error!` when callers might want to route by class.
894/// - [`AsRError`] — wraps `Result<_, E: std::error::Error>` for value-style
895/// propagation through Rust code; converts at the boundary.
896/// - [`crate::error_value`] — module-level rationale for the tagged-SEXP
897/// transport and the `error_in_r` default.
898///
899/// **Name-collision note.** Because `pub mod error` exists at the crate root,
900/// `use miniextendr_api::error` imports the module rather than this macro, and
901/// glob imports (`use miniextendr_api::*;`) hit the same shadow. Prefer the
902/// collision-free alias [`crate::rust_error!`], which has the identical
903/// expansion; otherwise invoke via `miniextendr_api::error!(...)` (fully
904/// qualified) or `mx::error!(...)` after `use miniextendr_api as mx;`.
905///
906/// # Examples
907///
908/// ```ignore
909/// use miniextendr_api as mx;
910///
911/// #[miniextendr]
912/// fn fail() {
913/// mx::error!("something went wrong: {}", 42);
914/// }
915///
916/// // With a custom class for tryCatch:
917/// #[miniextendr]
918/// fn typed_fail(name: &str) {
919/// mx::error!(class = "my_error", "missing field: {name}");
920/// }
921/// ```
922///
923/// ```r
924/// tryCatch(fail(), rust_error = function(e) conditionMessage(e))
925/// # [1] "something went wrong: 42"
926///
927/// tryCatch(typed_fail("x"), my_error = function(e) "caught!")
928/// # [1] "caught!"
929/// ```
930#[macro_export]
931macro_rules!error {
932 ($($t:tt)*) => {{
933let (__mx_call, __mx_class, __mx_data, __mx_message) =
934$crate::__mx_condition_parts!($($t)*);
935 ::std::panic::panic_any($crate::condition::RCondition::Error {
936 message: __mx_message,
937 class: __mx_class,
938 data: __mx_data,
939 call: __mx_call,
940 })
941 }};
942}
943944/// Collision-free alias for [`crate::error!`].
945///
946/// Identical expansion and grammar (`call = none`, `class = …`, `data = …`,
947/// and the plain `format!` forms) — it exists so that `use miniextendr_api::*;` or
948/// `use miniextendr_api::rust_error;` gives you a usable macro name. The bare
949/// `error!` name is shadowed at the crate root by `pub mod error` (see the
950/// name-collision note on [`crate::error!`]), so a glob or direct import
951/// resolves to the module, not the macro. `rust_error!` has no such clash.
952///
953/// # Example
954///
955/// ```ignore
956/// use miniextendr_api::rust_error;
957///
958/// #[miniextendr]
959/// fn typed_fail(name: &str) {
960/// rust_error!(class = "my_error", "missing field: {name}");
961/// }
962/// ```
963#[macro_export]
964macro_rules!rust_error {
965 ($($t:tt)*) => { $crate::error!($($t)*) };
966}
967968/// Raise an R warning from Rust with `rust_warning` class layering.
969///
970/// Rides the tagged-condition transport that every `#[miniextendr]` function uses.
971/// Unlike `panic!`, execution continues after `warning!` is caught by a handler.
972/// The raised condition has class `c("rust_warning", "simpleWarning", "warning", "condition")`.
973///
974/// An optional `class = "name"` form prepends a custom class. An optional
975/// `data = ...` form (after `class`, before the message) attaches named fields
976/// readable as `w$<name>` in handlers — same grammar and supported value types
977/// as [`crate::error!`] (see there for details):
978///
979/// ```ignore
980/// warning!(class = "truncation", data = ("dropped", n), "dropped {n} rows");
981/// ```
982///
983/// A leading `call = none` raises the warning without a call, like R's
984/// `warning(..., call. = FALSE)`: `conditionCall(w)` is `NULL` and the
985/// printed warning has no `In f(...) :` prefix. Use it for a warning about the
986/// data rather than about the call (see
987/// [conditions without a call](crate::error!#conditions-without-a-call)):
988///
989/// ```ignore
990/// warning!(call = none, class = "pkg_override", data = { row = row }, "row {row} overrides an earlier row");
991/// ```
992///
993/// # See also
994///
995/// - [`crate::error!`] — fatal sibling; aborts the call instead of continuing.
996/// - [`crate::defer_warning!`] / [`crate::defer_warning()`] — queue the warning
997/// and still return the value (signalled when the call returns).
998/// - [`crate::message!`] / [`crate::condition!`] — softer signal kinds (muffled
999/// by `suppressMessages` / silent without handler, respectively).
1000/// - [`std::panic!`] — escape hatch when "continue after this" is not a sensible
1001/// semantic.
1002/// - [`crate::error_value`] — tagged-SEXP transport rationale.
1003///
1004/// No name-collision caveat: there is no `pub mod warning`, so
1005/// `use miniextendr_api::warning;` then `warning!(...)` works directly.
1006///
1007/// # Example
1008///
1009/// ```ignore
1010/// use miniextendr_api::warning;
1011///
1012/// #[miniextendr]
1013/// fn maybe_warn(x: i32) -> i32 {
1014/// if x > 100 {
1015/// warning!("x is large: {x}");
1016/// }
1017/// x * 2
1018/// }
1019/// ```
1020///
1021/// ```r
1022/// withCallingHandlers(
1023/// maybe_warn(200L),
1024/// warning = function(w) { cat("saw:", conditionMessage(w)); invokeRestart("muffleWarning") }
1025/// )
1026/// # saw: x is large: 200
1027/// # [1] 400
1028/// ```
1029#[macro_export]
1030macro_rules!warning {
1031 ($($t:tt)*) => {{
1032let (__mx_call, __mx_class, __mx_data, __mx_message) =
1033$crate::__mx_condition_parts!($($t)*);
1034 ::std::panic::panic_any($crate::condition::RCondition::Warning {
1035 message: __mx_message,
1036 class: __mx_class,
1037 data: __mx_data,
1038 call: __mx_call,
1039 })
1040 }};
1041}
10421043/// Emit an R message from Rust with `rust_message` class layering.
1044///
1045/// Rides the tagged-condition transport that every `#[miniextendr]` function uses.
1046/// The raised condition has class `c("rust_message", "simpleMessage", "message", "condition")`.
1047/// Muffled by `suppressMessages()` automatically (standard R restart mechanism).
1048///
1049/// An optional `data = ...` form (before the message) attaches named fields
1050/// readable as `m$<name>` in `withCallingHandlers` — same grammar and
1051/// supported value types as [`crate::error!`] (see there for details). There
1052/// is no `class =` form for `message!`, and no `call =` form: an R message
1053/// carries no call (`conditionCall(m)` is `NULL`), so there is nothing to drop.
1054///
1055/// # See also
1056///
1057/// - [`crate::warning!`] / [`crate::condition!`] — louder/quieter sibling kinds.
1058/// - [`crate::error!`] — for fatal failures.
1059/// - [`std::panic!`] — escape hatch.
1060/// - [`crate::error_value`] — tagged-SEXP transport rationale.
1061///
1062/// No name-collision caveat: there is no `pub mod message`, so
1063/// `use miniextendr_api::message;` then `message!(...)` works directly.
1064///
1065/// # Example
1066///
1067/// ```ignore
1068/// use miniextendr_api::message;
1069///
1070/// #[miniextendr]
1071/// fn log_step(step: i32) {
1072/// message!("step {} complete", step);
1073/// }
1074/// ```
1075///
1076/// ```r
1077/// log_step(3L)
1078/// # step 3 complete
1079///
1080/// suppressMessages(log_step(3L)) # no output
1081/// ```
1082#[macro_export]
1083macro_rules!message {
1084 (call $($rest:tt)*) => {
1085::core::compile_error!(
1086"message!: there is no `call` option, an R message carries no call"
1087)
1088 };
1089 (data = $data:tt, $($arg:tt)*) => {
1090 ::std::panic::panic_any($crate::condition::RCondition::Message {
1091 message: ::std::format!($($arg)*),
1092 class: ::std::vec::Vec::new(),
1093 data: $crate::__mx_condition_data!($data),
1094 call: $crate::condition::ConditionCall::Inherit,
1095 })
1096 };
1097 ($($arg:tt)*) => {
1098 ::std::panic::panic_any($crate::condition::RCondition::Message {
1099 message: ::std::format!($($arg)*),
1100 class: ::std::vec::Vec::new(),
1101 data: ::std::option::Option::None,
1102 call: $crate::condition::ConditionCall::Inherit,
1103 })
1104 };
1105}
11061107/// Signal a generic R condition from Rust with `rust_condition` class layering.
1108///
1109/// Rides the tagged-condition transport that every `#[miniextendr]` function uses.
1110/// Unlike `error!`, a bare condition is a silent no-op if there is no handler.
1111/// The raised condition has class `c("rust_condition", "simpleCondition", "condition")`.
1112///
1113/// An optional `class = "name"` form prepends a custom class. An optional
1114/// `data = ...` form (after `class`, before the message) attaches named fields
1115/// readable as `c$<name>` in handlers — same grammar and supported value types
1116/// as [`crate::error!`] (see there for details). A leading `call = none`
1117/// signals the condition without a call (`conditionCall(c)` is `NULL`), like
1118/// [`crate::error!`]'s.
1119///
1120/// # See also
1121///
1122/// - [`crate::error!`] / [`crate::warning!`] / [`crate::message!`] — louder
1123/// condition kinds. Pick `condition!` when "no handler = silent" is the
1124/// right default (progress events, structured logging hooks).
1125/// - [`std::panic!`] — escape hatch when the failure cannot be ignored.
1126/// - [`crate::error_value`] — tagged-SEXP transport rationale.
1127///
1128/// **Name-collision note.** Because `pub mod condition` exists at the crate
1129/// root, `use miniextendr_api::condition` imports the module rather than this
1130/// macro, and glob imports (`use miniextendr_api::*;`) hit the same shadow.
1131/// Prefer the collision-free alias [`crate::rust_condition!`], which has the
1132/// identical expansion; otherwise invoke via `miniextendr_api::condition!(...)`
1133/// (fully qualified) or `mx::condition!(...)` after `use miniextendr_api as mx;`.
1134///
1135/// # Example
1136///
1137/// ```ignore
1138/// use miniextendr_api::condition;
1139///
1140/// #[miniextendr]
1141/// fn signal_progress(n: i32) {
1142/// condition!(class = "my_progress", "processed {n} items");
1143/// }
1144/// ```
1145///
1146/// ```r
1147/// withCallingHandlers(
1148/// signal_progress(42L),
1149/// my_progress = function(c) cat("progress:", conditionMessage(c), "\n")
1150/// )
1151/// # progress: processed 42 items
1152/// ```
1153#[macro_export]
1154macro_rules!condition {
1155 ($($t:tt)*) => {{
1156let (__mx_call, __mx_class, __mx_data, __mx_message) =
1157$crate::__mx_condition_parts!($($t)*);
1158 ::std::panic::panic_any($crate::condition::RCondition::Condition {
1159 message: __mx_message,
1160 class: __mx_class,
1161 data: __mx_data,
1162 call: __mx_call,
1163 })
1164 }};
1165}
11661167/// Collision-free alias for [`crate::condition!`].
1168///
1169/// Identical expansion and grammar (`call = none`, `class = …`, `data = …`,
1170/// and the plain `format!` forms) — it exists so that `use miniextendr_api::*;` or
1171/// `use miniextendr_api::rust_condition;` gives you a usable macro name. The
1172/// bare `condition!` name is shadowed at the crate root by `pub mod condition`
1173/// (see the name-collision note on [`crate::condition!`]), so a glob or direct
1174/// import resolves to the module, not the macro. `rust_condition!` has no such
1175/// clash.
1176///
1177/// # Example
1178///
1179/// ```ignore
1180/// use miniextendr_api::rust_condition;
1181///
1182/// #[miniextendr]
1183/// fn signal_progress(n: i32) {
1184/// rust_condition!(class = "my_progress", "processed {n} items");
1185/// }
1186/// ```
1187#[macro_export]
1188macro_rules!rust_condition {
1189 ($($t:tt)*) => { $crate::condition!($($t)*) };
1190}
11911192/// Raise the argument error the generated R wrapper raises for a bad
1193/// argument, from a `#[miniextendr]` function body.
1194///
1195/// `arg_error!(param = "mode", "'mode' should be one of {}", choices)` raises
1196/// the condition of the wrapper's own argument checks (`match_arg`,
1197/// `inherits`, `no_na`, type preconditions) and of a failed argument
1198/// conversion: `kind = "conversion"`, `e$param` set to `param` (the
1199/// argument's R name), the message as formatted, and the crate's
1200/// `conversion_error_class` (`[package.metadata.miniextendr]`) before the
1201/// `rust_error` layering:
1202///
1203/// ```r
1204/// class(e)
1205/// # [1] "pkg_error_argument" "pkg_error" "rust_error" "simpleError" "error" "condition"
1206/// ```
1207///
1208/// Use it for a check that applies on some paths only, where a
1209/// `#[miniextendr]` attribute on the parameter would check every call: an
1210/// argument passed on to another function's body, say, which skips that
1211/// function's R wrapper and its checks. [`crate::match_arg_param`] matches a
1212/// raw choice argument with the wrapper's semantics and message and raises
1213/// through this macro.
1214///
1215/// It diverges like [`crate::error!`] (the same `panic_any` transport), and
1216/// conditions queued with [`crate::defer_warning!`] & co. are signalled first.
1217/// The call is the one the wrapper's checks report: the wrapper's call as
1218/// written, or under `#[miniextendr(call = caller)]` its caller's. A leading
1219/// `call = none` raises it without a call, as for the other condition macros.
1220/// There is no `class =` or `data =`: the classes are the crate's and the only
1221/// field is `param`, so the condition is the wrapper's.
1222///
1223/// ```ignore
1224/// use miniextendr_api::{arg_error, miniextendr};
1225///
1226/// #[miniextendr]
1227/// pub fn in_metres(x: f64, unit: &str) -> f64 {
1228/// match unit {
1229/// "m" => x,
1230/// "cm" => x / 100.0,
1231/// _ => arg_error!(param = "unit", "'unit' should be one of \"m\", \"cm\""),
1232/// }
1233/// }
1234/// ```
1235///
1236/// # How the crate's classes get there
1237///
1238/// A `macro_rules!` macro cannot read `[package.metadata.miniextendr]`, so the
1239/// tagged condition value carries the marker class
1240/// [`CONVERSION_ERROR_CLASS_MARKER`]
1241/// and the generated wrapper's `.miniextendr_raise_condition` replaces it with
1242/// `.miniextendr_conversion_error_class`, the binding its own checks use. Across
1243/// a trait-ABI call the consumer package's wrapper raises the condition, so
1244/// its crate's classes apply. A raising guard (an ALTREP `RUnwind` callback,
1245/// `with_r_unwind_protect_or_raise`) has no generated R: it resolves the
1246/// marker in Rust against the classes `miniextendr_init!` registered (on
1247/// wasm32, the copy in the `wasm_registry.rs` snapshot), then raises the same
1248/// condition, `kind = "conversion"` included (#1768). A connection callback
1249/// returns its fallback, as for any panic.
1250///
1251/// The generated `no_na` check after a conversion builds the same condition
1252/// with [`crate::error_value::arg_check_condition_value`], which takes the
1253/// classes the macro read at expansion time.
1254///
1255/// # Grammar
1256///
1257/// `arg_error!([call = none,] param = <expr>, <format string>, <args>...)`.
1258/// `param` is anything `Into<String>`. A `call` anywhere but first, a repeated
1259/// one, a missing `param`, and `class =` / `data =` are compile errors:
1260///
1261/// ```compile_fail
1262/// # use miniextendr_api as mx;
1263/// # fn f() {
1264/// mx::arg_error!(param = "mode", call = none, "bad");
1265/// # }
1266/// ```
1267///
1268/// ```compile_fail
1269/// # use miniextendr_api as mx;
1270/// # fn f() {
1271/// mx::arg_error!("'mode' is bad");
1272/// # }
1273/// ```
1274///
1275/// ```compile_fail
1276/// # use miniextendr_api as mx;
1277/// # fn f() {
1278/// mx::arg_error!(param = "mode", class = "pkg_error", "bad");
1279/// # }
1280/// ```
1281///
1282/// ```
1283/// # use miniextendr_api as mx;
1284/// # fn f(mode: &str) -> ! {
1285/// mx::arg_error!(call = none, param = "mode", "'mode' is {mode:?}")
1286/// # }
1287/// ```
1288#[macro_export]
1289macro_rules!arg_error {
1290// Internal: after the optional `call = none`.
1291(@param $call:expr; call $($rest:tt)*) => {
1292::core::compile_error!("arg_error!: `call = none` is given twice")
1293 };
1294 (@param $call:expr; param = $param:expr, call $($rest:tt)*) => {
1295::core::compile_error!(
1296"arg_error!: `call = none` comes first, before `param`: \
1297 `arg_error!(call = none, param = \"mode\", \"...\")`"
1298)
1299 };
1300 (@param $call:expr; param = $param:expr, class $($rest:tt)*) => {
1301$crate::arg_error!(@no_class_or_data)
1302 };
1303 (@param $call:expr; param = $param:expr, data $($rest:tt)*) => {
1304$crate::arg_error!(@no_class_or_data)
1305 };
1306 (@param $call:expr; param = $param:expr, $($arg:tt)+) => {
1307 ::std::panic::panic_any($crate::condition::RCondition::arg_error(
1308$param,
1309::std::format!($($arg)+),
1310$call,
1311 ))
1312 };
1313 (@param $call:expr; $($rest:tt)*) => {
1314::core::compile_error!(
1315"arg_error!: name the argument, then give the message: \
1316 `arg_error!(param = \"mode\", \"'mode' should be one of ...\")`"
1317)
1318 };
1319 (@no_class_or_data) => {
1320::core::compile_error!(
1321"arg_error!: takes no `class` or `data`: its classes are the crate's \
1322 `conversion_error_class` and its only field is `param`, as for the wrapper's \
1323 own argument errors; use `error!` for a classed error"
1324)
1325 };
1326// Entry: the optional `call = none`, then `param = ...` and the message.
1327(call = none, $($rest:tt)*) => {
1328$crate::arg_error!(@param $crate::condition::ConditionCall::None; $($rest)*)
1329 };
1330 (call $($rest:tt)*) => {
1331::core::compile_error!(
1332"arg_error!: the only call option is `call = none` (no call, like R's \
1333 `call. = FALSE`), and it comes first, followed by a comma: \
1334 `arg_error!(call = none, param = \"mode\", \"...\")`"
1335)
1336 };
1337 ($($rest:tt)*) => {
1338$crate::arg_error!(@param $crate::condition::ConditionCall::Inherit; $($rest)*)
1339 };
1340}
13411342// endregion
13431344// region: Class vectors and reserved field names
13451346/// What the condition macros' `class = …` (and [`RError::class`]) accept: one
1347/// class or several, as `&str` / `String` / arrays / `Vec` / slices.
1348///
1349/// The classes are prepended in order to the `rust_*` layering, so put the most
1350/// specific first: `class = ["pkg_error_missing_field", "pkg_error"]` renders
1351/// as `c("pkg_error_missing_field", "pkg_error", "rust_error", …)` and both
1352/// `tryCatch(pkg_error_missing_field = …)` and `tryCatch(pkg_error = …)` match.
1353pub trait ConditionClass {
1354/// The class vector, most specific first.
1355fn into_condition_class(self) -> Vec<String>;
1356}
13571358impl ConditionClassfor &str {
1359fn into_condition_class(self) -> Vec<String> {
1360::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
[self.to_string()]))vec![self.to_string()]1361 }
1362}
1363impl ConditionClassfor String {
1364fn into_condition_class(self) -> Vec<String> {
1365::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
[self]))vec![self]1366 }
1367}
1368impl ConditionClassfor &String {
1369fn into_condition_class(self) -> Vec<String> {
1370::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
[self.clone()]))vec![self.clone()]1371 }
1372}
1373impl<const N: usize> ConditionClassfor [&str; N] {
1374fn into_condition_class(self) -> Vec<String> {
1375self.iter().map(|s| s.to_string()).collect()
1376 }
1377}
1378impl<const N: usize> ConditionClassfor [String; N] {
1379fn into_condition_class(self) -> Vec<String> {
1380self.into_iter().collect()
1381 }
1382}
1383impl ConditionClassfor Vec<&str> {
1384fn into_condition_class(self) -> Vec<String> {
1385self.into_iter().map(str::to_string).collect()
1386 }
1387}
1388impl ConditionClassfor Vec<String> {
1389fn into_condition_class(self) -> Vec<String> {
1390self1391 }
1392}
1393impl ConditionClassfor &[&str] {
1394fn into_condition_class(self) -> Vec<String> {
1395self.iter().map(|s| s.to_string()).collect()
1396 }
1397}
1398impl ConditionClassfor &[String] {
1399fn into_condition_class(self) -> Vec<String> {
1400self.to_vec()
1401 }
1402}
14031404/// Condition-data field names that would overwrite the condition object's own
1405/// slots when the R helper splices `data` in (`utils::modifyList` over
1406/// `list(message, call, kind)`).
1407pub const RESERVED_CONDITION_FIELDS: &[&str] = &["message", "call", "kind"];
14081409/// `true` for [`RESERVED_CONDITION_FIELDS`]. `const` so the macros can reject a
1410/// literal field name at compile time:
1411///
1412/// ```compile_fail
1413/// # use miniextendr_api as mx;
1414/// # fn f() {
1415/// mx::rust_error!(data = ("kind", 1), "boom");
1416/// # }
1417/// ```
1418///
1419/// Any other name is fine, so the fix for a clash is a rename at the source:
1420///
1421/// ```
1422/// # use miniextendr_api as mx;
1423/// # fn f() {
1424/// mx::rust_error!(data = ("rule", 1), "boom");
1425/// # }
1426/// ```
1427pub const fn is_reserved_condition_field(name: &str) -> bool {
1428str_eq(name, "message")
1429 || str_eq(name, "call")
1430 || str_eq(name, "kind")
1431 || str_eq(name, PARENT_FIELD)
1432}
14331434/// `a == b`, usable in `const fn`.
1435const fn str_eq(a: &str, b: &str) -> bool {
1436let (a, b) = (a.as_bytes(), b.as_bytes());
1437if a.len() != b.len() {
1438return false;
1439 }
1440let mut i = 0;
1441while i < a.len() {
1442if a[i] != b[i] {
1443return false;
1444 }
1445 i += 1;
1446 }
1447true
1448}
14491450/// The `data` field under which [`RCondition::with_parent`] sends a cause to
1451/// R; the raise helper turns it into the condition object `e$parent`.
1452const PARENT_FIELD: &str = ".mx_parent";
14531454/// Runtime half of the reserved-name check, for computed field names (the
1455/// macros' `expr` path and [`RConditionData::data`] on user types). Panics
1456/// when `name` is reserved; the panic becomes a plain `rust_error` in R,
1457/// replacing the former silent overwrite.
1458#[track_caller]
1459pub fn check_condition_field_name(name: String) -> String {
1460if is_reserved_condition_field(&name) {
1461{
::core::panicking::panic_fmt(format_args!("condition data field `{0}` is reserved: `message`, `call` and `kind` are the condition\'s own slots, and `.mx_parent` carries its parent; rename the field.",
name));
};panic!(
1462"condition data field `{name}` is reserved: `message`, `call` and `kind` are the \
1463 condition's own slots, and `.mx_parent` carries its parent; rename the field."
1464);
1465 }
1466name1467}
14681469/// Apply [`check_condition_field_name`] to every field of a payload.
1470#[track_caller]
1471pub fn check_condition_data(data: Option<ConditionData>) -> Option<ConditionData> {
1472data.map(|fields| {
1473fields1474 .into_iter()
1475 .map(|(name, value)| (check_condition_field_name(name), value))
1476 .collect()
1477 })
1478}
14791480// endregion
14811482// region: Classed `Result` errors — RConditionData + RError
14831484/// Give a `Result<T, E>` error type an R class vector and structured fields.
1485///
1486/// Every `#[miniextendr]` function or method returning `Result<T, E>` raises
1487/// `Err(e)` as an R error. Implement this trait for `E` (or return
1488/// [`RError`], which implements it) and the `Err` arm raises
1489/// `c(<class()…>, "rust_error", "simpleError", "error", "condition")` with
1490/// every `data()` field readable as `e$<name>`, so a thiserror-style error enum
1491/// can keep `?` composition *and* give R handlers something to dispatch on.
1492///
1493/// Without an impl, the `Err` arm falls back in two steps. Under the `serde`
1494/// feature an `E: serde::Serialize + Display` is classed from its serde shape
1495/// (see `serde_err_parts`: variant → member class, fields → data, `Display`
1496/// → message). Anything else is a bare `rust_error` whose message is
1497/// `format!("{e:?}")`.
1498///
1499/// ```ignore
1500/// use miniextendr_api::condition::{ConditionData, RConditionData};
1501///
1502/// #[derive(Debug, thiserror::Error)]
1503/// pub enum PkgError {
1504/// #[error("column `{column}` is missing")]
1505/// MissingColumn { column: String },
1506/// #[error("{value} exceeds {max}")]
1507/// TooLarge { value: f64, max: f64 },
1508/// }
1509///
1510/// impl RConditionData for PkgError {
1511/// fn message(&self) -> String { self.to_string() }
1512/// fn class(&self) -> Vec<String> {
1513/// let member = match self {
1514/// PkgError::MissingColumn { .. } => "pkg_error_missing_column",
1515/// PkgError::TooLarge { .. } => "pkg_error_too_large",
1516/// };
1517/// vec![member.into(), "pkg_error".into()]
1518/// }
1519/// fn data(&self) -> Option<ConditionData> {
1520/// Some(match self {
1521/// PkgError::MissingColumn { column } => vec![("column".into(), column.clone().into())],
1522/// PkgError::TooLarge { value, max } => vec![("value".into(), (*value).into()), ("max".into(), (*max).into())],
1523/// })
1524/// }
1525/// }
1526///
1527/// #[miniextendr]
1528/// pub fn check(value: f64) -> Result<f64, PkgError> { /* uses `?` freely */ }
1529/// ```
1530///
1531/// ```r
1532/// tryCatch(check(1e9), pkg_error_too_large = function(e) e$max) # member
1533/// tryCatch(check(1e9), pkg_error = function(e) conditionMessage(e)) # family
1534/// ```
1535///
1536/// Detection is by trait, not by attribute: the generated `Err` arm probes
1537/// `E: RConditionData` first and falls back to the `Debug` rendering
1538/// otherwise, so existing `Result<T, String>` / `Result<T, MyDebugError>`
1539/// functions are unchanged. The condition's `kind` stays `"result_err"`.
1540///
1541/// The same impl classes argument-conversion failures: when a parameter
1542/// type's `TryFromSexp::Error` implements this trait, a failed conversion
1543/// raises its classes and fields with `kind = "conversion"`, the message
1544/// `'<p>' must be <expected>: <message()>` (or `invalid '<p>' argument:
1545/// <message()>` when the argument's type has no R-facing expectation), the
1546/// parameter's R name as `e$param` and the Rust type as `e$rust_type`
1547/// (unless `data()` has a field of the same name, which then wins). An error
1548/// type without the impl renders with `Display` and keeps the plain
1549/// `rust_error` class vector.
1550///
1551/// `data()` field names must not be `message`, `call` or `kind` (see
1552/// [`RESERVED_CONDITION_FIELDS`]); a reserved name raises a plain
1553/// `rust_error` explaining the clash, so rename such a field at the source.
1554///
1555/// [`call()`](RConditionData::call) returning [`ConditionCall::None`] drops
1556/// the call (`conditionCall(e)` is `NULL`), for errors and warnings about the
1557/// data rather than the call.
1558pub trait RConditionData {
1559/// The condition message (`conditionMessage(e)`).
1560fn message(&self) -> String;
1561/// User classes, most specific first; empty for none.
1562fn class(&self) -> Vec<String> {
1563Vec::new()
1564 }
1565/// Structured fields spliced into the condition object (`e$<name>`).
1566fn data(&self) -> Option<ConditionData> {
1567None1568 }
1569/// Whether the condition reports a call. [`ConditionCall::None`] makes
1570 /// `conditionCall()` `NULL`, like R's `call. = FALSE`, for a condition
1571 /// about the data rather than the call; the default keeps the call the
1572 /// transport captured. Honoured by `defer_*` and by `Result` returns;
1573 /// an argument-conversion error always keeps the call (see
1574 /// [`conversion_err_parts`]). `#[condition(call = none)]` on a derived
1575 /// type or variant overrides this.
1576fn call(&self) -> ConditionCall {
1577 ConditionCall::Inherit1578 }
1579/// The whole message when this error refuses an argument, or `None`.
1580 ///
1581 /// When the error comes out of a parameter's conversion (the type's
1582 /// `TryFromSexp`, or its [`check_sexp`](crate::TryFromSexpElement::check_sexp)
1583 /// under `Option<T>` / `Vec<T>` / `Vec<Option<T>>`), a `Some` is the
1584 /// condition's message as given, in place of
1585 /// `'<p>' must be <expected>: <message()>`. The classes, `kind`,
1586 /// `e$param`, `e$rust_type` and the call are unchanged. This is how a type
1587 /// words a refusal once for every parameter of that type, as
1588 /// `#[miniextendr(inherits(..., message = ...))]` does for one parameter:
1589 /// both use the author's message as given and name the argument in
1590 /// `e$param`. Ignored everywhere else (`Result` returns, `defer_*`), and
1591 /// inside an `Either`, whose message names both arms. For a refused
1592 /// element of a list of objects (`Vec<T>` of a newtype over `List`), it
1593 /// is that element's reason, before the element's position, in the
1594 /// prefixed batched message: an element is not the whole argument.
1595fn argument_message(&self) -> Option<String> {
1596None1597 }
1598}
15991600/// `#[derive(RCondition)]`: [`RConditionData`] generated from a type's shape
1601/// (`#[condition(class = "…")]`, `#[condition(message = "…")]`, the
1602/// `rename` / `skip` / `debug` field options) and `From<T>` for
1603/// [`RCondition`](enum@RCondition) with the declared kind
1604/// (`#[condition(kind = …)]`, default `error`).
1605#[doc(inline)]
1606pub use miniextendr_macros::RCondition;
16071608/// A ready-made classed error value for `Result<T, RError>` returns.
1609///
1610/// Carries a message, an optional class vector, structured fields and the
1611/// call choice, and implements [`RConditionData`]. Any
1612/// `std::error::Error` converts into it with `?` / [`From`] (the message keeps
1613/// the `caused by:` chain, like [`AsRError`]), after which the builder methods
1614/// add the R-facing parts ([`RError::class`], [`RError::data`],
1615/// [`RError::without_call`]):
1616///
1617/// ```ignore
1618/// use miniextendr_api::condition::RError;
1619///
1620/// #[miniextendr]
1621/// pub fn parse_port(s: &str) -> Result<i32, RError> {
1622/// let port: i32 = s
1623/// .parse()
1624/// .map_err(|e| RError::from(e).class(["pkg_bad_port", "pkg_error"]).data("input", s))?;
1625/// Ok(port)
1626/// }
1627/// ```
1628///
1629/// ```r
1630/// e <- tryCatch(parse_port("x"), pkg_bad_port = function(e) e)
1631/// e$input # "x"
1632/// class(e)[1:2] # "pkg_bad_port" "pkg_error"
1633/// ```
1634///
1635/// `RError` deliberately does **not** implement `std::error::Error`: that
1636/// keeps the blanket `From<E: Error>` coherent. It does implement `Display`
1637/// (the message), so it also works with `#[miniextendr(unwrap_in_r)]`.
1638#[derive(#[automatically_derived]
impl ::core::fmt::Debug for RError {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field5_finish(f, "RError",
"message", &self.message, "class", &self.class, "data",
&self.data, "call", &self.call, "argument_message",
&&self.argument_message)
}
}Debug, #[automatically_derived]
impl ::core::clone::Clone for RError {
#[inline]
fn clone(&self) -> Self {
RError {
message: ::core::clone::Clone::clone(&self.message),
class: ::core::clone::Clone::clone(&self.class),
data: ::core::clone::Clone::clone(&self.data),
call: ::core::clone::Clone::clone(&self.call),
argument_message: ::core::clone::Clone::clone(&self.argument_message),
}
}
}Clone)]
1639pub struct RError {
1640 message: String,
1641 class: Vec<String>,
1642 data: ConditionData,
1643 call: ConditionCall,
1644 argument_message: Option<String>,
1645}
16461647impl RError {
1648/// A classless error with `message`.
1649pub fn new(message: impl Into<String>) -> Self {
1650RError {
1651 message: message.into(),
1652 class: Vec::new(),
1653 data: Vec::new(),
1654 call: ConditionCall::Inherit,
1655 argument_message: None,
1656 }
1657 }
16581659/// Raise the condition without a call: `conditionCall(e)` is `NULL` and
1660 /// the printed condition has no `In f(...) :` prefix, like R's
1661 /// `stop(..., call. = FALSE)`. For an error or warning about the data, not
1662 /// the call. Applies to `Result` returns and the `defer_*` functions; see
1663 /// [`ConditionCall`].
1664pub fn without_call(mut self) -> Self {
1665self.call = ConditionCall::None;
1666self1667 }
16681669/// Append user classes (most specific first). Accepts one string or a
1670 /// vector, see [`ConditionClass`].
1671pub fn class(mut self, class: impl ConditionClass) -> Self {
1672self.class.extend(class.into_condition_class());
1673self1674 }
16751676/// Attach a structured field readable as `e$<name>`. Any value with an
1677 /// [`RValue`](crate::RValue) `From` impl works. The name must not be
1678 /// `message`, `call` or `kind`; see [`RESERVED_CONDITION_FIELDS`].
1679pub fn data(mut self, name: impl Into<String>, value: impl Into<crate::RValue>) -> Self {
1680self.data.push((name.into(), value.into()));
1681self1682 }
16831684/// The whole message of the argument error when this error refuses an
1685 /// argument, used as given instead of `'<p>' must be <expected>:
1686 /// <message>`; see [`RConditionData::argument_message`]. `message` stays
1687 /// the message everywhere else.
1688 ///
1689 /// ```ignore
1690 /// fn model_object(x: SEXP) -> Result<(), RError> {
1691 /// if x.inherits_class(c"data.frame") {
1692 /// return Err(RError::new("got a data frame")
1693 /// .argument_message("use model_from_df() for a data frame"));
1694 /// }
1695 /// Ok(())
1696 /// }
1697 /// ```
1698pub fn argument_message(mut self, message: impl Into<String>) -> Self {
1699self.argument_message = Some(message.into());
1700self1701 }
17021703/// The message.
1704pub fn message_str(&self) -> &str {
1705&self.message
1706 }
17071708/// The whole argument-error message, if one was given
1709 /// ([`RError::argument_message`]).
1710pub fn argument_message_str(&self) -> Option<&str> {
1711self.argument_message.as_deref()
1712 }
17131714/// The user classes, most specific first.
1715pub fn classes(&self) -> &[String] {
1716&self.class
1717 }
17181719/// The structured fields as attached (unprefixed).
1720pub fn fields(&self) -> &ConditionData {
1721&self.data
1722 }
1723}
17241725impl<E: std::error::Error> From<E> for RError {
1726/// Message = the error's `Display` plus its `source()` chain, one
1727 /// `caused by:` line per link (same rendering as [`AsRError`]).
1728fn from(err: E) -> Self {
1729let mut message = err.to_string();
1730let mut current: &dyn std::error::Error = &err;
1731while let Some(source) = current.source() {
1732 message.push_str("\n caused by: ");
1733 message.push_str(&source.to_string());
1734 current = source;
1735 }
1736RError::new(message)
1737 }
1738}
17391740impl std::fmt::Displayfor RError {
1741fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1742f.write_str(&self.message)
1743 }
1744}
17451746impl RConditionDatafor RError {
1747fn message(&self) -> String {
1748self.message.clone()
1749 }
1750fn class(&self) -> Vec<String> {
1751self.class.clone()
1752 }
1753fn data(&self) -> Option<ConditionData> {
1754 (!self.data.is_empty()).then(|| self.data.clone())
1755 }
1756fn call(&self) -> ConditionCall {
1757self.call
1758 }
1759fn argument_message(&self) -> Option<String> {
1760self.argument_message.clone()
1761 }
1762}
17631764/// Any classed error (an [`RError`], a `#[derive(RCondition)]` type)
1765/// converts into a [`SexpError::Condition`](crate::from_r::SexpError::Condition)
1766/// with its message, classes, fields and call choice, so a newtype's
1767/// [`check_sexp`](crate::TryFromSexpElement::check_sexp) can refuse with it
1768/// and a conversion whose error converts into `SexpError` keeps its classes.
1769impl<E: RConditionData> From<E> for crate::from_r::SexpError {
1770fn from(e: E) -> Self {
1771crate::from_r::SexpError::Condition(RError {
1772 message: e.message(),
1773 class: e.class(),
1774 data: e.data().unwrap_or_default(),
1775 call: e.call(),
1776 argument_message: e.argument_message(),
1777 })
1778 }
1779}
17801781/// An argument error as a value: the parameter's R name and the message.
1782/// [`ArgError::raise`] raises it as the generated wrapper's own argument
1783/// error, through [`crate::arg_error!`].
1784///
1785/// [`crate::match_arg_param`] returns it for a choice argument the body
1786/// matches itself. Raise it where the argument is bad, or handle it:
1787///
1788/// ```ignore
1789/// use miniextendr_api::{SEXP, match_arg_param, miniextendr};
1790///
1791/// #[miniextendr]
1792/// pub fn prepare(data: SEXP, mode: SEXP) -> String {
1793/// if is_data_frame(data) {
1794/// let mode: Mode = match_arg_param(mode, "mode").unwrap_or_else(|e| e.raise());
1795/// // ...
1796/// }
1797/// // ...
1798/// }
1799/// ```
1800///
1801/// It does not implement [`RConditionData`]: returned as a body's `Err` it
1802/// would take the `Result` path (`kind = "result_err"`), not the argument
1803/// error's.
1804#[derive(#[automatically_derived]
impl ::core::fmt::Debug for ArgError {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field2_finish(f, "ArgError",
"param", &self.param, "message", &&self.message)
}
}Debug, #[automatically_derived]
impl ::core::clone::Clone for ArgError {
#[inline]
fn clone(&self) -> Self {
ArgError {
param: ::core::clone::Clone::clone(&self.param),
message: ::core::clone::Clone::clone(&self.message),
}
}
}Clone, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for ArgError { }
#[automatically_derived]
impl ::core::cmp::PartialEq for ArgError {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.param == other.param && self.message == other.message
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for ArgError {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<String>;
}
}Eq)]
1805pub struct ArgError {
1806 param: String,
1807 message: String,
1808}
18091810impl ArgError {
1811/// The argument error of parameter `param` (its R name) with `message`,
1812 /// used as given.
1813pub fn new(param: impl Into<String>, message: impl Into<String>) -> Self {
1814ArgError {
1815 param: param.into(),
1816 message: message.into(),
1817 }
1818 }
18191820/// The parameter's R name (`e$param`).
1821pub fn param(&self) -> &str {
1822&self.param
1823 }
18241825/// The message (`conditionMessage(e)`).
1826pub fn message(&self) -> &str {
1827&self.message
1828 }
18291830/// Raise it: `arg_error!(param = <param>, "{}", <message>)`. The
1831 /// condition is the one the generated wrapper raises for that parameter;
1832 /// see [`crate::arg_error!`].
1833pub fn raise(self) -> ! {
1834 std::panic::panic_any(RCondition::arg_error(
1835self.param,
1836self.message,
1837 ConditionCall::Inherit,
1838 ))
1839 }
1840}
18411842impl std::fmt::Displayfor ArgError {
1843fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1844f.write_str(&self.message)
1845 }
1846}
18471848impl std::error::Errorfor ArgError {}
18491850/// The parts of a condition the generated `Err` arms hand to
1851/// [`crate::error_value::result_err_condition_value`] /
1852/// [`crate::error_value::conversion_condition_value`], and that
1853/// [`RCondition::into_parts`] splits a payload into: message, user classes,
1854/// `data` fields and the [`ConditionCall`] choice.
1855#[doc(hidden)]
1856#[derive(#[automatically_derived]
impl ::core::fmt::Debug for ErrParts {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field4_finish(f, "ErrParts",
"message", &self.message, "class", &self.class, "data",
&self.data, "call", &&self.call)
}
}Debug)]
1857pub struct ErrParts {
1858pub message: String,
1859pub class: Vec<String>,
1860pub data: Option<ConditionData>,
1861pub call: ConditionCall,
1862}
18631864/// Autoref-specialisation probe, first stage, preferred arm: `E: RConditionData`.
1865///
1866/// Under the `serde` feature a declared-shape stage runs before this one: a
1867/// shape that `serde_error_shape!` declared for `E` wins over the impl (see
1868/// `ErrShapeDeclared`).
1869///
1870/// The generated code calls `(&e).__mx_classed_parts()` with both first-stage
1871/// traits in scope. Method resolution tries the receiver type `&E` by value
1872/// first, which only this impl (on `E`, taking `&self`) satisfies; the
1873/// [`ErrPartsUnclassed`] arm on `&E` needs one more auto-ref and is only
1874/// reached when `E` does not implement [`RConditionData`]. It yields `None`,
1875/// and the second stage ([`ErrPartsSerde`] / [`ErrPartsDebug`]) decides.
1876///
1877/// Two two-way probes rather than one three-way probe: the `Serialize` and
1878/// `Debug` bounds both hold for `&E` whenever they hold for `E`, so a single
1879/// method name with three impls at increasing reference depth is ambiguous at
1880/// the first step. Each stage has exactly one impl on `E` and one on `&E`.
1881#[doc(hidden)]
1882pub trait ErrPartsClassed {
1883fn __mx_classed_parts(&self) -> Option<ErrParts>;
1884}
18851886impl<E: RConditionData> ErrPartsClassedfor E {
1887#[track_caller]
1888fn __mx_classed_parts(&self) -> Option<ErrParts> {
1889Some(ErrParts {
1890 message: self.message(),
1891 class: self.class(),
1892 data: check_condition_data(self.data()),
1893 call: self.call(),
1894 })
1895 }
1896}
18971898/// First stage, fallback arm: no [`RConditionData`] impl.
1899#[doc(hidden)]
1900pub trait ErrPartsUnclassed {
1901fn __mx_classed_parts(&self) -> Option<ErrParts>;
1902}
19031904impl<E> ErrPartsUnclassedfor &E {
1905fn __mx_classed_parts(&self) -> Option<ErrParts> {
1906None1907 }
1908}
19091910/// Second stage, preferred arm: `E: Serialize + Display` under the `serde`
1911/// feature, classed via [`serde_err_parts`] with the defaults (`kind` tag,
1912/// the `<crate>_error` prefix the macro passes in, no shapes).
1913/// `#[miniextendr(serde_error(..))]` bypasses the probe with explicit options.
1914///
1915/// The trait exists without the feature so the generated `use` resolves; it
1916/// then has no impl and every type takes the [`ErrPartsDebug`] arm.
1917#[doc(hidden)]
1918pub trait ErrPartsSerde {
1919fn __mx_fallback_parts(&self, prefix: &str) -> ErrParts;
1920}
19211922#[cfg(feature = "serde")]
1923impl<E: ::serde::Serialize + std::fmt::Display> ErrPartsSerde for E {
1924#[track_caller]
1925fn __mx_fallback_parts(&self, prefix: &str) -> ErrParts {
1926 serde_err_parts(self, prefix, &[])
1927 }
1928}
19291930/// Second stage, fallback arm: any `E: Debug` (the historical
1931/// `format!("{e:?}")` rendering, no class, no data).
1932#[doc(hidden)]
1933pub trait ErrPartsDebug {
1934fn __mx_fallback_parts(&self, prefix: &str) -> ErrParts;
1935}
19361937impl<E: std::fmt::Debug> ErrPartsDebugfor &E {
1938fn __mx_fallback_parts(&self, _prefix: &str) -> ErrParts {
1939ErrParts {
1940 message: ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0:?}", self))
})format!("{self:?}"),
1941 class: Vec::new(),
1942 data: None,
1943 call: ConditionCall::Inherit,
1944 }
1945 }
1946}
19471948/// Internal: the `Err(e)` probe used by generated wrappers. Not public API.
1949///
1950/// `$prefix` is the family class for the serde arms (`<crate>_error`, computed
1951/// by the macro from `CARGO_CRATE_NAME` at expansion time). `$marker` is the
1952/// calling crate's marker type (`crate::__MiniextendrInitMarker`), under which
1953/// [`serde_error_shape!`](crate::serde_error_shape) declarations are looked up
1954/// first, under the `serde` feature: a declared shape wins over the stages
1955/// below (the `ErrShapeDeclared` probe).
1956#[doc(hidden)]
1957#[macro_export]
1958macro_rules!__mx_result_err_parts {
1959 ($e:expr, $prefix:expr, $marker:ty) => {{
1960#[allow(unused_imports)]
1961use $crate::condition::{
1962 ErrPartsClassed as _, ErrPartsDebug as _, ErrPartsSerde as _, ErrPartsUnclassed as _,
1963 };
1964match &$e {
1965 __mx_e => match $crate::__mx_declared_err_parts!(__mx_e, $prefix, $marker) {
1966 ::core::option::Option::Some(parts) => parts,
1967 ::core::option::Option::None => match __mx_e.__mx_classed_parts() {
1968 ::core::option::Option::Some(parts) => parts,
1969 ::core::option::Option::None => __mx_e.__mx_fallback_parts($prefix),
1970 },
1971 },
1972 }
1973 }};
1974}
19751976// endregion
19771978// region: Classed argument-conversion errors
19791980/// Conversion probe, preferred arm: `E: RConditionData`.
1981///
1982/// When an argument fails its `TryFromSexp` conversion, the generated wrapper
1983/// calls `(&e).__mx_conversion_parts(expected_known)` with the three
1984/// conversion-probe traits in scope (see [`crate::__mx_conversion_err_parts!`]).
1985/// This impl, on `E` taking `&self`, matches the receiver `&E` by value, so a
1986/// `TryFromSexp::Error` that implements [`RConditionData`] contributes its
1987/// class vector, message and data. [`ConversionErrBuiltin`] matches at the
1988/// same step for the built-in conversion errors, which never implement
1989/// [`RConditionData`], so the two never compete. [`ConversionErrDisplay`]
1990/// needs one more auto-ref and is only reached otherwise.
1991///
1992/// Unlike the `Result` probe ([`crate::__mx_result_err_parts!`]) there is no
1993/// serde stage and the fallback is `Display`, not `Debug`: the generated code
1994/// has always rendered conversion errors with `Display`, which every built-in
1995/// error type (`SexpError`, `MatchArgError`, ...) implements.
1996#[doc(hidden)]
1997pub trait ConversionErrClassed {
1998fn __mx_conversion_parts(&self, expected_known: bool) -> ErrParts;
1999}
20002001impl<E: RConditionData> ConversionErrClassedfor E {
2002#[track_caller]
2003fn __mx_conversion_parts(&self, _expected_known: bool) -> ErrParts {
2004ErrParts {
2005 message: self.message(),
2006 class: self.class(),
2007 data: check_condition_data(self.data()),
2008 call: self.call(),
2009 }
2010 }
2011}
20122013/// Conversion probe, built-in arm: the framework's own conversion errors
2014/// ([`SexpError`](crate::from_r::SexpError) and the
2015/// [`SexpTypeError`](crate::from_r::SexpTypeError) /
2016/// [`SexpClassError`](crate::from_r::SexpClassError) /
2017/// [`SexpLengthError`](crate::from_r::SexpLengthError) /
2018/// [`SexpNaError`](crate::from_r::SexpNaError) it wraps, and
2019/// [`MatchArgError`](crate::match_arg::MatchArgError)), reworded for R
2020/// users (#1591).
2021///
2022/// Their `Display` text is written for the package author: SEXPTYPE names
2023/// (`expected INTSXP, got STRSXP`) and a variant prefix (`invalid value: ...`).
2024/// The condition message says instead what the argument should have been,
2025/// with the reason from `SexpError::r_reason` (`got character`,
2026/// `got length 2`, `NA is not allowed`, the invalid value's own text, ...)
2027/// or `MatchArgError::r_reason` (`got "zzz"`).
2028///
2029/// `expected_known` is set by the macro when it knows the argument's R-facing
2030/// expectation and puts it in the message prefix (`'x' must be a single
2031/// integer`); the reason then does not repeat it. The type names are those of
2032/// [`sexptype_name`](crate::typed_list::sexptype_name) (`numeric` for a
2033/// double, `list`, `NULL`, ...).
2034#[doc(hidden)]
2035pub trait ConversionErrBuiltin {
2036fn __mx_conversion_parts(&self, expected_known: bool) -> ErrParts;
2037}
20382039/// The R-facing reason for a built-in conversion error, see
2040/// [`ConversionErrBuiltin`].
2041fn builtin_reason_parts(message: String) -> ErrParts {
2042ErrParts {
2043message,
2044 class: Vec::new(),
2045 data: None,
2046 call: ConditionCall::Inherit,
2047 }
2048}
20492050impl ConversionErrBuiltinfor crate::from_r::SexpTypeError {
2051fn __mx_conversion_parts(&self, expected_known: bool) -> ErrParts {
2052builtin_reason_parts(self.r_reason(expected_known))
2053 }
2054}
20552056impl ConversionErrBuiltinfor crate::from_r::SexpClassError {
2057fn __mx_conversion_parts(&self, expected_known: bool) -> ErrParts {
2058builtin_reason_parts(self.r_reason(expected_known))
2059 }
2060}
20612062impl ConversionErrBuiltinfor crate::from_r::SexpLengthError {
2063fn __mx_conversion_parts(&self, expected_known: bool) -> ErrParts {
2064builtin_reason_parts(self.r_reason(expected_known))
2065 }
2066}
20672068impl ConversionErrBuiltinfor crate::from_r::SexpNaError {
2069fn __mx_conversion_parts(&self, _expected_known: bool) -> ErrParts {
2070builtin_reason_parts("NA is not allowed".to_string())
2071 }
2072}
20732074/// A `SexpError` also carries the classes and fields of the check refusals
2075/// ([`SexpError::Condition`](crate::from_r::SexpError::Condition)) whose
2076/// reasons its message reports, directly or from an `Either` arm.
2077impl ConversionErrBuiltinfor crate::from_r::SexpError {
2078fn __mx_conversion_parts(&self, expected_known: bool) -> ErrParts {
2079let mut parts = builtin_reason_parts(self.r_reason(expected_known));
2080let mut data = ConditionData::new();
2081for refusal in self.reported_conditions() {
2082for class in refusal.classes() {
2083if !parts.class.contains(class) {
2084 parts.class.push(class.clone());
2085 }
2086 }
2087for (name, value) in refusal.fields() {
2088if !data.iter().any(|(seen, _)| seen == name) {
2089 data.push((name.clone(), value.clone()));
2090 }
2091 }
2092 }
2093parts.data = check_condition_data((!data.is_empty()).then_some(data));
2094parts2095 }
2096}
20972098impl ConversionErrBuiltinfor crate::match_arg::MatchArgError {
2099fn __mx_conversion_parts(&self, expected_known: bool) -> ErrParts {
2100builtin_reason_parts(self.r_reason(expected_known))
2101 }
2102}
21032104/// Conversion probe, fallback arm: any `E: Display`. The message is the
2105/// error's `Display` text, with no class and no data, so the condition keeps
2106/// the plain `rust_error` class vector.
2107#[doc(hidden)]
2108pub trait ConversionErrDisplay {
2109fn __mx_conversion_parts(&self, expected_known: bool) -> ErrParts;
2110}
21112112impl<E: std::fmt::Display> ConversionErrDisplayfor &E {
2113fn __mx_conversion_parts(&self, _expected_known: bool) -> ErrParts {
2114ErrParts {
2115 message: self.to_string(),
2116 class: Vec::new(),
2117 data: None,
2118 call: ConditionCall::Inherit,
2119 }
2120 }
2121}
21222123/// Internal: the argument-conversion `Err(e)` probe used by generated
2124/// wrappers. Not public API. Yields the error's own [`ErrParts`];
2125/// [`crate::error_value::conversion_condition_value`] adds the parameter
2126/// context.
2127///
2128/// The second argument is `expected_known` (see [`ConversionErrBuiltin`]):
2129/// `true` when the message prefix already states what the argument should
2130/// be. It defaults to `false`.
2131#[doc(hidden)]
2132#[macro_export]
2133macro_rules!__mx_conversion_err_parts {
2134 ($e:expr) => {
2135$crate::__mx_conversion_err_parts!($e, false)
2136 };
2137 ($e:expr, $expected_known:expr) => {{
2138#[allow(unused_imports)]
2139use $crate::condition::{
2140 ConversionErrBuiltin as _, ConversionErrClassed as _, ConversionErrDisplay as _,
2141 };
2142match &$e {
2143 __mx_e => __mx_e.__mx_conversion_parts($expected_known),
2144 }
2145 }};
2146}
21472148/// Expectation probe, built-in arm: an error that knows what the value should
2149/// have been. A `match_arg` choice error (`MatchArgError`, or a `SexpError`
2150/// carrying one, as `#[derive(MatchArg)]`'s `TryFromSexp` returns) names its
2151/// choices: `one of "fast", "slow"`; a class error names the class
2152/// (`a data frame`); an `Either` whose arms both refused the kind of value
2153/// names both (`one of "fast", "slow", or numeric`).
2154#[doc(hidden)]
2155pub trait ConversionExpectBuiltin {
2156fn __mx_conversion_expectation(&self) -> Option<String>;
2157}
21582159impl ConversionExpectBuiltinfor crate::from_r::SexpError {
2160fn __mx_conversion_expectation(&self) -> Option<String> {
2161self.r_expectation()
2162 }
2163}
21642165impl ConversionExpectBuiltinfor crate::match_arg::MatchArgError {
2166fn __mx_conversion_expectation(&self) -> Option<String> {
2167Some(self.expectation())
2168 }
2169}
21702171/// Expectation probe, fallback arm: any other error type says nothing about
2172/// what the value should have been.
2173#[doc(hidden)]
2174pub trait ConversionExpectNone {
2175fn __mx_conversion_expectation(&self) -> Option<String>;
2176}
21772178impl<E> ConversionExpectNonefor &E {
2179fn __mx_conversion_expectation(&self) -> Option<String> {
2180None2181 }
2182}
21832184/// Internal: what a failed argument conversion's error says the value should
2185/// have been, for a parameter whose Rust type gives the macro no R-facing
2186/// expectation (#1594). `Some("one of \"fast\", \"slow\"")` for a `match_arg`
2187/// choice error, `Some("a data frame")` for a class error, the joined
2188/// expectation of an `Either` whose arms both refused the kind of value,
2189/// `None` otherwise. Not public API.
2190#[doc(hidden)]
2191#[macro_export]
2192macro_rules!__mx_conversion_expectation {
2193 ($e:expr) => {{
2194#[allow(unused_imports)]
2195use $crate::condition::{ConversionExpectBuiltin as _, ConversionExpectNone as _};
2196match &$e {
2197 __mx_e => __mx_e.__mx_conversion_expectation(),
2198 }
2199 }};
2200}
22012202/// Whole-message probe, built-in arm: a `match_arg` choice error, as a
2203/// [`MatchArgError`](crate::match_arg::MatchArgError) or at the top of a
2204/// `SexpError` (`#[derive(MatchArg)]`'s `TryFromSexp` returns one), words the
2205/// whole argument error the way the wrapper's R-side `match_arg` check
2206/// (`.miniextendr_match_arg`) does, from the same
2207/// [`MatchArgError::arg_error`](crate::match_arg::MatchArgError::arg_error)
2208/// (#1767): `'mode' should be one of "fast", "slow"`, `'mode' must be of
2209/// length 1`, `'mode' must be NULL or a character vector`. So a `match_arg`
2210/// type converted without `#[miniextendr(match_arg)]` and one checked by the
2211/// wrapper raise the same message.
2212///
2213/// A choice error inside an `Either` is not one: the `Either` names both of
2214/// its arms (`'x' must be one of "oral", "bolus", or a single double: got
2215/// logical`).
2216#[doc(hidden)]
2217pub trait ConversionArgMessageBuiltin {
2218fn __mx_conversion_arg_message(&self, name: &str) -> Option<String>;
2219}
22202221impl ConversionArgMessageBuiltinfor crate::match_arg::MatchArgError {
2222fn __mx_conversion_arg_message(&self, name: &str) -> Option<String> {
2223Some(self.arg_error(name).message().to_string())
2224 }
2225}
22262227impl ConversionArgMessageBuiltinfor crate::from_r::SexpError {
2228fn __mx_conversion_arg_message(&self, name: &str) -> Option<String> {
2229match self {
2230crate::from_r::SexpError::MatchArg(e) => Some(e.arg_error(name).message().to_string()),
2231// A type's check refusal (`check_sexp` under `Option<T>` /
2232 // `Vec<T>`, or a conversion whose error converted into
2233 // `SexpError`) with its own whole message (#1833).
2234crate::from_r::SexpError::Condition(e) => e.argument_message_str().map(str::to_string),
2235_ => None,
2236 }
2237 }
2238}
22392240/// Whole-message probe, classed arm: any [`RConditionData`] answers with its
2241/// [`argument_message`](RConditionData::argument_message), so a type's own
2242/// conversion error can word the whole argument error (#1833). Same autoref
2243/// level as [`ConversionArgMessageBuiltin`], like [`ConversionErrClassed`]:
2244/// no built-in error type implements [`RConditionData`].
2245#[doc(hidden)]
2246pub trait ConversionArgMessageClassed {
2247fn __mx_conversion_arg_message(&self, name: &str) -> Option<String>;
2248}
22492250impl<E: RConditionData> ConversionArgMessageClassedfor E {
2251fn __mx_conversion_arg_message(&self, _name: &str) -> Option<String> {
2252self.argument_message()
2253 }
2254}
22552256/// Whole-message probe, fallback arm: any other error leaves the message to
2257/// the prefix and the error's reason.
2258#[doc(hidden)]
2259pub trait ConversionArgMessageNone {
2260fn __mx_conversion_arg_message(&self, name: &str) -> Option<String>;
2261}
22622263impl<E> ConversionArgMessageNonefor &E {
2264fn __mx_conversion_arg_message(&self, _name: &str) -> Option<String> {
2265None2266 }
2267}
22682269/// Internal: the whole message of a failed argument conversion whose error
2270/// words it itself (a `match_arg` choice error, see
2271/// [`ConversionArgMessageBuiltin`], or an error with an
2272/// [`argument_message`](RConditionData::argument_message), see
2273/// [`ConversionArgMessageClassed`]), for the argument `$name`; `None`
2274/// otherwise. [`crate::error_value::conversion_condition_value`] then uses it
2275/// in place of `<prefix>: <reason>`. Not public API.
2276#[doc(hidden)]
2277#[macro_export]
2278macro_rules!__mx_conversion_arg_message {
2279 ($e:expr, $name:expr) => {{
2280#[allow(unused_imports)]
2281use $crate::condition::{
2282 ConversionArgMessageBuiltin as _, ConversionArgMessageClassed as _,
2283 ConversionArgMessageNone as _,
2284 };
2285match &$e {
2286 __mx_e => __mx_e.__mx_conversion_arg_message($name),
2287 }
2288 }};
2289}
22902291/// The message prefix of a conversion failure whose expectation comes from
2292/// the error at run time (see [`crate::__mx_conversion_expectation!`]):
2293/// `'<p>' must be <expected>` (`'<p>' must be NULL or <expected>` for an
2294/// `Option<_>` parameter, `nullable`), else `invalid '<p>' argument`.
2295#[doc(hidden)]
2296pub fn conversion_prefix(param: &str, nullable: bool, expected: Option<&str>) -> String {
2297match expected {
2298Some(expected) if nullable => ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("\'{0}\' must be NULL or {1}",
param, expected))
})format!("'{param}' must be NULL or {expected}"),
2299Some(expected) => ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("\'{0}\' must be {1}", param,
expected))
})format!("'{param}' must be {expected}"),
2300None => ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("invalid \'{0}\' argument", param))
})format!("invalid '{param}' argument"),
2301 }
2302}
23032304/// The name of the structured field that carries the failing parameter's R
2305/// name on every argument-conversion condition (`e$param`).
2306const CONVERSION_PARAM_FIELD: &str = "param";
23072308/// The name of the structured field that carries the Rust type an argument
2309/// failed to convert to (`e$rust_type`, #1591).
2310const CONVERSION_RUST_TYPE_FIELD: &str = "rust_type";
23112312/// Combine an argument-conversion error's own parts with the wrapper's
2313/// parameter context.
2314///
2315/// - The message becomes `<prefix>: <error message>`. For an argument the
2316/// macro writes the prefix in R terms (#1591): `'<p>' must be <expected>`
2317/// when it knows what the argument should be (`'x' must be a single
2318/// integer`, `'dv' must be numeric`), `invalid '<p>' argument` otherwise.
2319/// A sidecar setter passes its own prefix. An error that words the whole
2320/// argument error itself, a `match_arg` choice error (#1767) or one with an
2321/// [`argument_message`](RConditionData::argument_message) (#1833), gives
2322/// `arg_message` ([`crate::__mx_conversion_arg_message!`]), which is the
2323/// message instead.
2324/// - The class vector is the error's own (empty for the built-in and
2325/// `Display` arms), followed by `crate_class`, the crate's
2326/// `conversion_error_class` from `[package.metadata.miniextendr]`, minus
2327/// any class the error already names. The R helper appends the
2328/// `rust_error` layering after both.
2329/// - `param` (the parameter's R name) is added as the first data field,
2330/// `e$param`, and `rust_type` (the Rust type as written in the signature,
2331/// when given) after it, `e$rust_type`. Neither is added when the error's
2332/// own data already has a field of that name: the type author's value is
2333/// kept, since the framework never overwrites a field the type author
2334/// chose, and R's `e$name` reads only the first of two same-named fields.
2335/// - The call is always the wrapper's ([`ConditionCall::Inherit`]), whatever
2336/// the error type's [`RConditionData::call`] says: the R-side checks
2337/// (`.miniextendr_arg_error`) raise the same condition with the call
2338/// (#1591), and an argument error must not differ by which side caught it.
2339#[doc(hidden)]
2340pub fn conversion_err_parts(
2341 prefix: &str,
2342 param: &str,
2343 rust_type: Option<&str>,
2344 crate_class: &[&str],
2345 parts: ErrParts,
2346 arg_message: Option<String>,
2347) -> ErrParts {
2348let ErrParts {
2349 message,
2350mut class,
2351 data,
2352 call: _,
2353 } = parts;
2354let message = arg_message.unwrap_or_else(|| ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}: {1}", prefix, message))
})format!("{prefix}: {message}"));
2355for extra in crate_class {
2356if !class.iter().any(|c| c == extra) {
2357 class.push((*extra).to_string());
2358 }
2359 }
2360let mut fields = data.unwrap_or_default();
2361let has_field =
2362 |fields: &ConditionData, field: &str| fields.iter().any(|(name, _)| name == field);
2363let mut at = 0;
2364if !has_field(&fields, CONVERSION_PARAM_FIELD) {
2365fields.insert(0, (CONVERSION_PARAM_FIELD.to_string(), param.into()));
2366at = 1;
2367 }
2368if let Some(rust_type) = rust_type2369 && !has_field(&fields, CONVERSION_RUST_TYPE_FIELD)
2370 {
2371fields.insert(
2372at,
2373 (CONVERSION_RUST_TYPE_FIELD.to_string(), rust_type.into()),
2374 );
2375 }
2376ErrParts {
2377message,
2378class,
2379 data: Some(fields),
2380 call: ConditionCall::Inherit,
2381 }
2382}
23832384/// The parts of an argument check that failed in Rust after the conversion:
2385/// `#[miniextendr(no_na)]` on a reading marker (`AsNumeric*`,
2386/// `AsCharacter*`) whose converted value holds an `NA` that R's `anyNA()`
2387/// did not see (the hidden `TryFromSexp::__mx_has_na` says so), or on an
2388/// `Either`, which has no R guard, an `NA` the arm taken refuses
2389/// (`TryFromSexp::__mx_input_has_na`).
2390///
2391/// The shape is the R guard's (`.miniextendr_arg_error`): `message` as given
2392/// (the macro writes `'<p>' must not be NA` or the author's own message),
2393/// the classes `crate_class` (the crate's `conversion_error_class`), and
2394/// `param` as the only data field, `e$param`. No `rust_type`: the R guard
2395/// has none, and the two paths raise one condition.
2396#[doc(hidden)]
2397pub fn arg_check_parts(message: &str, param: &str, crate_class: &[&str]) -> ErrParts {
2398ErrParts {
2399 message: message.to_string(),
2400 class: crate_class.iter().map(|c| (*c).to_string()).collect(),
2401 data: Some(::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
[(CONVERSION_PARAM_FIELD.to_string(), param.into())]))vec![(CONVERSION_PARAM_FIELD.to_string(), param.into())]),
2402 call: ConditionCall::Inherit,
2403 }
2404}
24052406// endregion
24072408// region: SerdeErrorShape — `serde_error` options as a shared const (#2207)
24092410/// The options of `#[miniextendr(serde_error(..))]` as a value: declared once,
2411/// shared by every function that returns one error type.
2412///
2413/// ```
2414/// use miniextendr_api::condition::SerdeErrorShape;
2415///
2416/// pub const ENGINE: SerdeErrorShape = SerdeErrorShape::new()
2417/// .prefix("engine")
2418/// .skip(&["message"])
2419/// .as_character(&["rule", "column"])
2420/// .as_integer(&["n"]);
2421/// ```
2422///
2423/// Declare it for the error type with
2424/// [`serde_error_shape!`](crate::serde_error_shape), and every
2425/// `#[miniextendr]` function or method of the crate that returns that type
2426/// uses it, without naming it:
2427///
2428/// ```ignore
2429/// miniextendr_api::serde_error_shape!(EngineError = ENGINE);
2430///
2431/// #[miniextendr]
2432/// pub fn check(x: f64) -> Result<f64, EngineError> { /* ... */ }
2433/// ```
2434///
2435/// A function can override it for itself: with options of its own, or with
2436/// another shape named by path, which applies after the declared one:
2437///
2438/// ```ignore
2439/// #[miniextendr(serde_error(as_double("n")))]
2440/// pub fn rate(x: f64) -> Result<f64, EngineError> { /* ... */ }
2441///
2442/// #[miniextendr(serde_error(shape = STRICT, as_double("n")))]
2443/// pub fn strict_rate(x: f64) -> Result<f64, EngineError> { /* ... */ }
2444/// ```
2445///
2446/// The path resolves where the function is defined (`STRICT`,
2447/// `crate::errors::STRICT`, or `Self::SHAPE` on a method for an associated
2448/// const of its type), so a misspelt one is a compile error. The options act
2449/// on the serde-classed `Err` arm, which needs the `serde` feature.
2450///
2451/// # Options
2452///
2453/// One method per `serde_error` option. Fields are named as the error
2454/// serializes them, before any `rename`:
2455///
2456/// - [`tag`](Self::tag): the internally tagged discriminator field consumed
2457/// as the variant; `"kind"` when unset.
2458/// - [`prefix`](Self::prefix): the family class; `<crate>_error`, from the
2459/// function's crate, when unset.
2460/// - [`skip`](Self::skip): fields to drop.
2461/// - [`rename`](Self::rename): `(from, to)` pairs; field `from` reaches R as
2462/// `e$to`.
2463/// - [`as_character`](Self::as_character), [`as_integer`](Self::as_integer),
2464/// [`as_double`](Self::as_double), [`as_logical`](Self::as_logical): the
2465/// field's usual serialization as one vector of that R type, as the
2466/// `miniextendr_api::serde::as_character` & co. field helpers give it from
2467/// the error type's own `#[serde(serialize_with = ..)]`: `character(0)` for
2468/// an empty sequence, `NA` for a `None` element. A value that doesn't fit
2469/// (a string under `as_integer`) raises an R error naming the field and the
2470/// option.
2471///
2472/// A field that a variant doesn't carry is a no-op for that variant, so one
2473/// shape covers an enum whose variants differ.
2474///
2475/// # Precedence
2476///
2477/// Shapes apply in order: the one declared for the error type, then
2478/// `serde_error(shape = ..)`, then the function's own options; for a field
2479/// set by more than one, the later wins. A later `skip` drops a field an
2480/// earlier shape types or renames, and a later `as_*` or `rename` keeps a
2481/// field an earlier shape skips. A type and a new name compose: the
2482/// function's `rename(n = "count")` keeps the shape's `as_integer(&["n"])`.
2483/// A later `tag` or `prefix` replaces an earlier one.
2484///
2485/// # Contradictions don't compile
2486///
2487/// Each method checks its fields against the options already set and panics
2488/// during const evaluation, so a `const` holding a contradictory shape is a
2489/// compile error (E0080) naming the field:
2490///
2491/// - a field in two `as_*` lists;
2492/// - a field skipped and also typed or renamed;
2493/// - a field listed twice, renamed twice, or two renames onto one name;
2494/// - a `rename` target that is one of the condition's own slots (`message`,
2495/// `call`, `kind`);
2496/// - an empty list or name, and an option set twice.
2497///
2498/// ```compile_fail,E0080
2499/// use miniextendr_api::condition::SerdeErrorShape;
2500///
2501/// const ENGINE: SerdeErrorShape = SerdeErrorShape::new()
2502/// .as_integer(&["n"])
2503/// .as_double(&["n"]);
2504/// ```
2505#[derive(#[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for SerdeErrorShape { }
#[automatically_derived]
impl ::core::clone::Clone for SerdeErrorShape {
#[inline]
fn clone(&self) -> Self {
let _: ::core::clone::AssertParamIsClone<Option<&'static str>>;
let _: ::core::clone::AssertParamIsClone<Option<&'static str>>;
let _: ::core::clone::AssertParamIsClone<&'static [&'static str]>;
let _:
::core::clone::AssertParamIsClone<&'static [(&'static str,
&'static str)]>;
let _:
::core::clone::AssertParamIsClone<[&'static [&'static str]; 4]>;
*self
}
}Clone, #[automatically_derived]
impl ::core::marker::Copy for SerdeErrorShape { }Copy, #[automatically_derived]
impl ::core::fmt::Debug for SerdeErrorShape {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field5_finish(f,
"SerdeErrorShape", "tag", &self.tag, "prefix", &self.prefix,
"skip", &self.skip, "rename", &self.rename, "types", &&self.types)
}
}Debug, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for SerdeErrorShape { }
#[automatically_derived]
impl ::core::cmp::PartialEq for SerdeErrorShape {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.tag == other.tag && self.prefix == other.prefix &&
self.skip == other.skip && self.rename == other.rename &&
self.types == other.types
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for SerdeErrorShape {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<Option<&'static str>>;
let _: ::core::cmp::AssertParamIsEq<Option<&'static str>>;
let _: ::core::cmp::AssertParamIsEq<&'static [&'static str]>;
let _:
::core::cmp::AssertParamIsEq<&'static [(&'static str,
&'static str)]>;
let _: ::core::cmp::AssertParamIsEq<[&'static [&'static str]; 4]>;
}
}Eq)]
2506pub struct SerdeErrorShape {
2507 tag: Option<&'static str>,
2508 prefix: Option<&'static str>,
2509 skip: &'static [&'static str],
2510 rename: &'static [(&'static str, &'static str)],
2511/// The `as_*` lists, in [`SHAPE_TYPE_OPTIONS`] order.
2512types: [&'static [&'static str]; 4],
2513}
25142515/// The `as_*` options, in the order of [`SerdeErrorShape`]'s type lists.
2516const SHAPE_TYPE_OPTIONS: [&str; 4] = ["as_logical", "as_integer", "as_double", "as_character"];
25172518impl SerdeErrorShape {
2519/// The discriminator field when no shape names one.
2520#[cfg(feature = "serde")]
2521const DEFAULT_TAG: &'static str = "kind";
25222523/// A shape that sets nothing.
2524pub const fn new() -> Self {
2525SerdeErrorShape {
2526 tag: None,
2527 prefix: None,
2528 skip: &[],
2529 rename: &[],
2530 types: [&[]; 4],
2531 }
2532 }
25332534/// The internally tagged discriminator field (`#[serde(tag = "...")]`),
2535 /// consumed as the variant.
2536#[track_caller]
2537pub const fn tag(mut self, tag: &'static str) -> Self {
2538self.tag = Some(scalar_option("tag", self.tag, tag));
2539self2540 }
25412542/// The family class: conditions get `c("<prefix>_<variant>", "<prefix>")`.
2543#[track_caller]
2544pub const fn prefix(mut self, prefix: &'static str) -> Self {
2545self.prefix = Some(scalar_option("prefix", self.prefix, prefix));
2546self2547 }
25482549/// Drop these payload fields.
2550#[track_caller]
2551pub const fn skip(mut self, fields: &'static [&'static str]) -> Self {
2552check_field_list("skip", self.skip.is_empty(), fields);
2553let mut i = 0;
2554while i < fields.len() {
2555let name = fields[i];
2556if let Some(option) = self.type_option(name) {
2557 conflict(name, option, "skip", "a skipped field has no R type");
2558 }
2559if self.renames(name) {
2560 conflict(name, "rename", "skip", "a skipped field has no name");
2561 }
2562 i += 1;
2563 }
2564self.skip = fields;
2565self2566 }
25672568/// Splice field `from` as `to`, for each `(from, to)` pair.
2569#[track_caller]
2570pub const fn rename(mut self, pairs: &'static [(&'static str, &'static str)]) -> Self {
2571if !self.rename.is_empty() {
2572set_twice("rename");
2573 }
2574if pairs.is_empty() {
2575fail(ShapeMessage::new().push("`rename` needs at least one pair"));
2576 }
2577let mut i = 0;
2578while i < pairs.len() {
2579let (from, to) = pairs[i];
2580if from.is_empty() || to.is_empty() {
2581 fail(ShapeMessage::new().push("`rename` has an empty name"));
2582 }
2583if is_reserved_condition_field(to) {
2584 fail(ShapeMessage::new().push("`rename` target `").push(to).push(
2585"` is reserved: `message`, `call` and `kind` are the condition's own slots",
2586 ));
2587 }
2588let mut j = 0;
2589while j < i {
2590if str_eq(pairs[j].0, from) {
2591 fail(
2592 ShapeMessage::new()
2593 .push("field `")
2594 .push(from)
2595 .push("` is renamed twice"),
2596 );
2597 }
2598if str_eq(pairs[j].1, to) {
2599 fail(
2600 ShapeMessage::new()
2601 .push("two `rename` pairs target `")
2602 .push(to)
2603 .push("`; R would read only the first"),
2604 );
2605 }
2606 j += 1;
2607 }
2608if const_contains(self.skip, from) {
2609 conflict(from, "skip", "rename", "a skipped field has no name");
2610 }
2611 i += 1;
2612 }
2613self.rename = pairs;
2614self2615 }
26162617/// Give these fields an R `logical` vector.
2618#[track_caller]
2619pub const fn as_logical(self, fields: &'static [&'static str]) -> Self {
2620self.with_type(0, fields)
2621 }
26222623/// Give these fields an R `integer` vector; a value outside R's integer
2624 /// range is an error, not a double.
2625#[track_caller]
2626pub const fn as_integer(self, fields: &'static [&'static str]) -> Self {
2627self.with_type(1, fields)
2628 }
26292630/// Give these fields an R `double` vector; integers widen.
2631#[track_caller]
2632pub const fn as_double(self, fields: &'static [&'static str]) -> Self {
2633self.with_type(2, fields)
2634 }
26352636/// Give these fields an R `character` vector.
2637#[track_caller]
2638pub const fn as_character(self, fields: &'static [&'static str]) -> Self {
2639self.with_type(3, fields)
2640 }
26412642#[track_caller]
2643const fn with_type(mut self, index: usize, fields: &'static [&'static str]) -> Self {
2644let option = SHAPE_TYPE_OPTIONS[index];
2645check_field_list(option, self.types[index].is_empty(), fields);
2646let mut i = 0;
2647while i < fields.len() {
2648let name = fields[i];
2649if const_contains(self.skip, name) {
2650 conflict(name, "skip", option, "a skipped field has no R type");
2651 }
2652if let Some(other) = self.type_option(name) {
2653 conflict(name, other, option, "a field has one R type");
2654 }
2655 i += 1;
2656 }
2657self.types[index] = fields;
2658self2659 }
26602661/// The `as_*` option that lists `name`.
2662const fn type_option(&self, name: &str) -> Option<&'static str> {
2663let mut i = 0;
2664while i < self.types.len() {
2665if const_contains(self.types[i], name) {
2666return Some(SHAPE_TYPE_OPTIONS[i]);
2667 }
2668 i += 1;
2669 }
2670None2671 }
26722673/// Whether a `rename` pair renames `name`.
2674const fn renames(&self, name: &str) -> bool {
2675let mut i = 0;
2676while i < self.rename.len() {
2677if str_eq(self.rename[i].0, name) {
2678return true;
2679 }
2680 i += 1;
2681 }
2682false
2683}
2684}
26852686impl Defaultfor SerdeErrorShape {
2687fn default() -> Self {
2688Self::new()
2689 }
2690}
26912692/// `tag` / `prefix`: set once, never empty.
2693#[track_caller]
2694const fn scalar_option(
2695 option: &str,
2696 current: Option<&'static str>,
2697 value: &'static str,
2698) -> &'static str {
2699if current.is_some() {
2700set_twice(option);
2701 }
2702if value.is_empty() {
2703fail(
2704ShapeMessage::new()
2705 .push("`")
2706 .push(option)
2707 .push("` must not be empty"),
2708 );
2709 }
2710value2711}
27122713/// A field list: set once, not empty, each name non-empty and listed once.
2714#[track_caller]
2715const fn check_field_list(option: &str, unset: bool, fields: &[&str]) {
2716if !unset {
2717set_twice(option);
2718 }
2719if fields.is_empty() {
2720fail(
2721ShapeMessage::new()
2722 .push("`")
2723 .push(option)
2724 .push("` needs at least one field"),
2725 );
2726 }
2727let mut i = 0;
2728while i < fields.len() {
2729if fields[i].is_empty() {
2730 fail(
2731 ShapeMessage::new()
2732 .push("`")
2733 .push(option)
2734 .push("` has an empty field name"),
2735 );
2736 }
2737let mut j = 0;
2738while j < i {
2739if str_eq(fields[j], fields[i]) {
2740 fail(
2741 ShapeMessage::new()
2742 .push("field `")
2743 .push(fields[i])
2744 .push("` is listed twice in `")
2745 .push(option)
2746 .push("`"),
2747 );
2748 }
2749 j += 1;
2750 }
2751 i += 1;
2752 }
2753}
27542755#[track_caller]
2756const fn set_twice(option: &str) -> ! {
2757fail(
2758ShapeMessage::new()
2759 .push("`")
2760 .push(option)
2761 .push("` is set twice; set each option once"),
2762 )
2763}
27642765#[track_caller]
2766const fn conflict(name: &str, first: &str, second: &str, why: &str) -> ! {
2767fail(
2768ShapeMessage::new()
2769 .push("field `")
2770 .push(name)
2771 .push("` is in both `")
2772 .push(first)
2773 .push("` and `")
2774 .push(second)
2775 .push("`; ")
2776 .push(why),
2777 )
2778}
27792780const fn const_contains(list: &[&str], name: &str) -> bool {
2781let mut i = 0;
2782while i < list.len() {
2783if str_eq(list[i], name) {
2784return true;
2785 }
2786 i += 1;
2787 }
2788false
2789}
27902791/// A const-evaluation panic message. `const fn` can't `format!`, so the parts
2792/// are copied into a fixed buffer.
2793struct ShapeMessage {
2794 buf: [u8; 256],
2795 len: usize,
2796}
27972798impl ShapeMessage {
2799const fn new() -> Self {
2800ShapeMessage {
2801 buf: [0; 256],
2802 len: 0,
2803 }
2804 .push("SerdeErrorShape: ")
2805 }
28062807const fn push(mut self, text: &str) -> Self {
2808let bytes = text.as_bytes();
2809let mut i = 0;
2810while i < bytes.len() && self.len < self.buf.len() {
2811self.buf[self.len] = bytes[i];
2812self.len += 1;
2813 i += 1;
2814 }
2815self2816 }
2817}
28182819/// Panic with `message`. `#[track_caller]` here and on every caller puts the
2820/// compile error at the user's `const`, not in this crate.
2821#[track_caller]
2822const fn fail(message: ShapeMessage) -> ! {
2823let (text, _) = message.buf.split_at(message.len);
2824match core::str::from_utf8(text) {
2825Ok(text) => { ::core::panicking::panic_display(&text); }panic!("{}", text),
2826// The buffer ended inside a long field name's character.
2827Err(_) => {
2828{
::core::panicking::panic_fmt(format_args!("SerdeErrorShape: contradictory options; a field name is too long to quote"));
}panic!("SerdeErrorShape: contradictory options; a field name is too long to quote")2829 }
2830 }
2831}
28322833// endregion
28342835// region: Serde-tagged Result errors (#[miniextendr(serde_error)])
28362837/// Build the `Err`-arm parts from the error's serde shape.
2838///
2839/// Reached three ways: for a `Result<T, E>` whose `E` has a shape declared
2840/// by [`serde_error_shape!`](crate::serde_error_shape) (the
2841/// [`ErrShapeDeclared`] probe arm, that one shape); automatically, for every
2842/// other `E` that is `Serialize + Display` without an [`RConditionData`] impl
2843/// (the [`ErrPartsSerde`] probe arm, no shapes); or explicitly through
2844/// `#[miniextendr(serde_error(..))]`, whose options arrive as `shapes`: the
2845/// declared shape first (one that sets nothing when there is none), then the
2846/// named `shape = ..`, then the function's own options as an inline const,
2847/// each applied in order (see [`SerdeErrorShape`], "Precedence").
2848/// `prefix` is the macro's `<crate>_error`, used when no shape sets one.
2849///
2850/// Message from `Display`; classes `[<prefix>_<variant>, <prefix>]` when the
2851/// serialized value carries a variant, `[<prefix>]` otherwise; data from the
2852/// serialized fields (checked against the reserved names). `tag` names the
2853/// internally-tagged discriminator field (`#[serde(tag = "kind")]`), which is
2854/// consumed as the member class rather than spliced as data; externally tagged
2855/// enums (serde's default) yield the variant name directly. See
2856/// `docs/CONDITIONS.md`, "Classed `Result` errors".
2857///
2858/// Field control, applied to the payload fields before the reserved-name
2859/// check (`serde_error(skip(..), rename(..), as_*(..))`, #1457, #2207):
2860///
2861/// - `skip`: fields with these names are dropped.
2862/// - `as_character` / `as_integer` / `as_double` / `as_logical`: the field's
2863/// value becomes one vector of that type, through the same conversion as
2864/// the `serde::as_*` field helpers; a value that doesn't fit panics, naming
2865/// the field and the option.
2866/// - `rename`: `(from, to)` pairs; a field named `from` is spliced as `to`.
2867/// - A remaining field named `message` whose value is the single string equal
2868/// to the `Display` output is dropped: it duplicates the condition's own
2869/// `message` slot, so a wrapped parser error of the shape
2870/// `Variant { message: String }` reaches R without any option.
2871///
2872/// Names that a given variant does not carry are a no-op for that variant.
2873/// Any other collision with `message`, `call` or `kind` still panics via
2874/// [`check_condition_data`].
2875///
2876/// After `skip` / `rename` the field names must be distinct: a `rename` whose
2877/// target is a name the variant already carries (or two payload fields that
2878/// serialize under one name) would give the condition two entries with the
2879/// same name, of which R's `e$name` reads only the first. That panics too,
2880/// naming the field and the rename that produced it (#1459).
2881///
2882/// Serialization failing (a map with non-string keys, a custom `Serialize`
2883/// erroring) panics with both failures in the message: the original error
2884/// text must not be lost behind the reporting problem.
2885#[cfg(feature = "serde")]
2886#[doc(hidden)]
2887#[track_caller]
2888pub fn serde_err_parts<E: ?Sized + ::serde::Serialize + std::fmt::Display>(
2889 e: &E,
2890 prefix: &str,
2891 shapes: &[&SerdeErrorShape],
2892) -> ErrParts {
2893let message = e.to_string();
2894let tag = shapes
2895 .iter()
2896 .rev()
2897 .find_map(|s| s.tag)
2898 .unwrap_or(SerdeErrorShape::DEFAULT_TAG);
2899let prefix = shapes.iter().rev().find_map(|s| s.prefix).unwrap_or(prefix);
2900let (member, fields) = match crate::serde::rvalue_ser::tagged_parts(e, tag) {
2901Ok(parts) => parts,
2902Err(err) => panic!(
2903"#[miniextendr(serde_error)]: serializing `{}` for its condition data failed ({err}); \
2904 the original error was: {message}",
2905 std::any::type_name::<E>()
2906 ),
2907 };
2908let mut class = Vec::with_capacity(2);
2909if let Some(member) = member {
2910 class.push(format!("{prefix}_{member}"));
2911 }
2912 class.push(prefix.to_string());
2913let mut renames = Vec::new();
2914let mut data: ConditionData = Vec::with_capacity(fields.len());
2915for (name, value) in fields {
2916let Some(field) = FieldShape::resolve(shapes, &name) else {
2917continue;
2918 };
2919let value = match field.atomic {
2920None => value,
2921Some(atomic) => match atomic.try_coerce(value) {
2922Ok(value) => value,
2923Err(element) => panic!(
2924"#[miniextendr(serde_error)]: `{}` on condition data field `{name}` failed: \
2925 {}; the original error was: {message}",
2926 atomic.helper(),
2927 atomic.mismatch(element)
2928 ),
2929 },
2930 };
2931if let Some((from, to)) = field.rename {
2932 renames.push((from, to));
2933 data.push((to.to_string(), value));
2934continue;
2935 }
2936let echoes_display = name == "message"
2937&& matches!(&value, crate::RValue::Character(v)
2938if v.len() == 1 && v[0].as_deref() == Some(message.as_str()));
2939if !echoes_display {
2940 data.push((name, value));
2941 }
2942 }
2943 check_distinct_field_names(&data, &renames);
2944 ErrParts {
2945 message,
2946 class,
2947 data: check_condition_data((!data.is_empty()).then_some(data)),
2948 call: ConditionCall::Inherit,
2949 }
2950}
29512952/// What the shapes, applied in order, give one payload field.
2953#[cfg(feature = "serde")]
2954#[derive(Default)]
2955struct FieldShape {
2956 atomic: Option<crate::serde::rvalue_ser::Atomic>,
2957 rename: Option<(&'static str, &'static str)>,
2958}
29592960#[cfg(feature = "serde")]
2961impl FieldShape {
2962/// `None` when the last shape that names the field skips it. A later
2963 /// `skip` drops what earlier shapes set; a later type or rename replaces
2964 /// that setting and keeps the field.
2965fn resolve(shapes: &[&SerdeErrorShape], name: &str) -> Option<Self> {
2966let mut skipped = false;
2967let mut field = FieldShape::default();
2968for shape in shapes {
2969if shape.skip.contains(&name) {
2970 skipped = true;
2971 field = FieldShape::default();
2972 }
2973if let Some(i) = shape.types.iter().position(|list| list.contains(&name)) {
2974 skipped = false;
2975 field.atomic = Some(crate::serde::rvalue_ser::Atomic::ALL[i]);
2976 }
2977if let Some(&pair) = shape.rename.iter().find(|(from, _)| *from == name) {
2978 skipped = false;
2979 field.rename = Some(pair);
2980 }
2981 }
2982 (!skipped).then_some(field)
2983 }
2984}
29852986/// Panic when two payload fields share a name after `skip` / `rename`.
2987///
2988/// R's `e$<name>` returns the first entry with that name and the second is
2989/// unreachable, which is the silent-overwrite failure the reserved-name rule
2990/// exists to remove (#1440). Neither first-wins nor last-wins would report it.
2991#[cfg(feature = "serde")]
2992#[track_caller]
2993fn check_distinct_field_names(fields: &[(String, crate::RValue)], rename: &[(&str, &str)]) {
2994let mut seen = std::collections::HashSet::with_capacity(fields.len());
2995for (name, _) in fields {
2996if seen.insert(name.as_str()) {
2997continue;
2998 }
2999let renames: Vec<String> = rename
3000 .iter()
3001 .filter(|(_, to)| *to == name)
3002 .map(|(from, to)| format!("`rename({from} = \"{to}\")`"))
3003 .collect();
3004if renames.is_empty() {
3005panic!(
3006"#[miniextendr(serde_error)]: condition data field `{name}` appears twice in the \
3007 serialized payload; R's `e${name}` would read only the first. Give the fields \
3008 distinct names or `skip` one of them."
3009);
3010 }
3011panic!(
3012"#[miniextendr(serde_error)]: condition data field `{name}` appears twice: {} targets \
3013 a name this variant already carries; R's `e${name}` would read only the first. Pick \
3014 another target or `skip` the existing field.",
3015 renames.join(" and ")
3016 );
3017 }
3018}
30193020// endregion
30213022// region: Declared shapes — one SerdeErrorShape per error type (#2207)
30233024/// The [`SerdeErrorShape`] declared for the error type `Self` by the crate
3025/// whose marker type is `M`. Implemented by
3026/// [`serde_error_shape!`](crate::serde_error_shape), not by hand.
3027///
3028/// `M` is a type local to the declaring crate (`miniextendr_init!` defines it
3029/// at the crate root). That is what lets an R-facing crate declare a shape
3030/// for an error type from a crate that doesn't depend on miniextendr: the
3031/// orphan rule accepts `impl ForeignTrait<LocalType> for ForeignType`. It also
3032/// scopes the declaration to the crate: another crate returning the same type
3033/// looks up its own declaration.
3034///
3035/// The supertraits make a declaration for a type that can't be serialized, or
3036/// has no `Display` for the condition message, an error at the declaration.
3037#[cfg(feature = "serde")]
3038pub trait SerdeErrorShapeFor<M>: ::serde::Serialize + std::fmt::Display {
3039/// The declared shape.
3040const SHAPE: &'static SerdeErrorShape;
3041}
30423043/// Declare the [`SerdeErrorShape`] of an error type once, for every
3044/// `#[miniextendr]` function and method of this crate that returns it.
3045///
3046/// ```ignore
3047/// use miniextendr_api::condition::SerdeErrorShape;
3048///
3049/// pub const ENGINE: SerdeErrorShape = SerdeErrorShape::new()
3050/// .prefix("engine")
3051/// .as_character(&["rule"])
3052/// .as_integer(&["n"]);
3053///
3054/// miniextendr_api::serde_error_shape!(engine::Error = ENGINE);
3055///
3056/// // Both raise with ENGINE's field types; neither names it.
3057/// #[miniextendr]
3058/// pub fn check(x: f64) -> Result<f64, engine::Error> { /* ... */ }
3059///
3060/// #[miniextendr]
3061/// pub fn rate(x: f64) -> Result<f64, engine::Error> { /* ... */ }
3062/// ```
3063///
3064/// The right-hand side is any constant expression of type
3065/// `SerdeErrorShape`: a path, as above, or the builder chain itself. The type
3066/// must be `Serialize + Display` and may come from any crate, including one
3067/// that can't depend on miniextendr. The declaration goes anywhere in the
3068/// crate that calls `miniextendr_init!`, which defines the marker type it is
3069/// keyed by (see [`SerdeErrorShapeFor`]); it applies to that crate's
3070/// functions only.
3071///
3072/// # Precedence
3073///
3074/// Every `Err` of the type, from a function with or without
3075/// `#[miniextendr(serde_error(..))]`, is classed through serde with the
3076/// declared shape applied first. A function's `serde_error(shape = X)` applies
3077/// next and its own options last; for a field set more than once, the later
3078/// setting wins, by the rules under [`SerdeErrorShape`], "Precedence".
3079///
3080/// The declaration also wins over an [`RConditionData`] impl of the type: it is
3081/// the R-facing crate's choice for its own functions, where the impl is the
3082/// error crate's general one.
3083///
3084/// # One declaration per type
3085///
3086/// A second declaration for a type in the same crate is the compiler's
3087/// conflicting-implementations error:
3088///
3089/// ```compile_fail,E0119
3090/// use miniextendr_api::condition::SerdeErrorShape;
3091///
3092/// miniextendr_api::miniextendr_init!(pkg);
3093///
3094/// const A: SerdeErrorShape = SerdeErrorShape::new().prefix("a");
3095/// const B: SerdeErrorShape = SerdeErrorShape::new().prefix("b");
3096///
3097/// miniextendr_api::serde_error_shape!(String = A);
3098/// miniextendr_api::serde_error_shape!(String = B);
3099///
3100/// fn main() {}
3101/// ```
3102#[cfg(feature = "serde")]
3103#[macro_export]
3104// `crate::` is meant: the marker is the calling crate's.
3105#[allow(clippy::crate_in_macro_def)]
3106macro_rules! serde_error_shape {
3107 ($error:ty = $shape:expr $(,)?) => {
3108impl $crate::condition::SerdeErrorShapeFor<crate::__MiniextendrInitMarker> for $error {
3109const SHAPE: &'static $crate::condition::SerdeErrorShape = &$shape;
3110 }
3111 };
3112}
31133114/// Declare the `SerdeErrorShape` of an error type once, for every
3115/// `#[miniextendr]` function and method of this crate that returns it. Needs
3116/// the `serde` feature, without which a declaration is a compile error.
3117#[cfg(not(feature = "serde"))]
3118#[macro_export]
3119macro_rules!serde_error_shape {
3120 ($($input:tt)*) => {
3121::core::compile_error!(
3122"`serde_error_shape!` needs the `serde` feature of miniextendr-api; \
3123 enable it to class errors through serde"
3124);
3125 };
3126}
31273128/// The receiver of the declared-shape probe: the error, and the marker `M` of
3129/// the crate whose declaration to look up.
3130#[cfg(feature = "serde")]
3131#[doc(hidden)]
3132pub struct DeclaredShapeProbe<'a, M, E>(pub &'a E, pub std::marker::PhantomData<fn() -> M>);
31333134/// Declared-shape probe, preferred arm: `E` has a shape declared under `M`.
3135///
3136/// The generated code calls `(&probe).__mx_declared_parts(..)` with both
3137/// traits in scope. This impl, on the probe taking `&self`, matches the
3138/// receiver `&DeclaredShapeProbe` by value; [`ErrShapeUndeclared`] on
3139/// `&DeclaredShapeProbe` needs one more auto-ref and is only reached when
3140/// nothing is declared.
3141#[cfg(feature = "serde")]
3142#[doc(hidden)]
3143pub trait ErrShapeDeclared {
3144/// The parts under the declared shape, for an `Err` arm without
3145 /// `serde_error(..)`. The call to [`serde_err_parts`] lives here, where
3146 /// `E: Serialize + Display` holds, so an undeclared `E` needn't be.
3147fn __mx_declared_parts(&self, prefix: &str) -> Option<ErrParts>;
3148/// The declared shape, first in `serde_error(..)`'s shape list.
3149fn __mx_declared_shape(&self) -> &'static SerdeErrorShape;
3150}
31513152#[cfg(feature = "serde")]
3153impl<M, E: SerdeErrorShapeFor<M>> ErrShapeDeclared for DeclaredShapeProbe<'_, M, E> {
3154#[track_caller]
3155fn __mx_declared_parts(&self, prefix: &str) -> Option<ErrParts> {
3156Some(serde_err_parts(self.0, prefix, &[E::SHAPE]))
3157 }
31583159fn __mx_declared_shape(&self) -> &'static SerdeErrorShape {
3160 E::SHAPE
3161 }
3162}
31633164/// Declared-shape probe, fallback arm: no declaration. No parts, and a shape
3165/// that sets nothing.
3166#[cfg(feature = "serde")]
3167#[doc(hidden)]
3168pub trait ErrShapeUndeclared {
3169fn __mx_declared_parts(&self, prefix: &str) -> Option<ErrParts>;
3170fn __mx_declared_shape(&self) -> &'static SerdeErrorShape;
3171}
31723173#[cfg(feature = "serde")]
3174impl<M, E> ErrShapeUndeclared for &DeclaredShapeProbe<'_, M, E> {
3175fn __mx_declared_parts(&self, _prefix: &str) -> Option<ErrParts> {
3176None
3177}
31783179fn __mx_declared_shape(&self) -> &'static SerdeErrorShape {
3180const NONE: &SerdeErrorShape = &SerdeErrorShape::new();
3181 NONE
3182 }
3183}
31843185/// Internal: the declared-shape stage of [`crate::__mx_result_err_parts!`].
3186/// Without the `serde` feature it is `None` and `$marker` is never resolved,
3187/// so a crate without `miniextendr_init!` (a test crate) still compiles.
3188#[cfg(feature = "serde")]
3189#[doc(hidden)]
3190#[macro_export]
3191macro_rules! __mx_declared_err_parts {
3192 ($e:ident, $prefix:expr, $marker:ty) => {{
3193#[allow(unused_imports)]
3194use $crate::condition::{ErrShapeDeclared as _, ErrShapeUndeclared as _};
3195 (&$crate::condition::DeclaredShapeProbe::<$marker, _>($e, ::core::marker::PhantomData))
3196 .__mx_declared_parts($prefix)
3197 }};
3198}
31993200/// Internal: the declared-shape stage of [`crate::__mx_result_err_parts!`];
3201/// nothing can be declared without the `serde` feature.
3202#[cfg(not(feature = "serde"))]
3203#[doc(hidden)]
3204#[macro_export]
3205macro_rules!__mx_declared_err_parts {
3206 ($e:ident, $prefix:expr, $marker:ty) => {
3207 ::core::option::Option::None::<$crate::condition::ErrParts>
3208 };
3209}
32103211/// Internal: the shape `serde_error_shape!` declared for the type of `$e`
3212/// under `$marker`, or one that sets nothing. It leads the shape list of a
3213/// `#[miniextendr(serde_error(..))]` `Err` arm.
3214#[cfg(feature = "serde")]
3215#[doc(hidden)]
3216#[macro_export]
3217macro_rules! __mx_declared_serde_shape {
3218 ($e:expr, $marker:ty) => {{
3219#[allow(unused_imports)]
3220use $crate::condition::{ErrShapeDeclared as _, ErrShapeUndeclared as _};
3221 (&$crate::condition::DeclaredShapeProbe::<$marker, _>(&$e, ::core::marker::PhantomData))
3222 .__mx_declared_shape()
3223 }};
3224}
32253226/// Internal: without the `serde` feature, a shape that sets nothing.
3227/// `serde_error(..)` needs the feature anyway: its `serde_err_parts` call
3228/// reports that, and `$marker` is never resolved.
3229#[cfg(not(feature = "serde"))]
3230#[doc(hidden)]
3231#[macro_export]
3232macro_rules!__mx_declared_serde_shape {
3233 ($e:expr, $marker:ty) => {{
3234const NONE: &$crate::condition::SerdeErrorShape =
3235&$crate::condition::SerdeErrorShape::new();
3236 NONE
3237 }};
3238}
32393240// endregion
32413242// region: from_tagged_sexp + repanic_if_rust_error — shim re-panic helpers
32433244impl RCondition {
3245/// Reconstruct an [`RCondition::Error`] from a tagged SEXP produced by
3246 /// [`crate::error_value::make_rust_condition_value`].
3247 ///
3248 /// Returns `Some(RCondition)` when `sexp` has class `"rust_condition_value"` AND
3249 /// the `"__rust_condition__"` attribute is `TRUE`. Returns `None` for all other
3250 /// SEXPs (normal return values, `R_NilValue`, etc.).
3251 ///
3252 /// Reconstructs the matching variant for each kind: `"error"`/`"panic"`/
3253 /// `"result_err"`/`"none_err"`/`"other_rust_error"` →
3254 /// [`RCondition::Error`] (class, message, data and a [`ConditionCall::None`]
3255 /// choice kept; the kind itself is not carried by the variant);
3256 /// `"conversion"` → [`RCondition::Conversion`], so an argument error keeps
3257 /// `kind = "conversion"` across a package boundary (and an
3258 /// [`crate::arg_error!`] its class marker, which the consumer's wrapper
3259 /// resolves); `"warning"` → [`RCondition::Warning`]; `"message"` →
3260 /// [`RCondition::Message`]; `"condition"` → [`RCondition::Condition`].
3261 /// Unknown kinds degrade to [`RCondition::Error`] with the kind string
3262 /// prefixed to the message.
3263 ///
3264 /// # Safety
3265 ///
3266 /// Must be called from R's main thread.
3267pub unsafe fn from_tagged_sexp(sexp: crate::SEXP) -> Option<Self> {
3268use crate::SexpExt;
3269use crate::from_r::TryFromSexp;
32703271// Use SexpExt::inherits_class — wraps Rf_inherits, already main-thread.
3272if !sexp.inherits_class(c"rust_condition_value") {
3273return None;
3274 }
32753276// Belt-and-suspenders PROTECT across the full inspection window. The reads
3277 // below are nominally non-allocating, but R-devel's GC is aggressive enough
3278 // (see MEMORY.md "Common gotchas") that a defensive guard is cheap and
3279 // closes the door on subtle regressions if the read path ever changes.
3280let _guard = unsafe { crate::gc_protect::OwnedProtect::new(sexp) };
32813282// Verify the __rust_condition__ marker attribute is TRUE (a length-1 LGLSXP
3283 // with value 1). This guards against coincidental class attribute collisions.
3284let attr_sym = crate::cached_class::rust_condition_attr_symbol();
3285let marker = sexp.get_attr(attr_sym);
3286// marker should be a scalar logical TRUE: is_logical() and logical_elt(0) == 1
3287if !marker.is_logical() || marker.logical_elt(0) != 1 {
3288return None;
3289 }
32903291// It's a tagged SEXP. Read the elements.
3292 // Both 3-element (legacy) and 4-element (condition) forms have:
3293 // [0] = error message (STRSXP)
3294 // [1] = kind string (STRSXP)
3295 // [2] = class name or NULL (only in 4-element form; absent in legacy)
32963297let len = sexp.len();
32983299// Defense-in-depth: a tagged SEXP must have at least the message and kind
3300 // slots. inherits_class + __rust_condition__ marker should already imply this,
3301 // but a corrupted/spoofed SEXP that satisfies both checks shouldn't OOB
3302 // the vector_elt reads below.
3303if len < 2 {
3304return None;
3305 }
33063307let msg_sexp = sexp.vector_elt(0);
3308let msg: String = msg_sexp3309 .string_elt_str(0)
3310 .unwrap_or("<invalid error message>")
3311 .to_string();
33123313let kind_sexp = sexp.vector_elt(1);
3314let kind: &str = kind_sexp3315 .string_elt_str(0)
3316 .unwrap_or(crate::error_value::kind::PANIC);
33173318// Class slot is element [2] in the 4-element form (NULL in legacy form):
3319 // a STRSXP of any length (the user class vector, family last).
3320let class: Vec<String> = if len >= 4 {
3321let class_sexp = sexp.vector_elt(2);
3322if class_sexp.is_nil() || !class_sexp.is_character() {
3323Vec::new()
3324 } else {
3325 (0..class_sexp.len() as isize)
3326 .filter_map(|i| class_sexp.string_elt_str(i).map(str::to_string))
3327 .collect()
3328 }
3329 } else {
3330Vec::new()
3331 };
33323333use crate::error_value::kindas kind_const;
33343335// Slot [4] is the optional named-list condition data, present when `len >= 5`.
3336 //
3337 // Each field value is decoded through the single SEXP→owned-tree walker,
3338 // [`RValue::try_from_sexp`], so structured fields survive the cross-package
3339 // trait-ABI re-panic path (`repanic_if_rust_error`): the consumer's outer
3340 // `with_r_unwind_protect` guard rebuilds the tagged SEXP from the
3341 // reconstructed `RCondition`, which now carries the data — so `e$field_name`
3342 // is accessible in R handlers even when the error crossed a package boundary.
3343 //
3344 // `RValue` is NA-aware (logical/integer/character carry `None`; double
3345 // carries the `NA_REAL` bit), so NA-bearing fields now round-trip faithfully
3346 // rather than being dropped. Fields whose name is missing/empty, or whose
3347 // value is not R data (closures, environments, …, which `try_from_sexp`
3348 // rejects) are dropped — safe degradation that preserves message/class/kind.
3349 //
3350 // All reads here are non-allocating copies into owned Rust values, so no new
3351 // SEXPs are created and the existing `_guard` OwnedProtect suffices.
3352let data: Option<ConditionData> = if len >= 5 {
3353let data_sexp = sexp.vector_elt(4);
3354if data_sexp.is_nil() || !data_sexp.is_list() {
3355None3356 } else {
3357let data_len = data_sexp.len();
3358let names_sexp = data_sexp.get_names();
3359let mut fields: ConditionData = Vec::with_capacity(data_len);
3360for i in 0..data_len as isize {
3361// Read the field name from the names attribute. If missing/empty, skip.
3362let name: String = if names_sexp.is_nil() || !names_sexp.is_character() {
3363continue;
3364 } else {
3365match names_sexp.string_elt_str(i) {
3366Some(s) if !s.is_empty() => s.to_string(),
3367_ => continue,
3368 }
3369 };
3370if let Ok(value) = crate::RValue::try_from_sexp(data_sexp.vector_elt(i)) {
3371 fields.push((name, value));
3372 }
3373 }
3374if fields.is_empty() {
3375None3376 } else {
3377Some(fields)
3378 }
3379 }
3380 } else {
3381None3382 };
33833384// Slot [3] is the call. Only the "no call" marker (`FALSE`, see
3385 // `make_rust_condition_value_with_data`) survives the re-panic: a call
3386 // object captured by the producer names the vtable shim's frame, and the
3387 // consumer's outer guard attributes the condition to its own call.
3388let call = if len >= 4 && is_no_call_marker(sexp.vector_elt(3)) {
3389 ConditionCall::None3390 } else {
3391 ConditionCall::Inherit3392 };
33933394let cond = match kind {
3395 kind_const::ERROR3396 | kind_const::PANIC3397 | kind_const::RESULT_ERR3398 | kind_const::NONE_ERR3399 | kind_const::OTHER_RUST_ERROR => RCondition::Error {
3400 message: msg,
3401class,
3402data,
3403call,
3404 },
3405 kind_const::CONVERSION => RCondition::Conversion {
3406 message: msg,
3407class,
3408data,
3409call,
3410 },
3411 kind_const::WARNING => RCondition::Warning {
3412 message: msg,
3413class,
3414data,
3415call,
3416 },
3417 kind_const::MESSAGE => RCondition::Message {
3418 message: msg,
3419class,
3420data,
3421call,
3422 },
3423 kind_const::CONDITION => RCondition::Condition {
3424 message: msg,
3425class,
3426data,
3427call,
3428 },
3429 other => {
3430// Unknown kind — degrade to error
3431RCondition::Error {
3432 message: ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("[{0}] {1}", other, msg))
})format!("[{other}] {msg}"),
3433class,
3434data,
3435call,
3436 }
3437 }
3438 };
3439Some(cond)
3440 }
3441}
34423443/// `true` for the tagged value's "no call" marker in the `call` slot: a
3444/// length-1 logical `FALSE` ([`ConditionCall::None`]). A call object or `NULL`
3445/// is not the marker.
3446fn is_no_call_marker(call: crate::SEXP) -> bool {
3447use crate::SexpExt;
3448call.is_logical() && call.len() == 1 && call.logical_elt(0) == 0
3449}
34503451/// Inspect a SEXP returned by a trait-ABI vtable shim and, if it is a tagged
3452/// error value, re-panic with the reconstructed [`enum@RCondition`].
3453///
3454/// This is the "re-panic at the View boundary" step of Approach 1 from the
3455/// issue-345 plan. The caller (a generated View method wrapper) does:
3456///
3457/// ```ignore
3458/// let result = { vtable_call };
3459/// ::miniextendr_api::trait_abi::repanic_if_rust_error(result);
3460/// // ... convert result normally if we reach here
3461/// ```
3462///
3463/// When `sexp` is a tagged error value:
3464/// - `RCondition::Error` / `RCondition::Warning` / etc. → `panic_any!(cond)`.
3465/// The outer `with_r_unwind_protect` in the consumer's C entry point will
3466/// catch this and produce a tagged SEXP for the consumer's R wrapper.
3467///
3468/// When `sexp` is a normal value: this is a no-op.
3469///
3470/// # Safety
3471///
3472/// Must be called from R's main thread. `sexp` must be a valid (possibly
3473/// tagged) SEXP.
3474pub unsafe fn repanic_if_rust_error(sexp: crate::SEXP) {
3475if let Some(cond) = unsafe { RCondition::from_tagged_sexp(sexp) } {
3476 std::panic::panic_any(cond);
3477 }
3478}
34793480// endregion
34813482// region: AsRError struct — wraps std::error::Error for Result returns
34833484/// Structured error wrapper that preserves the `std::error::Error` cause chain.
3485///
3486/// When displayed, formats the error message with its full source chain:
3487/// ```text
3488/// top-level message
3489/// caused by: middle error
3490/// caused by: root cause
3491/// ```
3492///
3493/// Implements `From<E>` so it works with `?` and `.map_err(AsRError)`.
3494///
3495/// # Example
3496///
3497/// ```ignore
3498/// use miniextendr_api::condition::AsRError;
3499/// use std::num::ParseIntError;
3500///
3501/// #[miniextendr]
3502/// fn parse_number(s: &str) -> Result<i32, AsRError<ParseIntError>> {
3503/// s.parse::<i32>().map_err(AsRError)
3504/// }
3505/// ```
3506pub struct AsRError<E: std::error::Error>(pub E);
35073508impl<E: std::error::Error> From<E> for AsRError<E> {
3509#[inline]
3510fn from(err: E) -> Self {
3511AsRError(err)
3512 }
3513}
35143515impl<E: std::error::Error> std::fmt::Displayfor AsRError<E> {
3516fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3517// Write the top-level message
3518f.write_fmt(format_args!("{0}", self.0))write!(f, "{}", self.0)?;
35193520// Walk the cause chain
3521let mut current: &dyn std::error::Error = &self.0;
3522while let Some(source) = current.source() {
3523f.write_fmt(format_args!("\n caused by: {0}", source))write!(f, "\n caused by: {source}")?;
3524 current = source;
3525 }
35263527Ok(())
3528 }
3529}
35303531impl<E: std::error::Error> std::fmt::Debugfor AsRError<E> {
3532fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3533f.write_fmt(format_args!("AsRError<{0}>({1})", std::any::type_name::<E>(),
self))write!(f, "AsRError<{}>({})", std::any::type_name::<E>(), self)3534 }
3535}
35363537impl<E: std::error::Error> AsRError<E> {
3538/// Get the inner error.
3539#[inline]
3540pub fn into_inner(self) -> E {
3541self.0
3542}
35433544/// Get the Rust type name of the wrapped error (for programmatic matching).
3545#[inline]
3546pub fn rust_type_name(&self) -> &'static str {
3547 std::any::type_name::<E>()
3548 }
35493550/// Collect the full cause chain as a `Vec<String>`.
3551pub fn cause_chain(&self) -> Vec<String> {
3552let mut chain = ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
[self.0.to_string()]))vec![self.0.to_string()];
3553let mut current: &dyn std::error::Error = &self.0;
3554while let Some(source) = current.source() {
3555 chain.push(source.to_string());
3556 current = source;
3557 }
3558chain3559 }
3560}
35613562// endregion
35633564// region: Tests — macro grammar + payload contents (no R runtime needed)
35653566#[cfg(test)]
3567mod condition_macro_tests {
3568use super::{ConditionData, RCondition};
3569use crate::RValue;
35703571/// The marker type of a crate that declares no `serde_error_shape!`, for
3572 /// the `Err` probe's marker argument (resolved only under `serde`).
3573#[cfg(feature = "serde")]
3574enum NoShapes {}
35753576/// Catch the `panic_any(RCondition)` raised by a macro invocation and
3577 /// return the payload. No R runtime needed — the macros panic before any
3578 /// R API call.
3579fn catch(f: impl FnOnce() + std::panic::UnwindSafe) -> RCondition {
3580let payload = std::panic::catch_unwind(f).expect_err("macro must panic");
3581*payload
3582 .downcast::<RCondition>()
3583 .expect("payload must be RCondition")
3584 }
35853586fn assert_data(data: &Option<ConditionData>, expected: &[(&str, RValue)]) {
3587let data = data.as_ref().expect("data must be Some");
3588assert_eq!(data.len(), expected.len());
3589for ((name, value), (exp_name, exp_value)) in data.iter().zip(expected) {
3590assert_eq!(name, exp_name);
3591// RValue has no PartialEq (f64); compare via Debug.
3592assert_eq!(format!("{value:?}"), format!("{exp_value:?}"));
3593 }
3594 }
35953596/// `catch_condition` returns every condition kind as `Err` with its
3597 /// classes and data, and the value on success.
3598#[test]
3599fn catch_condition_returns_each_kind() {
3600use super::catch_condition;
3601use crate::error_value::kind;
3602assert_eq!(catch_condition(|| 3).unwrap(), 3);
3603let error = catch_condition::<()>(|| {
3604crate::error!(
3605 class = ["member", "family"],
3606 data = ("n", 2),
3607"failed {}",
36081
3609)
3610 })
3611 .unwrap_err();
3612let (error_kind, parts) = error.into_parts();
3613assert_eq!(error_kind, kind::ERROR);
3614assert_eq!(parts.message, "failed 1");
3615assert_eq!(parts.class, ["member", "family"]);
3616 assert_data(&parts.data, &[("n", RValue::from(2))]);
3617let kinds = [
3618 catch_condition::<()>(|| crate::warning!("w")),
3619 catch_condition::<()>(|| crate::message!("m")),
3620 catch_condition::<()>(|| crate::condition!("c")),
3621 catch_condition::<()>(|| {
3622 RCondition::arg_error("x", "bad".to_string(), super::ConditionCall::Inherit).raise()
3623 }),
3624 ]
3625 .map(|caught| caught.unwrap_err().into_parts().0);
3626assert_eq!(
3627 kinds,
3628 [
3629 kind::WARNING,
3630 kind::MESSAGE,
3631 kind::CONDITION,
3632 kind::CONVERSION
3633 ]
3634 );
3635 }
36363637/// Any other payload, a plain `panic!()` included, resumes unchanged.
3638#[test]
3639fn catch_condition_resumes_other_panics() {
3640use super::catch_condition;
3641struct Foreign(u8);
3642let payload = std::panic::catch_unwind(|| catch_condition::<()>(|| panic!("plain")))
3643 .expect_err("a plain panic must resume");
3644assert_eq!(payload.downcast_ref::<&str>(), Some(&"plain"));
3645let payload = std::panic::catch_unwind(|| {
3646 catch_condition::<()>(|| std::panic::panic_any(Foreign(9)))
3647 })
3648 .expect_err("a foreign payload must resume");
3649assert_eq!(payload.downcast_ref::<Foreign>().map(|f| f.0), Some(9));
3650 }
36513652/// `arg_error!` (#1740): the conversion kind, the crate-class marker as
3653 /// the only class, `param` as the only field, and the call choice.
3654#[test]
3655fn arg_error_payload() {
3656use super::{CONVERSION_ERROR_CLASS_MARKER, ConditionCall};
3657let choices = r#""fast", "slow""#;
3658let check = |cond: RCondition, call: ConditionCall| {
3659let RCondition::Conversion {
3660 message,
3661 class,
3662 data,
3663 call: got_call,
3664 } = cond
3665else {
3666panic!("expected RCondition::Conversion, got {cond:?}");
3667 };
3668assert_eq!(message, r#"'mode' should be one of "fast", "slow""#);
3669assert_eq!(class, [CONVERSION_ERROR_CLASS_MARKER]);
3670 assert_data(&data, &[("param", RValue::from("mode"))]);
3671assert_eq!(got_call, call);
3672 };
3673 check(
3674 catch(|| crate::arg_error!(param = "mode", "'mode' should be one of {choices}")),
3675 ConditionCall::Inherit,
3676 );
3677let param = String::from("mode");
3678 check(
3679 catch(move || {
3680crate::arg_error!(
3681 call = none,
3682 param = param,
3683"'mode' should be one of {}",
3684 choices
3685 )
3686 }),
3687 ConditionCall::None,
3688 );
3689 check(
3690 catch(|| {
3691super::ArgError::new("mode", format!("'mode' should be one of {choices}")).raise()
3692 }),
3693 ConditionCall::Inherit,
3694 );
36953696let (kind, parts) = catch(|| crate::arg_error!(param = "x", "bad")).into_parts();
3697assert_eq!(kind, crate::error_value::kind::CONVERSION);
3698assert_eq!(parts.message, "bad");
3699 }
37003701/// A raising guard resolves the marker in Rust: the crate's classes take
3702 /// its place, every other class keeps its position, and with no crate
3703 /// classes the marker just goes.
3704#[test]
3705fn raising_guard_resolves_the_conversion_class_marker() {
3706use super::CONVERSION_ERROR_CLASS_MARKER as MARKER;
3707use super::resolve_conversion_error_class_marker as resolve;
3708let class = || vec!["a".to_string(), MARKER.to_string(), "b".to_string()];
3709assert_eq!(
3710 resolve(class(), &["pkg_error_argument", "pkg_error"]),
3711 ["a", "pkg_error_argument", "pkg_error", "b"]
3712 );
3713assert_eq!(resolve(class(), &[]), ["a", "b"]);
3714assert_eq!(
3715 resolve(vec![MARKER.to_string()], &["pkg_error"]),
3716 ["pkg_error"]
3717 );
3718assert_eq!(resolve(vec!["a".to_string()], &["pkg_error"]), ["a"]);
3719 }
37203721#[test]
3722fn error_class_vector_and_keyed_data() {
3723let cond = catch(|| {
3724crate::error!(
3725 class = ["member", "family"],
3726 data = { rule = 1, detail = "inner" },
3727"layered"
3728)
3729 });
3730match cond {
3731 RCondition::Error {
3732 message,
3733 class,
3734 data,
3735 call: super::ConditionCall::Inherit,
3736 } => {
3737assert_eq!(message, "layered");
3738assert_eq!(class, vec!["member", "family"]);
3739 assert_data(
3740&data,
3741&[
3742 ("rule", RValue::Integer(vec![Some(1)])),
3743 ("detail", RValue::Character(vec![Some("inner".to_string())])),
3744 ],
3745 );
3746 }
3747 other => panic!("wrong variant: {other:?}"),
3748 }
3749 }
37503751#[test]
3752fn error_class_vector_from_vec_string() {
3753let classes = vec!["a".to_string(), "b".to_string()];
3754let cond = catch(move || crate::warning!(class = classes, "w"));
3755match cond {
3756 RCondition::Warning { class, .. } => assert_eq!(class, vec!["a", "b"]),
3757 other => panic!("wrong variant: {other:?}"),
3758 }
3759 }
37603761#[test]
3762fn reserved_field_name_runtime_check_panics() {
3763let name = String::from("kind");
3764let payload = std::panic::catch_unwind(move || crate::error!(data = (name, 1), "x"))
3765 .expect_err("must panic");
3766let msg = payload
3767 .downcast_ref::<String>()
3768 .cloned()
3769 .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
3770 .expect("plain panic message");
3771assert!(msg.contains("reserved"), "got: {msg}");
3772assert!(msg.contains("rename"), "got: {msg}");
3773 }
37743775#[cfg(feature = "serde")]
3776mod serde_err_parts {
3777use super::super::{SerdeErrorShape, serde_err_parts};
3778use crate::RValue;
3779use ::serde::Serialize;
37803781#[derive(Serialize)]
3782 #[serde(tag = "kind", rename_all = "snake_case")]
3783enum Tagged {
3784 MissingField { field: String },
3785 Io,
3786 }
37873788impl std::fmt::Display for Tagged {
3789fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3790match self {
3791 Tagged::MissingField { field } => write!(f, "field `{field}` is missing"),
3792 Tagged::Io => f.write_str("io"),
3793 }
3794 }
3795 }
37963797#[derive(Serialize)]
3798struct Plain {
3799 code: i32,
3800 }
38013802impl std::fmt::Display for Plain {
3803fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3804write!(f, "plain {}", self.code)
3805 }
3806 }
38073808#[derive(Serialize)]
3809enum Clash {
3810 Bad { kind: String },
3811 }
38123813impl std::fmt::Display for Clash {
3814fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3815 f.write_str("clash")
3816 }
3817 }
38183819#[test]
3820fn internally_tagged_variant_becomes_member_class_and_fields_become_data() {
3821let parts =
3822 serde_err_parts(&Tagged::MissingField { field: "id".into() }, "engine", &[]);
3823assert_eq!(parts.message, "field `id` is missing");
3824assert_eq!(parts.class, ["engine_missing_field", "engine"]);
3825let data = parts.data.expect("fields become data");
3826assert_eq!(data.len(), 1);
3827assert_eq!(data[0].0, "field");
3828assert!(
3829matches!(&data[0].1, RValue::Character(v) if v == &vec![Some("id".to_string())])
3830 );
3831 }
38323833#[test]
3834fn unit_variant_has_classes_but_no_data() {
3835let parts = serde_err_parts(&Tagged::Io, "engine", &[]);
3836assert_eq!(parts.class, ["engine_io", "engine"]);
3837assert!(parts.data.is_none());
3838 }
38393840#[test]
3841fn value_without_variant_gets_only_the_family_class() {
3842let parts = serde_err_parts(&Plain { code: 3 }, "engine", &[]);
3843assert_eq!(parts.message, "plain 3");
3844assert_eq!(parts.class, ["engine"]);
3845let data = parts.data.expect("struct fields become data");
3846assert_eq!(data[0].0, "code");
3847 }
38483849#[test]
3850fn reserved_payload_field_is_rejected() {
3851let err = std::panic::AssertUnwindSafe(Clash::Bad { kind: "x".into() });
3852let payload = std::panic::catch_unwind(move || {
3853 serde_err_parts(&err.0, "p", &[&SerdeErrorShape::new().tag("type")])
3854 })
3855 .expect_err("must panic");
3856let msg = payload
3857 .downcast_ref::<String>()
3858 .cloned()
3859 .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
3860 .unwrap_or_default();
3861assert!(msg.contains("reserved"), "{msg}");
3862assert!(msg.contains("`kind`"), "{msg}");
3863 }
38643865/// The wrapped-parser-error shape (#1457): a `message` payload field.
3866#[derive(Serialize)]
3867 #[serde(tag = "kind", rename_all = "snake_case")]
3868enum Wrapped {
3869/// `Display` adds context, so `message` is not redundant.
3870Parse {
3871 message: String,
3872 line: u32,
3873 },
3874/// `Display` is the field verbatim.
3875Echo {
3876 message: String,
3877 },
3878 Bad {
3879 call: String,
3880 },
3881 }
38823883impl std::fmt::Display for Wrapped {
3884fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3885match self {
3886 Wrapped::Parse { message, line } => write!(f, "line {line}: {message}"),
3887 Wrapped::Echo { message } => f.write_str(message),
3888 Wrapped::Bad { .. } => f.write_str("bad"),
3889 }
3890 }
3891 }
38923893fn panic_message(f: impl FnOnce() + std::panic::UnwindSafe) -> String {
3894let payload = std::panic::catch_unwind(f).expect_err("must panic");
3895 payload
3896 .downcast_ref::<String>()
3897 .cloned()
3898 .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
3899 .unwrap_or_default()
3900 }
39013902fn names(parts: &super::super::ErrParts) -> Vec<&str> {
3903 parts
3904 .data
3905 .as_deref()
3906 .unwrap_or_default()
3907 .iter()
3908 .map(|(n, _)| n.as_str())
3909 .collect()
3910 }
39113912#[test]
3913fn message_field_equal_to_display_is_dropped_without_options() {
3914let parts = serde_err_parts(
3915&Wrapped::Echo {
3916 message: "boom".into(),
3917 },
3918"p",
3919&[],
3920 );
3921assert_eq!(parts.message, "boom");
3922assert_eq!(parts.class, ["p_echo", "p"]);
3923assert!(parts.data.is_none(), "{:?}", parts.data);
3924 }
39253926#[test]
3927fn message_field_differing_from_display_is_still_rejected() {
3928let err = std::panic::AssertUnwindSafe(Wrapped::Parse {
3929 message: "unexpected token".into(),
3930 line: 3,
3931 });
3932let msg = panic_message(move || {
3933 serde_err_parts(&err.0, "p", &[]);
3934 });
3935assert!(msg.contains("reserved"), "{msg}");
3936assert!(msg.contains("`message`"), "{msg}");
3937 }
39383939#[test]
3940fn skip_drops_the_named_field_and_is_a_no_op_elsewhere() {
3941let parts = serde_err_parts(
3942&Wrapped::Parse {
3943 message: "unexpected token".into(),
3944 line: 3,
3945 },
3946"p",
3947&[&SerdeErrorShape::new().skip(&["message", "absent"])],
3948 );
3949assert_eq!(parts.message, "line 3: unexpected token");
3950assert_eq!(parts.class, ["p_parse", "p"]);
3951assert_eq!(names(&parts), ["line"]);
39523953// `skip` runs before the Display-equality rule; same outcome.
3954let parts = serde_err_parts(
3955&Wrapped::Echo {
3956 message: "boom".into(),
3957 },
3958"p",
3959&[&SerdeErrorShape::new().skip(&["message"])],
3960 );
3961assert!(parts.data.is_none());
39623963// A variant without the skipped field but with another reserved
3964 // name still fails the reserved-name check.
3965let err = std::panic::AssertUnwindSafe(Wrapped::Bad { call: "f()".into() });
3966let msg = panic_message(move || {
3967 serde_err_parts(&err.0, "p", &[&SerdeErrorShape::new().skip(&["message"])]);
3968 });
3969assert!(msg.contains("`call`"), "{msg}");
3970 }
39713972#[test]
3973fn rename_splices_the_field_under_the_new_name() {
3974let parts = serde_err_parts(
3975&Wrapped::Parse {
3976 message: "unexpected token".into(),
3977 line: 3,
3978 },
3979"p",
3980&[&SerdeErrorShape::new().rename(&[("message", "detail"), ("absent", "other")])],
3981 );
3982assert_eq!(names(&parts), ["detail", "line"]);
3983let data = parts.data.expect("data");
3984assert!(matches!(
3985&data[0].1,
3986 RValue::Character(v) if v == &vec![Some("unexpected token".to_string())]
3987 ));
39883989// `rename` wins over the Display-equality drop: the caller asked
3990 // for the field explicitly.
3991let parts = serde_err_parts(
3992&Wrapped::Echo {
3993 message: "boom".into(),
3994 },
3995"p",
3996&[&SerdeErrorShape::new().rename(&[("message", "detail")])],
3997 );
3998assert_eq!(names(&parts), ["detail"]);
3999 }
40004001/// The probe's second stage under the `serde` feature: an
4002 /// `E: Serialize + Display` is classed with the defaults and the
4003 /// macro-supplied prefix; a `String` error gets the family class and
4004 /// its text unquoted.
4005#[test]
4006fn err_parts_probe_classes_serialize_display_types() {
4007let parts = crate::__mx_result_err_parts!(Tagged::Io, "p", super::NoShapes);
4008assert_eq!(parts.message, "io");
4009assert_eq!(parts.class, ["p_io", "p"]);
4010assert!(parts.data.is_none());
40114012let parts = crate::__mx_result_err_parts!(
4013 Tagged::MissingField { field: "id".into() },
4014"p",
4015super::NoShapes
4016 );
4017assert_eq!(parts.class, ["p_missing_field", "p"]);
4018assert_eq!(names(&parts), ["field"]);
40194020let parts =
4021crate::__mx_result_err_parts!(String::from("bad input"), "p", super::NoShapes);
4022assert_eq!(parts.message, "bad input");
4023assert_eq!(parts.class, ["p"]);
4024assert!(parts.data.is_none());
40254026let parts = crate::__mx_result_err_parts!("static text", "p", super::NoShapes);
4027assert_eq!(parts.message, "static text");
4028assert_eq!(parts.class, ["p"]);
4029 }
40304031/// #1459: a `rename` target the variant already carries would give the
4032 /// condition two `line` entries; R reads only the first.
4033#[test]
4034fn rename_onto_a_name_the_variant_carries_panics() {
4035let err = std::panic::AssertUnwindSafe(Wrapped::Parse {
4036 message: "unexpected token".into(),
4037 line: 3,
4038 });
4039let msg = panic_message(move || {
4040 serde_err_parts(
4041&err.0,
4042"p",
4043&[&SerdeErrorShape::new().rename(&[("message", "line")])],
4044 );
4045 });
4046assert!(msg.contains("field `line` appears twice"), "{msg}");
4047assert!(msg.contains("`rename(message = \"line\")`"), "{msg}");
40484049// The same attribute is fine on a variant without `line`.
4050let parts = serde_err_parts(
4051&Wrapped::Echo {
4052 message: "boom".into(),
4053 },
4054"p",
4055&[&SerdeErrorShape::new().rename(&[("message", "line")])],
4056 );
4057assert_eq!(names(&parts), ["line"]);
4058 }
40594060/// A target that only *another* variant carries is no collision, and
4061 /// `skip` frees a name for `rename` because both run before the check.
4062#[test]
4063fn rename_target_is_checked_per_variant_after_skip() {
4064let parts = serde_err_parts(
4065&Wrapped::Bad { call: "f()".into() },
4066"p",
4067&[&SerdeErrorShape::new().rename(&[("call", "line")])],
4068 );
4069assert_eq!(names(&parts), ["line"]);
40704071let parts = serde_err_parts(
4072&Wrapped::Parse {
4073 message: "unexpected token".into(),
4074 line: 3,
4075 },
4076"p",
4077&[&SerdeErrorShape::new()
4078 .skip(&["line"])
4079 .rename(&[("message", "line")])],
4080 );
4081assert_eq!(names(&parts), ["line"]);
4082let data = parts.data.expect("data");
4083assert!(matches!(
4084&data[0].1,
4085 RValue::Character(v) if v == &vec![Some("unexpected token".to_string())]
4086 ));
4087 }
4088 }
40894090/// `SerdeErrorShape`'s const checks (#2207). Called at run time here, the
4091 /// same panics a `const` turns into a compile error.
4092mod serde_error_shape_checks {
4093use super::super::SerdeErrorShape;
40944095fn panic_message(f: impl FnOnce() -> SerdeErrorShape + std::panic::UnwindSafe) -> String {
4096let payload = std::panic::catch_unwind(f).expect_err("must panic");
4097 payload
4098 .downcast_ref::<String>()
4099 .cloned()
4100 .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
4101 .unwrap_or_default()
4102 }
41034104#[test]
4105fn a_consistent_shape_builds_in_a_const() {
4106const ENGINE: SerdeErrorShape = SerdeErrorShape::new()
4107 .tag("type")
4108 .prefix("engine")
4109 .skip(&["message"])
4110 .rename(&[("source", "cause")])
4111 .as_character(&["rule", "column", "source"])
4112 .as_integer(&["n"])
4113 .as_double(&["rate"])
4114 .as_logical(&["exact"]);
4115assert_ne!(ENGINE, SerdeErrorShape::new());
4116assert_eq!(SerdeErrorShape::default(), SerdeErrorShape::new());
4117 }
41184119#[test]
4120fn a_field_in_two_type_lists_is_refused() {
4121let msg = panic_message(|| {
4122 SerdeErrorShape::new()
4123 .as_integer(&["rule", "n"])
4124 .as_double(&["n"])
4125 });
4126assert_eq!(
4127 msg,
4128"SerdeErrorShape: field `n` is in both `as_integer` and `as_double`; a field has \
4129 one R type"
4130);
4131 }
41324133#[test]
4134fn a_skipped_field_takes_no_type_or_name() {
4135let msg = panic_message(|| SerdeErrorShape::new().skip(&["n"]).as_integer(&["n"]));
4136assert!(
4137 msg.contains("field `n` is in both `skip` and `as_integer`"),
4138"{msg}"
4139);
4140let msg = panic_message(|| SerdeErrorShape::new().as_logical(&["n"]).skip(&["n"]));
4141assert!(
4142 msg.contains("field `n` is in both `as_logical` and `skip`"),
4143"{msg}"
4144);
4145let msg = panic_message(|| {
4146 SerdeErrorShape::new()
4147 .skip(&["message"])
4148 .rename(&[("message", "detail")])
4149 });
4150assert!(
4151 msg.contains("field `message` is in both `skip` and `rename`"),
4152"{msg}"
4153);
4154let msg = panic_message(|| {
4155 SerdeErrorShape::new()
4156 .rename(&[("message", "detail")])
4157 .skip(&["message"])
4158 });
4159assert!(
4160 msg.contains("field `message` is in both `rename` and `skip`"),
4161"{msg}"
4162);
4163 }
41644165#[test]
4166fn rename_mirrors_the_attribute_grammar() {
4167let msg = panic_message(|| SerdeErrorShape::new().rename(&[("detail", "kind")]));
4168assert!(msg.contains("target `kind` is reserved"), "{msg}");
4169let msg = panic_message(|| SerdeErrorShape::new().rename(&[("a", "b"), ("a", "c")]));
4170assert!(msg.contains("field `a` is renamed twice"), "{msg}");
4171let msg = panic_message(|| SerdeErrorShape::new().rename(&[("a", "b"), ("c", "b")]));
4172assert!(msg.contains("two `rename` pairs target `b`"), "{msg}");
4173let msg = panic_message(|| SerdeErrorShape::new().rename(&[("a", "")]));
4174assert!(msg.contains("`rename` has an empty name"), "{msg}");
4175let msg = panic_message(|| SerdeErrorShape::new().rename(&[]));
4176assert!(msg.contains("`rename` needs at least one pair"), "{msg}");
4177 }
41784179#[test]
4180fn lists_and_options_are_set_once_and_not_empty() {
4181let msg = panic_message(|| SerdeErrorShape::new().skip(&["a"]).skip(&["b"]));
4182assert!(msg.contains("`skip` is set twice"), "{msg}");
4183let msg = panic_message(|| SerdeErrorShape::new().as_double(&["a"]).as_double(&["b"]));
4184assert!(msg.contains("`as_double` is set twice"), "{msg}");
4185let msg = panic_message(|| SerdeErrorShape::new().prefix("a").prefix("b"));
4186assert!(msg.contains("`prefix` is set twice"), "{msg}");
4187let msg = panic_message(|| SerdeErrorShape::new().tag(""));
4188assert!(msg.contains("`tag` must not be empty"), "{msg}");
4189let msg = panic_message(|| SerdeErrorShape::new().as_character(&[]));
4190assert!(
4191 msg.contains("`as_character` needs at least one field"),
4192"{msg}"
4193);
4194let msg = panic_message(|| SerdeErrorShape::new().skip(&[""]));
4195assert!(msg.contains("`skip` has an empty field name"), "{msg}");
4196let msg = panic_message(|| SerdeErrorShape::new().as_integer(&["n", "n"]));
4197assert!(
4198 msg.contains("field `n` is listed twice in `as_integer`"),
4199"{msg}"
4200);
4201 }
42024203/// The message buffer is 256 bytes; a name it cuts inside a
4204 /// character falls back to a message without the name.
4205#[test]
4206fn a_long_field_name_still_panics_with_a_message() {
4207// "SerdeErrorShape: field `" is 24 bytes; the buffer ends inside `é`.
4208let name: &'static str = Box::leak(format!("{}é", "x".repeat(231)).into_boxed_str());
4209let names: &'static [&'static str] = Box::leak(Box::new([name]));
4210let msg = panic_message(move || SerdeErrorShape::new().as_integer(names).skip(names));
4211assert_eq!(
4212 msg,
4213"SerdeErrorShape: contradictory options; a field name is too long to quote"
4214);
4215 }
4216 }
42174218/// The `as_*` options and shape precedence at the condition boundary
4219 /// (#2207), on a hand-written `Serialize` that uses no field helper.
4220#[cfg(feature = "serde")]
4221mod serde_error_shape {
4222use super::super::{SHAPE_TYPE_OPTIONS, SerdeErrorShape, serde_err_parts};
4223use crate::RValue;
4224use crate::serde::rvalue_ser::Atomic;
42254226/// The shape of an error type in a crate that can't depend on
4227 /// miniextendr: internally tagged by hand.
4228struct Engine {
4229 rule: Vec<String>,
4230 n: u32,
4231 rate: i64,
4232 flags: Vec<Option<bool>>,
4233 }
42344235impl ::serde::Serialize for Engine {
4236fn serialize<S: ::serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
4237use ::serde::ser::SerializeStruct;
4238let mut st = s.serialize_struct("Engine", 5)?;
4239 st.serialize_field("kind", "rule")?;
4240 st.serialize_field("rule", &self.rule)?;
4241 st.serialize_field("n", &self.n)?;
4242 st.serialize_field("rate", &self.rate)?;
4243 st.serialize_field("flags", &self.flags)?;
4244 st.end()
4245 }
4246 }
42474248impl std::fmt::Display for Engine {
4249fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4250 f.write_str("rule failed")
4251 }
4252 }
42534254fn engine() -> Engine {
4255 Engine {
4256 rule: Vec::new(),
4257 n: 3,
4258 rate: 5,
4259 flags: vec![Some(true), None],
4260 }
4261 }
42624263fn field<'a>(parts: &'a super::super::ErrParts, name: &str) -> &'a RValue {
4264 parts
4265 .data
4266 .as_deref()
4267 .unwrap_or_default()
4268 .iter()
4269 .find(|(n, _)| n == name)
4270 .map(|(_, v)| v)
4271 .unwrap_or_else(|| panic!("no field `{name}` in {:?}", parts.data))
4272 }
42734274fn panic_message(f: impl FnOnce() + std::panic::UnwindSafe) -> String {
4275let payload = std::panic::catch_unwind(f).expect_err("must panic");
4276 payload
4277 .downcast_ref::<String>()
4278 .cloned()
4279 .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
4280 .unwrap_or_default()
4281 }
42824283#[test]
4284fn atomic_order_matches_the_shape_lists() {
4285for (atomic, option) in Atomic::ALL.iter().zip(SHAPE_TYPE_OPTIONS) {
4286assert_eq!(atomic.helper(), option);
4287 }
4288 }
42894290#[test]
4291fn without_options_the_fields_keep_their_usual_serialization() {
4292let parts = serde_err_parts(&engine(), "p", &[]);
4293assert_eq!(parts.class, ["p_rule", "p"]);
4294assert!(matches!(field(&parts, "rule"), RValue::List(v) if v.is_empty()));
4295assert!(matches!(field(&parts, "n"), RValue::Integer(v) if v == &[Some(3)]));
4296assert!(matches!(field(&parts, "flags"), RValue::List(v) if v.len() == 2));
4297 }
42984299#[test]
4300fn each_type_option_gives_its_vector() {
4301const SHAPE: SerdeErrorShape = SerdeErrorShape::new()
4302 .as_character(&["rule"])
4303 .as_double(&["n"])
4304 .as_integer(&["rate"])
4305 .as_logical(&["flags"]);
4306let parts = serde_err_parts(&engine(), "p", &[&SHAPE]);
4307assert!(matches!(field(&parts, "rule"), RValue::Character(v) if v.is_empty()));
4308assert!(matches!(field(&parts, "n"), RValue::Double(v) if v == &[3.0]));
4309assert!(matches!(field(&parts, "rate"), RValue::Integer(v) if v == &[Some(5)]));
4310assert!(matches!(
4311 field(&parts, "flags"),
4312 RValue::Logical(v) if v == &[Some(true), None]
4313 ));
4314 }
43154316#[test]
4317fn a_value_that_does_not_fit_names_the_field_and_the_option() {
4318let shape = SerdeErrorShape::new().as_integer(&["rate"]);
4319let err = std::panic::AssertUnwindSafe(Engine {
4320 rate: 1 << 40,
4321 ..engine()
4322 });
4323let msg = panic_message(move || {
4324 serde_err_parts(&err.0, "p", &[&shape]);
4325 });
4326assert_eq!(
4327 msg,
4328"#[miniextendr(serde_error)]: `as_integer` on condition data field `rate` \
4329 failed: the field is not an integer that fits an R integer or `None`; the \
4330 original error was: rule failed"
4331);
43324333let shape = SerdeErrorShape::new().as_logical(&["rule"]);
4334let err = std::panic::AssertUnwindSafe(Engine {
4335 rule: vec!["a".into(), "b".into()],
4336 ..engine()
4337 });
4338let msg = panic_message(move || {
4339 serde_err_parts(&err.0, "p", &[&shape]);
4340 });
4341assert!(
4342 msg.contains(
4343"`as_logical` on condition data field `rule` failed: the field is not a logical"
4344),
4345"{msg}"
4346);
4347 }
43484349/// The keys are the names as the error serializes them: an `as_*`
4350 /// option and a `rename` of one field compose.
4351#[test]
4352fn type_and_rename_compose_on_the_serialized_name() {
4353let shape = SerdeErrorShape::new()
4354 .rename(&[("rule", "rules")])
4355 .as_character(&["rule"]);
4356let parts = serde_err_parts(&engine(), "p", &[&shape]);
4357assert!(matches!(field(&parts, "rules"), RValue::Character(v) if v.is_empty()));
4358 }
43594360/// The function's options (the last shape) win for each setting of a
4361 /// field; what only the named shape sets stays.
4362#[test]
4363fn later_shapes_win_per_field() {
4364let named = SerdeErrorShape::new()
4365 .tag("type")
4366 .prefix("engine")
4367 .skip(&["n"])
4368 .as_integer(&["rate"])
4369 .rename(&[("rule", "rules")])
4370 .as_character(&["rule"]);
4371let own = SerdeErrorShape::new().tag("kind").as_double(&["n", "rate"]);
4372let parts = serde_err_parts(&engine(), "p", &[&named, &own]);
4373assert_eq!(parts.class, ["engine_rule", "engine"]);
4374assert!(matches!(field(&parts, "n"), RValue::Double(v) if v == &[3.0]));
4375assert!(matches!(field(&parts, "rate"), RValue::Double(v) if v == &[5.0]));
4376assert!(matches!(field(&parts, "rules"), RValue::Character(v) if v.is_empty()));
43774378// A later `skip` drops a field the earlier shape typed; a later
4379 // `rename` keeps the earlier type.
4380let named = SerdeErrorShape::new()
4381 .as_integer(&["n"])
4382 .as_double(&["rate"]);
4383let own = SerdeErrorShape::new()
4384 .prefix("own")
4385 .skip(&["rate"])
4386 .rename(&[("n", "count")]);
4387let parts = serde_err_parts(&engine(), "p", &[&named, &own]);
4388assert_eq!(parts.class, ["own_rule", "own"]);
4389let names: Vec<&str> = parts
4390 .data
4391 .as_deref()
4392 .unwrap_or_default()
4393 .iter()
4394 .map(|(n, _)| n.as_str())
4395 .collect();
4396assert_eq!(names, ["rule", "count", "flags"]);
4397assert!(matches!(field(&parts, "count"), RValue::Integer(v) if v == &[Some(3)]));
43984399// A later rename keeps a field an earlier shape skipped.
4400let named = SerdeErrorShape::new().skip(&["n"]);
4401let own = SerdeErrorShape::new().rename(&[("n", "count")]);
4402let parts = serde_err_parts(&engine(), "p", &[&named, &own]);
4403assert!(matches!(field(&parts, "count"), RValue::Integer(v) if v == &[Some(3)]));
4404 }
44054406/// A field the payload lacks is a no-op, as for `skip` / `rename`.
4407#[test]
4408fn a_field_the_payload_lacks_is_a_no_op() {
4409let shape = SerdeErrorShape::new()
4410 .as_character(&["column"])
4411 .as_integer(&["absent"]);
4412let parts = serde_err_parts(&engine(), "p", &[&shape]);
4413assert_eq!(parts.data.as_deref().unwrap_or_default().len(), 4);
4414 }
4415 }
44164417#[test]
4418fn rerror_reserved_field_rejected_at_err_arm() {
4419use super::RError;
4420let err = std::panic::AssertUnwindSafe(RError::new("m").data("kind", 2));
4421let payload =
4422 std::panic::catch_unwind(move || crate::__mx_result_err_parts!(err.0, "p", NoShapes))
4423 .expect_err("must panic");
4424let msg = payload
4425 .downcast_ref::<String>()
4426 .cloned()
4427 .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
4428 .expect("plain panic message");
4429assert!(msg.contains("reserved"), "got: {msg}");
4430assert!(msg.contains("`kind`"), "got: {msg}");
4431 }
44324433/// The probe's first stage: an `RConditionData` impl wins outright.
4434#[test]
4435fn err_parts_probe_prefers_rconditionerror() {
4436use super::RError;
4437let e = RError::new("m")
4438 .class(["member", "family"])
4439 .data("input", 1);
4440let parts = crate::__mx_result_err_parts!(e, "p", NoShapes);
4441assert_eq!(parts.message, "m");
4442assert_eq!(parts.class, ["member", "family"]);
4443assert_eq!(parts.data.expect("data")[0].0, "input");
4444 }
44454446/// The probe's last resort: a type that is neither `RConditionData` nor
4447 /// `Serialize` renders with `Debug`, no class, no data.
4448#[test]
4449fn err_parts_probe_falls_back_to_debug() {
4450#[derive(Debug)]
4451struct Opaque;
4452let parts = crate::__mx_result_err_parts!(Opaque, "p", NoShapes);
4453assert_eq!(parts.message, "Opaque");
4454assert!(parts.class.is_empty());
4455assert!(parts.data.is_none());
4456 }
44574458/// The conversion probe's built-in arm (#1591): the framework's own
4459 /// conversion errors are not `RConditionData`, keep the plain class
4460 /// vector and no data, and read in R terms instead of their `Display`
4461 /// text. Other error types still render with `Display` (not `Debug`).
4462#[test]
4463fn conversion_probe_rewords_builtin_errors() {
4464use crate::SEXPTYPE;
4465use crate::from_r::{
4466 SexpClassError, SexpError, SexpLengthError, SexpNaError, SexpTypeError,
4467 };
44684469// The probe takes the owned error, as in the generated `Err(e)` arm.
4470let message = |e: &SexpError, known: bool| {
4471crate::__mx_conversion_err_parts!(e.clone(), known).message
4472 };
44734474let e = SexpError::InvalidValue("non-numeric value(s): \"BLQ\" (element 2)".into());
4475let parts = crate::__mx_conversion_err_parts!(e, true);
4476assert_eq!(parts.message, "non-numeric value(s): \"BLQ\" (element 2)");
4477assert!(parts.class.is_empty());
4478assert!(parts.data.is_none());
4479assert_eq!(message(&e, false), message(&e, true));
44804481let e = SexpError::Type(SexpTypeError {
4482 expected: SEXPTYPE::INTSXP,
4483 actual: SEXPTYPE::STRSXP,
4484 });
4485assert_eq!(message(&e, true), "got character");
4486assert_eq!(message(&e, false), "expected integer, got character");
4487// The macro states a type error's expectation; the error gives none.
4488assert_eq!(e.r_expectation(), None);
4489// A class error names the class, as the expectation and in the
4490 // per-element reason.
4491let e = SexpError::Class(SexpClassError {
4492 expected: "a data frame",
4493 actual: SEXPTYPE::VECSXP,
4494 });
4495assert_eq!(message(&e, true), "got list");
4496assert_eq!(message(&e, false), "expected a data frame, got list");
4497assert_eq!(e.r_expectation().as_deref(), Some("a data frame"));
4498let e = SexpError::Length(SexpLengthError {
4499 expected: 1,
4500 actual: 2,
4501 });
4502assert_eq!(message(&e, true), "got length 2");
4503assert_eq!(message(&e, false), "expected length 1, got length 2");
4504let e = SexpError::Na(SexpNaError {
4505 sexp_type: SEXPTYPE::REALSXP,
4506 });
4507assert_eq!(message(&e, true), "NA is not allowed");
4508assert_eq!(message(&e, false), "NA is not allowed");
4509assert_eq!(
4510 message(&SexpError::MissingField("id".into()), true),
4511"missing field 'id'"
4512);
4513assert_eq!(
4514 message(&SexpError::DuplicateName("id".into()), true),
4515"duplicate name 'id'"
4516);
4517// The bare error structs some conversions return take the same arm.
4518let e = SexpTypeError {
4519 expected: SEXPTYPE::REALSXP,
4520 actual: SEXPTYPE::VECSXP,
4521 };
4522assert_eq!(
4523crate::__mx_conversion_err_parts!(e, true).message,
4524"got list"
4525);
4526// The one-argument form is the no-expectation form.
4527assert_eq!(
4528crate::__mx_conversion_err_parts!(e).message,
4529"expected numeric, got list"
4530);
4531let e = SexpClassError {
4532 expected: "a data frame",
4533 actual: SEXPTYPE::INTSXP,
4534 };
4535assert_eq!(
4536crate::__mx_conversion_err_parts!(e, true).message,
4537"got integer"
4538);
4539assert_eq!(
4540crate::__mx_conversion_err_parts!(e).message,
4541"expected a data frame, got integer"
4542);
45434544// `MatchArgError` takes the built-in arm too: R-worded, and after the
4545 // `one of ...` expectation just the input.
4546use crate::match_arg::MatchArgError;
4547const CHOICES: &[&str] = &["fast", "slow"];
4548let e = MatchArgError::IsNa {
4549 choices: CHOICES,
4550 aliases: &[],
4551 };
4552let parts = crate::__mx_conversion_err_parts!(e.clone());
4553assert_eq!(parts.message, r#"expected one of "fast", "slow", got NA"#);
4554assert!(parts.class.is_empty());
4555assert!(parts.data.is_none());
4556assert_eq!(
4557crate::__mx_conversion_err_parts!(e, true).message,
4558"NA is not allowed"
4559);
4560let e = MatchArgError::NoMatch {
4561 input: "zzz".into(),
4562 choices: CHOICES,
4563 aliases: &[],
4564 };
4565assert_eq!(
4566crate::__mx_conversion_err_parts!(e.clone()).message,
4567r#"expected one of "fast", "slow", got "zzz""#
4568);
4569assert_eq!(
4570crate::__mx_conversion_err_parts!(e, true).message,
4571r#"got "zzz""#
4572);
4573let e = MatchArgError::InvalidType {
4574 actual: SEXPTYPE::REALSXP,
4575 choices: CHOICES,
4576 aliases: &[],
4577 };
4578assert_eq!(
4579crate::__mx_conversion_err_parts!(e.clone()).message,
4580r#"expected one of "fast", "slow" (a string or factor), got numeric"#
4581);
4582assert_eq!(
4583crate::__mx_conversion_err_parts!(e, true).message,
4584"got numeric"
4585);
4586let e = MatchArgError::InvalidLength {
4587 actual: 2,
4588 choices: CHOICES,
4589 aliases: &[],
4590 };
4591assert_eq!(
4592crate::__mx_conversion_err_parts!(e, true).message,
4593"got length 2"
4594);
45954596// A plain `&str` / `String` error (neither `Serialize`-classed nor
4597 // `Debug`-quoted, unlike the `Result` probe).
4598let parts = crate::__mx_conversion_err_parts!(String::from("bad input"), true);
4599assert_eq!(parts.message, "bad input");
4600assert!(parts.class.is_empty());
4601 }
46024603/// A `match_arg` choice error knows what the value should have been, as a
4604 /// `MatchArgError` or inside the `SexpError` `#[derive(MatchArg)]`'s
4605 /// `TryFromSexp` returns; other errors do not (#1594). The prefix built
4606 /// from it reads like a statically known one.
4607#[test]
4608fn conversion_expectation_probe_names_match_arg_choices() {
4609use super::conversion_prefix;
4610use crate::from_r::SexpError;
4611use crate::match_arg::MatchArgError;
4612let e = MatchArgError::NoMatch {
4613 input: "zzz".into(),
4614 choices: &["fast", "slow"],
4615 aliases: &[],
4616 };
4617let expected = Some(r#"one of "fast", "slow""#.to_string());
4618assert_eq!(crate::__mx_conversion_expectation!(e.clone()), expected);
4619let e = SexpError::from(e);
4620assert_eq!(crate::__mx_conversion_expectation!(e.clone()), expected);
4621assert_eq!(
4622crate::__mx_conversion_err_parts!(e, true).message,
4623r#"got "zzz""#
4624);
4625assert_eq!(
4626crate::__mx_conversion_expectation!(SexpError::InvalidValue("x".into())),
4627None
4628);
4629assert_eq!(
4630crate::__mx_conversion_expectation!(String::from("bad input")),
4631None
4632);
46334634assert_eq!(
4635 conversion_prefix("mode", false, expected.as_deref()),
4636r#"'mode' must be one of "fast", "slow""#
4637);
4638assert_eq!(
4639 conversion_prefix("mode", true, expected.as_deref()),
4640r#"'mode' must be NULL or one of "fast", "slow""#
4641);
4642assert_eq!(conversion_prefix("x", true, None), "invalid 'x' argument");
4643 }
46444645/// An `Either` whose arms both refused the kind of value (a type or class
4646 /// error, a `match_arg` refusal of a non-string, or a nested `Either` of
4647 /// those) names both arms, as the reason and as the expectation; an arm
4648 /// that got further gives the reason instead.
4649#[cfg(feature = "either")]
4650 #[test]
4651fn either_conversion_names_both_arms_that_refused_the_kind() {
4652use crate::SEXPTYPE;
4653use crate::from_r::{SexpClassError, SexpError, SexpLengthError, SexpTypeError};
4654use crate::match_arg::MatchArgError;
46554656let either = |l: SexpError, r: SexpError| SexpError::EitherConversion {
4657 left_error: Box::new(l),
4658 right_error: Box::new(r),
4659 };
4660let ty = |expected, actual| SexpError::Type(SexpTypeError { expected, actual });
4661let frame = |actual| {
4662 SexpError::Class(SexpClassError {
4663 expected: "a data frame",
4664 actual,
4665 })
4666 };
4667let refused = |actual, choices| {
4668 SexpError::MatchArg(MatchArgError::InvalidType {
4669 actual,
4670 choices,
4671 aliases: &[],
4672 })
4673 };
4674const ROUTES: &[&str] = &["oral", "bolus"];
4675const MODES: &[&str] = &["fast", "bolus"];
4676// (error, reason with the expectation known, reason without it,
4677 // expectation)
4678let check = |e: SexpError, known: &str, unknown: &str, expected: Option<&str>| {
4679assert_eq!(e.r_reason(true), known, "{e}");
4680assert_eq!(e.r_reason(false), unknown, "{e}");
4681assert_eq!(e.r_expectation().as_deref(), expected, "{e}");
4682 };
4683use SEXPTYPE::{INTSXP, LGLSXP, REALSXP, STRSXP, VECSXP};
46844685 check(
4686 either(ty(INTSXP, REALSXP), ty(STRSXP, REALSXP)),
4687"got numeric",
4688"expected integer or character, got numeric",
4689Some("integer or character"),
4690 );
4691 check(
4692 either(ty(REALSXP, VECSXP), frame(VECSXP)),
4693"got list",
4694"expected numeric or a data frame, got list",
4695Some("numeric or a data frame"),
4696 );
4697 check(
4698 either(frame(INTSXP), ty(STRSXP, INTSXP)),
4699"got integer",
4700"expected a data frame or character, got integer",
4701Some("a data frame or character"),
4702 );
4703 check(
4704 either(refused(LGLSXP, ROUTES), ty(REALSXP, LGLSXP)),
4705"got logical",
4706r#"expected one of "oral", "bolus", or numeric, got logical"#,
4707Some(r#"one of "oral", "bolus", or numeric"#),
4708 );
4709 check(
4710 either(ty(REALSXP, LGLSXP), refused(LGLSXP, ROUTES)),
4711"got logical",
4712r#"expected numeric or one of "oral", "bolus", got logical"#,
4713Some(r#"numeric or one of "oral", "bolus""#),
4714 );
4715// Two choice lists read as one, each choice once.
4716check(
4717 either(refused(REALSXP, ROUTES), refused(REALSXP, MODES)),
4718"got numeric",
4719r#"expected one of "oral", "bolus", "fast", got numeric"#,
4720Some(r#"one of "oral", "bolus", "fast""#),
4721 );
4722// The listed aliases follow the choices, before the other arm; an
4723 // unlisted one is left out (#2148). Two choice lists keep both
4724 // lists' aliases, in the order of the joined choices.
4725use crate::match_arg::MatchArgAlias;
4726const ROUTE_ALIASES: &[MatchArgAlias] = &[
4727 MatchArgAlias::new("po", "oral"),
4728 MatchArgAlias::unlisted("iv", "bolus"),
4729 ];
4730const MODE_ALIASES: &[MatchArgAlias] = &[MatchArgAlias::new("quick", "fast")];
4731let aliased = |actual, choices, aliases| {
4732 SexpError::MatchArg(MatchArgError::InvalidType {
4733 actual,
4734 choices,
4735 aliases,
4736 })
4737 };
4738 check(
4739 either(aliased(LGLSXP, ROUTES, ROUTE_ALIASES), ty(REALSXP, LGLSXP)),
4740"got logical",
4741r#"expected one of "oral", "bolus" (or "po" for "oral"), or numeric, got logical"#,
4742Some(r#"one of "oral", "bolus" (or "po" for "oral"), or numeric"#),
4743 );
4744 check(
4745 either(ty(REALSXP, LGLSXP), aliased(LGLSXP, ROUTES, ROUTE_ALIASES)),
4746"got logical",
4747r#"expected numeric or one of "oral", "bolus" (or "po" for "oral"), got logical"#,
4748Some(r#"numeric or one of "oral", "bolus" (or "po" for "oral")"#),
4749 );
4750 check(
4751 either(
4752 aliased(REALSXP, MODES, MODE_ALIASES),
4753 aliased(REALSXP, ROUTES, ROUTE_ALIASES),
4754 ),
4755"got numeric",
4756r#"expected one of "fast", "bolus", "oral" (or "quick" for "fast", "po" for "oral"), got numeric"#,
4757Some(r#"one of "fast", "bolus", "oral" (or "quick" for "fast", "po" for "oral")"#),
4758 );
4759// A nested `Either` whose arms both refused the kind is one arm.
4760let inner = either(refused(VECSXP, ROUTES), ty(REALSXP, VECSXP));
4761 check(
4762 either(ty(LGLSXP, VECSXP), inner),
4763"got list",
4764r#"expected logical or one of "oral", "bolus", or numeric, got list"#,
4765Some(r#"logical or one of "oral", "bolus", or numeric"#),
4766 );
47674768// A string that matched no choice got further than a type error: its
4769 // reason, and no joined expectation.
4770let no_match = SexpError::MatchArg(MatchArgError::NoMatch {
4771 input: "zzz".into(),
4772 choices: ROUTES,
4773 aliases: &[],
4774 });
4775 check(
4776 either(no_match.clone(), ty(REALSXP, STRSXP)),
4777r#"got "zzz""#,
4778r#"expected one of "oral", "bolus", got "zzz""#,
4779None,
4780 );
4781let length = SexpError::Length(SexpLengthError {
4782 expected: 1,
4783 actual: 2,
4784 });
4785 check(
4786 either(refused(REALSXP, ROUTES), length.clone()),
4787"got length 2",
4788"expected length 1, got length 2",
4789None,
4790 );
4791let bad_value = SexpError::InvalidValue("data.frame has no column names".into());
4792 check(
4793 either(ty(REALSXP, VECSXP), bad_value.clone()),
4794"data.frame has no column names",
4795"data.frame has no column names",
4796None,
4797 );
4798 check(
4799 either(bad_value, length),
4800"data.frame has no column names; got length 2",
4801"data.frame has no column names; expected length 1, got length 2",
4802None,
4803 );
4804// A nested `Either` that got further is not a kind mismatch.
4805check(
4806 either(ty(LGLSXP, STRSXP), either(no_match, ty(REALSXP, STRSXP))),
4807r#"got "zzz""#,
4808r#"expected one of "oral", "bolus", got "zzz""#,
4809None,
4810 );
4811 }
48124813/// An `Either` parameter is worded side by side from what the wrapper
4814 /// knows (`from_r::either_expectation`): a known side by its text,
4815 /// whatever its error, so the text is the same for every failure; an
4816 /// opaque side by what its type declares, else by what its own error
4817 /// expected (a kind, the choices of a `match_arg` enum), else by its noun.
4818 /// `None` only when no side is named by more than its noun.
4819#[cfg(feature = "either")]
4820 #[test]
4821fn either_expectation_words_each_side() {
4822use crate::SEXPTYPE::{REALSXP, STRSXP, VECSXP};
4823use crate::from_r::{
4824 ExpectedArm, SexpClassError, SexpError, SexpLengthError, SexpTypeError,
4825 either_expectation,
4826 };
4827use crate::match_arg::MatchArgError;
48284829let either = |l: SexpError, r: SexpError| SexpError::EitherConversion {
4830 left_error: Box::new(l),
4831 right_error: Box::new(r),
4832 };
4833let ty = |expected, actual| SexpError::Type(SexpTypeError { expected, actual });
4834let frame = || {
4835 SexpError::Class(SexpClassError {
4836 expected: "a data frame",
4837 actual: REALSXP,
4838 })
4839 };
4840let length = || {
4841 SexpError::Length(SexpLengthError {
4842 expected: 1,
4843 actual: 2,
4844 })
4845 };
4846let bad_value = || SexpError::InvalidValue("invalid UUID".into());
4847const ROUTES: &[&str] = &["oral", "bolus"];
4848let no_match = || {
4849 SexpError::MatchArg(MatchArgError::NoMatch {
4850 input: "zzz".into(),
4851 choices: ROUTES,
4852 aliases: &[],
4853 })
4854 };
48554856let number = ExpectedArm::Known("a single number");
4857let table = ExpectedArm::Known("a data frame");
4858let dose = ExpectedArm::Opaque {
4859 declared: Some("a single number"),
4860 noun: "a `Dose`",
4861 };
4862let route = ExpectedArm::Opaque {
4863 declared: None,
4864 noun: "a `Route`",
4865 };
4866let doc_id = ExpectedArm::Opaque {
4867 declared: None,
4868 noun: "a `DocId`",
4869 };
4870let pattern = ExpectedArm::Opaque {
4871 declared: None,
4872 noun: "a `Pattern`",
4873 };
4874let words = |arms: &ExpectedArm<'_>, e: SexpError| either_expectation(&e, arms);
48754876// Known sides: the same text whichever side got further.
4877let plain = ExpectedArm::Either(&number, &table);
4878for e in [
4879 either(length(), frame()),
4880 either(ty(REALSXP, VECSXP), frame()),
4881 ] {
4882assert_eq!(
4883 words(&plain, e).as_deref(),
4884Some("a single number or a data frame")
4885 );
4886 }
4887// A newtype side declares its inner type's text.
4888let newtype = ExpectedArm::Either(&dose, &table);
4889assert_eq!(
4890 words(&newtype, either(length(), frame())).as_deref(),
4891Some("a single number or a data frame")
4892 );
4893// An opaque side named by its error: a type error's kind, a
4894 // `match_arg` enum's choices whether or not the value was a string.
4895let by_kind = ExpectedArm::Either(&doc_id, &number);
4896assert_eq!(
4897 words(&by_kind, either(ty(STRSXP, VECSXP), length())).as_deref(),
4898Some("character or a single number")
4899 );
4900let choices = ExpectedArm::Either(&route, &number);
4901for e in [either(no_match(), frame()), either(no_match(), length())] {
4902assert_eq!(
4903 words(&choices, e).as_deref(),
4904Some(r#"one of "oral", "bolus", or a single number"#)
4905 );
4906 }
4907// An opaque side whose error names nothing: its noun, beside a side
4908 // that is known.
4909assert_eq!(
4910 words(&by_kind, either(bad_value(), ty(REALSXP, STRSXP))).as_deref(),
4911Some("a `DocId` or a single number")
4912 );
4913// `NULL or` an optional side, and a nested `Either` side by side.
4914let optional = ExpectedArm::Nullable(&table);
4915let nullable = ExpectedArm::Either(&number, &optional);
4916assert_eq!(
4917 words(&nullable, either(length(), frame())).as_deref(),
4918Some("a single number or NULL or a data frame")
4919 );
4920let inner = ExpectedArm::Either(&route, &table);
4921let nested = ExpectedArm::Either(&number, &inner);
4922assert_eq!(
4923 words(&nested, either(length(), either(no_match(), frame()))).as_deref(),
4924Some(r#"a single number or one of "oral", "bolus", or a data frame"#)
4925 );
4926// No side named by more than its noun: no expectation.
4927let opaque = ExpectedArm::Either(&doc_id, &pattern);
4928assert_eq!(words(&opaque, either(bad_value(), length())), None);
4929// An error that is not an `Either` one does not fit the sides.
4930assert_eq!(words(&plain, length()), None);
4931 }
49324933/// The conversion probe's preferred arm: an `RConditionData` error type
4934 /// contributes its class vector, message and data.
4935#[test]
4936fn conversion_probe_prefers_rconditionerror() {
4937use super::RError;
4938let e = RError::new("must be positive")
4939 .class(["pkg_bad_arg", "pkg_error"])
4940 .data("value", -1);
4941let parts = crate::__mx_conversion_err_parts!(e, true);
4942assert_eq!(parts.message, "must be positive");
4943assert_eq!(parts.class, ["pkg_bad_arg", "pkg_error"]);
4944assert_eq!(parts.data.expect("data")[0].0, "value");
4945 }
49464947/// A reserved data name on a conversion error is rejected like on a
4948 /// `Result` error, instead of overwriting `e$kind`.
4949#[test]
4950fn conversion_probe_rejects_reserved_fields() {
4951use super::RError;
4952let err = std::panic::AssertUnwindSafe(RError::new("m").data("kind", 2));
4953let payload = std::panic::catch_unwind(move || crate::__mx_conversion_err_parts!(err.0))
4954 .expect_err("must panic");
4955let msg = payload
4956 .downcast_ref::<String>()
4957 .cloned()
4958 .expect("plain panic message");
4959assert!(msg.contains("reserved"), "got: {msg}");
4960 }
49614962fn field_names(parts: &super::ErrParts) -> Vec<&str> {
4963 parts
4964 .data
4965 .as_ref()
4966 .map(|d| d.iter().map(|(n, _)| n.as_str()).collect())
4967 .unwrap_or_default()
4968 }
49694970fn character_field<'a>(parts: &'a super::ErrParts, name: &str) -> Option<&'a str> {
4971 parts
4972 .data
4973 .as_ref()?
4974.iter()
4975 .find(|(n, _)| n == name)
4976 .and_then(|(_, v)| match v {
4977crate::RValue::Character(v) => v.first()?.as_deref(),
4978_ => None,
4979 })
4980 }
49814982/// `conversion_err_parts`: the prefix leads the message, the class is
4983 /// kept, `param` comes first in the data and `rust_type` second.
4984#[test]
4985fn conversion_err_parts_adds_prefix_param_and_rust_type() {
4986use super::{ErrParts, conversion_err_parts};
4987use crate::RValue;
49884989let plain = conversion_err_parts(
4990"'dv' must be numeric",
4991"dv",
4992Some("AsNumericVec"),
4993&[],
4994 ErrParts {
4995 message: "non-numeric value(s): \"BLQ\" (element 2)".into(),
4996 class: Vec::new(),
4997 data: None,
4998 call: super::ConditionCall::Inherit,
4999 },
5000None,
5001 );
5002assert_eq!(
5003 plain.message,
5004"'dv' must be numeric: non-numeric value(s): \"BLQ\" (element 2)"
5005);
5006assert!(plain.class.is_empty());
5007assert_eq!(field_names(&plain), ["param", "rust_type"]);
5008assert_eq!(character_field(&plain, "param"), Some("dv"));
5009assert_eq!(character_field(&plain, "rust_type"), Some("AsNumericVec"));
50105011let classed = conversion_err_parts(
5012"invalid 'x' argument",
5013"x",
5014Some("Count"),
5015&[],
5016 ErrParts {
5017 message: "must be positive".into(),
5018 class: vec!["pkg_bad_arg".into(), "pkg_error".into()],
5019 data: Some(vec![("value".into(), RValue::from(-1))]),
5020 call: super::ConditionCall::Inherit,
5021 },
5022None,
5023 );
5024assert_eq!(classed.message, "invalid 'x' argument: must be positive");
5025assert_eq!(classed.class, ["pkg_bad_arg", "pkg_error"]);
5026assert_eq!(field_names(&classed), ["param", "rust_type", "value"]);
50275028// Without a Rust type only `param` is added.
5029let untyped = conversion_err_parts(
5030"'value' must be a number",
5031"value",
5032None,
5033&[],
5034 ErrParts {
5035 message: "got \"abc\"".into(),
5036 class: Vec::new(),
5037 data: None,
5038 call: super::ConditionCall::Inherit,
5039 },
5040None,
5041 );
5042assert_eq!(untyped.message, "'value' must be a number: got \"abc\"");
5043assert_eq!(field_names(&untyped), ["param"]);
5044 }
50455046/// An argument error keeps the call whatever the error type's `call()`
5047 /// says: the R-side checks raise the same condition with the call.
5048#[test]
5049fn conversion_err_parts_keeps_the_call() {
5050use super::{ConditionCall, ErrParts, conversion_err_parts};
50515052let parts = conversion_err_parts(
5053"invalid 'x' argument",
5054"x",
5055None,
5056&[],
5057 ErrParts {
5058 message: "bad".into(),
5059 class: Vec::new(),
5060 data: None,
5061 call: ConditionCall::None,
5062 },
5063None,
5064 );
5065assert_eq!(parts.call, ConditionCall::Inherit);
5066 }
50675068/// The crate's `conversion_error_class` follows the error's own classes,
5069 /// skipping those it already names; a plain error gets only the crate's.
5070#[test]
5071fn conversion_err_parts_appends_the_crate_class() {
5072use super::{ErrParts, conversion_err_parts};
50735074let crate_class = ["pkg_error_argument", "pkg_error"];
5075let plain = conversion_err_parts(
5076"'x' must be a single integer",
5077"x",
5078Some("i32"),
5079&crate_class,
5080 ErrParts {
5081 message: "got character".into(),
5082 class: Vec::new(),
5083 data: None,
5084 call: super::ConditionCall::Inherit,
5085 },
5086None,
5087 );
5088assert_eq!(plain.class, ["pkg_error_argument", "pkg_error"]);
50895090let classed = conversion_err_parts(
5091"invalid 'x' argument",
5092"x",
5093Some("Count"),
5094&crate_class,
5095 ErrParts {
5096 message: "must be positive".into(),
5097 class: vec!["pkg_error_negative".into(), "pkg_error".into()],
5098 data: None,
5099 call: super::ConditionCall::Inherit,
5100 },
5101None,
5102 );
5103assert_eq!(
5104 classed.class,
5105 ["pkg_error_negative", "pkg_error", "pkg_error_argument"]
5106 );
5107 }
51085109/// `arg_check_parts`: the message as given, the crate class alone, and
5110 /// `param` as the only field (no `rust_type`), as the R guard raises it.
5111#[test]
5112fn arg_check_parts_matches_the_r_guard() {
5113use super::arg_check_parts;
51145115let parts = arg_check_parts("'dv' must not contain NA", "dv", &[]);
5116assert_eq!(parts.message, "'dv' must not contain NA");
5117assert!(parts.class.is_empty());
5118assert_eq!(field_names(&parts), ["param"]);
5119assert_eq!(character_field(&parts, "param"), Some("dv"));
51205121let custom = "`x` can't be NA: 100% \"sure\"\ncaf\u{e9}()";
5122let parts = arg_check_parts(custom, "x", &["pkg_error_argument", "pkg_error"]);
5123assert_eq!(parts.message, custom);
5124assert_eq!(parts.class, ["pkg_error_argument", "pkg_error"]);
5125assert_eq!(field_names(&parts), ["param"]);
5126 }
51275128/// A `param` or `rust_type` field of the error type's own wins: the
5129 /// framework's field is not added, so `e$param` / `e$rust_type` read the
5130 /// type's value.
5131#[test]
5132fn conversion_err_parts_keeps_the_types_own_fields() {
5133use super::{ErrParts, conversion_err_parts};
5134use crate::RValue;
51355136let parts = conversion_err_parts(
5137"invalid 'hyper' argument",
5138"hyper",
5139Some("Hyperparams"),
5140&[],
5141 ErrParts {
5142 message: "hyperparameter 'beta' must be non-negative".into(),
5143 class: Vec::new(),
5144 data: Some(vec![
5145 ("value".into(), RValue::from(-2.0)),
5146 ("param".into(), RValue::from("beta")),
5147 ]),
5148 call: super::ConditionCall::Inherit,
5149 },
5150None,
5151 );
5152assert_eq!(field_names(&parts), ["rust_type", "value", "param"]);
5153assert_eq!(character_field(&parts, "param"), Some("beta"));
51545155let parts = conversion_err_parts(
5156"invalid 'x' argument",
5157"x",
5158Some("Wrapped"),
5159&[],
5160 ErrParts {
5161 message: "bad".into(),
5162 class: Vec::new(),
5163 data: Some(vec![("rust_type".into(), RValue::from("Inner"))]),
5164 call: super::ConditionCall::Inherit,
5165 },
5166None,
5167 );
5168assert_eq!(field_names(&parts), ["param", "rust_type"]);
5169assert_eq!(character_field(&parts, "rust_type"), Some("Inner"));
5170 }
51715172/// An error that words the whole argument error gives the message in
5173 /// place of `<prefix>: <reason>`; class and fields are as for any other.
5174#[test]
5175fn conversion_err_parts_takes_a_whole_message() {
5176use super::{ErrParts, conversion_err_parts};
51775178let parts = conversion_err_parts(
5179"'mode' must be one of \"fast\", \"slow\"",
5180"mode",
5181Some("Mode"),
5182&["pkg_error_argument"],
5183 ErrParts {
5184 message: "got \"zzz\"".into(),
5185 class: Vec::new(),
5186 data: None,
5187 call: super::ConditionCall::Inherit,
5188 },
5189Some("'mode' should be one of \"fast\", \"slow\"".into()),
5190 );
5191assert_eq!(parts.message, "'mode' should be one of \"fast\", \"slow\"");
5192assert_eq!(parts.class, ["pkg_error_argument"]);
5193assert_eq!(field_names(&parts), ["param", "rust_type"]);
5194 }
51955196/// A `match_arg` choice error, bare or at the top of a `SexpError`, words
5197 /// the whole argument error as `.miniextendr_match_arg` does (#1767);
5198 /// other errors do not.
5199#[test]
5200fn conversion_arg_message_probe_words_match_arg_errors_as_the_check() {
5201use crate::SEXPTYPE;
5202use crate::from_r::SexpError;
5203use crate::match_arg::MatchArgError;
52045205const CHOICES: &[&str] = &["fast", "slow"];
5206let one_of = Some(r#"'mode' should be one of "fast", "slow""#.to_string());
5207let no_match = MatchArgError::NoMatch {
5208 input: "zzz".into(),
5209 choices: CHOICES,
5210 aliases: &[],
5211 };
5212assert_eq!(
5213crate::__mx_conversion_arg_message!(no_match.clone(), "mode"),
5214 one_of
5215 );
5216assert_eq!(
5217crate::__mx_conversion_arg_message!(SexpError::from(no_match), "mode"),
5218 one_of
5219 );
5220let na = MatchArgError::IsNa {
5221 choices: CHOICES,
5222 aliases: &[],
5223 };
5224assert_eq!(crate::__mx_conversion_arg_message!(na, "mode"), one_of);
5225let long = MatchArgError::InvalidLength {
5226 actual: 2,
5227 choices: CHOICES,
5228 aliases: &[],
5229 };
5230assert_eq!(
5231crate::__mx_conversion_arg_message!(SexpError::from(long), "mode").as_deref(),
5232Some("'mode' must be of length 1")
5233 );
5234let number = MatchArgError::InvalidType {
5235 actual: SEXPTYPE::REALSXP,
5236 choices: CHOICES,
5237 aliases: &[],
5238 };
5239assert_eq!(
5240crate::__mx_conversion_arg_message!(number, "speed").as_deref(),
5241Some("'speed' must be NULL or a character vector")
5242 );
52435244assert_eq!(
5245crate::__mx_conversion_arg_message!(SexpError::InvalidValue("x".into()), "mode"),
5246None
5247);
5248assert_eq!(
5249crate::__mx_conversion_arg_message!(String::from("bad input"), "mode"),
5250None
5251);
5252 }
52535254/// A choice error inside an `Either` leaves the message to the `Either`,
5255 /// which names both arms.
5256#[cfg(feature = "either")]
5257 #[test]
5258fn conversion_arg_message_probe_leaves_an_either_alone() {
5259use crate::from_r::SexpError;
5260use crate::match_arg::MatchArgError;
52615262let either = SexpError::EitherConversion {
5263 left_error: Box::new(SexpError::from(MatchArgError::NoMatch {
5264 input: "zzz".into(),
5265 choices: &["fast", "slow"],
5266 aliases: &[],
5267 })),
5268 right_error: Box::new(SexpError::InvalidValue("x".into())),
5269 };
5270assert_eq!(crate::__mx_conversion_arg_message!(either, "mode"), None);
5271 }
52725273/// A classed error with an `argument_message` words the whole argument
5274 /// error, directly or carried in a `SexpError` from a type's
5275 /// `check_sexp`; without one, or inside an `Either`, it does not (#1833).
5276#[test]
5277fn conversion_arg_message_probe_takes_a_classed_errors_argument_message() {
5278use super::{ConditionCall, RConditionData, RError};
5279use crate::from_r::SexpError;
52805281let advice = "use model_from_df() for a data frame";
5282let refusal = || {
5283 RError::new("got a data frame")
5284 .class("mx_model_refused")
5285 .argument_message(advice)
5286 };
5287assert_eq!(
5288crate::__mx_conversion_arg_message!(refusal(), "fit").as_deref(),
5289Some(advice)
5290 );
5291let carried = SexpError::from(refusal());
5292assert_eq!(
5293crate::__mx_conversion_arg_message!(carried, "fit").as_deref(),
5294Some(advice)
5295 );
5296assert_eq!(
5297crate::__mx_conversion_arg_message!(RError::new("got a data frame"), "fit"),
5298None
5299);
5300assert_eq!(
5301crate::__mx_conversion_arg_message!(SexpError::from(RError::new("no")), "fit"),
5302None
5303);
53045305// A hand-written impl answers through the trait method.
5306struct ModelRefused;
5307impl RConditionData for ModelRefused {
5308fn message(&self) -> String {
5309"got a data frame".into()
5310 }
5311fn argument_message(&self) -> Option<String> {
5312Some("use model_from_df() for a data frame".into())
5313 }
5314 }
5315assert_eq!(
5316crate::__mx_conversion_arg_message!(ModelRefused, "fit").as_deref(),
5317Some(advice)
5318 );
5319// The conversion into `SexpError` keeps it, and the call choice.
5320let SexpError::Condition(kept) = SexpError::from(ModelRefused) else {
5321panic!("a classed error converts into SexpError::Condition");
5322 };
5323assert_eq!(kept.argument_message_str(), Some(advice));
5324assert!(matches!(kept.call(), ConditionCall::Inherit));
5325 }
53265327/// A type's refusal inside an `Either` leaves the message to the
5328 /// `Either`, which names both arms.
5329#[cfg(feature = "either")]
5330 #[test]
5331fn conversion_arg_message_probe_leaves_a_classed_refusal_in_an_either_alone() {
5332use super::RError;
5333use crate::from_r::SexpError;
53345335let either = SexpError::EitherConversion {
5336 left_error: Box::new(SexpError::from(
5337 RError::new("got a data frame").argument_message("use model_from_df()"),
5338 )),
5339 right_error: Box::new(SexpError::InvalidValue("x".into())),
5340 };
5341assert_eq!(crate::__mx_conversion_arg_message!(either, "fit"), None);
5342 }
53435344#[test]
5345fn rerror_builder_and_from_error() {
5346use super::{RConditionData, RError};
5347let e: RError = "x".parse::<i32>().unwrap_err().into();
5348let e = e.class(["member", "family"]).data("input", "x");
5349assert!(e.message_str().contains("invalid digit"));
5350assert_eq!(RConditionData::class(&e), vec!["member", "family"]);
5351let data = RConditionData::data(&e).expect("data");
5352assert_eq!(data[0].0, "input");
53535354assert!(RConditionData::data(&RError::new("m")).is_none());
5355assert!(super::is_reserved_condition_field("call"));
5356assert!(!super::is_reserved_condition_field("calls"));
5357 }
53585359#[test]
5360fn error_message_only_backcompat() {
5361let cond = catch(|| crate::error!("plain {}", 42));
5362match cond {
5363 RCondition::Error {
5364 message,
5365 class,
5366 data,
5367 call: super::ConditionCall::Inherit,
5368 } => {
5369assert_eq!(message, "plain 42");
5370assert!(class.is_empty());
5371assert!(data.is_none());
5372 }
5373 other => panic!("wrong variant: {other:?}"),
5374 }
5375 }
53765377#[test]
5378fn error_class_only_backcompat() {
5379let cond = catch(|| crate::error!(class = "my_error", "missing field: {}", "x"));
5380match cond {
5381 RCondition::Error {
5382 message,
5383 class,
5384 data,
5385 call: super::ConditionCall::Inherit,
5386 } => {
5387assert_eq!(message, "missing field: x");
5388assert_eq!(class, vec!["my_error"]);
5389assert!(data.is_none());
5390 }
5391 other => panic!("wrong variant: {other:?}"),
5392 }
5393 }
53945395#[test]
5396fn error_single_data_pair() {
5397let value = 41_i32;
5398let cond = catch(move || crate::error!(data = ("value", value), "v = {value}"));
5399match cond {
5400 RCondition::Error {
5401 message,
5402 class,
5403 data,
5404 call: super::ConditionCall::Inherit,
5405 } => {
5406assert_eq!(message, "v = 41");
5407assert!(class.is_empty());
5408 assert_data(&data, &[("value", RValue::Integer(vec![Some(41)]))]);
5409 }
5410 other => panic!("wrong variant: {other:?}"),
5411 }
5412 }
54135414#[test]
5415fn error_class_and_data_list_all_value_types() {
5416let cond = catch(|| {
5417crate::error!(
5418 class = "validation_error",
5419 data = [
5420 ("value", 1.5),
5421 ("code", 7),
5422 ("label", "lhs"),
5423 ("fatal", false),
5424 ("ints", vec![1, 2]),
5425 ("reals", vec![0.5_f64]),
5426 ("flags", vec![true]),
5427 ("labels", vec!["a".to_string()])
5428 ],
5429"out of range"
5430)
5431 });
5432match cond {
5433 RCondition::Error {
5434 message,
5435 class,
5436 data,
5437 call: super::ConditionCall::Inherit,
5438 } => {
5439assert_eq!(message, "out of range");
5440assert_eq!(class, vec!["validation_error"]);
5441 assert_data(
5442&data,
5443&[
5444 ("value", RValue::Double(vec![1.5])),
5445 ("code", RValue::Integer(vec![Some(7)])),
5446 ("label", RValue::Character(vec![Some("lhs".into())])),
5447 ("fatal", RValue::Logical(vec![Some(false)])),
5448 ("ints", RValue::Integer(vec![Some(1), Some(2)])),
5449 ("reals", RValue::Double(vec![0.5])),
5450 ("flags", RValue::Logical(vec![Some(true)])),
5451 ("labels", RValue::Character(vec![Some("a".into())])),
5452 ],
5453 );
5454 }
5455 other => panic!("wrong variant: {other:?}"),
5456 }
5457 }
54585459#[test]
5460fn warning_with_class_and_data() {
5461let cond = catch(|| crate::warning!(class = "trunc", data = ("dropped", 3), "dropped"));
5462match cond {
5463 RCondition::Warning {
5464 message,
5465 class,
5466 data,
5467 call: super::ConditionCall::Inherit,
5468 } => {
5469assert_eq!(message, "dropped");
5470assert_eq!(class, vec!["trunc"]);
5471 assert_data(&data, &[("dropped", RValue::Integer(vec![Some(3)]))]);
5472 }
5473 other => panic!("wrong variant: {other:?}"),
5474 }
5475 }
54765477#[test]
5478fn message_with_data() {
5479let cond = catch(|| crate::message!(data = ("step", 2), "step {}", 2));
5480match cond {
5481 RCondition::Message { message, data, .. } => {
5482assert_eq!(message, "step 2");
5483 assert_data(&data, &[("step", RValue::Integer(vec![Some(2)]))]);
5484 }
5485 other => panic!("wrong variant: {other:?}"),
5486 }
5487 }
54885489#[test]
5490fn condition_with_class_and_data() {
5491let cond =
5492 catch(|| crate::condition!(class = "progress", data = [("n", 10)], "processed {}", 10));
5493match cond {
5494 RCondition::Condition {
5495 message,
5496 class,
5497 data,
5498 call: super::ConditionCall::Inherit,
5499 } => {
5500assert_eq!(message, "processed 10");
5501assert_eq!(class, vec!["progress"]);
5502 assert_data(&data, &[("n", RValue::Integer(vec![Some(10)]))]);
5503 }
5504 other => panic!("wrong variant: {other:?}"),
5505 }
5506 }
55075508#[test]
5509fn call_none_on_every_condition_macro() {
5510use super::ConditionCall;
55115512match catch(|| crate::error!(call = none, "plain {}", 1)) {
5513 RCondition::Error {
5514 message,
5515 class,
5516 data,
5517 call,
5518 } => {
5519assert_eq!(message, "plain 1");
5520assert!(class.is_empty());
5521assert!(data.is_none());
5522assert_eq!(call, ConditionCall::None);
5523 }
5524 other => panic!("wrong variant: {other:?}"),
5525 }
5526match catch(|| crate::rust_error!(call = none, class = ["member", "family"], "e")) {
5527 RCondition::Error { class, call, .. } => {
5528assert_eq!(class, vec!["member", "family"]);
5529assert_eq!(call, ConditionCall::None);
5530 }
5531 other => panic!("wrong variant: {other:?}"),
5532 }
5533match catch(|| {
5534crate::warning!(
5535 call = none,
5536 class = "pkg_override",
5537 data = { profile = 3 },
5538"overrides an earlier row"
5539)
5540 }) {
5541 RCondition::Warning {
5542 message,
5543 class,
5544 data,
5545 call,
5546 } => {
5547assert_eq!(message, "overrides an earlier row");
5548assert_eq!(class, vec!["pkg_override"]);
5549 assert_data(&data, &[("profile", RValue::Integer(vec![Some(3)]))]);
5550assert_eq!(call, ConditionCall::None);
5551 }
5552 other => panic!("wrong variant: {other:?}"),
5553 }
5554match catch(|| crate::condition!(call = none, data = ("n", 1), "tick")) {
5555 RCondition::Condition { data, call, .. } => {
5556 assert_data(&data, &[("n", RValue::Integer(vec![Some(1)]))]);
5557assert_eq!(call, ConditionCall::None);
5558 }
5559 other => panic!("wrong variant: {other:?}"),
5560 }
5561match catch(|| crate::rust_condition!(call = none, "tick")) {
5562 RCondition::Condition { call, .. } => assert_eq!(call, ConditionCall::None),
5563 other => panic!("wrong variant: {other:?}"),
5564 }
5565// Without `call = none` the call is inherited.
5566match catch(|| crate::warning!("sibling")) {
5567 RCondition::Warning { call, .. } => assert_eq!(call, ConditionCall::Inherit),
5568 other => panic!("wrong variant: {other:?}"),
5569 }
5570 }
55715572#[test]
5573fn rerror_without_call_reaches_the_err_parts() {
5574use super::{ConditionCall, RConditionData, RError};
55755576let plain = RError::new("m").class("c");
5577assert_eq!(RConditionData::call(&plain), ConditionCall::Inherit);
5578let callless = plain.clone().without_call();
5579assert_eq!(RConditionData::call(&callless), ConditionCall::None);
5580let parts = crate::__mx_result_err_parts!(callless, "pkg_error", NoShapes);
5581assert_eq!(parts.call, ConditionCall::None);
5582assert_eq!(parts.class, vec!["c"]);
5583let parts = crate::__mx_result_err_parts!(plain, "pkg_error", NoShapes);
5584assert_eq!(parts.call, ConditionCall::Inherit);
5585 }
55865587#[test]
5588fn data_list_trailing_comma() {
5589let cond = catch(|| crate::error!(data = [("a", 1), ("b", 2),], "msg"));
5590match cond {
5591 RCondition::Error { data, .. } => {
5592 assert_data(
5593&data,
5594&[
5595 ("a", RValue::Integer(vec![Some(1)])),
5596 ("b", RValue::Integer(vec![Some(2)])),
5597 ],
5598 );
5599 }
5600 other => panic!("wrong variant: {other:?}"),
5601 }
5602 }
56035604// region: keyed builder sugar (ported from #1044/#995)
56055606#[test]
5607fn keyed_builder_arm_stringifies_idents() {
5608let cond = catch(|| crate::error!(data = { value = 42, code = 7 }, "boom"));
5609match cond {
5610 RCondition::Error { data, .. } => {
5611 assert_data(
5612&data,
5613&[
5614 ("value", RValue::Integer(vec![Some(42)])),
5615 ("code", RValue::Integer(vec![Some(7)])),
5616 ],
5617 );
5618 }
5619 other => panic!("wrong variant: {other:?}"),
5620 }
5621 }
56225623#[test]
5624fn keyed_builder_arm_trailing_comma_and_mixed_types() {
5625let cond = catch(|| {
5626crate::warning!(
5627 class = "trunc",
5628 data = { dropped = 3, ratio = 0.5_f64, tag = "rows", },
5629"dropped some"
5630)
5631 });
5632match cond {
5633 RCondition::Warning { data, class, .. } => {
5634assert_eq!(class, vec!["trunc"]);
5635 assert_data(
5636&data,
5637&[
5638 ("dropped", RValue::Integer(vec![Some(3)])),
5639 ("ratio", RValue::Double(vec![0.5])),
5640 ("tag", RValue::Character(vec![Some("rows".into())])),
5641 ],
5642 );
5643 }
5644 other => panic!("wrong variant: {other:?}"),
5645 }
5646 }
56475648// endregion
56495650 // region: NA-aware + wide-int + debug value types via the macro (#995)
56515652#[test]
5653fn option_scalar_fields_carry_na() {
5654let cond = catch(|| {
5655crate::error!(
5656 data = [("present", Some(9_i32)), ("missing", None::<i32>)],
5657"opts"
5658)
5659 });
5660match cond {
5661 RCondition::Error { data, .. } => {
5662 assert_data(
5663&data,
5664&[
5665 ("present", RValue::Integer(vec![Some(9)])),
5666 ("missing", RValue::Integer(vec![None])),
5667 ],
5668 );
5669 }
5670 other => panic!("wrong variant: {other:?}"),
5671 }
5672 }
56735674#[test]
5675fn vec_option_field_carries_embedded_na() {
5676let cond =
5677 catch(|| crate::error!(data = ("codes", vec![Some(1_i32), None, Some(3)]), "vec"));
5678match cond {
5679 RCondition::Error { data, .. } => {
5680 assert_data(
5681&data,
5682&[("codes", RValue::Integer(vec![Some(1), None, Some(3)]))],
5683 );
5684 }
5685 other => panic!("wrong variant: {other:?}"),
5686 }
5687 }
56885689#[test]
5690fn wide_integer_ladder_via_macro() {
5691// Fits in i32 → integer; beyond → double.
5692let cond = catch(|| {
5693crate::error!(
5694 data = [("small", 42_i64), ("big", 5_000_000_000_i64)],
5695"wide"
5696)
5697 });
5698match cond {
5699 RCondition::Error { data, .. } => {
5700 assert_data(
5701&data,
5702&[
5703 ("small", RValue::Integer(vec![Some(42)])),
5704 ("big", RValue::Double(vec![5_000_000_000.0])),
5705 ],
5706 );
5707 }
5708 other => panic!("wrong variant: {other:?}"),
5709 }
5710 }
57115712#[test]
5713fn debug_fallback_via_macro() {
5714let cond = catch(|| crate::error!(data = ("range", RValue::debug(0..=100)), "dbg"));
5715match cond {
5716 RCondition::Error { data, .. } => {
5717 assert_data(
5718&data,
5719&[("range", RValue::Character(vec![Some("0..=100".into())]))],
5720 );
5721 }
5722 other => panic!("wrong variant: {other:?}"),
5723 }
5724 }
57255726// endregion
5727}
57285729// endregion