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 f34b3889 [FEAT][Rust] Preserve unchanged structural mutation results 
(#785)
f34b3889 is described below

commit f34b3889f6ca3372cb18efc6d48924b067ed835c
Author: Shushi Hong <[email protected]>
AuthorDate: Mon Sep 14 19:39:35 2026 -0400

    [FEAT][Rust] Preserve unchanged structural mutation results (#785)
    
    Add `Unchanged` and `UnchangedOr<T>` to Rust and preserve unchanged
    results through recursive mutation, native hooks, and reflected field
    updates. Fix pre-order structural mapping so callbacks returning
    `Unchanged` still traverse the original value's children.
    
    Add `mutate_result` and `default_mutate_result` helpers, with typed
    conversion and original-value recovery support. Existing callbacks and
    owning-value APIs remain compatible.
---
 docs/guides/rust_lang_guide.md               |   7 +
 rust/tvm-ffi/src/any.rs                      |   1 +
 rust/tvm-ffi/src/extra/mod.rs                |   1 +
 rust/tvm-ffi/src/extra/structural_common.rs  |   7 +
 rust/tvm-ffi/src/extra/structural_mutate.rs  | 219 ++++++++++++++++----
 rust/tvm-ffi/src/extra/unchanged.rs          | 294 +++++++++++++++++++++++++++
 rust/tvm-ffi/src/lib.rs                      |   1 +
 rust/tvm-ffi/src/type_traits.rs              |  15 ++
 rust/tvm-ffi/tests/test_structural_mutate.rs |  78 ++++++-
 9 files changed, 583 insertions(+), 40 deletions(-)

diff --git a/docs/guides/rust_lang_guide.md b/docs/guides/rust_lang_guide.md
index 20c34901..d76561f8 100644
--- a/docs/guides/rust_lang_guide.md
+++ b/docs/guides/rust_lang_guide.md
@@ -495,6 +495,13 @@ example, an integer handler can return `Result<i64>` to 
report failures and use
 completed before a later error are not rolled back, and the consumed root is
 not returned on error.
 
+Callbacks can also return `Unchanged` or `UnchangedOr<T>`, optionally wrapped
+in `Result`. A pre-order map still maps the original value's children when
+a callback returns `Unchanged`. Use `mutate_result` and
+`default_mutate_result` to preserve unchanged during recursion; existing
+owning-value helpers and top-level functions resolve the marker to the
+original value.
+
 `structural_mutate` accepts typed callback chains in addition to a
 `StructuralMutator`. Closure callbacks receive a `CallbackMutator`;
 `MutateCallbacks` adds state shared by that callback chain:
diff --git a/rust/tvm-ffi/src/any.rs b/rust/tvm-ffi/src/any.rs
index 514c0048..7c9b9d92 100644
--- a/rust/tvm-ffi/src/any.rs
+++ b/rust/tvm-ffi/src/any.rs
@@ -304,6 +304,7 @@ pub(crate) fn is_plain_inline(type_index: i32) -> bool {
     type_index < TypeIndex::kTVMFFIRawStr as i32
         || type_index == TypeIndex::kTVMFFISmallStr as i32
         || type_index == TypeIndex::kTVMFFISmallBytes as i32
+        || type_index == TypeIndex::kTVMFFIUnchanged as i32
 }
 
 // convert AnyView to Any
diff --git a/rust/tvm-ffi/src/extra/mod.rs b/rust/tvm-ffi/src/extra/mod.rs
index 86677a5a..3abc1702 100644
--- a/rust/tvm-ffi/src/extra/mod.rs
+++ b/rust/tvm-ffi/src/extra/mod.rs
@@ -21,3 +21,4 @@ pub mod module;
 mod structural_common;
 pub mod structural_mutate;
 pub mod structural_visit;
+pub mod unchanged;
diff --git a/rust/tvm-ffi/src/extra/structural_common.rs 
b/rust/tvm-ffi/src/extra/structural_common.rs
index 7b52efc1..be87a044 100644
--- a/rust/tvm-ffi/src/extra/structural_common.rs
+++ b/rust/tvm-ffi/src/extra/structural_common.rs
@@ -155,6 +155,13 @@ pub(crate) fn try_to_owned_without_normalization(raw: 
TVMFFIAny) -> Option<Any>
 
 pub(crate) use crate::any::is_plain_inline;
 
+#[inline]
+pub(crate) fn same_shallow(lhs: TVMFFIAny, rhs: TVMFFIAny) -> bool {
+    lhs.type_index == rhs.type_index
+        && lhs.small_str_len == rhs.small_str_len
+        && unsafe { lhs.data_union.v_uint64 == rhs.data_union.v_uint64 }
+}
+
 /// Subtype check with the base's inheritance depth supplied by the caller
 /// (`ObjectCore::TYPE_DEPTH`), so only the object's type info is fetched.
 #[inline]
diff --git a/rust/tvm-ffi/src/extra/structural_mutate.rs 
b/rust/tvm-ffi/src/extra/structural_mutate.rs
index 1d630e63..6461a766 100644
--- a/rust/tvm-ffi/src/extra/structural_mutate.rs
+++ b/rust/tvm-ffi/src/extra/structural_mutate.rs
@@ -49,13 +49,15 @@ use crate::tvm_ffi_sys::{
 use crate::tvm_ffi_sys::{TVMFFIObjectHandle, TVMFFISEqHashKind};
 
 use super::structural_common::{
-    impl_callback_chain_tuple_arities, is_plain_inline, 
try_to_owned_without_normalization,
-    with_structural_error_context,
+    impl_callback_chain_tuple_arities, is_plain_inline, same_shallow,
+    try_to_owned_without_normalization, with_structural_error_context,
 };
 use super::structural_visit::{
     field_def_region, for_each_field_info, free_var_child_region, 
type_attr_column, type_key_of,
     DefRegionKind, WalkOrder,
 };
+use super::unchanged::is_unchanged;
+pub use super::unchanged::{Unchanged, UnchangedOr};
 
 const STRUCTURAL_MUTATE_ATTR: &str = "__s_mutate__";
 const STRUCTURAL_MAYBE_INPLACE_MUTATE_ATTR: &str = 
"__s_maybe_inplace_mutate__";
@@ -209,6 +211,40 @@ impl Mutator {
         StructuralMutator::default_mutate(dispatch, &self.current, 
self.def_region_kind)
     }
 
+    /// Mutate a borrowed child while preserving an unchanged result.
+    #[inline]
+    pub fn mutate_result<D, T>(&mut self, dispatch: &mut D, value: &T) -> 
Result<UnchangedOr<Any>>
+    where
+        D: MutateDispatch,
+        for<'x> AnyView<'x>: From<&'x T>,
+    {
+        self.mutate_with_result(dispatch, value, self.def_region_kind)
+    }
+
+    /// Mutate a borrowed child under an explicit region, preserving unchanged.
+    #[inline]
+    pub fn mutate_with_result<D, T>(
+        &mut self,
+        dispatch: &mut D,
+        value: &T,
+        kind: DefRegionKind,
+    ) -> Result<UnchangedOr<Any>>
+    where
+        D: MutateDispatch,
+        for<'x> AnyView<'x>: From<&'x T>,
+    {
+        StructuralMutator::mutate_result(dispatch, value, kind)
+    }
+
+    /// Apply default mutation without materializing an unchanged original.
+    #[inline]
+    pub fn default_mutate_result<D: MutateDispatch>(
+        &mut self,
+        dispatch: &mut D,
+    ) -> Result<UnchangedOr<Any>> {
+        StructuralMutator::default_mutate_result(dispatch, &self.current, 
self.def_region_kind)
+    }
+
     /// Look up an invocation-local identity substitution.
     #[inline(always)]
     pub fn var_remap_get<D: MutateDispatch>(
@@ -305,6 +341,7 @@ where
         let view = AnyView::from(value);
         self.driver
             .mutate_raw(*view.as_raw_ffi_any(), def_region_kind, Permit::Copy)
+            .and_then(|result| resolve_result(result, *view.as_raw_ffi_any()))
     }
 
     /// Mutate an owned value, allowing an in-place attempt when it remains
@@ -322,11 +359,12 @@ where
         def_region_kind: DefRegionKind,
     ) -> Result<Any> {
         let value = value.into();
-        self.driver.mutate_raw(
+        let result = self.driver.mutate_raw(
             *value.as_raw_ffi_any(),
             def_region_kind,
             Permit::MaybeInPlace,
-        )
+        )?;
+        Ok(if is_unchanged(&result) { value } else { result })
     }
 
     /// Apply default mutation to the callback's current value.
@@ -337,6 +375,40 @@ where
     pub fn default_mutate(&mut self) -> Result<Any> {
         self.driver
             .default_mutate_raw(self.current.raw(), self.def_region_kind)
+            .and_then(|result| resolve_result(result, self.current.raw()))
+    }
+
+    /// Mutate a borrowed child while preserving an unchanged result.
+    #[inline]
+    pub fn mutate_result<T>(&mut self, value: &T) -> Result<UnchangedOr<Any>>
+    where
+        for<'x> AnyView<'x>: From<&'x T>,
+    {
+        self.mutate_with_result(value, self.def_region_kind)
+    }
+
+    /// Mutate a borrowed child under an explicit region, preserving unchanged.
+    #[inline]
+    pub fn mutate_with_result<T>(
+        &mut self,
+        value: &T,
+        kind: DefRegionKind,
+    ) -> Result<UnchangedOr<Any>>
+    where
+        for<'x> AnyView<'x>: From<&'x T>,
+    {
+        let view = AnyView::from(value);
+        self.driver
+            .mutate_raw(*view.as_raw_ffi_any(), kind, Permit::Copy)
+            .and_then(UnchangedOr::from_carrier)
+    }
+
+    /// Apply default mutation without materializing an unchanged original.
+    #[inline]
+    pub fn default_mutate_result(&mut self) -> Result<UnchangedOr<Any>> {
+        self.driver
+            .default_mutate_raw(self.current.raw(), self.def_region_kind)
+            .and_then(UnchangedOr::from_carrier)
     }
 
     /// Look up an invocation-local identity substitution.
@@ -1113,6 +1185,7 @@ pub trait StructuralMutator: Sized {
     {
         let view = AnyView::from(value);
         dispatch_user_raw(self, *view.as_raw_ffi_any(), def_region_kind, 
Permit::Copy)
+            .and_then(|result| resolve_result(result, *view.as_raw_ffi_any()))
     }
 
     /// Re-enter this mutator for an owned value, permitting reuse only when
@@ -1122,17 +1195,19 @@ pub trait StructuralMutator: Sized {
         T: Into<Any>,
     {
         let value = value.into();
-        dispatch_user_raw(
+        let result = dispatch_user_raw(
             self,
             *value.as_raw_ffi_any(),
             def_region_kind,
             Permit::MaybeInPlace,
-        )
+        )?;
+        Ok(if is_unchanged(&result) { value } else { result })
     }
 
     /// Apply default non-in-place mutation to `value`'s children.
     fn default_mutate(&mut self, value: &MapValue, def_region_kind: 
DefRegionKind) -> Result<Any> {
         user_default_mutate(self, value.raw(), def_region_kind, Permit::Copy)
+            .and_then(|result| resolve_result(result, value.raw()))
     }
 
     /// Apply default non-in-place mutation to a borrowed typed value.
@@ -1147,6 +1222,7 @@ pub trait StructuralMutator: Sized {
     {
         let view = AnyView::from(value);
         user_default_mutate(self, *view.as_raw_ffi_any(), def_region_kind, 
Permit::Copy)
+            .and_then(|result| resolve_result(result, *view.as_raw_ffi_any()))
     }
 
     /// Apply the default mutation under an engine-issued in-place capability.
@@ -1165,6 +1241,56 @@ pub trait StructuralMutator: Sized {
             Permit::Copy
         };
         user_default_mutate(self, raw, def_region_kind, permit)
+            .and_then(|result| resolve_result(result, raw))
+    }
+
+    /// Re-enter this mutator for a borrowed value, preserving unchanged.
+    fn mutate_result<T>(&mut self, value: &T, kind: DefRegionKind) -> 
Result<UnchangedOr<Any>>
+    where
+        for<'x> AnyView<'x>: From<&'x T>,
+    {
+        let view = AnyView::from(value);
+        dispatch_user_raw(self, *view.as_raw_ffi_any(), kind, Permit::Copy)
+            .and_then(UnchangedOr::from_carrier)
+    }
+
+    /// Default non-in-place mutation with an unchanged-or-replacement result.
+    fn default_mutate_result(
+        &mut self,
+        value: &MapValue,
+        kind: DefRegionKind,
+    ) -> Result<UnchangedOr<Any>> {
+        user_default_mutate(self, value.raw(), kind, Permit::Copy)
+            .and_then(UnchangedOr::from_carrier)
+    }
+
+    /// Default mutation of a borrowed typed value, preserving unchanged.
+    fn default_mutate_value_result<T>(
+        &mut self,
+        value: &T,
+        kind: DefRegionKind,
+    ) -> Result<UnchangedOr<Any>>
+    where
+        for<'x> AnyView<'x>: From<&'x T>,
+    {
+        let view = AnyView::from(value);
+        user_default_mutate(self, *view.as_raw_ffi_any(), kind, Permit::Copy)
+            .and_then(UnchangedOr::from_carrier)
+    }
+
+    /// Default mutation under an engine-issued capability, preserving 
unchanged.
+    fn default_maybe_inplace_mutate_result(
+        &mut self,
+        value: InplaceValue<'_>,
+        kind: DefRegionKind,
+    ) -> Result<UnchangedOr<Any>> {
+        let raw = value.raw();
+        let permit = if object_is_unique(raw) {
+            Permit::MaybeInPlace
+        } else {
+            Permit::Copy
+        };
+        user_default_mutate(self, raw, kind, 
permit).and_then(UnchangedOr::from_carrier)
     }
 
     /// Look up a FreeVar or DAG-node substitution from the active mutation.
@@ -1293,7 +1419,7 @@ where
             def_region_kind,
         ) {
             Some(result) => result,
-            None => self.default_mutate(value, def_region_kind),
+            None => user_default_mutate(self, value.raw(), def_region_kind, 
Permit::Copy),
         }
     }
 
@@ -1311,7 +1437,9 @@ where
             def_region_kind,
         ) {
             Some(result) => result,
-            None => self.default_maybe_inplace_mutate(value, def_region_kind),
+            None => self
+                .default_maybe_inplace_mutate_result(value, def_region_kind)
+                .map(Any::from),
         }
     }
 }
