Skip to main content

miniextendr_api/
error_value.rs

1//! Tagged condition value transport.
2//!
3//! Rust-origin failures (panics, `Result::Err`, `Option::None`) and user-raised
4//! conditions (`error!()`, `warning!()`, `message!()`, `condition!()`) are
5//! converted to a tagged SEXP value instead of raising an R error immediately.
6//! The generated R wrapper inspects this tagged value and escalates it to a
7//! proper R condition past the Rust boundary, with `rust_*` class layering.
8//!
9//! # Why tagged SEXP instead of `Rf_error`
10//!
11//! The naive way to surface a Rust error in R is to call `Rf_error`, which
12//! `longjmp`s out of the call frame. That works for C — C has no destructors —
13//! but in Rust it skips every drop on the stack: open files, `Mutex` guards,
14//! `Box::into_raw` round-trips, the worker-thread continuation token. Anything
15//! holding a resource leaks or corrupts.
16//!
17//! The framework instead catches every Rust panic (and every `RCondition`
18//! `panic_any` payload) at the boundary inside
19//! [`crate::unwind_protect::with_r_unwind_protect`], encodes it as the
20//! 4-element list described below, and *returns* that SEXP normally. The
21//! generated R wrapper then re-raises with `stop(structure(..., class =
22//! c("rust_*", ...)))`. Destructors run; `tryCatch` sees the right class.
23//!
24//! There is one accepted leak: on the R-longjmp branch inside
25//! `with_r_unwind_protect` (when an R-origin error is propagated through via
26//! `R_ContinueUnwind`), the `RErrorMarker` panic payload — about 8 bytes plus
27//! `Box` header — escapes Rust drop ordering. This is the price we pay for
28//! routing Rust failures through real R conditions instead of
29//! `Rf_error`-via-longjmp, and is exactly why lint MXL300 forbids direct
30//! `Rf_error` / `Rf_errorcall` in user code: every `Rf_error` skips Rust
31//! destructors unconditionally, not just on the (rare) R-longjmp path.
32//!
33//! # The three error-emission entry points
34//!
35//! Authors of `#[miniextendr]` functions reach for one of:
36//!
37//! 1. **`panic!(msg)`** — escape hatch. Produces `kind = "panic"` and R class
38//!    `c("rust_error", "simpleError", "error", "condition")`. Use for true
39//!    bugs / impossible states; the caller has nothing to catch by class.
40//! 2. **`miniextendr_api::error!("msg")`** — typed condition. Produces `kind
41//!    = "error"` and the same `rust_error` class layering. The `class =
42//!    "my_class"` form prepends a user class, giving R-side
43//!    `c("rust_my_class", "rust_error", "simpleError", "error", "condition")`
44//!    — exactly what a caller's `tryCatch(my_class = …)` matches on. The
45//!    sibling [`crate::warning!`], [`crate::message!`], [`crate::condition!`]
46//!    macros cover the non-error condition kinds.
47//! 3. **`Result<_, E>` where `E: std::error::Error`**, often via
48//!    [`crate::condition::AsRError`] — value-style propagation through Rust
49//!    code. Converts at the boundary using `kind = "result_err"`.
50//!    `Option::None` follows the same path with `kind = "none_err"`.
51//!
52//! # `error_in_r` is the default
53//!
54//! For every `#[miniextendr]` fn / method, the proc macro emits a wrapper that
55//! routes through this tagged-SEXP transport — i.e. `error_in_r = true` is the
56//! default. The opt-outs are documented on the macro:
57//!
58//! - `#[miniextendr(no_error_in_r)]` — bypass the tagged-SEXP path entirely.
59//!   Useful for trait-ABI vtable shims and benchmarks; Rust panics become
60//!   classic `Rf_error` longjmps. Drops the leak above at the cost of skipping
61//!   Rust destructors universally.
62//! - `#[miniextendr(unwrap_in_r)]` — `Result<T, E>` returns are unwrapped on
63//!   the R side rather than encoded as `kind = "result_err"`. Orthogonal to
64//!   the transport: still rides this SEXP path, just changes how `Err` is
65//!   stringified.
66//!
67//! Older comments suggesting `Rf_error` is the user-facing path predate PR
68//! #344 and are wrong. The wrapper preambles now consistently use this
69//! transport.
70//!
71//! # Condition value structure (`make_rust_condition_value`)
72//!
73//! The tagged SEXP is a 5-element named list:
74//! - `error`: error message (character scalar)
75//! - `kind`: condition kind string — one of the constants in [`kind`]
76//! - `class`: optional user-supplied custom class (character scalar or `NULL`)
77//! - `call`: the R call SEXP, `NULL` if not available (the R helper then uses
78//!   the wrapper's call), or `FALSE` for a condition raised without a call
79//!   ([`ConditionCall::None`]: the helper signals it with `call = NULL`, like
80//!   R's `call. = FALSE`)
81//! - `data`: optional named-list condition-data payload (from the macros'
82//!   `data = ...` form), or `NULL`. The R helper splices these named fields
83//!   into the condition object so handlers can read `e$<name>`.
84//! - class attribute: `"rust_condition_value"`
85//! - `__rust_condition__` attribute: `TRUE`
86//!
87//! # PROTECT discipline (read before editing)
88//!
89//! [`make_rust_condition_value`] allocates SEXPs that must remain live
90//! across subsequent allocations (`SET_VECTOR_ELT` / `SETATTRIB` both
91//! trigger old-to-new GC barriers): the list itself, the message scalar
92//! STRSXP, the kind scalar STRSXP, the optional class scalar STRSXP, the
93//! `FALSE` no-call marker LGLSXP when the condition has no call, the
94//! `TRUE` marker LGLSXP, and — when a `data` payload is present — the data
95//! VECSXP, its names STRSXP, and each materialised field value. Each is
96//! added to a single [`ProtectScope`](crate::ProtectScope) before the next
97//! allocation; the scope releases them all together (`UNPROTECT`) when it
98//! drops at function exit — on every branch. Field values are
99//! materialised one at a time and rooted into the protected data list
100//! immediately (same shape as `List::from_pairs`) so an unrooted value SEXP
101//! never survives across the next allocation.
102//!
103//! R-devel runs a more aggressive GC than R-release/oldrel and *will* fire
104//! inside the window between two allocations. PR #344 commit `af6b4875`
105//! tracked down a `recursive gc invocation` segfault that lit up only on
106//! R-devel because the pre-existing 3-element version was lucky-not-safe;
107//! adding the class slot crossed the threshold. **If you add another fresh
108//! allocation, protect it.** A green R-release CI run is *not* proof of
109//! safety here; run `gctorture(TRUE)` on R-devel before merging.
110
111use crate::cached_class::{
112    condition_names_sexp, rust_condition_attr_symbol, rust_condition_class_sexp,
113};
114use crate::condition::{ConditionCall, ErrParts, RCondition};
115use crate::sexp_types::CE_UTF8;
116use crate::sys::{self};
117use crate::{IntoR, SEXP, SEXPTYPE, SexpExt};
118
119/// Canonical kind strings for tagged condition values.
120///
121/// These constants are emitted into the `kind` slot of
122/// [`make_rust_condition_value`] and consumed by the R-side
123/// `.miniextendr_raise_condition` switch (see
124/// `registry::write_r_wrappers_to_file`). Reference these constants
125/// from codegen and runtime sites instead of bare string literals so a
126/// typo cannot silently change which switch arm fires.
127///
128/// The constants are kept in lockstep with the generated R helper; if a new
129/// kind is added, both the emission site and the R helper need to learn it.
130pub mod kind {
131    /// Default kind for Rust panics that surface to R via the generic panic
132    /// path (no `RCondition` payload). Layered as `rust_error`.
133    pub const PANIC: &str = "panic";
134    /// `Result<_, E>::Err(...)` formatted via `Debug` (raised when the user
135    /// returns an `Err` from a `#[miniextendr]` fn/method).
136    pub const RESULT_ERR: &str = "result_err";
137    /// `Option<T>::None` reached where a value was required (raised by the
138    /// `NoneOnErr` / required-Option return paths).
139    pub const NONE_ERR: &str = "none_err";
140    /// An argument error: a `TryFromSexp` / coerce / strict-mode conversion
141    /// failed at argument unmarshalling, a post-conversion check refused the
142    /// value, or a body raised `arg_error!(...)`.
143    pub const CONVERSION: &str = "conversion";
144    /// User-raised `error!(...)` condition.
145    pub const ERROR: &str = "error";
146    /// User-raised `warning!(...)` condition.
147    pub const WARNING: &str = "warning";
148    /// User-raised `message!(...)` condition.
149    pub const MESSAGE: &str = "message";
150    /// User-raised `condition!(...)` condition.
151    pub const CONDITION: &str = "condition";
152    /// Fallback kind written by [`super::make_rust_condition_value`] when the
153    /// caller's `kind` argument contained an interior NUL and could not be
154    /// converted to a `CString`. Should not appear in normal flow; the match
155    /// arm in [`crate::condition::RCondition::from_tagged_sexp`] handles it
156    /// defensively by degrading to `RCondition::Error`.
157    pub const OTHER_RUST_ERROR: &str = "other_rust_error";
158}
159
160/// Convert a `&str` to a `CString`, falling back to `fallback` on interior NUL bytes.
161///
162/// Used internally by [`make_rust_condition_value`] to avoid duplicating the
163/// `CString::new(s).unwrap_or_else(…)` pattern across every slot.
164fn to_cstring_lossy(s: &str, fallback: &str) -> std::ffi::CString {
165    std::ffi::CString::new(s).unwrap_or_else(|_| std::ffi::CString::new(fallback).unwrap())
166}
167
168/// Build a tagged condition value with no structured `data` payload.
169///
170/// Thin wrapper over [`make_rust_condition_value_with_data`] with `data =
171/// None` and at most one class. Generated code uses it for the unclassed
172/// kinds (`Option::None`); classed `Result::Err` values go through
173/// [`result_err_condition_value`], argument-conversion failures through
174/// [`conversion_condition_value`], a post-conversion `no_na` refusal through
175/// [`arg_check_condition_value`], and the user-facing `error!()` /
176/// `warning!()` / `message!()` / `condition!()` macros (routed through
177/// [`crate::unwind_protect`]) attach their own `data`.
178///
179/// # Safety
180///
181/// Must be called from R's main thread in a valid R allocation context — the
182/// body performs raw R allocation (`Rf_allocVector`, `SET_VECTOR_ELT`,
183/// `SETATTRIB`) which is UB off the main thread. See
184/// [`make_rust_condition_value_with_data`] for the full contract.
185#[inline]
186pub unsafe fn make_rust_condition_value(
187    message: &str,
188    kind: &str,
189    class: Option<&str>,
190    call: Option<SEXP>,
191) -> SEXP {
192    let class: Vec<String> = class.map(|c| ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [c.to_string()]))vec![c.to_string()]).unwrap_or_default();
193    // SAFETY: caller upholds the main-thread + valid-allocation-context contract.
194    unsafe {
195        make_rust_condition_value_with_data(
196            message,
197            kind,
198            &class,
199            call,
200            None,
201            ConditionCall::Inherit,
202        )
203    }
204}
205
206/// Build the tagged value for an [`RCondition`] payload (`error!()`,
207/// `warning!()`, `message!()`, `condition!()`, a queued `defer_*` entry, or a
208/// payload reconstructed across a package boundary): its kind, message,
209/// classes and data, and `call` unless the payload says
210/// [`ConditionCall::None`].
211///
212/// # Safety
213///
214/// Same contract as [`make_rust_condition_value_with_data`]: R main thread,
215/// valid allocation context.
216#[doc(hidden)]
217pub unsafe fn rust_condition_value(condition: RCondition, call: Option<SEXP>) -> SEXP {
218    let (kind, parts) = condition.into_parts();
219    // SAFETY: forwarded from the caller.
220    unsafe { condition_parts_value(kind, parts, call) }
221}
222
223/// [`make_rust_condition_value_with_data`] over the [`ErrParts`] of a
224/// condition.
225///
226/// # Safety
227///
228/// Same contract as [`make_rust_condition_value_with_data`].
229pub(crate) unsafe fn condition_parts_value(
230    kind: &str,
231    parts: ErrParts,
232    call: Option<SEXP>,
233) -> SEXP {
234    // SAFETY: forwarded from the caller.
235    unsafe {
236        make_rust_condition_value_with_data(
237            &parts.message,
238            kind,
239            &parts.class,
240            call,
241            parts.data,
242            parts.call,
243        )
244    }
245}
246
247/// Build the tagged value for a `Result<T, E>::Err` from the parts the
248/// generated wrapper probed off `e` (see
249/// [`crate::__mx_result_err_parts!`]): `kind = "result_err"`, the error's
250/// class vector (empty for the plain `Debug` fallback), its structured
251/// fields, and no call when its [`RConditionData::call`](crate::condition::RConditionData::call)
252/// is [`ConditionCall::None`] (an [`RError::without_call`](crate::condition::RError::without_call)).
253///
254/// # Safety
255///
256/// Same contract as [`make_rust_condition_value_with_data`]: R main thread,
257/// valid allocation context. Every generated `Err` arm runs inside the
258/// wrapper's `with_r_unwind_protect` closure, which satisfies it.
259pub unsafe fn result_err_condition_value(parts: ErrParts, call: Option<SEXP>) -> SEXP {
260    // SAFETY: forwarded from the caller.
261    unsafe { condition_parts_value(kind::RESULT_ERR, parts, call) }
262}
263
264/// Build the tagged value for an argument that failed its Rust-side
265/// conversion: `kind = "conversion"`, the parts probed off the error by
266/// [`crate::__mx_conversion_err_parts!`] (class, message and data from an
267/// [`RConditionData`](crate::condition::RConditionData) impl, the R-worded
268/// reason for a built-in conversion error, else the `Display` text), with
269/// `prefix` before the message (`'x' must be a single integer`, or
270/// `invalid 'x' argument` when the macro has no R-facing expectation for the
271/// type), the crate's `conversion_error_class` (`crate_class`, emitted by the
272/// macro from `[package.metadata.miniextendr]`) after the error's own
273/// classes, the parameter's R name as `e$param` and the Rust type as
274/// `e$rust_type`. `arg_message`, when the error words the whole argument
275/// error itself ([`crate::__mx_conversion_arg_message!`]: a `match_arg`
276/// choice error, in the words of the R-side `match_arg` check, #1767), is
277/// the message instead of `<prefix>: <reason>`. See
278/// [`crate::condition::conversion_err_parts`] for how they combine.
279///
280/// The R-side argument checks raise the same condition from R
281/// (`.miniextendr_arg_error`, #1591), so a handler for the crate class, or
282/// for `rust_error`, sees every argument error whichever side caught it.
283///
284/// # Safety
285///
286/// Same contract as [`make_rust_condition_value_with_data`]: R main thread,
287/// valid allocation context. Every generated conversion `Err` arm runs inside
288/// the wrapper's `with_r_unwind_protect` closure, which satisfies it.
289pub unsafe fn conversion_condition_value(
290    prefix: &str,
291    param: &str,
292    rust_type: Option<&str>,
293    crate_class: &[&str],
294    parts: ErrParts,
295    arg_message: Option<String>,
296    call: Option<SEXP>,
297) -> SEXP {
298    let parts = crate::condition::conversion_err_parts(
299        prefix,
300        param,
301        rust_type,
302        crate_class,
303        parts,
304        arg_message,
305    );
306    // SAFETY: forwarded from the caller.
307    unsafe { condition_parts_value(kind::CONVERSION, parts, call) }
308}
309
310/// Build the tagged value for an argument check that failed in Rust after
311/// the conversion: `#[miniextendr(no_na)]` on a value its type reads as
312/// missing where R's `anyNA()` saw none (the text `"NA"` or a blank string
313/// for `AsNumeric*`, a factor `NA` level for `AsCharacter*`, ...), or on an
314/// `Either`, which has no R guard, an `NA` the arm taken refuses.
315///
316/// The condition is the one `no_na`'s R guard raises
317/// (`.miniextendr_arg_error`): `kind = "conversion"`, `message` as given,
318/// the crate's `conversion_error_class` (`crate_class`) before `rust_error`,
319/// and `e$param`. See [`crate::condition::arg_check_parts`].
320///
321/// This is the generated code's form of the argument error: it returns the
322/// value from the conversion arm and takes the classes the macro read from
323/// `[package.metadata.miniextendr]` at expansion time. A function body raises
324/// the same condition with the safe [`crate::arg_error!`] instead, whose
325/// classes the generated R wrapper fills in (#1740).
326///
327/// # Safety
328///
329/// Same contract as [`make_rust_condition_value_with_data`]: R main thread,
330/// valid allocation context. The generated check runs right after the
331/// argument's conversion, where the conversion `Err` arms run, which
332/// satisfies it.
333pub unsafe fn arg_check_condition_value(
334    message: &str,
335    param: &str,
336    crate_class: &[&str],
337    call: Option<SEXP>,
338) -> SEXP {
339    let parts = crate::condition::arg_check_parts(message, param, crate_class);
340    // SAFETY: forwarded from the caller.
341    unsafe { condition_parts_value(kind::CONVERSION, parts, call) }
342}
343
344/// Build a tagged condition-value SEXP for transport across the Rust→R boundary.
345///
346/// Used for all Rust-origin failures and user-facing conditions. The R-side
347/// switch in `condition_check_lines` reads `.val$kind` to select the condition
348/// type and `.val$class` to prepend optional user classes before the standard
349/// `rust_*` layering.
350///
351/// # Safety
352///
353/// Must be called from R's main thread in a valid R allocation context
354/// (standard R API constraint): the body performs raw R allocation
355/// (`Rf_allocVector`, `SET_VECTOR_ELT`, `SETATTRIB`) which is UB off the main
356/// thread. In practice every caller reaches this through
357/// [`crate::unwind_protect::with_r_unwind_protect`] (or a proc-macro-generated
358/// panic handler), both of which run on the main thread.
359/// The returned SEXP is unprotected — caller must protect if needed.
360///
361/// # PROTECT discipline
362///
363/// Every fresh allocation (msg, kind, optional class, the no-call marker,
364/// true-marker, and — when present — the `data` VECSXP, its names, and each
365/// field value) is added to a
366/// single [`ProtectScope`](crate::ProtectScope) before the next allocation
367/// that might trigger a GC barrier. The scope releases them all together
368/// (`UNPROTECT`) when it drops at function exit, on every branch — the RAII
369/// equivalent of the former hand-counted `prot` / `Rf_unprotect(prot)` ladder.
370/// This discipline was established by PR #344 commit `af6b4875` to fix a
371/// `recursive gc invocation` segfault on R-devel.
372///
373/// # Arguments
374///
375/// * `message` - Human-readable condition message
376/// * `kind` - Condition kind — one of the constants in [`kind`].
377/// * `class` - User-supplied classes to prepend to the layered vector, most
378///   specific first (`c(<class…>, "rust_error", …)`); empty for none.
379/// * `call` - Optional R call SEXP for error context. When `None`, uses `R_NilValue`.
380/// * `data` - Optional named condition-data payload (from the macros' `data =
381///   ...` form). When `Some`, each `(name, value)` becomes a named element of a
382///   list stored in slot `[4]`; the R helper splices these into the condition
383///   object so handlers can read `e$<name>`. When `None`, slot `[4]` is `NULL`.
384/// * `call_choice` - [`ConditionCall::None`] writes the "no call" marker,
385///   `FALSE`, into slot `[3]` instead of `call`: the R helper then signals the
386///   condition with `call = NULL` rather than falling back to the wrapper's
387///   call, and [`RCondition::from_tagged_sexp`] reads the choice back across
388///   a package boundary.
389pub unsafe fn make_rust_condition_value_with_data(
390    message: &str,
391    kind: &str,
392    class: &[String],
393    call: Option<SEXP>,
394    data: Option<crate::condition::ConditionData>,
395    call_choice: ConditionCall,
396) -> SEXP {
397    unsafe {
398        // PROTECT discipline: every fresh allocation that's live across another
399        // allocation must be protected. SET_VECTOR_ELT and SETATTRIB can both
400        // trigger old-to-new GC barriers; R-devel's GC fires more aggressively
401        // here than R-release/oldrel, so unprotected intermediates corrupt the
402        // heap on R-devel even when R 4.5/4.4 happen to survive (PR #344 fix).
403        // A single ProtectScope roots every intermediate; it releases them all
404        // when it drops at function exit.
405        let scope = crate::ProtectScope::new();
406        let list = scope.protect_raw(sys::Rf_allocVector(SEXPTYPE::VECSXP, 5));
407
408        // Element 0: error message
409        let msg_cstr = to_cstring_lossy(message, "<invalid error message>");
410        let msg_charsxp = sys::Rf_mkCharCE(msg_cstr.as_ptr(), CE_UTF8);
411        let msg_sexp = scope.protect_raw(SEXP::scalar_string(msg_charsxp));
412        list.set_vector_elt(0, msg_sexp);
413
414        // Element 1: kind string
415        let kind_cstr = to_cstring_lossy(kind, kind::OTHER_RUST_ERROR);
416        let kind_charsxp = sys::Rf_mkCharCE(kind_cstr.as_ptr(), CE_UTF8);
417        let kind_sexp = scope.protect_raw(SEXP::scalar_string(kind_charsxp));
418        list.set_vector_elt(1, kind_sexp);
419
420        // Element 2: optional custom class vector (NULL when empty). The STRSXP
421        // is protected before its CHARSXPs are allocated; the R helper splices
422        // it with `c(.class, "rust_error", ...)`, so any length works.
423        let class_sexp = if class.is_empty() {
424            SEXP::nil()
425        } else {
426            let n: isize = class
427                .len()
428                .try_into()
429                .expect("condition class count exceeds isize::MAX");
430            let class_vec = scope.protect_raw(sys::Rf_allocVector(SEXPTYPE::STRSXP, n));
431            for (i, class_name) in class.iter().enumerate() {
432                let idx: isize = i.try_into().expect("index exceeds isize::MAX");
433                let class_cstr = to_cstring_lossy(class_name, "rust_condition");
434                let class_charsxp = sys::Rf_mkCharCE(class_cstr.as_ptr(), CE_UTF8);
435                class_vec.set_string_elt(idx, class_charsxp);
436            }
437            class_vec
438        };
439        list.set_vector_elt(2, class_sexp);
440
441        // Element 3: the call. A caller-owned SEXP (already protected) or
442        // R_NilValue; for a call-less condition the "no call" marker, a fresh
443        // FALSE LGLSXP, protected until the list roots it.
444        let call_sexp = match call_choice {
445            ConditionCall::Inherit => call.unwrap_or(SEXP::nil()),
446            ConditionCall::None => scope.protect_raw(SEXP::scalar_logical(false)),
447        };
448        list.set_vector_elt(3, call_sexp);
449
450        // Element 4: optional named-list condition data (NULL when absent).
451        //
452        // PROTECT discipline: we build a fresh VECSXP `data_list` plus a STRSXP
453        // `data_names`, and each field's materialised SEXP. Every one of these
454        // is live across subsequent allocations, so each is protected before
455        // the next alloc:
456        //   - data_list protected before any field materialisation,
457        //   - data_names protected before per-field CHARSXP allocations,
458        //   - each field value materialised then immediately stored into the
459        //     protected data_list (so it is rooted by the list before the next
460        //     field allocates — same shape as `List::from_pairs`).
461        let data_sexp = if let Some(fields) = data {
462            let n: isize = fields
463                .len()
464                .try_into()
465                .expect("condition data length exceeds isize::MAX");
466            let data_list = scope.protect_raw(sys::Rf_allocVector(SEXPTYPE::VECSXP, n));
467            let data_names = scope.protect_raw(sys::Rf_allocVector(SEXPTYPE::STRSXP, n));
468            for (i, (name, value)) in fields.into_iter().enumerate() {
469                let idx: isize = i.try_into().expect("index exceeds isize::MAX");
470                // Materialise the value and immediately root it in data_list
471                // (protected) before the name CHARSXP allocation below.
472                let value_sexp = value.into_sexp();
473                data_list.set_vector_elt(idx, value_sexp);
474                let name_cstr = to_cstring_lossy(&name, "<invalid name>");
475                let name_charsxp = sys::Rf_mkCharCE(name_cstr.as_ptr(), CE_UTF8);
476                data_names.set_string_elt(idx, name_charsxp);
477            }
478            data_list.set_names(data_names);
479            data_list
480        } else {
481            SEXP::nil()
482        };
483        list.set_vector_elt(4, data_sexp);
484
485        // Names / class symbols are cached. The TRUE marker on set_attr is a
486        // fresh LGLSXP — protect across the SETATTRIB call.
487        list.set_names(condition_names_sexp());
488        list.set_class(rust_condition_class_sexp());
489        let true_marker = scope.protect_raw(SEXP::scalar_logical(true));
490        list.set_attr(rust_condition_attr_symbol(), true_marker);
491
492        list
493    }
494}