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 4ffc1f35 [FEAT] Add strict narrowing for Expected values (#787)
4ffc1f35 is described below

commit 4ffc1f35f3c9853b53f2e854768a9acc6cbf3b85
Author: Tianqi Chen <[email protected]>
AuthorDate: Mon Sep 14 13:16:09 2026 -0400

    [FEAT] Add strict narrowing for Expected values (#787)
    
    Add `const &` and `&&` overloads of `Expected<T>::as_or_error<U>()` for
    strict narrowing while preserving existing errors. Accepted values reuse
    their FFI storage; mismatches return `TypeError` without fallback
    conversion.
    
    The lvalue overload copies storage and preserves its source, while the
    rvalue overload moves storage. Targets follow owning FFI storage
    constraints.
---
 include/tvm/ffi/expected.h | 42 ++++++++++++++++++++++++++++++++++++++++++
 tests/cpp/test_expected.cc | 39 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 81 insertions(+)

diff --git a/include/tvm/ffi/expected.h b/include/tvm/ffi/expected.h
index 4ba7d292..3a4f87d7 100644
--- a/include/tvm/ffi/expected.h
+++ b/include/tvm/ffi/expected.h
@@ -242,6 +242,48 @@ class Expected {
     throw details::AnyUnsafe::MoveFromAnyAfterCheck<Error>(std::move(data_));
   }
 
+  /*!
+   * \brief Strictly reinterpret the success value as U, or return an error.
+   * \tparam U An unqualified owning storage type, excluding Error and its 
subclasses and void.
+   * \return A copy of the stored value or existing error, or TypeError on a 
type mismatch.
+   * \note This function does not perform value conversions. The source is 
preserved.
+   */
+  template <typename U, typename = std::enable_if_t<
+                            std::is_same_v<U, std::decay_t<U>> && 
!std::is_base_of_v<Error, U> &&
+                            (TypeTraits<U>::storage_enabled || 
std::is_same_v<U, Any>)>>
+  TVM_FFI_INLINE Expected<U> as_or_error() const& {
+    if (TVM_FFI_PREDICT_FALSE(data_.type_index() != TypeIndex::kTVMFFIError &&
+                              !details::AnyUnsafe::CheckAnyStrict<U>(data_))) {
+      // Conversion-failure diagnostics may try fallback conversions, so use 
the stored type key.
+      return Error("TypeError",
+                   "Cannot treat type `" + data_.GetTypeKey() + "` as type `" +
+                       details::Type2Str<U>::v() + "`",
+                   "");
+    }
+    return Expected<U>(UnsafeInit{}, 
details::AnyUnsafe::MoveAnyToTVMFFIAny(Any(data_)));
+  }
+
+  /*!
+   * \brief Strictly reinterpret the success value as U, or return an error, 
moving the storage.
+   * \tparam U An unqualified owning storage type, excluding Error and its 
subclasses and void.
+   * \return The moved stored value or existing error, or TypeError on a type 
mismatch.
+   * \note This function does not perform value conversions. A type mismatch 
preserves the source.
+   */
+  template <typename U, typename = std::enable_if_t<
+                            std::is_same_v<U, std::decay_t<U>> && 
!std::is_base_of_v<Error, U> &&
+                            (TypeTraits<U>::storage_enabled || 
std::is_same_v<U, Any>)>>
+  TVM_FFI_INLINE Expected<U> as_or_error() && {
+    if (TVM_FFI_PREDICT_FALSE(data_.type_index() != TypeIndex::kTVMFFIError &&
+                              !details::AnyUnsafe::CheckAnyStrict<U>(data_))) {
+      // Conversion-failure diagnostics may try fallback conversions, so use 
the stored type key.
+      return Error("TypeError",
+                   "Cannot treat type `" + data_.GetTypeKey() + "` as type `" +
+                       details::Type2Str<U>::v() + "`",
+                   "");
+    }
+    return Expected<U>(UnsafeInit{}, 
details::AnyUnsafe::MoveAnyToTVMFFIAny(std::move(data_)));
+  }
+
   /*! \brief Returns the contained error, or throws RuntimeError if is_ok(). */
   TVM_FFI_INLINE Error error() const& {
     // No branch hint: error() is itself a cold path — callers only invoke it
diff --git a/tests/cpp/test_expected.cc b/tests/cpp/test_expected.cc
index 3e365359..52cdb3a5 100644
--- a/tests/cpp/test_expected.cc
+++ b/tests/cpp/test_expected.cc
@@ -28,6 +28,8 @@
 #include <tvm/ffi/optional.h>
 #include <tvm/ffi/reflection/registry.h>
 
+#include <utility>
+
 #include "./testing_object.h"
 
 namespace {
@@ -124,6 +126,43 @@ TEST(Expected, ImplicitConvertingConstructor) {
   EXPECT_EQ(subsumed_failure.error().message(), "subsumed error");
 }
 
+TEST(Expected, AsOrError) {
+  TInt object(42);
+  Expected<Any> source = object;
+  auto copied = std::as_const(source).as_or_error<TInt>();
+  EXPECT_TRUE(copied.value().same_as(object));
+  EXPECT_TRUE(source.value().cast<TInt>().same_as(object));
+  EXPECT_EQ(object.use_count(), 3);
+  auto moved = std::move(source).as_or_error<TInt>();
+  EXPECT_TRUE(moved.value().same_as(object));
+  EXPECT_EQ(object.use_count(), 3);
+  // NOLINTNEXTLINE(bugprone-use-after-move): verify the transferred storage 
is cleared.
+  EXPECT_EQ(source.type_index(), TypeIndex::kTVMFFINone);
+
+  EXPECT_EQ(Expected<int>(42).as_or_error<double>().error().kind(), 
"TypeError");
+  // Strict container mismatches must not enter diagnostics that try fallback 
conversions.
+  Expected<Any> array = Array<int>{1};
+  EXPECT_EQ(std::as_const(array).as_or_error<Array<double>>().error().kind(), 
"TypeError");
+  EXPECT_EQ(std::move(array).as_or_error<Array<double>>().error().kind(), 
"TypeError");
+  // NOLINTNEXTLINE(bugprone-use-after-move): a strict mismatch preserves the 
source.
+  EXPECT_EQ(array.value().cast<Array<int>>()[0], 1);
+}
+
+TEST(Expected, AsOrErrorPreservesError) {
+  Error error("ValueError", "original message", "original backtrace");
+  Expected<int> source = error;
+  auto copied = std::as_const(source).as_or_error<Any>();
+  EXPECT_TRUE(copied.error().same_as(error));
+  EXPECT_TRUE(source.error().same_as(error));
+  EXPECT_EQ(error.use_count(), 3);
+  auto moved = std::move(source).as_or_error<Any>();
+  EXPECT_TRUE(moved.error().same_as(error));
+  EXPECT_EQ(error.use_count(), 3);
+  // NOLINTNEXTLINE(bugprone-use-after-move): verify the transferred storage 
is cleared.
+  EXPECT_EQ(source.type_index(), TypeIndex::kTVMFFINone);
+  EXPECT_TRUE(copied.as_or_error<String>().error().same_as(error));
+}
+
 // Test with String type
 TEST(Expected, StringType) {
   Expected<String> result = String("hello");

Reply via email to