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");