@@ -1331,7 +1459,7 @@ where
             def_region_kind,
         ) {
             Some(result) => result,
-            None => self.default_mutate(value, def_region_kind),
+            None => user_default_mutate(self, value.raw(), def_region_kind, 
Permit::Copy),
         }
     }
 
@@ -1349,7 +1477,9 @@ where
             def_region_kind,
         ) {
             Some(result) => result,
-            None => self.default_maybe_inplace_mutate(value, def_region_kind),
+            None => self
+                .default_maybe_inplace_mutate_result(value, def_region_kind)
+                .map(Any::from),
         }
     }
 }
@@ -1438,7 +1568,13 @@ impl<D: MapDispatch> NativeMapper<D> {
                     let mapped = result?;
                     // A pre-order callback may replace an inline leaf with a 
subtree.
                     if self.order == WalkOrder::PreOrder && 
!is_plain_inline(mapped.type_index()) {
-                        self.map_default_root(&mapped, def_region_kind, 
Permit::MaybeInPlace)
+                        let descended =
+                            self.map_default_root(&mapped, def_region_kind, 
Permit::MaybeInPlace)?;
+                        Ok(if is_unchanged(&descended) {
+                            mapped
+                        } else {
+                            descended
+                        })
                     } else {
                         Ok(mapped)
                     }
@@ -1476,7 +1612,7 @@ impl<D: MapDispatch> NativeMapper<D> {
                 key,
                 MemoEntry {
                     _original: original,
-                    result: result.clone(),
+                    result: resolve_result(result.clone(), raw)?,
                 },
             );
         }
