Skip to main content

Module deferred_condition

Module deferred_condition 

Source
Expand description

Deferred conditions: signal an R warning, message or condition and still return a value.

crate::warning!, crate::message! and crate::condition! unwind the Rust function: R sees the signal, the call returns invisible(NULL). That is the right shape for “stop here, but softly”, not for “here is the result, and by the way …”. The defer_* family queues the condition instead; the generated C wrapper signals everything queued during the call once the Rust body has returned, in queue order, and then hands the value to R.

ⓘ
use miniextendr_api::{defer_warning, miniextendr};
use miniextendr_api::RCondition;

#[derive(Debug, RCondition)]
#[condition(class = "pkg_warning")]
enum PkgWarning {
    #[condition(message = "dropped {dropped} of {total} rows")]
    Truncated { dropped: i32, total: i32 },
}

#[miniextendr]
fn trim(n: i32) -> i32 {
    defer_warning(PkgWarning::Truncated { dropped: 2, total: n });
    n - 2
}
trim(5L)
# [1] 3
# Warning message:
# In trim(5L) : dropped 2 of 5 rows
w <- tryCatch(trim(5L), pkg_warning_truncated = function(w) w)
w$dropped               # 2
suppressWarnings(trim(5L))   # 3

§What R sees

Exactly what warning() / message() / signalCondition() called from an R function would produce, because that is what runs: the same R helper the generated wrappers use for the panicking macros re-raises each queued condition. Class layering (c(<classes…>, "rust_warning", "simpleWarning", "warning", "condition")), data fields as w$<name> and conditionCall() (the wrapper’s call, or NULL for a condition queued with call = none) are identical to the immediate forms. tryCatch(warning = ) exits the call before the value is returned; withCallingHandlers + invokeRestart("muffleWarning") keeps it; suppressWarnings() / suppressMessages() work as usual.

§Ordering

Queued conditions are signalled after the Rust body finished and before R sees its outcome, so a deferred warning followed by rust_error! (or an Err return) is signalled first and the error second, and two deferred conditions keep their queue order. Nested calls (R code evaluated from Rust that calls back into another #[miniextendr] function) flush only what they queued themselves.

§Capturing a block’s conditions

capture runs a closure and returns what it queued instead of signalling it, for code whose diagnostics the caller already has (a replay of the steps that built a saved object). Conditions queued outside the closure are untouched.

§Warning placement

Conditions queued before a failure still signal. If a body must not warn on failure, collect warning payloads locally and queue them only after its last fallible step (including any ?). Moving a warning before a later ? changes that behavior: returning Err does not cancel the queued warning.

use miniextendr_api::{defer_warning, miniextendr};

#[miniextendr]
pub fn parse_trimmed(text: String) -> Result<i32, std::num::ParseIntError> {
    let trimmed = text.trim();
    let value = trimmed.parse::<i32>()?;
    if trimmed != text {
        defer_warning!(class = "pkg_trimmed", "removed surrounding whitespace");
    }
    Ok(value)
}

Here, parsing " bad " returns an error without a warning; parsing " 42 " queues the whitespace warning and returns 42.

§Payloads

defer_warning(), defer_message() and defer_condition() take any RConditionData value: a #[derive(RCondition)] enum or struct (class from the type and variant names, message from a format! string or Display, fields as data), or a hand-built crate::condition::RError. The defer_warning! / defer_message! / defer_condition! macros take the crate::warning! grammar (class = …, data = …, format message) for one-off conditions.

§Threads and boundaries

The queue is a process-wide Mutex, so #[miniextendr(worker)] bodies push from the worker thread; the signalling always happens on R’s main thread after the worker result is back. Every generated .Call entry point and every trait-ABI vtable shim flushes what was queued inside it. ALTREP RUnwind / RustUnwind callbacks and connection I/O callbacks also signal locally, with conditionCall() = NULL; raising guards use their explicit call argument. An R-origin error discards the abandoned queue.

External-pointer finalizers and connection close/destroy/Drop callbacks suppress deferred conditions: R finalization removes the caller’s handlers. No R signalling or delayed delivery is attempted from destructors guarded by drop_catching_panic. Unguarded raw FFI / AltrepGuard::Unsafe callbacks must not queue conditions; choose a guarded callback instead.

Structs§

Suppress 🔒
Finalization has no caller condition handlers. Suppress queues throughout the destructor, including any nested framework boundaries, without R calls.

Constants§

SUPPRESSED 🔒

Statics§

PENDING 🔒
Conditions queued during the current guarded callback (and enclosing calls).

Functions§

capture
Run f and return the conditions it queued, oldest first, instead of signalling them when the guarded call returns.
defer 👻
Queue an already-built condition payload. Used by the defer_*! macros.
defer_condition
Queue a plain condition (signalCondition(): silent without a handler) to be signalled when the current guarded call or callback returns.
defer_message
Queue a message (message(), muffled by suppressMessages()) to be emitted when the current guarded call or callback returns.
defer_warning
Queue a warning to be signalled when the current guarded call or callback returns; the call still returns its value.
discard 🔒
Discard conditions whose Rust/R call was abandoned by an R longjmp.
finish 👻 ⚠
Signal everything queued since mark, then return value.
finish_guarded 🔒
Flush a non-raising callback’s conditions after its Rust guard has finished. R objects in value must already be rooted (the same contract as or_raise).
mark 👻
The queue position at the start of a call. Pair with finish.
pending 🔒
raise_condition_helper 🔒
The .miniextendr_raise_condition closure, evaluated once from the shared source (crate::registry::RAISE_CONDITION_HELPER_FN) in the base namespace and preserved for the life of the process.
signal_now 🔒 ⚠
Materialise each queued condition and hand it to the R helper.
take_pending 👻
Remove and return everything queued since mark, oldest first. Entries below mark belong to enclosing calls and stay.