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;