This is an automated email from the ASF dual-hosted git repository.

tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-ffi.git


The following commit(s) were added to refs/heads/main by this push:
     new 9288282f [Rust] Append visit error context in place (#940)
9288282f is described below

commit 9288282fee38e0ba681411b80bb36d211d436e60
Author: Tianqi Chen <[email protected]>
AuthorDate: Fri Nov 6 00:15:44 2026 -0500

    [Rust] Append visit error context in place (#940)
    
    Rust structural visit and map annotate an escaping error with a
    `VisitErrorContext` breadcrumb per frame. Today
    `with_visit_error_context` reads the whole pattern through
    `ffi.ArraySize`/`ffi.ArrayGetItem`, then rebuilds the Array, the context
    (`ffi.MakeObjectFromPackedArgs`) and the Error at every frame, which is
    quadratic in the unwind depth.
    
    This follows C++ `detail::UpdateVisitErrorContext` instead, entirely in
    Rust: copy-on-write at each level, so an Error owned by the unwinding
    frame has its pattern appended in place, while a shared Error, context
    or pattern is copied before the write. The same-frame dedupe is kept,
    and a fresh context keeps the previous `extra_context` as
    `prev_error_context`.
    
    - A crate-internal `#[repr(C)]` binding of `ffi.VisitErrorContext` with
    the C++ field layout (`reverse_visit_pattern`, `prev_error_context`),
    checked against the registered field offsets in a unit test.
    - A crate-internal `Error::extra_context_mut`, which first replaces a
    shared Error by a shallow copy.
    - A crate-internal copy-on-write `Array::push` that writes in place when
    unique with room and otherwise moves or copies into a buffer that grows
    geometrically. To support it, a Rust-allocated `ArrayObj` now sizes its
    inline buffer by `capacity`, and releases its elements when dropped
    (previously they were never released).
    
    No public API changes.
    
    
    Also adds `String::is_empty` and `Bytes::is_empty` (next to `len`, as in
    std) and `impl Default for Shape` (the empty shape), so they are part of
    the API before stabilization.
---
 rust/tvm-ffi/src/collections/array.rs        |  62 +++++++++-
 rust/tvm-ffi/src/collections/shape.rs        |   8 ++
 rust/tvm-ffi/src/error.rs                    |  21 ++++
 rust/tvm-ffi/src/extra/structural_common.rs  | 164 ++++++++++++++-------------
 rust/tvm-ffi/src/string.rs                   |  10 ++
 rust/tvm-ffi/tests/test_structural_mutate.rs |  69 ++++-------
 6 files changed, 207 insertions(+), 127 deletions(-)

diff --git a/rust/tvm-ffi/src/collections/array.rs 
b/rust/tvm-ffi/src/collections/array.rs
index 6a6f989e..930458d6 100644
--- a/rust/tvm-ffi/src/collections/array.rs
+++ b/rust/tvm-ffi/src/collections/array.rs
@@ -43,10 +43,24 @@ pub struct ArrayObj {
     pub data_deleter: Option<unsafe extern "C" fn(*mut core::ffi::c_void)>,
 }
 
+// A Rust-allocated array holds `capacity` cells inline, of which the first 
`size`
+// are initialized.
 unsafe impl ObjectCoreWithExtraItems for ArrayObj {
     type ExtraItem = TVMFFIAny;
     fn extra_items_count(this: &Self) -> usize {
-        this.size as usize
+        this.capacity as usize
+    }
+}
+
+// Runs only for a Rust-allocated array; one allocated in C++ is released by 
its
+// own deleter.
+impl Drop for ArrayObj {
+    fn drop(&mut self) {
+        let cells = self.data as *const TVMFFIAny;
+        for i in 0..self.size as usize {
+            // SAFETY: the first `size` cells are initialized and owned by the 
array.
+            drop(unsafe { Any::from_raw_ffi_any(*cells.add(i)) });
+        }
     }
 }
 
@@ -199,7 +213,7 @@ impl<T: ContainerElement + Clone> Array<T> {
             let raw_ptr = ObjectArc::as_ptr(&arc) as *mut ArrayObj;
             let container = &mut *raw_ptr;
 
-            let base_ptr = ArrayObj::extra_items_mut(container).as_ptr() as 
*mut TVMFFIAny;
+            let base_ptr = ArrayObj::extra_items_mut(container).as_mut_ptr();
             container.data = base_ptr as *mut _;
 
             for (i, item) in items.into_iter().enumerate() {
@@ -212,6 +226,50 @@ impl<T: ContainerElement + Clone> Array<T> {
         unsafe { Self::from_data(arc) }
     }
 
+    /// Append `value`, copy-on-write like C++ `Array::push_back`: in place 
when
+    /// this handle is the only reference and the buffer has room, otherwise 
into
+    /// a new buffer that grows geometrically when full.
+    pub(crate) fn push(&mut self, value: T) {
+        let size = self.len();
+        let capacity = self.data.capacity as usize;
+        let unique = ObjectArc::strong_count(&self.data) == 1;
+        if !unique || size == capacity {
+            let capacity = if size == capacity {
+                (capacity * 2).max(size + 1)
+            } else {
+                capacity
+            };
+            let mut grown = Self::new_with_capacity(Vec::new(), capacity);
+            let source = self.as_container().data as *const TVMFFIAny;
+            // SAFETY: `grown` is fresh with room for `size` cells. A unique 
source
+            // moves its cells and forgets them; a shared one keeps its cells, 
so each
+            // copy takes its own reference.
+            unsafe {
+                let target = ObjectArc::as_mut_ptr(&mut grown.data);
+                let cells = (*target).data as *mut TVMFFIAny;
+                if unique {
+                    std::ptr::copy_nonoverlapping(source, cells, size);
+                    (*ObjectArc::as_mut_ptr(&mut self.data)).size = 0;
+                } else {
+                    for i in 0..size {
+                        let copy = 
Any::from(AnyView::from_raw_ffi_any(*source.add(i)));
+                        std::ptr::write(cells.add(i), 
Any::into_raw_ffi_any(copy));
+                    }
+                }
+                (*target).size = size as i64;
+            }
+            *self = grown;
+        }
+        // SAFETY: this handle is now the only reference, with room for one 
more cell.
+        unsafe {
+            let container = ObjectArc::as_mut_ptr(&mut self.data);
+            let mut cell = TVMFFIAny::new();
+            T::container_move_to_any(value, &mut cell);
+            std::ptr::write(((*container).data as *mut TVMFFIAny).add(size), 
cell);
+            (*container).size += 1;
+        }
+    }
+
     /// Return the number of elements.
     ///
     /// # Returns
diff --git a/rust/tvm-ffi/src/collections/shape.rs 
b/rust/tvm-ffi/src/collections/shape.rs
index 0600a4a4..7abfb7ff 100644
--- a/rust/tvm-ffi/src/collections/shape.rs
+++ b/rust/tvm-ffi/src/collections/shape.rs
@@ -58,6 +58,14 @@ pub struct Shape {
 unsafe impl Send for Shape {}
 unsafe impl Sync for Shape {}
 
+impl Default for Shape {
+    /// The empty shape, like [`Shape::new`].
+    #[inline]
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
 impl Shape {
     /// Create a new empty Shape
     pub fn new() -> Self {
diff --git a/rust/tvm-ffi/src/error.rs b/rust/tvm-ffi/src/error.rs
index be0b7675..42c3a053 100644
--- a/rust/tvm-ffi/src/error.rs
+++ b/rust/tvm-ffi/src/error.rs
@@ -323,6 +323,27 @@ impl Error {
         Some(unsafe { &*(handle as *const _ as *const object::ObjectRef) })
     }
 
+    /// The `extra_context` slot, to update in place. A shared error is first
+    /// replaced by a shallow copy, so the update reaches no other holder.
+    pub(crate) fn extra_context_mut(&mut self) -> &mut 
Option<object::ObjectRef> {
+        if ObjectArc::strong_count(&self.data) != 1 {
+            *self = Self::new_with_cause_and_extra_context(
+                self.kind(),
+                self.message(),
+                self.backtrace(),
+                self.cause_chain(),
+                self.extra_context(),
+            );
+        }
+        // SAFETY: this handle is the only reference, and the slot owns a 
nullable
+        // object handle, which is exactly an `Option<ObjectRef>`.
+        unsafe {
+            let error = ObjectArc::as_mut_ptr(&mut self.data);
+            &mut *std::ptr::addr_of_mut!((*error).cell.extra_context)
+                .cast::<Option<object::ObjectRef>>()
+        }
+    }
+
     /// Get the traceback of the error in the order of most recent call last, 
as
     /// Python prints it.
     ///
diff --git a/rust/tvm-ffi/src/extra/structural_common.rs 
b/rust/tvm-ffi/src/extra/structural_common.rs
index 058e8672..0c1ed75f 100644
--- a/rust/tvm-ffi/src/extra/structural_common.rs
+++ b/rust/tvm-ffi/src/extra/structural_common.rs
@@ -18,13 +18,12 @@
  */
 
 use crate::any::{Any, AnyView};
+use crate::collections::array::Array;
+use crate::derive::Object;
 use crate::error::{Error, Result, RUNTIME_ERROR};
-use crate::function::Function;
-use crate::object::{self, ObjectRefCore};
-use crate::reflection::FieldGetter;
+use crate::object::{self, Object, ObjectArc, ObjectRef, ObjectRefCore};
 use crate::tvm_ffi_sys::{
-    TVMFFIAny, TVMFFIAnyDataUnion, TVMFFIByteArray, TVMFFIGetTypeInfo, 
TVMFFITypeIndex,
-    TVMFFITypeKeyToIndex,
+    TVMFFIAny, TVMFFIAnyDataUnion, TVMFFIGetTypeInfo, TVMFFIObject, 
TVMFFITypeIndex,
 };
 
 // 
-----------------------------------------------------------------------------
@@ -108,88 +107,95 @@ pub(crate) fn with_structural_error_context(error: Error, 
operation: &str, frame
     Error::with_appended_backtrace(error, &format!("[native structural 
{operation}] {frame}\n"))
 }
 
+/// Rust binding of C++ `VisitErrorContextObj`, the payload that
+/// `Error::extra_context` carries to name the nodes a failed visit unwound 
through.
+#[repr(C)]
+#[derive(Object)]
+#[type_key = "ffi.VisitErrorContext"]
+#[type_final]
+pub(crate) struct VisitErrorContextObj {
+    base: Object,
+    /// Visited nodes, innermost first; C++ `ObjectRef` is nullable.
+    reverse_visit_pattern: Array<Option<ObjectRef>>,
+    /// The `extra_context` the error carried before this context replaced it.
+    prev_error_context: Option<ObjectRef>,
+}
+
+/// Name the visited object in `raw` in the error's visit context, like C++
+/// `detail::UpdateVisitErrorContext`. Copy-on-write at each level: the Error, 
its
+/// context and the context's pattern are each copied only when shared, so an 
error
+/// owned by the unwinding frame is appended in place.
 #[cold]
-pub(crate) fn with_visit_error_context(error: Error, raw: TVMFFIAny) -> Error {
+pub(crate) fn with_visit_error_context(mut error: Error, raw: TVMFFIAny) -> 
Error {
     if raw.type_index < TVMFFITypeIndex::kTVMFFIStaticObjectBegin as i32 {
         return error;
     }
-    let context = (|| {
-        let mut context_type = 0;
+    // SAFETY: the traversal input remains live during annotation. Conversion
+    // acquires ownership before the temporary view ends; raw is an object tag.
+    let Ok(node) = ObjectRef::try_from(unsafe { AnyView::from_raw_ffi_any(raw) 
}) else {
+        return error;
+    };
+    let as_context = |context: &ObjectRef| {
+        let object = ObjectArc::as_ptr(ObjectRef::data(context));
+        // SAFETY: `context` keeps its object alive; the type is checked first.
         unsafe {
-            crate::check_safe_call!(TVMFFITypeKeyToIndex(
-                &TVMFFIByteArray::from_str("ffi.VisitErrorContext"),
-                &mut context_type,
-            ))
-            .ok()?;
+            object::is_instance_of::<VisitErrorContextObj>(
+                (*object.cast::<TVMFFIObject>()).type_index,
+            )
+            .then(|| &*object.cast::<VisitErrorContextObj>())
         }
-        // SAFETY: the traversal input remains live during annotation. 
Conversion
-        // acquires ownership before the temporary view ends; raw is an object 
tag.
-        let node = object::ObjectRef::try_from(unsafe { 
AnyView::from_raw_ffi_any(raw) }).ok()?;
-        let mut previous = error.extra_context().cloned();
-        let mut nodes = Vec::new();
-        if let Some(prior) = previous.as_ref() {
-            if AnyView::from(prior).type_index() == context_type {
-                let object = &**object::ObjectRef::data(prior);
-                let records = FieldGetter::new(context_type, 
"reverse_visit_pattern")
-                    .ok()?
-                    .get_any(object)
-                    .ok()?;
-                let size = Function::get_global("ffi.ArraySize")
-                    .ok()?
-                    .call_tuple((records.clone(),))
-                    .ok()?
-                    .downcast_ref::<i64>()
-                    .copied()?;
-                let get_item = Function::get_global("ffi.ArrayGetItem").ok()?;
-                if size > 0 {
-                    let last: Option<object::ObjectRef> = get_item
-                        .call_tuple((records.clone(), size - 1))
-                        .ok()?
-                        .try_into()
-                        .ok();
-                    // Callback, policy and default descent can report the 
same frame.
-                    if last.is_some_and(|last| last.same_as(&node)) {
-                        return None;
-                    }
-                }
-                for i in 0..size {
-                    nodes.push(get_item.call_tuple((records.clone(), 
i)).ok()?);
-                }
-                previous = FieldGetter::new(context_type, "prev_error_context")
-                    .ok()?
-                    .get::<_, Option<object::ObjectRef>>(object)
-                    .ok()?;
-            }
+    };
+    // Callback, policy and default descent can report the same frame.
+    if let Some(context) = error.extra_context().and_then(as_context) {
+        if context
+            .reverse_visit_pattern
+            .iter()
+            .next_back()
+            .and_then(Option::as_ref)
+            .is_some_and(|last| last.same_as(&node))
+        {
+            return error;
         }
-        nodes.push(Any::from(node));
-        let records = Function::get_global("ffi.Array")
-            .ok()?
-            .call_packed(&nodes.iter().map(AnyView::from).collect::<Vec<_>>())
-            .ok()?;
-        Function::get_global("ffi.MakeObjectFromPackedArgs")
-            .ok()?
-            .call_tuple((
-                context_type,
-                crate::String::from("reverse_visit_pattern"),
-                records,
-                crate::String::from("prev_error_context"),
-                previous,
-            ))
-            .ok()?
-            .try_into()
-            .ok()
-    })();
-    // Diagnostic enrichment must not replace the original error on failure.
-    match context {
-        Some(context) => Error::new_with_cause_and_extra_context(
-            error.kind(),
-            error.message(),
-            error.backtrace(),
-            error.cause_chain(),
-            Some(&context),
-        ),
-        None => error,
     }
+    let slot = error.extra_context_mut();
+    let previous = slot.take();
+    let context = match previous {
+        Some(previous) if as_context(&previous).is_some() => {
+            // SAFETY: `previous` was checked to be a `VisitErrorContextObj`.
+            let mut context = unsafe {
+                ObjectArc::from_raw(
+                    ObjectArc::into_raw(ObjectRef::into_data(previous))
+                        .cast::<VisitErrorContextObj>(),
+                )
+            };
+            if ObjectArc::strong_count(&context) != 1 {
+                context = ObjectArc::new(VisitErrorContextObj {
+                    base: Object::new(),
+                    reverse_visit_pattern: 
context.reverse_visit_pattern.clone(),
+                    prev_error_context: context.prev_error_context.clone(),
+                });
+            }
+            // SAFETY: this handle is the context's only reference.
+            unsafe {
+                (*ObjectArc::as_mut_ptr(&mut context))
+                    .reverse_visit_pattern
+                    .push(Some(node))
+            };
+            context
+        }
+        previous => ObjectArc::new(VisitErrorContextObj {
+            base: Object::new(),
+            reverse_visit_pattern: Array::from_vec(vec![Some(node)]),
+            prev_error_context: previous,
+        }),
+    };
+    // SAFETY: every object starts with the root `Object` header.
+    *slot = Some(unsafe {
+        ObjectRef::from_data(ObjectArc::from_raw(
+            ObjectArc::into_raw(context).cast::<Object>(),
+        ))
+    });
+    error
 }
 
 // Generate the tuple arities supported by the standard library (1 through 12).
diff --git a/rust/tvm-ffi/src/string.rs b/rust/tvm-ffi/src/string.rs
index ec55ea5a..d9c6a7a5 100644
--- a/rust/tvm-ffi/src/string.rs
+++ b/rust/tvm-ffi/src/string.rs
@@ -76,6 +76,11 @@ impl Bytes {
         self.as_slice().len()
     }
 
+    /// Return `true` if there are no bytes.
+    pub fn is_empty(&self) -> bool {
+        self.len() == 0
+    }
+
     /// Get the bytes as a slice
     pub fn as_slice(&self) -> &[u8] {
         unsafe {
@@ -300,6 +305,11 @@ impl String {
         self.as_bytes().len()
     }
 
+    /// Return `true` if the string is empty.
+    pub fn is_empty(&self) -> bool {
+        self.len() == 0
+    }
+
     /// Get the string as a byte slice
     pub fn as_bytes(&self) -> &[u8] {
         unsafe {
diff --git a/rust/tvm-ffi/tests/test_structural_mutate.rs 
b/rust/tvm-ffi/tests/test_structural_mutate.rs
index 00c454d5..1059b79f 100644
--- a/rust/tvm-ffi/tests/test_structural_mutate.rs
+++ b/rust/tvm-ffi/tests/test_structural_mutate.rs
@@ -279,6 +279,12 @@ fn list_item(list: &Any, index: i64) -> i64 {
         .unwrap()
 }
 
+type Pattern = Array<Option<ObjectRef>>;
+
+fn pattern_pointer(records: &Pattern, index: usize) -> *const Object {
+    ObjectArc::as_ptr(ObjectRef::data(&records.get(index).unwrap().unwrap()))
+}
+
 fn array_item(array: &Any, index: i64) -> Any {
     Function::get_global("ffi.ArrayGetItem")
         .unwrap()
@@ -1167,20 +1173,11 @@ fn 
callback_errors_preserve_message_and_add_object_context() {
         assert!(error.cause_chain().unwrap().same_as(&cause));
         let context = Any::from(error.extra_context().unwrap().clone());
         assert!(reflected_field::<ObjectRef>(&context, 
"prev_error_context").same_as(&payload));
-        let records = Any::from(reflected_field::<ObjectRef>(
-            &context,
-            "reverse_visit_pattern",
-        ));
-        let size = i64::try_from(call_global("ffi.ArraySize", 
&[records.clone()])).unwrap();
-        assert_eq!(size, 3);
-        assert_eq!(
-            call_global("ffi.ArrayGetItem", &[records.clone(), 
0_i64.into()]).type_index(),
-            TypeIndex::kTVMFFINone as i32
-        );
-        let innermost = call_global("ffi.ArrayGetItem", &[records.clone(), 
1_i64.into()]);
-        assert_eq!(any_object_pointer(&innermost), any_object_pointer(&child));
-        let outermost = call_global("ffi.ArrayGetItem", &[records, (size - 
1).into()]);
-        assert_eq!(any_object_pointer(&outermost), any_object_pointer(&root));
+        let records: Pattern = reflected_field(&context, 
"reverse_visit_pattern");
+        assert_eq!(records.len(), 3);
+        assert!(records.get(0).unwrap().is_none());
+        assert_eq!(pattern_pointer(&records, 1), any_object_pointer(&child));
+        assert_eq!(pattern_pointer(&records, 2), any_object_pointer(&root));
         let paths = call_global(
             "ffi.VisitErrorContext.FindAccessPaths",
             &[root.clone(), context, false.into()],
@@ -1196,14 +1193,8 @@ fn 
callback_errors_preserve_message_and_add_object_context() {
             "<root>.value"
         );
     }
-    let records = Any::from(reflected_field::<ObjectRef>(
-        &context,
-        "reverse_visit_pattern",
-    ));
-    assert_eq!(
-        i64::try_from(call_global("ffi.ArraySize", &[records])).unwrap(),
-        1
-    );
+    let records: Pattern = reflected_field(&context, "reverse_visit_pattern");
+    assert_eq!(records.len(), 1);
     assert_eq!(source.backtrace(), "origin");
 
     // Keep nonconsecutive occurrences: child -> root -> child is a distinct 
path.
@@ -1215,20 +1206,13 @@ fn 
callback_errors_preserve_message_and_add_object_context() {
     .err()
     .unwrap();
     let context = Any::from(error.extra_context().unwrap().clone());
-    let records = Any::from(reflected_field::<ObjectRef>(
-        &context,
-        "reverse_visit_pattern",
-    ));
-    assert_eq!(
-        i64::try_from(call_global("ffi.ArraySize", 
&[records.clone()])).unwrap(),
-        4
-    );
+    let records: Pattern = reflected_field(&context, "reverse_visit_pattern");
+    assert_eq!(records.len(), 4);
     for (i, expected) in [&child, &root, &child].into_iter().enumerate() {
-        let node = call_global(
-            "ffi.ArrayGetItem",
-            &[records.clone(), (i as i64 + 1).into()],
+        assert_eq!(
+            pattern_pointer(&records, i + 1),
+            any_object_pointer(expected)
         );
-        assert_eq!(any_object_pointer(&node), any_object_pointer(expected));
     }
 
     // The failing node may be a pre-order replacement or a post-order rebuilt 
parent.
@@ -1264,20 +1248,13 @@ fn 
callback_errors_preserve_message_and_add_object_context() {
         .err()
         .unwrap();
         let context = Any::from(error.extra_context().unwrap().clone());
-        let records = Any::from(reflected_field::<ObjectRef>(
-            &context,
-            "reverse_visit_pattern",
-        ));
-        let node = call_global("ffi.ArrayGetItem", &[records.clone(), 
0i64.into()]);
-        assert!(node
-            .downcast_ref::<ObjectRef>()
-            .cloned()
+        let records: Pattern = reflected_field(&context, 
"reverse_visit_pattern");
+        assert!(records
+            .get(0)
+            .unwrap()
             .unwrap()
             .same_as(&failed_node.unwrap()));
-        assert_eq!(
-            i64::try_from(call_global("ffi.ArraySize", &[records])).unwrap(),
-            1
-        );
+        assert_eq!(records.len(), 1);
         assert_eq!(root.get(0).unwrap(), 0);
     }
 }

Reply via email to