@@ -1498,18 +1634,28 @@ impl<D: MapDispatch> NativeMapper<D> {
                 };
                 let mapped = callback_result?;
                 let mapped_raw = *mapped.as_raw_ffi_any();
-                if same_shallow(raw, mapped_raw) {
+                if is_unchanged(&mapped) || same_shallow(raw, mapped_raw) {
                     // Release the callback's temporary ownership before the
                     // runtime uniqueness check observes the original.
                     drop(mapped);
                     self.default_map_current_raw(raw, def_region_kind, permit)
                 } else {
-                    self.map_default_root(&mapped, def_region_kind, 
Permit::MaybeInPlace)
+                    let descended =
+                        self.map_default_root(&mapped, def_region_kind, 
Permit::MaybeInPlace)?;
+                    Ok(if is_unchanged(&descended) {
+                        mapped
+                    } else {
+                        descended
+                    })
                 }
             }
             WalkOrder::PostOrder => {
                 let mapped = self.default_map_current_raw(raw, 
def_region_kind, permit)?;
-                let mapped_raw = *mapped.as_raw_ffi_any();
+                let mapped_raw = if is_unchanged(&mapped) {
+                    raw
+                } else {
+                    *mapped.as_raw_ffi_any()
+                };
                 let value = MapValue::from_raw(mapped_raw);
                 match self.dispatch.dispatch_map(&value, def_region_kind) {
                     Some(result) => result,
@@ -1549,7 +1695,7 @@ impl<D: MapDispatch> NativeMapper<D> {
                 key,
                 MemoEntry {
                     _original: original,
-                    result: result.clone(),
+                    result: resolve_result(result.clone(), raw)?,
                 },
             );
         }
@@ -1649,7 +1795,7 @@ trait MutationDriver: Sized {
         if field_changed {
             Ok(output)
         } else {
-            owned_from_raw(raw)
+            Ok(Unchanged.into())
         }
     }
 
@@ -1696,7 +1842,7 @@ trait MutationDriver: Sized {
             .map_err(|error| {
                 with_error_context(error, &format!("field `{}`", 
field.name.as_str()))
             })?;
-        if same_shallow(child_raw, *mapped.as_raw_ffi_any()) {
+        if is_unchanged(&mapped) || same_shallow(child_raw, 
*mapped.as_raw_ffi_any()) {
             return Ok(());
         }
 
@@ -2225,7 +2371,7 @@ fn run_structural_mutator_with_context(
         drop(result);
         resume_unwind(payload);
     }
-    result
+    result.map(|result| if is_unchanged(&result) { root } else { result })
 }
 
 fn call_mutator(
@@ -2247,12 +2393,7 @@ fn call_mutator(
     };
     with_mutator_def_region(mutator, def_region_kind, || unsafe {
         let view = AnyView::from_raw_ffi_any(raw);
-        let result = result_from_raw(callback(mutator, view))?;
-        if result.type_index() == TVMFFITypeIndex::kTVMFFIUnchanged as i32 {
-            owned_from_raw(raw)
-        } else {
-            Ok(result)
-        }
+        result_from_raw(callback(mutator, view))
     })
 }
 
@@ -2292,7 +2433,7 @@ fn call_structural_mutate_hook(
     attr: TVMFFIAny,
 ) -> Result<Any> {
     with_mutator_def_region(mutator, def_region_kind, || unsafe {
-        let result = match attr.type_index {
+        match attr.type_index {
             x if x == TVMFFITypeIndex::kTVMFFIOpaquePtr as i32 => {
                 let pointer = attr.data_union.v_ptr;
                 if pointer.is_null() {
@@ -2316,11 +2457,6 @@ fn call_structural_mutate_hook(
                 "__s_mutate__ must be an opaque function pointer or 
ffi.Function",
                 "",
             )),
-        }?;
-        if result.type_index() == TVMFFITypeIndex::kTVMFFIUnchanged as i32 {
-            owned_from_raw(raw)
-        } else {
-            Ok(result)
         }
     })
 }
@@ -2349,6 +2485,16 @@ fn result_into_raw(result: Result<Any>) -> TVMFFIAny {
     }
 }
 
+/// Resolve only at an owning-value API boundary; internal Any carriers and
+/// native hooks keep the unchanged tag to avoid acquiring the original.
+fn resolve_result(result: Any, original: TVMFFIAny) -> Result<Any> {
+    if is_unchanged(&result) {
+        owned_from_raw(original)
+    } else {
+        Ok(result)
+    }
+}
+
 /// Take ownership of one value returned by a structural-mutation ABI hook.
 ///
 /// # Safety
@@ -2442,7 +2588,9 @@ fn default_mutate_driver<D: MutationDriver>(
 
     let result = driver.map_reflected(raw, def_region_kind)?;
     if remappable {
-        driver.var_remap_set_raw(raw, &result)?;
+        // The invocation's existing remap policy stores resolved values.
+        let cached = resolve_result(result.clone(), raw)?;
+        driver.var_remap_set_raw(raw, &cached)?;
     }
     Ok(result)
 }
@@ -2637,13 +2785,6 @@ fn object_is_unique(raw: TVMFFIAny) -> bool {
     !pointer.is_null() && unsafe { object::unsafe_::strong_count(pointer) == 1 
}
 }
 
-#[inline]
-fn same_shallow(lhs: TVMFFIAny, rhs: TVMFFIAny) -> bool {
-    lhs.type_index == rhs.type_index
-        && lhs.small_str_len == rhs.small_str_len
-        && unsafe { lhs.data_union.v_uint64 == rhs.data_union.v_uint64 }
-}
-
 fn owned_from_raw(raw: TVMFFIAny) -> Result<Any> {
     if let Some(owned) = try_to_owned_without_normalization(raw) {
         return Ok(owned);
diff --git a/rust/tvm-ffi/src/extra/unchanged.rs 
b/rust/tvm-ffi/src/extra/unchanged.rs
new file mode 100644
index 00000000..cbf0f140
--- /dev/null
+++ b/rust/tvm-ffi/src/extra/unchanged.rs
@@ -0,0 +1,294 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+//! Typed unchanged-or-replacement results for structural mutation.
+
+use std::marker::PhantomData;
+
+use crate::any::{Any, AnyView, TryFromTemp};
+use crate::error::{Error, Result, TYPE_ERROR};
+use crate::tvm_ffi_sys::{TVMFFIAny, TVMFFITypeIndex};
+use crate::type_traits::{AnyCompatible, ContainerElement};
+
+/// A structural mutation that keeps its input without acquiring another owner.
+///
+/// This marker may be returned directly from a map or mutation callback. A
+/// pre-order map still descends into the original value's children.
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub struct Unchanged;
+
+// SAFETY: the unchanged ABI tag has no payload or owned resources.
+unsafe impl AnyCompatible for Unchanged {
+    unsafe fn copy_to_any_view(_src: &Self, data: &mut TVMFFIAny) {
+        *data = TVMFFIAny::new();
+        data.type_index = TVMFFITypeIndex::kTVMFFIUnchanged as i32;
+    }
+
+    unsafe fn move_to_any(src: Self, data: &mut TVMFFIAny) {
+        Self::copy_to_any_view(&src, data);
+    }
+
+    unsafe fn check_any_strict(data: &TVMFFIAny) -> bool {
+        data.type_index == TVMFFITypeIndex::kTVMFFIUnchanged as i32
+    }
+
+    unsafe fn copy_from_any_view_after_check(_data: &TVMFFIAny) -> Self {
+        Self
+    }
+
+    unsafe fn move_from_any_after_check(_data: &mut TVMFFIAny) -> Self {
+        Self
+    }
+
+    unsafe fn try_cast_from_any_view(data: &TVMFFIAny) -> 
std::result::Result<Self, ()> {
+        Self::check_any_strict(data).then_some(Self).ok_or(())
+    }
+
+    fn type_str() -> std::string::String {
+        "Unchanged".into()
+    }
+}
+
+crate::impl_try_from_any!(Unchanged);
+
+/// A typed replacement or an [`Unchanged`] marker, stored in one FFI Any cell.
+///
+/// `T` may be an FFI value type or [`Any`]. Use `Result<UnchangedOr<T>>` to
+/// propagate errors. Callbacks can return this wrapper, [`Unchanged`], a
+/// replacement value, or a `Result` containing any of these.
+///
+/// Converting this wrapper to `Any` preserves the marker. Use
+/// [`Self::value_or`] or [`Self::value_or_else`] when an actual value is 
needed.
+#[repr(transparent)]
+pub struct UnchangedOr<T: ContainerElement = Any> {
+    data: Any,
+    _marker: PhantomData<T>,
+}
+
+impl<T: ContainerElement> UnchangedOr<T> {
+    /// Keep the original value without borrowing or cloning it.
+    #[inline]
+    pub fn unchanged() -> Self {
+        Self {
+            data: Unchanged.into(),
+            _marker: PhantomData,
+        }
+    }
+
+    /// Supply an owning replacement value.
+    #[inline]
+    pub fn changed(value: T) -> Self {
+        let mut raw = TVMFFIAny::new();
+        // SAFETY: ContainerElement transfers exactly one owning value.
+        unsafe {
+            T::container_move_to_any(value, &mut raw);
+            Self {
+                data: Any::from_raw_ffi_any(raw),
+                _marker: PhantomData,
+            }
+        }
+    }
+
+    /// Whether this result asks its caller to keep the original value.
+    #[inline]
+    pub fn is_unchanged(&self) -> bool {
+        is_unchanged(&self.data)
+    }
+
+    /// Whether the result is unchanged or has the original shallow identity.
+    #[inline]
+    pub fn unchanged_or_same_as(&self, original: &T) -> bool {
+        if self.is_unchanged() {
+            return true;
+        }
+        let mut raw = TVMFFIAny::new();
+        // SAFETY: the borrowed cell is used only while original is alive.
+        unsafe {
+            T::container_copy_to_any_view(original, &mut raw);
+        }
+        super::structural_common::same_shallow(raw, 
*self.data.as_raw_ffi_any())
+    }
+
+    /// Move out the replacement, returning None when unchanged.
+    #[inline]
+    pub fn into_option(self) -> Option<T> {
+        if self.is_unchanged() {
+            return None;
+        }
+        // SAFETY: constructors and conversions maintain the declared T.
+        unsafe {
+            let mut raw = Any::into_raw_ffi_any(self.data);
+            Some(T::container_move_from_any_after_check(&mut raw))
+        }
+    }
+
+    /// Move the replacement or the supplied original value out of this result.
+    #[inline]
+    pub fn value_or(self, original: T) -> T {
+        self.value_or_else(|| original)
+    }
+
+    /// Materialize the original only if this result is unchanged.
+    #[inline]
+    pub fn value_or_else(self, original: impl FnOnce() -> T) -> T {
+        self.into_option().unwrap_or_else(original)
+    }
+
+    /// Convert only the replacement, preserving an unchanged marker.
+    #[inline]
+    pub fn map<U: ContainerElement>(self, convert: impl FnOnce(T) -> U) -> 
UnchangedOr<U> {
+        match self.into_option() {
+            Some(value) => UnchangedOr::changed(convert(value)),
+            None => UnchangedOr::unchanged(),
+        }
+    }
+
+    /// Fallibly convert only the replacement, propagating its error unchanged.
+    #[inline]
+    pub fn try_map<U: ContainerElement, E>(
+        self,
+        convert: impl FnOnce(T) -> std::result::Result<U, E>,
+    ) -> std::result::Result<UnchangedOr<U>, E> {
+        match self.into_option() {
+            Some(value) => convert(value).map(UnchangedOr::changed),
+            None => Ok(UnchangedOr::unchanged()),
+        }
+    }
+
+    /// Check a replacement's FFI type without cloning it.
+    ///
+    /// An unchanged result is compatible with every replacement type. Erased
+    /// and narrowing conversions retain the strict type check used by
+    /// [`Any::try_as`]. For a known typed conversion, use `map(Into::into)`.
+    #[inline]
+    pub fn try_cast<U: ContainerElement>(self) -> Result<UnchangedOr<U>> {
+        if unsafe { 
UnchangedOr::<U>::check_any_strict(self.data.as_raw_ffi_any()) } {
+            Ok(UnchangedOr {
+                data: self.data,
+                _marker: PhantomData,
+            })
+        } else {
+            Err(Error::new(
+                TYPE_ERROR,
+                &format!(
+                    "structural mutation result does not match {}",
+                    U::container_type_str()
+                ),
+                "",
+            ))
+        }
+    }
+}
+
+impl UnchangedOr<Any> {
+    #[inline]
+    pub(crate) fn from_carrier(data: Any) -> Result<Self> {
+        if data.type_index() == TVMFFITypeIndex::kTVMFFIError as i32 {
+            return match Error::try_from(data) {
+                Ok(error) | Err(error) => Err(error),
+            };
+        }
+        Ok(Self {
+            data,
+            _marker: PhantomData,
+        })
+    }
+}
+
+impl<T: ContainerElement> Clone for UnchangedOr<T> {
+    #[inline]
+    fn clone(&self) -> Self {
+        Self {
+            data: self.data.clone(),
+            _marker: PhantomData,
+        }
+    }
+}
+
+// SAFETY: the carrier contains either the resource-free marker or exactly
+// T's owning representation. Borrowing never acquires ownership; moving
+// transfers the carrier once. Non-strict conversions materialize a valid T.
+unsafe impl<T: ContainerElement> AnyCompatible for UnchangedOr<T> {
+    unsafe fn copy_to_any_view(src: &Self, data: &mut TVMFFIAny) {
+        *data = *src.data.as_raw_ffi_any();
+    }
+
+    unsafe fn move_to_any(src: Self, data: &mut TVMFFIAny) {
+        *data = Any::into_raw_ffi_any(src.data);
+    }
+
+    unsafe fn check_any_strict(data: &TVMFFIAny) -> bool {
+        if T::CONTAINER_IS_ANY {
+            // An erased successful result excludes the ABI's error channel.
+            data.type_index != TVMFFITypeIndex::kTVMFFIError as i32
+        } else {
+            Unchanged::check_any_strict(data) || 
T::container_check_any_strict(data)
+        }
+    }
+
+    unsafe fn copy_from_any_view_after_check(data: &TVMFFIAny) -> Self {
+        if Unchanged::check_any_strict(data) {
+            return Self::unchanged();
+        }
+        // Materialize T, including numeric narrowing, before storing its 
value.
+        Self::changed(T::container_copy_from_any_view_after_check(data))
+    }
+
+    unsafe fn move_from_any_after_check(data: &mut TVMFFIAny) -> Self {
+        Self {
+            data: Any::from_raw_ffi_any(std::mem::replace(data, 
TVMFFIAny::new())),
+            _marker: PhantomData,
+        }
+    }
+
+    unsafe fn try_cast_from_any_view(data: &TVMFFIAny) -> 
std::result::Result<Self, ()> {
+        if T::CONTAINER_IS_ANY && data.type_index == 
TVMFFITypeIndex::kTVMFFIError as i32 {
+            return Err(());
+        }
+        if Unchanged::check_any_strict(data) {
+            return Ok(Self::unchanged());
+        }
+        T::container_try_cast_from_any_view(data).map(Self::changed)
+    }
+
+    fn type_str() -> std::string::String {
+        format!("UnchangedOr<{}>", T::container_type_str())
+    }
+}
+
+impl<T: ContainerElement> TryFrom<Any> for UnchangedOr<T> {
+    type Error = Error;
+    #[inline]
+    fn try_from(value: Any) -> Result<Self> {
+        TryFromTemp::<Self>::try_from(value).map(TryFromTemp::into_value)
+    }
+}
+
+impl<'a, T: ContainerElement> TryFrom<AnyView<'a>> for UnchangedOr<T> {
+    type Error = Error;
+    #[inline]
+    fn try_from(value: AnyView<'a>) -> Result<Self> {
+        TryFromTemp::<Self>::try_from(value).map(TryFromTemp::into_value)
+    }
+}
+
+#[inline]
+pub(crate) fn is_unchanged(value: &Any) -> bool {
+    value.type_index() == TVMFFITypeIndex::kTVMFFIUnchanged as i32
+}
diff --git a/rust/tvm-ffi/src/lib.rs b/rust/tvm-ffi/src/lib.rs
index 149f3103..107399ff 100644
--- a/rust/tvm-ffi/src/lib.rs
+++ b/rust/tvm-ffi/src/lib.rs
@@ -58,6 +58,7 @@ pub use crate::extra::structural_visit::{
     StructuralVisitor, VisitCallbacks, VisitChainLink, VisitContext, 
VisitInterrupt, VisitValue,
     WalkChainLink, WalkDispatch, WalkOrder, WalkResult,
 };
+pub use crate::extra::unchanged::{Unchanged, UnchangedOr};
 pub use crate::function::Function;
 pub use crate::object::ObjectRefCast;
 pub use crate::object::{
diff --git a/rust/tvm-ffi/src/type_traits.rs b/rust/tvm-ffi/src/type_traits.rs
index 63662454..c298db9e 100644
--- a/rust/tvm-ffi/src/type_traits.rs
+++ b/rust/tvm-ffi/src/type_traits.rs
@@ -102,9 +102,12 @@ mod container_element_ops {
     use super::{Any, AnyCompatible, AnyView, TVMFFIAny};
 
     pub trait Ops: Sized {
+        const CONTAINER_IS_ANY: bool = false;
+
         unsafe fn container_copy_to_any_view(src: &Self, data: &mut TVMFFIAny);
         unsafe fn container_move_to_any(src: Self, data: &mut TVMFFIAny);
         unsafe fn container_check_any_strict(data: &TVMFFIAny) -> bool;
+        unsafe fn container_copy_from_any_view_after_check(data: &TVMFFIAny) 
-> Self;
         unsafe fn container_move_from_any_after_check(data: &mut TVMFFIAny) -> 
Self;
         unsafe fn container_try_cast_from_any_view(data: &TVMFFIAny) -> 
Result<Self, ()>;
         fn container_get_mismatch_type_info(data: &TVMFFIAny) -> String;
@@ -127,6 +130,11 @@ mod container_element_ops {
             <T as AnyCompatible>::check_any_strict(data)
         }
 
+        #[inline]
+        unsafe fn container_copy_from_any_view_after_check(data: &TVMFFIAny) 
-> Self {
+            <T as AnyCompatible>::copy_from_any_view_after_check(data)
+        }
+
         #[inline]
         unsafe fn container_move_from_any_after_check(data: &mut TVMFFIAny) -> 
Self {
             <T as AnyCompatible>::move_from_any_after_check(data)
@@ -149,6 +157,8 @@ mod container_element_ops {
     }
 
     impl Ops for Any {
+        const CONTAINER_IS_ANY: bool = true;
+
         #[inline]
         unsafe fn container_copy_to_any_view(src: &Self, data: &mut TVMFFIAny) 
{
             *data = *src.as_raw_ffi_any();
@@ -164,6 +174,11 @@ mod container_element_ops {
             true
         }
 
+        #[inline]
+        unsafe fn container_copy_from_any_view_after_check(data: &TVMFFIAny) 
-> Self {
+            Any::from(AnyView::from_raw_ffi_any(*data))
+        }
+
         #[inline]
         unsafe fn container_move_from_any_after_check(data: &mut TVMFFIAny) -> 
Self {
             Any::from_raw_ffi_any(std::mem::replace(data, TVMFFIAny::new()))
diff --git a/rust/tvm-ffi/tests/test_structural_mutate.rs 
b/rust/tvm-ffi/tests/test_structural_mutate.rs
index 0e951bba..14670061 100644
--- a/rust/tvm-ffi/tests/test_structural_mutate.rs
+++ b/rust/tvm-ffi/tests/test_structural_mutate.rs
@@ -25,7 +25,8 @@ use tvm_ffi::{
     dispatch, structural_map, structural_mutate, Any, AnyView, Array, 
CallbackMutator,
     DefRegionKind, Error, FieldGetter, Function, InplaceValue, Map, 
MapDispatch, MapValue,
     MutateCallbacks, Mutator, Object, ObjectArc, ObjectRefCore, Result, String 
as FfiString,
-    StructuralMutator, StructuralVarRemap, TypeIndex, WalkOrder, RUNTIME_ERROR,
+    StructuralMutator, StructuralVarRemap, TypeIndex, Unchanged, UnchangedOr, 
WalkOrder,
+    RUNTIME_ERROR,
 };
 
 struct IncrementIntegers;
@@ -816,6 +817,81 @@ fn callbacks_return_values_convertible_into_any() {
     assert_eq!(mutated.iter().collect::<Vec<_>>(), vec![2, 4]);
 }
 
+#[test]
+fn recursive_mutate_returns_unchanged_or_a_replacement() {
+    fn clamp_negative_integers(
+        value: &MapValue,
+        mutator: &mut CallbackMutator,
+    ) -> Result<UnchangedOr<Any>> {
+        if let Some(integer) = value.cast::<i64>() {
+            if integer >= 0 {
+                // Keep this input without constructing an owning return value.
+                return Ok(UnchangedOr::unchanged());
+            }
+            return Ok(UnchangedOr::changed(Any::from(0i64)));
+        }
+
+        // Recurse into containers. Keep the unchanged marker if no child 
changed.
+        mutator.default_mutate_result()
+    }
+
+    // No rewrite: the public entry resolves unchanged to the original array.
+    let source = Array::new(vec![1i64, 2]);
+    let result = structural_mutate(source.clone(), clamp_negative_integers)
+        .and_then(Array::<i64>::try_from)
+        .unwrap();
+    assert_eq!(result.iter().collect::<Vec<_>>(), vec![1, 2]);
+    assert_eq!(array_pointer(&result), array_pointer(&source));
+
+    // One rewrite: replace only the negative integer and keep the source 
intact.
+    let source = Array::new(vec![-1i64, 2]);
+    let result = structural_mutate(source.clone(), clamp_negative_integers)
+        .and_then(Array::<i64>::try_from)
+        .unwrap();
+    assert_eq!(result.iter().collect::<Vec<_>>(), vec![0, 2]);
+    assert_ne!(array_pointer(&result), array_pointer(&source));
+    assert_eq!(source.iter().collect::<Vec<_>>(), vec![-1, 2]);
+}
+
+#[test]
+fn pre_order_unchanged_reuses_unmodified_subtrees() {
+    let unchanged = Array::new(vec![1i64, 2]);
+    let changed = Array::new(vec![-1i64, 2]);
+    let source = Array::new(vec![unchanged.clone(), changed.clone()]);
+
+    let mapped = structural_map(
+        source.clone(),
+        (
+            |integer: i64| -> UnchangedOr<i64> {
+                if integer < 0 {
+                    UnchangedOr::changed(0)
+                } else {
+                    UnchangedOr::unchanged()
+                }
+            },
+            // Keeping an array still lets pre-order map transform its 
children.
+            |_value: &MapValue| Unchanged,
+        ),
+        WalkOrder::PreOrder,
+    )
+    .and_then(Array::<Array<i64>>::try_from)
+    .unwrap();
+
+    let mapped_unchanged = mapped.get(0).unwrap();
+    let mapped_changed = mapped.get(1).unwrap();
+    assert_eq!(mapped_unchanged.iter().collect::<Vec<_>>(), vec![1, 2]);
+    assert_eq!(mapped_changed.iter().collect::<Vec<_>>(), vec![0, 2]);
+
+    // Reuse the untouched subtree and copy the shared containers that changed.
+    assert_eq!(array_pointer(&mapped_unchanged), array_pointer(&unchanged));
+    assert_ne!(array_pointer(&mapped_changed), array_pointer(&changed));
+    assert_ne!(array_pointer(&mapped), array_pointer(&source));
+    assert_eq!(
+        source.get(1).unwrap().iter().collect::<Vec<_>>(),
+        vec![-1, 2]
+    );
+}
+
 #[test]
 fn twelve_link_tuple_reaches_final_map_dispatch() {
     let mut final_dispatch = IncrementIntegers;

Reply via email to