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 a6071f6  [EXTRA][FEAT] VisitErrorContext for visit-context-aware error 
reporting (#587)
a6071f6 is described below

commit a6071f671364563012cd830c98d6df1d935f880f
Author: Tianqi Chen <[email protected]>
AuthorDate: Thu May 21 14:32:20 2026 -0700

    [EXTRA][FEAT] VisitErrorContext for visit-context-aware error reporting 
(#587)
    
    ## Summary
    
    Introduces `VisitErrorContext`, a typed payload for
    `Error::extra_context` that records the chain of `ObjectRef` nodes
    visited during a recursive visit when an error is thrown. This enables
    visit-context-aware error reporting with structured access paths.
    
    ### Key components
    
    - **`VisitErrorContextObj` / `VisitErrorContext`**
    (`include/tvm/ffi/extra/visit_error_context.h`): Object/Ref pair that
    stores `reverse_visit_pattern` (the breadcrumb chain of visited nodes,
    innermost-first) and `prev_error_context` (any pre-existing error
    context that was displaced).
    
    - **Macros** for instrumenting recursive visits:
    - `TVM_FFI_VISIT_BEGIN()` / `TVM_FFI_VISIT_END(node)`: Wrap a visit body
    in a try/catch that appends `node` to the error context on unwind.
    - `TVM_FFI_VISIT_THROW(ErrorKind, node)`: Throw an error with `node` as
    the innermost frame of the context (for pinpointing a child field as the
    error site).
    
    - **`VisitErrorContext::FindAccessPaths(root, ctx)`**: Resolves the
    breadcrumb chain against a tree rooted at `root`, producing
    `reflection::AccessPath` instances (e.g., `.body[2].cond.lhs`) for
    richer error messages.
    
    - **`VisitErrorAccessPathFinder`** (internal, in `.cc`): DFS matcher
    that walks the tree using reflection field info, matching the breadcrumb
    pattern with support for Attr/ArrayItem/MapItem access kinds, CSE
    multi-match, sparse anchors, prefix matching, and
    consecutive-duplicate/null cleanup.
    
    ### Tests
    
    Comprehensive GoogleTest coverage in
    `tests/cpp/extra/test_visit_error_context.cc`:
    - Layer A (Macro to Chain): `MacroBuildsChain`,
    `MacroPreExistingPayloadWrap`, `MacroThrowBuildsChain`
    - Layer B (Chain to AccessPaths): `BasicMatch`, `AccessKindCoverage`,
    `SparsePatternAnchors`, `PartialChain`, `EdgeCases`, `RecordsCleanup`
    - Integration: `TryGetFromError`
---
 CMakeLists.txt                              |   1 +
 docs/conf.py                                |   3 +
 include/tvm/ffi/error.h                     |  13 +-
 include/tvm/ffi/extra/visit_error_context.h | 255 +++++++++++
 python/tvm_ffi/cython/error.pxi             |  17 +
 src/ffi/extra/visit_error_context.cc        | 331 ++++++++++++++
 tests/cpp/extra/test_visit_error_context.cc | 652 ++++++++++++++++++++++++++++
 7 files changed, 1271 insertions(+), 1 deletion(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6f07d4a..018f8c3 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -75,6 +75,7 @@ set(_tvm_ffi_objs_sources
 set(_tvm_ffi_extra_objs_sources
     "${CMAKE_CURRENT_SOURCE_DIR}/src/ffi/extra/structural_equal.cc"
     "${CMAKE_CURRENT_SOURCE_DIR}/src/ffi/extra/structural_hash.cc"
+    "${CMAKE_CURRENT_SOURCE_DIR}/src/ffi/extra/visit_error_context.cc"
     "${CMAKE_CURRENT_SOURCE_DIR}/src/ffi/extra/json_parser.cc"
     "${CMAKE_CURRENT_SOURCE_DIR}/src/ffi/extra/json_writer.cc"
     "${CMAKE_CURRENT_SOURCE_DIR}/src/ffi/extra/serialization.cc"
diff --git a/docs/conf.py b/docs/conf.py
index 14eb15b..07befa1 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -90,6 +90,8 @@ exhaleDoxygenStdin = """
 INPUT = ../include
 PREDEFINED             += TVM_FFI_DLL= TVM_FFI_DLL_EXPORT= TVM_FFI_INLINE= \
                           TVM_FFI_EXTRA_CXX_API= TVM_FFI_WEAK= 
TVM_FFI_DOXYGEN_MODE \
+                          TVM_FFI_COLD_CODE= \
+                          TVM_FFI_PREDICT_FALSE(x)=x TVM_FFI_PREDICT_TRUE(x)=x 
\
                           __cplusplus=201703
 EXCLUDE_SYMBOLS        += *details*  *TypeTraits* std \
                          *use_default_type_traits_v* *is_optional_type_v* 
*operator* \
@@ -124,6 +126,7 @@ cpp_id_attributes = [
     "TVM_FFI_INLINE",
     "TVM_FFI_EXTRA_CXX_API",
     "TVM_FFI_WEAK",
+    "TVM_FFI_COLD_CODE",
 ]
 
 c_id_attributes = [
diff --git a/include/tvm/ffi/error.h b/include/tvm/ffi/error.h
index 4931703..5421d6d 100644
--- a/include/tvm/ffi/error.h
+++ b/include/tvm/ffi/error.h
@@ -349,6 +349,14 @@ class ErrorBuilder {
       : ErrorBuilder(std::move(kind), std::string(backtrace->data, 
backtrace->size),
                      log_before_throw) {}
 
+  TVM_FFI_COLD_CODE
+  explicit ErrorBuilder(std::string kind, const TVMFFIByteArray* backtrace, 
bool log_before_throw,
+                        std::optional<Error> cause_chain, 
std::optional<ObjectRef> extra_context)
+      : ErrorBuilder(std::move(kind), backtrace, log_before_throw) {
+    cause_chain_ = std::move(cause_chain);
+    extra_context_ = std::move(extra_context);
+  }
+
 // MSVC disable warning in error builder as it is exepected
 #ifdef _MSC_VER
 #pragma warning(push)
@@ -356,7 +364,8 @@ class ErrorBuilder {
 #endif
   // avoid inline to reduce binary size, error throw path do not need to be 
fast
   [[noreturn]] TVM_FFI_COLD_CODE ~ErrorBuilder() noexcept(false) {
-    ::tvm::ffi::Error error(std::move(kind_), stream_.str(), 
std::move(backtrace_));
+    ::tvm::ffi::Error error(std::move(kind_), stream_.str(), 
std::move(backtrace_),
+                            std::move(cause_chain_), 
std::move(extra_context_));
     if (log_before_throw_) {
       std::cerr << error.FullMessage();
     }
@@ -373,6 +382,8 @@ class ErrorBuilder {
   std::ostringstream stream_;
   std::string backtrace_;
   bool log_before_throw_;
+  std::optional<Error> cause_chain_;
+  std::optional<ObjectRef> extra_context_;
 };
 
 }  // namespace details
diff --git a/include/tvm/ffi/extra/visit_error_context.h 
b/include/tvm/ffi/extra/visit_error_context.h
new file mode 100644
index 0000000..fd9310c
--- /dev/null
+++ b/include/tvm/ffi/extra/visit_error_context.h
@@ -0,0 +1,255 @@
+/*
+ * 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.
+ */
+/*!
+ * \file tvm/ffi/extra/visit_error_context.h
+ * \brief VisitErrorContext: typed payload for Error::extra_context that 
records the
+ *        chain of ObjectRefs visited during a recursive visit when an error 
is thrown.
+ */
+#ifndef TVM_FFI_EXTRA_VISIT_ERROR_CONTEXT_H_
+#define TVM_FFI_EXTRA_VISIT_ERROR_CONTEXT_H_
+
+#include <tvm/ffi/container/array.h>
+#include <tvm/ffi/container/list.h>
+#include <tvm/ffi/error.h>
+#include <tvm/ffi/extra/base.h>
+#include <tvm/ffi/memory.h>
+#include <tvm/ffi/object.h>
+#include <tvm/ffi/optional.h>
+#include <tvm/ffi/reflection/access_path.h>
+
+namespace tvm {
+namespace ffi {
+
+/*!
+ * \brief Object class for VisitErrorContext.
+ *
+ * \sa VisitErrorContext
+ */
+class VisitErrorContextObj : public Object {
+ public:
+  /*!
+   * \brief Visit records that get populated, which include the object visit
+   *        path pattern in innermost-first order. Best-effort — not 
exhaustive.
+   */
+  List<ObjectRef> reverse_visit_pattern;
+
+  /*!
+   * \brief Pre-existing Error::extra_context payload before we placed the
+   *        VisitErrorContext.
+   */
+  Optional<ObjectRef> prev_error_context;
+
+  /// \cond Doxygen_Suppress
+  static constexpr bool _type_mutable = true;
+  static constexpr TVMFFISEqHashKind _type_s_eq_hash_kind = 
kTVMFFISEqHashKindUnsupported;
+  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("ffi.VisitErrorContext", 
VisitErrorContextObj, Object);
+  /// \endcond
+};
+
+/*!
+ * \brief Typed payload attached to Error::extra_context to support
+ *        visit-context-aware error reporting.
+ *
+ * The VisitErrorContext captures the reverse_visit_pattern —
+ * the chain of nodes visited before an error was thrown — so callers
+ * can translate it via FindAccessPaths into a structured access path
+ * for richer error messages.
+ *
+ * Typical usage:
+ *
+ *   1. A recursive visit is instrumented with
+ *      TVM_FFI_VISIT_BEGIN / _END(node). The deepest detection
+ *      point throws via TVM_FFI_VISIT_THROW(kind, node), which
+ *      seeds the context with the throw site; enclosing BEGIN/END pairs
+ *      append parent nodes on rethrow.
+ *
+ *   2. The root catch handler retrieves the context via
+ *      TryGetFromError(err), then resolves the chain into one or more
+ *      reflection::AccessPath instances via FindAccessPaths(root, ctx).
+ *
+ *   3. The caller uses the AccessPath to enrich the error message
+ *      with structured position info (e.g., ".body[2].cond.lhs").
+ */
+class VisitErrorContext : public ObjectRef {
+ public:
+  /*! \brief Get the VisitErrorContext attached to err's extra_context.
+   *  \param err The error to inspect.
+   *  \return The attached VisitErrorContext, or NullOpt if absent.
+   */
+  TVM_FFI_COLD_CODE
+  static Optional<VisitErrorContext> TryGetFromError(const Error& err) {
+    std::optional<ObjectRef> extra_context = err.extra_context();
+    if (extra_context) {
+      return extra_context->as<VisitErrorContext>();
+    }
+    return std::nullopt;
+  }
+
+  /*! \brief Find all access paths that match the pattern specified in the
+   *         VisitErrorContext.
+   *  \param root The root ObjectRef to search from.
+   *  \param visit_context The VisitErrorContext to match against.
+   *  \param allow_prefix_match If true, also report paths where only a
+   *                            prefix of the pattern was matched (i.e.,
+   *                            the algorithm descended through some
+   *                            matched records but could not find further
+   *                            matches before reaching a leaf). Default
+   *                            false — only full pattern matches are
+   *                            reported.
+   *  \return Array of matched access paths.
+   */
+  TVM_FFI_COLD_CODE
+  TVM_FFI_EXTRA_CXX_API static Array<reflection::AccessPath> FindAccessPaths(
+      const ObjectRef& root, const VisitErrorContext& visit_context,
+      bool allow_prefix_match = false);
+
+  /// \cond Doxygen_Suppress
+  explicit VisitErrorContext(ObjectPtr<VisitErrorContextObj> n) : 
ObjectRef(std::move(n)) {}
+  TVM_FFI_DEFINE_OBJECT_REF_METHODS_NOTNULLABLE(VisitErrorContext, ObjectRef, 
VisitErrorContextObj);
+  /// \endcond
+};
+
+/*!
+ * \brief Begin a visit try block.
+ *
+ * Must be paired with TVM_FFI_VISIT_END(node) at the end of the
+ * visit body. Expands to an open `try {` — a mismatched _END macro is a
+ * compile error (unclosed try block).
+ *
+ * \code{.cpp}
+ * void MyVisitor::VisitNode(const ObjectRef& node) {
+ *   TVM_FFI_VISIT_BEGIN();
+ *   DispatchVisit(node);
+ *   TVM_FFI_VISIT_END(node);
+ * }
+ * \endcode
+ */
+#define TVM_FFI_VISIT_BEGIN() try {
+/*!
+ * \brief End a visit try block and catch+re-throw any Error,
+ *        appending node to the VisitErrorContext on the way up.
+ *
+ * Must be paired with TVM_FFI_VISIT_BEGIN() above the visit body.
+ *
+ * \param node The ObjectRef at the current visit level (appended to the
+ *             error context's reverse_visit_pattern on exception).
+ */
+#define TVM_FFI_VISIT_END(node)                                                
\
+  }                                                                            
\
+  catch (::tvm::ffi::Error & _tvm_ffi_visit_err_) {                            
\
+    ::tvm::ffi::details::UpdateVisitErrorContext(_tvm_ffi_visit_err_, (node)); 
\
+    throw;                                                                     
\
+  }
+
+/*!
+ * \brief Throw an error from inside a visit, with `node` recorded
+ *        as the innermost frame of the resulting VisitErrorContext.
+ *
+ * Use this when the bad spot is somewhere the BEGIN/END pair does not
+ * already record — typically a child field of the currently-visited node,
+ * or a helper called from a visit that has no BEGIN/END of its own. The
+ * throw site is seeded as the innermost frame; enclosing
+ * TVM_FFI_VISIT_BEGIN/END pairs continue to append their nodes
+ * on rethrow as the stack unwinds. The macro mirrors TVM_FFI_THROW —
+ * it returns an ostream you stream a message into.
+ *
+ * If `node` here is the same as the enclosing END's node (a redundant
+ * throw at the same level), FindAccessPaths normalizes the consecutive
+ * duplicate during matching, so user code does not need to guard against
+ * it — but in that case the throw would have been recorded by END
+ * anyway and a plain TVM_FFI_THROW would suffice.
+ *
+ * \code{.cpp}
+ * // Visiting a TPair node; pin the bad subfield (.lhs) as the throw
+ * // site so the resulting AccessPath ends at .lhs, not at the TPair
+ * // node itself. The surrounding END appends `node` as the next frame.
+ * void Visitor::Visit(const ObjectRef& node) {
+ *   TVM_FFI_VISIT_BEGIN();
+ *   if (auto pair = node.as<TPair>()) {
+ *     if (!IsValid(pair.value()->lhs)) {
+ *       TVM_FFI_VISIT_THROW(ValueError, pair.value()->lhs)
+ *           << "invalid lhs";
+ *     }
+ *   }
+ *   TVM_FFI_VISIT_END(node);
+ * }
+ * \endcode
+ *
+ * \param ErrorKind The kind of error to throw (e.g. TypeError, ValueError).
+ * \param node The ObjectRef at the throw site (innermost frame).
+ */
+#define TVM_FFI_VISIT_THROW(ErrorKind, node)                                   
                 \
+  ::tvm::ffi::details::ErrorBuilder(                                           
                 \
+      #ErrorKind, TVMFFIBacktrace(__FILE__, __LINE__, TVM_FFI_FUNC_SIG, 0),    
                 \
+      TVM_FFI_ALWAYS_LOG_BEFORE_THROW, ::std::nullopt,                         
                 \
+      
::std::optional<::tvm::ffi::ObjectRef>(::tvm::ffi::details::MakeVisitErrorContext(node)))
 \
+      .stream()
+
+namespace details {
+/*!
+ * \brief Build a fresh VisitErrorContext seeded with `node` as the
+ *        innermost (and currently only) frame of reverse_visit_pattern.
+ *
+ *        Used by TVM_FFI_VISIT_THROW to attach the throw-site
+ *        node to the Error's extra_context at construction time.
+ */
+TVM_FFI_COLD_CODE
+inline VisitErrorContext MakeVisitErrorContext(const ObjectRef& node) {
+  ObjectPtr<VisitErrorContextObj> obj = make_object<VisitErrorContextObj>();
+  obj->reverse_visit_pattern = List<ObjectRef>{node};
+  return VisitErrorContext(std::move(obj));
+}
+
+/*!
+ * \brief Implementation helper for TVM_FFI_VISIT_END(node).
+ *        Calling convention may change; do not call directly from user code.
+ */
+TVM_FFI_COLD_CODE
+inline void UpdateVisitErrorContext(Error& err, const ObjectRef& node) {  // 
NOLINT(*)
+  // NOTE: This function mutates the ErrorObj in place via ObjectUnsafe.
+  // Expected to run only inside the exception throw chain, where the Error
+  // is single-owned by this thread. The tradeoff avoids reallocating a
+  // fresh Error per catch frame; the immutability invariant returns once
+  // the unwind window closes.
+  std::optional<ObjectRef> extra_context = err.extra_context();
+  if (extra_context) {
+    Optional<VisitErrorContext> visit_context = 
extra_context->as<VisitErrorContext>();
+    if (visit_context) {
+      visit_context.value()->reverse_visit_pattern.push_back(node);
+      return;
+    }
+  }
+  // Build a fresh VisitErrorContext, preserving any pre-existing payload.
+  ObjectPtr<VisitErrorContextObj> new_context = 
make_object<VisitErrorContextObj>();
+  new_context->reverse_visit_pattern = List<ObjectRef>{node};
+  if (extra_context) new_context->prev_error_context = *extra_context;
+
+  ErrorObj* error_obj =
+      
static_cast<ErrorObj*>(details::ObjectUnsafe::RawObjectPtrFromObjectRef(err));
+  if (error_obj->extra_context != nullptr) {
+    details::ObjectUnsafe::DecRefObjectHandle(error_obj->extra_context);
+  }
+  error_obj->extra_context =
+      
details::ObjectUnsafe::MoveObjectPtrToTVMFFIObjectPtr(std::move(new_context));
+}
+}  // namespace details
+
+}  // namespace ffi
+}  // namespace tvm
+#endif  // TVM_FFI_EXTRA_VISIT_ERROR_CONTEXT_H_
diff --git a/python/tvm_ffi/cython/error.pxi b/python/tvm_ffi/cython/error.pxi
index cbd1194..16390ed 100644
--- a/python/tvm_ffi/cython/error.pxi
+++ b/python/tvm_ffi/cython/error.pxi
@@ -104,6 +104,23 @@ cdef class Error(CObject):
     def backtrace(self):
         return 
bytearray_to_str(&(TVMFFIErrorGetCellPtr(self.chandle).backtrace))
 
+    @property
+    def extra_context(self):
+        """Optional structured payload attached to this error.
+
+        Returns ``None`` if nothing is attached.  May be inspected via the
+        appropriate type-specific helpers.
+        """
+        cdef TVMFFIObjectHandle ctx_handle = 
TVMFFIErrorGetCellPtr(self.chandle).extra_context
+        if ctx_handle == NULL:
+            return None
+        # Build an owned Any from the unowned handle by incrementing the 
refcount.
+        cdef TVMFFIAny any_val
+        any_val.type_index = TVMFFIObjectGetTypeIndex(ctx_handle)
+        any_val.v_obj = <TVMFFIObject*>ctx_handle
+        TVMFFIObjectIncRef(ctx_handle)
+        return make_ret_object(any_val)
+
 
 cdef inline Error move_from_last_error():
     # raise last error
diff --git a/src/ffi/extra/visit_error_context.cc 
b/src/ffi/extra/visit_error_context.cc
new file mode 100644
index 0000000..bd2f0a8
--- /dev/null
+++ b/src/ffi/extra/visit_error_context.cc
@@ -0,0 +1,331 @@
+/*
+ * 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.
+ */
+/*
+ * \file src/ffi/extra/visit_error_context.cc
+ * \brief VisitErrorContext implementation — breadcrumb-trail collection and 
access-path
+ *        extraction for recursive Object visits.
+ */
+#include <tvm/ffi/container/array.h>
+#include <tvm/ffi/container/dict.h>
+#include <tvm/ffi/container/list.h>
+#include <tvm/ffi/container/map.h>
+#include <tvm/ffi/extra/visit_error_context.h>
+#include <tvm/ffi/memory.h>
+#include <tvm/ffi/reflection/accessor.h>
+#include <tvm/ffi/string.h>
+
+#include <utility>
+#include <vector>
+
+namespace tvm {
+namespace ffi {
+
+/**
+ * \brief Internal handler for finding all access paths in a root ObjectRef
+ *        that match the breadcrumb pattern stored in a VisitErrorContext.
+ */
+class VisitErrorAccessPathFinder {
+ public:
+  TVM_FFI_COLD_CODE
+  explicit VisitErrorAccessPathFinder(VisitErrorContext context, bool 
allow_prefix_match)
+      : context_(std::move(context)), allow_prefix_match_(allow_prefix_match) {
+    // Normalize the breadcrumb pattern before matching. Two kinds of noise can
+    // accumulate in reverse_visit_pattern at recording time:
+    //
+    //   1. Consecutive duplicates of the same ObjectRef. This is expected when
+    //      TVM_FFI_VISIT_THROW(kind, node) and an enclosing
+    //      TVM_FFI_VISIT_END(node) at the same level both record
+    //      the same `node` — the throw site seeds the context with `node`, and
+    //      the catch handler immediately above appends `node` again. Treat any
+    //      run of identical adjacent entries as a single frame.
+    //
+    //   2. Null / undefined ObjectRefs. These can leak in when a visited node
+    //      was mutated or torn down between recording and matching, leaving a
+    //      stale slot. We can never match a null pointer against a live tree,
+    //      so drop them silently rather than letting them break the chain.
+    //
+    // The matcher operates on `records_` from here on; the original
+    // `context_->reverse_visit_pattern` is left untouched so callers that
+    // inspect the context for debugging still see the raw history.
+    const List<ObjectRef>& raw = context_->reverse_visit_pattern;
+    records_.reserve(raw.size());
+    for (const ObjectRef& entry : raw) {
+      if (!entry.defined()) continue;
+      if (!records_.empty() && entry.same_as(records_.back())) continue;
+      records_.push_back(entry);
+    }
+  }
+
+  TVM_FFI_COLD_CODE
+  Array<reflection::AccessPath> Find(const ObjectRef& root) {
+    this->VisitObject(root);
+    return Array<reflection::AccessPath>(results_.begin(), results_.end());
+  }
+
+ private:
+  /**
+   * \brief Stack-allocated mirror of AccessStepObj used during the descent 
hot path.
+   *        For kAttr: stores const TVMFFIFieldInfo* encoded as void* in key 
(via
+   *        kTVMFFIOpaquePtr). String allocation is deferred to ToAccessStep().
+   *        Not an Object — pure struct, no Object header / refcount.
+   */
+  struct TempAccessStep {
+    reflection::AccessKind kind;
+    Any key{};  // For kAttr: FieldInfo pointer encoded as void* 
(kTVMFFIOpaquePtr).
+                // For kArrayItem: int64 index.
+                // For kMapItem: the user's key.
+
+    static TempAccessStep Attr(const TVMFFIFieldInfo* fi) {
+      TempAccessStep s;
+      s.kind = reflection::AccessKind::kAttr;
+      // We store `fi` as an OPAQUE POINTER inside `Any` (kTVMFFIOpaquePtr).
+      // `Any` is used here purely as a value-carrying slot for the bits of
+      // the pointer — it does NOT take ownership, does NOT dereference
+      // through it, and does NOT mutate the pointee. The FieldInfo struct
+      // is read-only metadata owned by the type-info registry and lives
+      // for the lifetime of the type info, so the underlying object is
+      // unaffected by the const_cast below.
+      //
+      // The const_cast is needed only because `Any` provides TypeTraits for
+      // `void*` but not for `const void*`. On retrieval (ToAccessStep()
+      // below) we cast back to `const TVMFFIFieldInfo*` before any use.
+      s.key = Any(const_cast<void*>(static_cast<const void*>(fi)));
+      return s;
+    }
+
+    static TempAccessStep ArrayItem(int64_t index) {
+      TempAccessStep s;
+      s.kind = reflection::AccessKind::kArrayItem;
+      s.key = Any(index);
+      return s;
+    }
+
+    static TempAccessStep MapItem(Any k) {
+      TempAccessStep s;
+      s.kind = reflection::AccessKind::kMapItem;
+      s.key = std::move(k);
+      return s;
+    }
+
+    /*! \brief Materialize the heap-allocated AccessStep. Called only at match 
time.
+     *         The String allocation for fi->name happens HERE, once. */
+    reflection::AccessStep ToAccessStep() const {
+      if (kind == reflection::AccessKind::kAttr) {
+        // Recover the original `const TVMFFIFieldInfo*` from the opaque
+        // pointer stored in `key`. The bits round-trip unchanged; the
+        // const-qualifier is restored here, immediately before any use.
+        const TVMFFIFieldInfo* fi = static_cast<const 
TVMFFIFieldInfo*>(key.cast<void*>());
+        return reflection::AccessStep::Attr(String(fi->name.data, 
fi->name.size));
+      } else if (kind == reflection::AccessKind::kArrayItem) {
+        return reflection::AccessStep::ArrayItem(key.cast<int64_t>());
+      } else {
+        // kMapItem
+        return reflection::AccessStep::MapItem(key);
+      }
+    }
+  };
+
+  void VisitAny(Any value) {
+    // Skip null Any silently — error-path defensive.
+    if (value == nullptr) return;
+    const int32_t type_index = value.type_index();
+    if (type_index < TypeIndex::kTVMFFIStaticObjectBegin) {
+      // Primitive — cannot hold an ObjectRef chain entry.
+      return;
+    }
+    switch (type_index) {
+      case TypeIndex::kTVMFFIArray:
+        this->VisitSequence(
+            
details::AnyUnsafe::MoveFromAnyAfterCheck<Array<Any>>(std::move(value)));
+        break;
+      case TypeIndex::kTVMFFIList:
+        
this->VisitSequence(details::AnyUnsafe::MoveFromAnyAfterCheck<List<Any>>(std::move(value)));
+        break;
+      case TypeIndex::kTVMFFIMap:
+        this->VisitMap(details::AnyUnsafe::MoveFromAnyAfterCheck<Map<Any, 
Any>>(std::move(value)));
+        break;
+      case TypeIndex::kTVMFFIDict:
+        this->VisitMap(details::AnyUnsafe::MoveFromAnyAfterCheck<Dict<Any, 
Any>>(std::move(value)));
+        break;
+      default:
+        if (type_index >= TypeIndex::kTVMFFIStaticObjectBegin) {
+          ObjectRef obj = 
details::AnyUnsafe::MoveFromAnyAfterCheck<ObjectRef>(std::move(value));
+          this->VisitObject(obj);
+        }
+        break;
+    }
+  }
+
+  void VisitObject(const ObjectRef& node) {
+    // Defensive: error path; never throw.
+    if (!node.defined()) return;
+    const TVMFFITypeInfo* type_info = TVMFFIGetTypeInfo(node->type_index());
+    if (type_info == nullptr || type_info->metadata == nullptr) return;
+
+    bool matched_step = num_pattern_step_matched_ < records_.size() &&
+                        node.same_as(records_[records_.size() - 1 - 
num_pattern_step_matched_]);
+    if (matched_step) {
+      ++num_pattern_step_matched_;
+      if (num_pattern_step_matched_ == records_.size()) {
+        // Full match — materialize the AccessPath and record.
+        results_.push_back(this->MaterializeAccessPath());
+        --num_pattern_step_matched_;
+        return;
+      }
+    }
+
+    this->VisitChildrenFields(node, type_info);
+
+    if (matched_step) --num_pattern_step_matched_;
+  }
+
+  void VisitChildrenFields(ObjectRef node, const TVMFFITypeInfo* type_info) {
+    // Snapshot results_.size() before descent to detect any inner full or
+    // prefix match recorded by a deeper call. If results_ grew, some inner
+    // node was recorded — we do not record again here.
+    size_t results_before = results_.size();
+    reflection::ForEachFieldInfo(type_info, [&](const TVMFFIFieldInfo* 
field_info) {
+      reflection::FieldGetter getter(field_info);
+      Any child_val = getter(node);
+      this->PushStep(TempAccessStep::Attr(field_info));
+      this->VisitAny(std::move(child_val));
+      this->PopStep();
+    });
+
+    // Leaf-with-prefix-match: this node contributed a match step, but no
+    // inner result was recorded from its subtree. Record the current node's
+    // path as the best-effort prefix. path_steps_ reflects the path TO this
+    // node (the step leading here was pushed by our caller), so
+    // MaterializeAccessPath() yields the correct prefix path.
+    // Skip when path_steps_ is empty — AccessPath::Root() gives no useful 
info.
+    if (allow_prefix_match_ && num_pattern_step_matched_ > 0 &&
+        num_pattern_step_matched_ < records_.size() && !path_steps_.empty() &&
+        results_.size() == results_before) {
+      results_.push_back(this->MaterializeAccessPath());
+    }
+  }
+
+  template <typename SeqType>
+  void VisitSequence(const SeqType& seq) {
+    for (size_t i = 0; i < seq.size(); ++i) {
+      Any item = seq[static_cast<int64_t>(i)];
+      if (item == nullptr) continue;
+      this->PushStep(TempAccessStep::ArrayItem(static_cast<int64_t>(i)));
+      this->VisitAny(std::move(item));
+      this->PopStep();
+    }
+  }
+
+  template <typename MapType>
+  void VisitMap(const MapType& m) {
+    for (const std::pair<Any, Any>& kv : m) {
+      if (kv.first == nullptr || kv.second == nullptr) continue;
+      this->PushStep(TempAccessStep::MapItem(kv.first));
+      this->VisitAny(kv.second);
+      this->PopStep();
+    }
+  }
+
+  /*! \brief Append a TempAccessStep to the descent stack; cache unchanged. */
+  void PushStep(TempAccessStep step) { path_steps_.push_back(std::move(step)); 
}
+
+  /*! \brief Pop the top TempAccessStep; truncate materialized_paths_ to 
match. */
+  void PopStep() {
+    path_steps_.pop_back();
+    if (materialized_paths_.size() > path_steps_.size()) {
+      materialized_paths_.erase(
+          materialized_paths_.begin() + 
static_cast<std::ptrdiff_t>(path_steps_.size()),
+          materialized_paths_.end());
+    }
+  }
+
+  // Cache invariant maintained jointly with PushStep / PopStep:
+  //   materialized_paths_[k] (when present) is the AccessPath built
+  //   from path_steps_[0..k+1] as they currently exist, and
+  //   materialized_paths_.size() <= path_steps_.size().
+  //
+  // - PushStep:              appends to path_steps_; cache unchanged.
+  // - PopStep:               pops path_steps_; truncates cache to match.
+  // - MaterializeAccessPath: extends cache up to path_steps_.size(),
+  //                          chaining new AccessPath nodes via Extend.
+  //
+  // Lazy materialization avoids per-descent AccessPath allocation;
+  // cache amortizes across consecutive matches with shared prefix
+  // (LCA sharing).
+  /*! \brief Materialize the AccessPath at the current descent depth. */
+  reflection::AccessPath MaterializeAccessPath() {
+    // Root-itself match: matching fired before any descent step was pushed,
+    // so the access path is just Root(). Returning early also avoids reading
+    // back() from an empty materialized_paths_.
+    if (path_steps_.empty()) return reflection::AccessPath::Root();
+    for (size_t idx = materialized_paths_.size(); idx < path_steps_.size(); 
++idx) {
+      reflection::AccessPath parent =
+          (idx == 0) ? reflection::AccessPath::Root() : 
materialized_paths_[idx - 1];
+      
materialized_paths_.push_back(parent->Extend(path_steps_[idx].ToAccessStep()));
+    }
+    return materialized_paths_.back();
+  }
+
+  // The visit error context whose pattern we're matching against.
+  VisitErrorContext context_;
+  // When true, record prefix matches at leaves (partial pattern match).
+  bool allow_prefix_match_;
+  // Normalized breadcrumb pattern derived from context_->reverse_visit_pattern
+  // by dropping null/undefined entries and collapsing consecutive duplicates.
+  // See the constructor for rationale.
+  std::vector<ObjectRef> records_;
+  // Count of pattern entries matched so far (root-closest first). Incremented 
on
+  // match, decremented on unwind; full match when equal to pattern size.
+  size_t num_pattern_step_matched_{0};
+  // Current descent path — entries pushed on field/index/key descent, popped 
on unwind.
+  std::vector<TempAccessStep> path_steps_;
+  // Lazy cache of materialized AccessPath nodes, parallel to path_steps_.
+  // materialized_paths_[k] corresponds to path_steps_[0..k+1] as they 
currently
+  // stand. Extended on match; truncated by PopStep. size <= 
path_steps_.size().
+  std::vector<reflection::AccessPath> materialized_paths_;
+  // Recorded full-match paths.
+  std::vector<reflection::AccessPath> results_;
+};
+
+// ---------------------------------------------------------------------------
+// VisitErrorContext::FindAccessPaths
+// ---------------------------------------------------------------------------
+
+TVM_FFI_COLD_CODE
+Array<reflection::AccessPath> VisitErrorContext::FindAccessPaths(
+    const ObjectRef& root, const VisitErrorContext& visit_context, bool 
allow_prefix_match) {
+  VisitErrorAccessPathFinder finder(visit_context, allow_prefix_match);
+  return finder.Find(root);
+}
+
+// ---------------------------------------------------------------------------
+// Registration
+// ---------------------------------------------------------------------------
+
+TVM_FFI_STATIC_INIT_BLOCK() {
+  namespace refl = tvm::ffi::reflection;
+  refl::ObjectDef<VisitErrorContextObj>()
+      .def_ro("reverse_visit_pattern", 
&VisitErrorContextObj::reverse_visit_pattern)
+      .def_ro("prev_error_context", &VisitErrorContextObj::prev_error_context);
+  refl::GlobalDef().def("ffi.VisitErrorContext.FindAccessPaths",
+                        &VisitErrorContext::FindAccessPaths);
+}
+
+}  // namespace ffi
+}  // namespace tvm
diff --git a/tests/cpp/extra/test_visit_error_context.cc 
b/tests/cpp/extra/test_visit_error_context.cc
new file mode 100644
index 0000000..91c3361
--- /dev/null
+++ b/tests/cpp/extra/test_visit_error_context.cc
@@ -0,0 +1,652 @@
+/*
+ * 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.
+ */
+#include <gtest/gtest.h>
+#include <tvm/ffi/container/array.h>
+#include <tvm/ffi/container/map.h>
+#include <tvm/ffi/error.h>
+#include <tvm/ffi/extra/structural_equal.h>
+#include <tvm/ffi/extra/visit_error_context.h>
+#include <tvm/ffi/object.h>
+#include <tvm/ffi/reflection/access_path.h>
+#include <tvm/ffi/reflection/registry.h>
+#include <tvm/ffi/string.h>
+
+namespace {
+
+using namespace tvm::ffi;
+namespace refl = tvm::ffi::reflection;
+
+// ---------------------------------------------------------------------------
+// Test fixture: TPair — a minimal two-field Object with lhs and rhs slots.
+//
+// Covers kAttr AccessKind via its named fields.
+// Array<ObjectRef> and Map<Any, Any> are used alongside TPair in individual
+// tests to cover kArrayItem and kMapItem.
+// ---------------------------------------------------------------------------
+
+class TPair : public Object {
+ public:
+  ObjectRef lhs;
+  ObjectRef rhs;
+
+  static constexpr TVMFFISEqHashKind _type_s_eq_hash_kind = 
kTVMFFISEqHashKindTreeNode;
+  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("test.TPair", TPair, Object);
+};
+
+class TPairRef : public ObjectRef {
+ public:
+  TPairRef(ObjectRef lhs, ObjectRef rhs) {
+    ObjectPtr<TPair> n = make_object<TPair>();
+    n->lhs = std::move(lhs);
+    n->rhs = std::move(rhs);
+    data_ = std::move(n);
+  }
+  TVM_FFI_DEFINE_OBJECT_REF_METHODS_NOTNULLABLE(TPairRef, ObjectRef, TPair);
+};
+
+TVM_FFI_STATIC_INIT_BLOCK() {
+  namespace r = tvm::ffi::reflection;
+  r::ObjectDef<TPair>().def_ro("lhs", &TPair::lhs).def_ro("rhs", &TPair::rhs);
+}
+
+// ---------------------------------------------------------------------------
+// Helper: Visit — recursively walks a tree and throws when it reaches
+// throw_target, wrapping each recursion level with the visit
+// macros so the error chain accumulates on unwind.
+// ---------------------------------------------------------------------------
+
+void Visit(const ObjectRef& node, const ObjectRef& throw_target);
+
+void Visit(const ObjectRef& node, const ObjectRef& throw_target) {
+  TVM_FFI_VISIT_BEGIN();
+
+  if (node.same_as(throw_target)) {
+    TVM_FFI_THROW(ValueError) << "boom";
+  } else if (Optional<TPairRef> pair = node.as<TPairRef>()) {
+    Visit(pair.value()->lhs, throw_target);
+    Visit(pair.value()->rhs, throw_target);
+  } else if (Optional<Array<ObjectRef>> arr = node.as<Array<ObjectRef>>()) {
+    for (const ObjectRef& child : *arr) {
+      Visit(child, throw_target);
+    }
+  } else if (Optional<Map<Any, Any>> m = node.as<Map<Any, Any>>()) {
+    for (const std::pair<Any, Any>& kv : *m) {
+      Visit(kv.second.cast<ObjectRef>(), throw_target);
+    }
+  }
+
+  TVM_FFI_VISIT_END(node);
+}
+
+// ===========================================================================
+// Layer A — Macro → Chain
+//
+// Verify that TVM_FFI_VISIT_BEGIN/_END correctly builds the
+// reverse_visit_pattern as an exception unwinds through visit levels.
+// ===========================================================================
+
+// ---------------------------------------------------------------------------
+// MacroBuildsChain: throw deep in a two-level visit; confirm the chain
+// records nodes in innermost-first order (throw site first, root last).
+// ---------------------------------------------------------------------------
+
+TEST(VisitErrorContext, MacroBuildsChain) {
+  // Tree: root = (lhs=leaf, rhs=empty)
+  // Visiting root -> descends into leaf -> throws.
+  Array<ObjectRef> empty;
+  TPairRef leaf(empty, empty);
+  TPairRef root(leaf, empty);
+
+  Error caught("RuntimeError", "", "");
+  bool did_catch = false;
+  try {
+    Visit(root, leaf);
+  } catch (Error& err) {
+    caught = err;
+    did_catch = true;
+  }
+
+  EXPECT_TRUE(did_catch);
+
+  Optional<VisitErrorContext> visit_context = 
VisitErrorContext::TryGetFromError(caught);
+  ASSERT_TRUE(visit_context.has_value());
+
+  // reverse_visit_pattern is innermost-first: leaf pushed first (inner catch
+  // fires first during unwind), root pushed last.
+  const List<ObjectRef>& chain = visit_context.value()->reverse_visit_pattern;
+  EXPECT_GE(chain.size(), 2u);
+  // Innermost entry is leaf (the throw site).
+  EXPECT_TRUE(chain[0].same_as(leaf));
+  // Outermost entry is root.
+  EXPECT_TRUE(chain[chain.size() - 1].same_as(root));
+
+  // Verify order structurally: [leaf, ..., root].
+  List<ObjectRef> expected_chain = {leaf, root};
+  EXPECT_TRUE(StructuralEqual::Equal(expected_chain, chain));
+}
+
+// ---------------------------------------------------------------------------
+// MacroPreExistingPayloadWrap: when UpdateVisitErrorContext is called on
+// an error that already has a non-context extra_context payload, the existing
+// payload is stashed in prev_error_context and the visit chain starts
+// fresh.  Subsequent pushes append to the same context object; 
prev_error_context
+// is not modified.
+// ---------------------------------------------------------------------------
+
+TEST(VisitErrorContext, MacroPreExistingPayloadWrap) {
+  // Stand-in for a pre-existing extra_context payload.
+  Array<ObjectRef> empty;
+  TPairRef existing_payload(empty, empty);
+  TPairRef leaf(empty, empty);
+  TPairRef root(leaf, empty);
+
+  // Construct an error that already carries a non-context extra_context 
payload.
+  Error err("RuntimeError", "test", "", std::nullopt, 
std::optional<ObjectRef>(existing_payload));
+  ASSERT_TRUE(err.extra_context().has_value());
+
+  // First push: existing payload is not a VisitErrorContext → wrapped.
+  tvm::ffi::details::UpdateVisitErrorContext(err, leaf);
+
+  Optional<VisitErrorContext> visit_context = 
VisitErrorContext::TryGetFromError(err);
+  ASSERT_TRUE(visit_context.has_value());
+  ASSERT_EQ(visit_context.value()->reverse_visit_pattern.size(), 1u);
+  EXPECT_TRUE(visit_context.value()->reverse_visit_pattern[0].same_as(leaf));
+  ASSERT_TRUE(visit_context.value()->prev_error_context.has_value());
+  
EXPECT_TRUE(visit_context.value()->prev_error_context.value().same_as(existing_payload));
+
+  // Second push: context already exists → node appended; prev_error_context 
unchanged.
+  tvm::ffi::details::UpdateVisitErrorContext(err, root);
+
+  Optional<VisitErrorContext> visit_context_after = 
VisitErrorContext::TryGetFromError(err);
+  ASSERT_TRUE(visit_context_after.has_value());
+
+  List<ObjectRef> expected_chain = {leaf, root};
+  EXPECT_TRUE(
+      StructuralEqual::Equal(expected_chain, 
visit_context_after.value()->reverse_visit_pattern));
+
+  ASSERT_TRUE(visit_context_after.value()->prev_error_context.has_value());
+  
EXPECT_TRUE(visit_context_after.value()->prev_error_context.value().same_as(existing_payload));
+}
+
+// ---------------------------------------------------------------------------
+// MacroThrowBuildsChain: TVM_FFI_VISIT_THROW seeds the
+// VisitErrorContext with the throw-site node directly (no separate
+// UpdateVisitErrorContext call). Enclosing VISIT_BEGIN/END frames then
+// append parent nodes on rethrow.
+//
+// Sub-scenario A — standalone: THROW outside any BEGIN/END wrapper produces
+// a context whose reverse_visit_pattern is exactly [throw_site].
+//
+// Sub-scenario B — nested under BEGIN/END: when THROW fires inside a
+// VISIT_BEGIN/END(root) pair, the enclosing END appends root. The throw site
+// itself is recorded by THROW. If the throw_target happens to equal `node`
+// at the surrounding level (a common case), FindAccessPaths' cleanup
+// collapses the consecutive duplicate; tested separately in
+// FindAccessPaths.RecordsCleanup.
+// ---------------------------------------------------------------------------
+
+TEST(VisitErrorContext, MacroThrowBuildsChain) {
+  Array<ObjectRef> empty;
+
+  // Sub-scenario A — standalone THROW (no surrounding BEGIN/END).
+  {
+    TPairRef throw_site(empty, empty);
+    bool did_catch = false;
+    Error caught("placeholder", "", "");
+    try {
+      TVM_FFI_VISIT_THROW(ValueError, throw_site) << "boom";
+    } catch (Error& err) {
+      caught = err;
+      did_catch = true;
+    }
+    ASSERT_TRUE(did_catch);
+
+    Optional<VisitErrorContext> visit_context = 
VisitErrorContext::TryGetFromError(caught);
+    ASSERT_TRUE(visit_context.has_value());
+    const List<ObjectRef>& chain = 
visit_context.value()->reverse_visit_pattern;
+    ASSERT_EQ(chain.size(), 1u);
+    EXPECT_TRUE(chain[0].same_as(throw_site));
+  }
+
+  // Sub-scenario B — THROW inside a BEGIN/END(root) wrapper.
+  {
+    TPairRef throw_site(empty, empty);
+    TPairRef root(throw_site, empty);
+    bool did_catch = false;
+    Error caught("placeholder", "", "");
+    try {
+      TVM_FFI_VISIT_BEGIN();
+      TVM_FFI_VISIT_THROW(ValueError, throw_site) << "boom";
+      TVM_FFI_VISIT_END(root);
+    } catch (Error& err) {
+      caught = err;
+      did_catch = true;
+    }
+    ASSERT_TRUE(did_catch);
+
+    Optional<VisitErrorContext> visit_context = 
VisitErrorContext::TryGetFromError(caught);
+    ASSERT_TRUE(visit_context.has_value());
+    const List<ObjectRef>& chain = 
visit_context.value()->reverse_visit_pattern;
+    // THROW seeds with throw_site; END appends root.
+    List<ObjectRef> expected = {throw_site, root};
+    EXPECT_TRUE(StructuralEqual::Equal(expected, chain));
+  }
+}
+
+// ===========================================================================
+// Layer B — Chain → AccessPaths
+//
+// Verify that FindAccessPaths(root, ctx) resolves a manually-constructed
+// VisitErrorContext against a tree, producing AccessPaths that describe
+// where matched nodes live in the tree.
+//
+// Each test:
+//   1. Builds a root tree.
+//   2. Constructs a VisitErrorContext with a specific reverse_visit_pattern.
+//   3. Calls FindAccessPaths(root, ctx).
+//   4. Asserts on the result using StructuralEqual::Equal on AccessPath
+//      (mirrors the pattern used in test_structural_equal_hash.cc).
+// ===========================================================================
+
+// ---------------------------------------------------------------------------
+// BasicMatch: unambiguous single-node path via two Attr steps; and CSE
+// multi-match when the same pointer appears in two sibling slots.
+//
+// Sub-scenario A — SingleMatch:
+//   Tree: root.lhs = mid, mid.lhs = leaf
+//   Pattern: [leaf, mid, root]  (innermost-first)
+//   Expected path: Root->Attr("lhs")->Attr("lhs")
+//
+// Sub-scenario B — CSEMultiMatch:
+//   Tree: root.lhs = shared, root.rhs = shared  (identical pointer)
+//   Pattern: [shared, root]
+//   Expected: two paths, one for each slot (Attr("lhs") and Attr("rhs")).
+// ---------------------------------------------------------------------------
+
+TEST(FindAccessPaths, BasicMatch) {
+  Array<ObjectRef> empty;
+
+  // Sub-scenario A: single unambiguous path.
+  {
+    TPairRef leaf(empty, empty);
+    TPairRef mid(leaf, empty);
+    TPairRef root(mid, empty);
+
+    ObjectPtr<VisitErrorContextObj> ctx_obj = 
make_object<VisitErrorContextObj>();
+    ctx_obj->reverse_visit_pattern = List<ObjectRef>{leaf, mid, root};
+    VisitErrorContext ctx(std::move(ctx_obj));
+
+    Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, 
ctx);
+    ASSERT_EQ(paths.size(), 1u);
+
+    refl::AccessPath expected = 
refl::AccessPath::Root()->Attr("lhs")->Attr("lhs");
+    EXPECT_TRUE(StructuralEqual::Equal(expected, paths[0]));
+  }
+
+  // Sub-scenario B: same pointer in two slots → two paths.
+  {
+    TPairRef shared(empty, empty);
+    TPairRef root(shared, shared);
+
+    ObjectPtr<VisitErrorContextObj> ctx_obj = 
make_object<VisitErrorContextObj>();
+    ctx_obj->reverse_visit_pattern = List<ObjectRef>{shared, root};
+    VisitErrorContext ctx(std::move(ctx_obj));
+
+    Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, 
ctx);
+    ASSERT_EQ(paths.size(), 2u);
+
+    refl::AccessPath expected_lhs = refl::AccessPath::Root()->Attr("lhs");
+    refl::AccessPath expected_rhs = refl::AccessPath::Root()->Attr("rhs");
+    bool found_lhs = false;
+    bool found_rhs = false;
+    for (const refl::AccessPath& p : paths) {
+      if (StructuralEqual::Equal(p, expected_lhs)) found_lhs = true;
+      if (StructuralEqual::Equal(p, expected_rhs)) found_rhs = true;
+    }
+    EXPECT_TRUE(found_lhs);
+    EXPECT_TRUE(found_rhs);
+  }
+}
+
+// ---------------------------------------------------------------------------
+// AccessKindCoverage: exercises Attr (TPair fields), ArrayItem
+// (Array<ObjectRef>), and MapItem (Map<Any, Any>) access kinds.
+//
+// Sub-scenario A — Attr (kAttr):
+//   Tree: root.lhs = mid, mid.lhs = leaf
+//   Pattern: [leaf, root]
+//   Expected path: Root->Attr("lhs")->Attr("lhs")
+//
+// Sub-scenario B — ArrayItem (kArrayItem):
+//   Tree: root.lhs = [leaf1, leaf2]  (Array in ObjectRef slot)
+//   Pattern: [leaf1, root]
+//   Expected path: Root->Attr("lhs")->ArrayItem(0)
+//
+// Sub-scenario C — MapItem (kMapItem):
+//   Tree: root.lhs = {"key" -> target}  (Map in ObjectRef slot)
+//   Pattern: [target, root]
+//   Expected path: Root->Attr("lhs")->MapItem("key")
+// ---------------------------------------------------------------------------
+
+TEST(FindAccessPaths, AccessKindCoverage) {
+  Array<ObjectRef> empty;
+
+  // Sub-scenario A: Attr steps.
+  {
+    TPairRef leaf(empty, empty);
+    TPairRef mid(leaf, empty);
+    TPairRef root(mid, empty);
+
+    ObjectPtr<VisitErrorContextObj> ctx_obj = 
make_object<VisitErrorContextObj>();
+    ctx_obj->reverse_visit_pattern = List<ObjectRef>{leaf, root};
+    VisitErrorContext ctx(std::move(ctx_obj));
+
+    Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, 
ctx);
+    ASSERT_EQ(paths.size(), 1u);
+
+    refl::AccessPath expected = 
refl::AccessPath::Root()->Attr("lhs")->Attr("lhs");
+    EXPECT_TRUE(StructuralEqual::Equal(expected, paths[0]));
+  }
+
+  // Sub-scenario B: ArrayItem step.
+  {
+    TPairRef leaf1(empty, empty);
+    TPairRef leaf2(empty, empty);
+    Array<ObjectRef> arr = {leaf1, leaf2};
+    TPairRef root(arr, empty);
+
+    ObjectPtr<VisitErrorContextObj> ctx_obj = 
make_object<VisitErrorContextObj>();
+    ctx_obj->reverse_visit_pattern = List<ObjectRef>{leaf1, root};
+    VisitErrorContext ctx(std::move(ctx_obj));
+
+    Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, 
ctx);
+    ASSERT_EQ(paths.size(), 1u);
+
+    refl::AccessPath expected = 
refl::AccessPath::Root()->Attr("lhs")->ArrayItem(0);
+    EXPECT_TRUE(StructuralEqual::Equal(expected, paths[0]));
+  }
+
+  // Sub-scenario C: MapItem step.
+  {
+    TPairRef target(empty, empty);
+    Map<Any, Any> m;
+    m.Set(String("key"), target);
+    TPairRef root(m, empty);
+
+    ObjectPtr<VisitErrorContextObj> ctx_obj = 
make_object<VisitErrorContextObj>();
+    ctx_obj->reverse_visit_pattern = List<ObjectRef>{target, root};
+    VisitErrorContext ctx(std::move(ctx_obj));
+
+    Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, 
ctx);
+    ASSERT_EQ(paths.size(), 1u);
+
+    refl::AccessPath expected = 
refl::AccessPath::Root()->Attr("lhs")->MapItem(String("key"));
+    EXPECT_TRUE(StructuralEqual::Equal(expected, paths[0]));
+  }
+}
+
+// ---------------------------------------------------------------------------
+// SparsePatternAnchors: demonstrates that intermediate breadcrumb anchors in
+// the pattern disambiguate when an innermost target appears in multiple 
branches.
+//
+// Tree layout:
+//   root
+//   ├─ branch_a: tpa → m_inner_a   (m_inner shared, no m_outer ancestor)
+//   └─ branch_b: tpb → m_outer → tpc → m_inner_b  (same m_inner pointer)
+//
+// m_inner is a POINTER-IDENTICAL ObjectRef (ref-count shared) used as both
+// m_inner_a (under branch_a) and m_inner_b (under branch_b via m_outer).
+// m_outer appears only under branch_b.
+//
+// Scenario 1: pattern with only innermost — 2 candidate paths.
+//   Pattern: [m_inner, root]
+//   Expected: paths.size() == 2
+//
+// Scenario 2: anchor narrows to branch_b only.
+//   Pattern: [m_inner, m_outer, root]
+//   Expected: paths.size() == 1, path through branch_b
+// ---------------------------------------------------------------------------
+
+TEST(FindAccessPaths, SparsePatternAnchors) {
+  Array<ObjectRef> empty;
+
+  // Shared inner node — identical pointer in both branches.
+  TPairRef m_inner(empty, empty);
+
+  // branch_a: tpa.lhs = m_inner (no m_outer ancestor)
+  TPairRef tpa(m_inner, empty);
+
+  // branch_b: tpb.lhs = m_outer, m_outer.lhs = tpc, tpc.lhs = m_inner
+  TPairRef tpc(m_inner, empty);
+  TPairRef m_outer(tpc, empty);
+  TPairRef tpb(m_outer, empty);
+
+  // root: lhs = tpa (branch_a), rhs = tpb (branch_b)
+  TPairRef root(tpa, tpb);
+
+  // Scenario 1: pattern = [m_inner, root] → 2 paths (one per branch).
+  {
+    ObjectPtr<VisitErrorContextObj> ctx_obj = 
make_object<VisitErrorContextObj>();
+    ctx_obj->reverse_visit_pattern = List<ObjectRef>{m_inner, root};
+    VisitErrorContext ctx(std::move(ctx_obj));
+
+    Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, 
ctx);
+    EXPECT_EQ(paths.size(), 2u);
+  }
+
+  // Scenario 2: pattern = [m_inner, m_outer, root] → 1 path through branch_b.
+  {
+    ObjectPtr<VisitErrorContextObj> ctx_obj = 
make_object<VisitErrorContextObj>();
+    ctx_obj->reverse_visit_pattern = List<ObjectRef>{m_inner, m_outer, root};
+    VisitErrorContext ctx(std::move(ctx_obj));
+
+    Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, 
ctx);
+    ASSERT_EQ(paths.size(), 1u);
+
+    // Path must go through branch_b (rhs), then m_outer (lhs), then tpc 
(lhs), then m_inner (lhs).
+    refl::AccessPath expected =
+        
refl::AccessPath::Root()->Attr("rhs")->Attr("lhs")->Attr("lhs")->Attr("lhs");
+    EXPECT_TRUE(StructuralEqual::Equal(expected, paths[0]));
+  }
+}
+
+// ---------------------------------------------------------------------------
+// PartialChain: strict mode returns nothing when the innermost pattern entry
+// is unreachable; prefix-match mode returns the deepest matched prefix path.
+//
+// Sub-scenario A — strict (allow_prefix_match=false):
+//   Pattern: [unreachable, child, root]
+//   Expected: 0 results (unreachable breaks full match)
+//
+// Sub-scenario B — prefix match (allow_prefix_match=true):
+//   Same pattern, same tree.
+//   Expected: at least one result at the path to child (Root->Attr("lhs")).
+// ---------------------------------------------------------------------------
+
+TEST(FindAccessPaths, PartialChain) {
+  Array<ObjectRef> empty;
+  // unreachable is not present anywhere in the tree below root.
+  TPairRef unreachable(empty, empty);
+  TPairRef child(empty, empty);
+  TPairRef root(child, empty);
+
+  ObjectPtr<VisitErrorContextObj> ctx_obj = 
make_object<VisitErrorContextObj>();
+  ctx_obj->reverse_visit_pattern = List<ObjectRef>{unreachable, child, root};
+  VisitErrorContext ctx(std::move(ctx_obj));
+
+  // Sub-scenario A: strict mode — no full match.
+  {
+    Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, 
ctx);
+    EXPECT_EQ(paths.size(), 0u);
+  }
+
+  // Sub-scenario B: prefix-match mode — path to child reported.
+  {
+    Array<refl::AccessPath> paths =
+        VisitErrorContext::FindAccessPaths(root, ctx, 
/*allow_prefix_match=*/true);
+    ASSERT_GE(paths.size(), 1u);
+
+    refl::AccessPath expected = refl::AccessPath::Root()->Attr("lhs");
+    bool found_expected = false;
+    for (const refl::AccessPath& p : paths) {
+      if (StructuralEqual::Equal(p, expected)) {
+        found_expected = true;
+        break;
+      }
+    }
+    EXPECT_TRUE(found_expected);
+  }
+}
+
+// ---------------------------------------------------------------------------
+// EdgeCases: empty pattern produces no results; null entries in the pattern
+// never match any live node and do not crash.
+// ---------------------------------------------------------------------------
+
+TEST(FindAccessPaths, EdgeCases) {
+  Array<ObjectRef> empty;
+
+  // Sub-scenario A: empty pattern.
+  {
+    TPairRef root(empty, empty);
+
+    ObjectPtr<VisitErrorContextObj> ctx_obj = 
make_object<VisitErrorContextObj>();
+    // reverse_visit_pattern left empty.
+    VisitErrorContext ctx(std::move(ctx_obj));
+
+    Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, 
ctx);
+    EXPECT_EQ(paths.size(), 0u);
+  }
+
+  // Sub-scenario A2: root-itself match — pattern is [root], the throw fired
+  // before any descent. Expected: a single AccessPath equal to Root() (no
+  // crash from materializing on an empty descent stack).
+  {
+    TPairRef root(empty, empty);
+
+    ObjectPtr<VisitErrorContextObj> ctx_obj = 
make_object<VisitErrorContextObj>();
+    ctx_obj->reverse_visit_pattern = List<ObjectRef>{root};
+    VisitErrorContext ctx(std::move(ctx_obj));
+
+    Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, 
ctx);
+    ASSERT_EQ(paths.size(), 1u);
+    EXPECT_TRUE(StructuralEqual::Equal(refl::AccessPath::Root(), paths[0]));
+  }
+
+  // Sub-scenario B: null entries in pattern.
+  {
+    TPairRef root(empty, empty);
+
+    ObjectPtr<VisitErrorContextObj> ctx_obj = 
make_object<VisitErrorContextObj>();
+    ObjectRef null_ref;
+    ctx_obj->reverse_visit_pattern = List<ObjectRef>{null_ref, null_ref};
+    VisitErrorContext ctx(std::move(ctx_obj));
+
+    Array<refl::AccessPath> paths;
+    ASSERT_NO_THROW({ paths = VisitErrorContext::FindAccessPaths(root, ctx); 
});
+    EXPECT_EQ(paths.size(), 0u);
+  }
+}
+
+// ---------------------------------------------------------------------------
+// RecordsCleanup: FindAccessPaths normalizes reverse_visit_pattern before
+// matching by (a) dropping null entries and (b) collapsing runs of
+// consecutive duplicates. Both arise naturally — null from stale/torn-down
+// nodes; duplicates when TVM_FFI_VISIT_THROW(node) is wrapped by
+// a TVM_FFI_VISIT_END(node) at the same level. Without cleanup
+// the redundant frame would over-constrain the match and yield zero paths.
+// ---------------------------------------------------------------------------
+
+TEST(FindAccessPaths, RecordsCleanup) {
+  Array<ObjectRef> empty;
+  TPairRef leaf(empty, empty);
+  TPairRef root(leaf, empty);
+  ObjectRef null_ref;
+
+  refl::AccessPath expected = refl::AccessPath::Root()->Attr("lhs");
+
+  // Sub-scenario A — consecutive duplicates collapse.
+  // Raw pattern: [leaf, leaf, root, root]; effective: [leaf, root].
+  {
+    ObjectPtr<VisitErrorContextObj> ctx_obj = 
make_object<VisitErrorContextObj>();
+    ctx_obj->reverse_visit_pattern = List<ObjectRef>{leaf, leaf, root, root};
+    VisitErrorContext ctx(std::move(ctx_obj));
+
+    Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, 
ctx);
+    ASSERT_EQ(paths.size(), 1u);
+    EXPECT_TRUE(StructuralEqual::Equal(expected, paths[0]));
+  }
+
+  // Sub-scenario B — nulls are skipped, surrounding entries still match.
+  // Raw pattern: [null, leaf, null, root, null]; effective: [leaf, root].
+  {
+    ObjectPtr<VisitErrorContextObj> ctx_obj = 
make_object<VisitErrorContextObj>();
+    ctx_obj->reverse_visit_pattern = List<ObjectRef>{null_ref, leaf, null_ref, 
root, null_ref};
+    VisitErrorContext ctx(std::move(ctx_obj));
+
+    Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, 
ctx);
+    ASSERT_EQ(paths.size(), 1u);
+    EXPECT_TRUE(StructuralEqual::Equal(expected, paths[0]));
+  }
+
+  // Sub-scenario C — combined: nulls plus dup-consecutive.
+  // Raw pattern: [leaf, null, leaf, root, root, null]; effective: [leaf, 
root].
+  {
+    ObjectPtr<VisitErrorContextObj> ctx_obj = 
make_object<VisitErrorContextObj>();
+    ctx_obj->reverse_visit_pattern = List<ObjectRef>{leaf, null_ref, leaf, 
root, root, null_ref};
+    VisitErrorContext ctx(std::move(ctx_obj));
+
+    Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, 
ctx);
+    ASSERT_EQ(paths.size(), 1u);
+    EXPECT_TRUE(StructuralEqual::Equal(expected, paths[0]));
+  }
+}
+
+// ---------------------------------------------------------------------------
+// TryGetFromError: returns NullOpt when absent, the context when present.
+// Composes correctly with FindAccessPaths.
+// ---------------------------------------------------------------------------
+
+TEST(VisitErrorContext, TryGetFromError) {
+  Array<ObjectRef> empty;
+  TPairRef leaf(empty, empty);
+  TPairRef root(leaf, empty);
+
+  Error err("RuntimeError", "test", "");
+
+  // No context attached → TryGetFromError returns NullOpt.
+  Optional<VisitErrorContext> no_context = 
VisitErrorContext::TryGetFromError(err);
+  EXPECT_FALSE(no_context.has_value());
+
+  // Attach context via UpdateVisitErrorContext.
+  tvm::ffi::details::UpdateVisitErrorContext(err, leaf);
+  tvm::ffi::details::UpdateVisitErrorContext(err, root);
+
+  Optional<VisitErrorContext> visit_context = 
VisitErrorContext::TryGetFromError(err);
+  ASSERT_TRUE(visit_context.has_value());
+
+  // Compose TryGetFromError + FindAccessPaths → should resolve to root.lhs.
+  Array<refl::AccessPath> paths = VisitErrorContext::FindAccessPaths(root, 
visit_context.value());
+  ASSERT_EQ(paths.size(), 1u);
+
+  refl::AccessPath expected = refl::AccessPath::Root()->Attr("lhs");
+  EXPECT_TRUE(StructuralEqual::Equal(expected, paths[0]));
+}
+
+}  // namespace

Reply via email to