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 0e51eddf [FIX] Preserve post-order structural map descent (#866)
0e51eddf is described below

commit 0e51eddf5976ea5b031a82bacfe6a16c6e5fd1eb
Author: Tianqi Chen <[email protected]>
AuthorDate: Thu Oct 22 18:02:37 2026 -0400

    [FIX] Preserve post-order structural map descent (#866)
---
 include/tvm/ffi/extra/structural_mutate.h | 23 ++++++++++----
 tests/cpp/extra/test_structural_mutate.cc | 53 +++++++++++++++++++++++++------
 2 files changed, 61 insertions(+), 15 deletions(-)

diff --git a/include/tvm/ffi/extra/structural_mutate.h 
b/include/tvm/ffi/extra/structural_mutate.h
index d768c3a0..975dd333 100644
--- a/include/tvm/ffi/extra/structural_mutate.h
+++ b/include/tvm/ffi/extra/structural_mutate.h
@@ -1350,7 +1350,7 @@ class StructuralMapEngine : public Parent {
         matched = mapped_view.template as<TSub>();
         if (!matched.has_value()) return false;
       }
-      *out = [&]() -> Expected<Any> {
+      Expected<Any> callback_result = [&]() -> Expected<Any> {
         if constexpr (std::is_same_v<TSub, AnyView>) {
           return InvokeTypedCallbackLink(callback, mapped_view, 
StateIndices{});
         } else if constexpr (std::is_same_v<TSub, Any>) {
@@ -1359,10 +1359,15 @@ class StructuralMapEngine : public Parent {
           return InvokeTypedCallbackLink(callback, *std::move(matched), 
StateIndices{});
         }
       }();
-      if (TVM_FFI_PREDICT_FALSE(out->is_err())) {
-        this->UpdateVisitErrorContext(*out, mapped_view);
+      if (TVM_FFI_PREDICT_FALSE(callback_result.is_err())) {
+        this->UpdateVisitErrorContext(callback_result, mapped_view);
+        *out = std::move(callback_result);
         return true;
       }
+      // Unchanged refers to the descended node, whose owner stays live 
through the callback.
+      if (ExpectedUnsafe::GetData(callback_result).type_index() != 
TypeIndex::kTVMFFIUnchanged) {
+        *out = std::move(callback_result);
+      }
       return true;
     }
   }
@@ -1570,11 +1575,17 @@ class StructuralMapDynEngine : public Parent {
       if (!matched.has_value()) return false;
       // WithDefRegionKind restores its state through RAII, so this late read 
is equivalent to
       // the typed engine's invocation-time read even after recursive descent.
-      *out = InvokeLink(*matched, with_kind, mapped_view, 
this->def_region_kind());
-      if (TVM_FFI_PREDICT_FALSE(out->is_err())) {
-        this->UpdateVisitErrorContext(*out, mapped_view);
+      Expected<Any> callback_result =
+          InvokeLink(*matched, with_kind, mapped_view, 
this->def_region_kind());
+      if (TVM_FFI_PREDICT_FALSE(callback_result.is_err())) {
+        this->UpdateVisitErrorContext(callback_result, mapped_view);
+        *out = std::move(callback_result);
         return true;
       }
+      // Preserve descent and its lifetime just as in the typed engine.
+      if (ExpectedUnsafe::GetData(callback_result).type_index() != 
TypeIndex::kTVMFFIUnchanged) {
+        *out = std::move(callback_result);
+      }
       return true;
     }
   }
diff --git a/tests/cpp/extra/test_structural_mutate.cc 
b/tests/cpp/extra/test_structural_mutate.cc
index 6973eb94..02ea9f86 100644
--- a/tests/cpp/extra/test_structural_mutate.cc
+++ b/tests/cpp/extra/test_structural_mutate.cc
@@ -733,7 +733,11 @@ TEST(StructuralMap, 
RootByValueProtectsSharedParentSubvalue) {
   AnyArray outer{Any(std::move(child))};
   const Object* child_address = outer[0].cast<AnyArray>().get();
 
-  AnyArray mapped = StructuralMap<WalkOrder::kPostOrder>(outer[0], 
Increment).cast<AnyArray>();
+  AnyArray mapped =
+      StructuralMap<WalkOrder::kPostOrder>(outer[0], Increment, [](const 
AnyArray& array) {
+        EXPECT_EQ(array[0].cast<int64_t>(), 2);
+        return Unchanged();
+      }).cast<AnyArray>();
 
   EXPECT_NE(mapped.get(), child_address);
   EXPECT_EQ(outer[0].cast<AnyArray>()[0].cast<int64_t>(), 1);
@@ -1137,18 +1141,49 @@ Any CallDynStructuralMap(AnyView root, const 
Array<Tuple<int32_t, Function>>& ca
   return fn(root, callbacks, Array<Tuple<int32_t, Function>>(), 
static_cast<int32_t>(order));
 }
 
-TEST(StructuralMapDyn, PreOrderDescendsOriginalAfterUnchangedCallback) {
+TEST(StructuralMapDyn, UnchangedCallbackPreservesDescent) {
   AnyArray root{int64_t{1}};
   Function unchanged = Function::FromTyped([](const AnyArray&) -> Any { return 
Unchanged(); });
   Function increment = Function::FromTyped([](int64_t value) -> Any { return 
Any(value + 1); });
 
-  AnyArray mapped =
-      CallDynStructuralMap(root,
-                           {Tuple<int32_t, Function>(TypeIndex::kTVMFFIArray, 
unchanged),
-                            Tuple<int32_t, Function>(TypeIndex::kTVMFFIInt, 
increment)},
-                           WalkOrder::kPreOrder)
-          .cast<AnyArray>();
-  EXPECT_EQ(mapped[0].cast<int64_t>(), 2);
+  for (WalkOrder order : {WalkOrder::kPreOrder, WalkOrder::kPostOrder}) {
+    AnyArray mapped =
+        CallDynStructuralMap(root,
+                             {Tuple<int32_t, 
Function>(TypeIndex::kTVMFFIArray, unchanged),
+                              Tuple<int32_t, Function>(TypeIndex::kTVMFFIInt, 
increment)},
+                             order)
+            .cast<AnyArray>();
+    EXPECT_EQ(mapped[0].cast<int64_t>(), 2);
+    EXPECT_EQ(root[0].cast<int64_t>(), 1);
+  }
+}
+
+TEST(StructuralMap, PostOrderCallbackErrorKeepsDescendedNode) {
+  AnyArray root{int64_t{1}};
+  auto fail = [](const ArrayObj*) -> Any {
+    return Any(Error("ValueError", "post-order callback failed", ""));
+  };
+  auto check_error = [&](const Expected<Any>& result) {
+    ASSERT_TRUE(result.is_err());
+    EXPECT_EQ(result.error().message(), "post-order callback failed");
+    Optional<VisitErrorContext> context = 
VisitErrorContext::TryGetFromError(result.error());
+    ASSERT_TRUE(context.has_value());
+    ASSERT_EQ(context.value()->reverse_visit_pattern.size(), 1U);
+    AnyArray descended = 
context.value()->reverse_visit_pattern[0].as_or_throw<AnyArray>();
+    EXPECT_FALSE(descended.same_as(root));
+    EXPECT_EQ(descended[0].cast<int64_t>(), 2);
+  };
+
+  check_error(StructuralMapExpected<WalkOrder::kPostOrder>(root, Increment, 
fail));
+  Function map = Function::GetGlobalRequired("ffi.StructuralMap");
+  check_error(map.CallExpected<Any>(
+      root,
+      Array<Tuple<int32_t, Function>>{
+          Tuple<int32_t, Function>(TypeIndex::kTVMFFIArray, 
Function::FromTyped(fail)),
+          Tuple<int32_t, Function>(TypeIndex::kTVMFFIInt,
+                                   Function::FromTyped([](int64_t value) { 
return value + 1; }))},
+      Array<Tuple<int32_t, Function>>(), 
static_cast<int32_t>(WalkOrder::kPostOrder)));
+  EXPECT_EQ(root[0].cast<int64_t>(), 1);
 }
 
 TEST(StructuralMapDyn, InvokesCallbackForEveryFreeVarOccurrence) {

Reply via email to