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 cab37f41 feat: add Expected error-return checks (#789)
cab37f41 is described below
commit cab37f41c6c1472605a07fb4d4ce4600334572b9
Author: Tianqi Chen <[email protected]>
AuthorDate: Mon Sep 14 15:52:48 2026 -0400
feat: add Expected error-return checks (#789)
Add streamed source-located errors and return-check helpers for Expected
functions. Consolidate the caller-guaranteed unsafe structural
assignment helper while preserving existing safe structural macro names.
---
include/tvm/ffi/expected.h | 117 +++++++++++++++++++++++++++
include/tvm/ffi/extra/structural_mutate.h | 47 +++--------
tests/cpp/test_expected_checks.cc | 128 ++++++++++++++++++++++++++++++
3 files changed, 254 insertions(+), 38 deletions(-)
diff --git a/include/tvm/ffi/expected.h b/include/tvm/ffi/expected.h
index 3a4f87d7..420e0d29 100644
--- a/include/tvm/ffi/expected.h
+++ b/include/tvm/ffi/expected.h
@@ -27,6 +27,8 @@
#include <tvm/ffi/any.h>
#include <tvm/ffi/error.h>
+#include <sstream>
+#include <string>
#include <type_traits>
#include <utility>
@@ -684,6 +686,121 @@ struct TypeTraits<Expected<void>> : public TypeTraitsBase
{
}
};
+// check macros for expected land
+// RET_ means the macro contains return; UNEXPECTED is a value and the caller
writes return.
+// While guards preserve an enclosing if/else. Errors record
file/line/function only, without
+// a stack walk.
+namespace details {
+
+class UnexpectedBuilder {
+ public:
+ UnexpectedBuilder(const char* kind, const char* file, int line, const char*
function)
+ : kind_(kind), file_(file), line_(line), function_(function) {}
+
+ template <typename T>
+ UnexpectedBuilder&& operator<<(T&& value) && {
+ stream_ << std::forward<T>(value);
+ return std::move(*this);
+ }
+
+ UnexpectedBuilder&& operator<<(std::ostream& (*manipulator)(std::ostream&))
&& {
+ manipulator(stream_);
+ return std::move(*this);
+ }
+
+ // NOLINTNEXTLINE(google-explicit-constructor,runtime/explicit)
+ operator Unexpected<Error>() && {
+ std::ostringstream backtrace;
+ backtrace << " File \"" << file_ << "\", line " << line_ << ", in " <<
function_ << '\n';
+ return Unexpected(Error(kind_, stream_.str(), backtrace.str()));
+ }
+
+ template <typename T>
+ // NOLINTNEXTLINE(google-explicit-constructor,runtime/explicit)
+ operator Expected<T>() && {
+ // A return expression cannot chain builder -> Unexpected -> Expected
conversions.
+ return static_cast<Unexpected<Error>>(std::move(*this));
+ }
+
+ private:
+ const char* kind_;
+ const char* file_;
+ int line_;
+ const char* function_;
+ std::ostringstream stream_;
+};
+
+} // namespace details
+
+/*!
+ * \brief Build a streamed error for an Unexpected or Expected return value.
+ *
+ * Records file, line, and function without walking the stack. The caller
writes the return;
+ * the builder's streaming and conversions are rvalue-only. Allocations and
user-defined
+ * streaming may still throw.
+ *
+ * \code{.cpp}
+ * Expected<int> Fail() {
+ * return TVM_FFI_UNEXPECTED(ValueError) << "invalid input";
+ * }
+ * \endcode
+ */
+#define TVM_FFI_UNEXPECTED(ErrorKind) \
+ ::tvm::ffi::details::UnexpectedBuilder(#ErrorKind, __FILE__, __LINE__,
TVM_FFI_FUNC_SIG)
+
+/*!
+ * \brief Return a streamed error when the condition is false.
+ *
+ * RET_ macros contain the return. The condition is evaluated once; streaming
is evaluated only
+ * on failure. The while guard preserves an enclosing if/else. \sa
TVM_FFI_UNEXPECTED
+ *
+ * \code{.cpp}
+ * Expected<int> Divide(int x, int y) {
+ * TVM_FFI_RET_CHECK(y != 0, ValueError) << "zero divisor";
+ * return x / y;
+ * }
+ * \endcode
+ */
+#define TVM_FFI_RET_CHECK(cond, ErrorKind) \
+ while (TVM_FFI_PREDICT_FALSE(!(cond))) \
+ return TVM_FFI_UNEXPECTED(ErrorKind) << "Check failed: (" #cond ") is false:
"
+
+/// \cond Doxygen_Suppress
+#define TVM_FFI_RET_CHECK_BINARY_OP(name, op, x, y, ErrorKind) \
+ while (auto __tvm_ffi_log_err = /* NOLINT(bugprone-reserved-identifier) */ \
+ ::tvm::ffi::details::LogCheck##name(x, y)) \
+ return TVM_FFI_UNEXPECTED(ErrorKind) \
+ << "Check failed: " << #x " " #op " " #y << (*__tvm_ffi_log_err) <<
": "
+/// \endcond
+
+/*! \brief Return an error if x < y is false, evaluating each operand once. */
+#define TVM_FFI_RET_CHECK_LT(x, y, ErrorKind) TVM_FFI_RET_CHECK_BINARY_OP(_LT,
<, x, y, ErrorKind)
+/*! \brief Return an error if x > y is false, evaluating each operand once. */
+#define TVM_FFI_RET_CHECK_GT(x, y, ErrorKind) TVM_FFI_RET_CHECK_BINARY_OP(_GT,
>, x, y, ErrorKind)
+/*! \brief Return an error if x <= y is false, evaluating each operand once. */
+#define TVM_FFI_RET_CHECK_LE(x, y, ErrorKind) TVM_FFI_RET_CHECK_BINARY_OP(_LE,
<=, x, y, ErrorKind)
+/*! \brief Return an error if x >= y is false, evaluating each operand once. */
+#define TVM_FFI_RET_CHECK_GE(x, y, ErrorKind) TVM_FFI_RET_CHECK_BINARY_OP(_GE,
>=, x, y, ErrorKind)
+/*! \brief Return an error if x == y is false, evaluating each operand once. */
+#define TVM_FFI_RET_CHECK_EQ(x, y, ErrorKind) TVM_FFI_RET_CHECK_BINARY_OP(_EQ,
==, x, y, ErrorKind)
+/*! \brief Return an error if x != y is false, evaluating each operand once. */
+#define TVM_FFI_RET_CHECK_NE(x, y, ErrorKind) TVM_FFI_RET_CHECK_BINARY_OP(_NE,
!=, x, y, ErrorKind)
+
+/*! \brief Check a condition with TVM_FFI_RET_CHECK, returning InternalError
on failure. */
+#define TVM_FFI_RET_ICHECK(x) TVM_FFI_RET_CHECK(x, InternalError)
+/*! \brief Check x < y, returning InternalError on failure. */
+#define TVM_FFI_RET_ICHECK_LT(x, y) TVM_FFI_RET_CHECK_LT(x, y, InternalError)
+/*! \brief Check x > y, returning InternalError on failure. */
+#define TVM_FFI_RET_ICHECK_GT(x, y) TVM_FFI_RET_CHECK_GT(x, y, InternalError)
+/*! \brief Check x <= y, returning InternalError on failure. */
+#define TVM_FFI_RET_ICHECK_LE(x, y) TVM_FFI_RET_CHECK_LE(x, y, InternalError)
+/*! \brief Check x >= y, returning InternalError on failure. */
+#define TVM_FFI_RET_ICHECK_GE(x, y) TVM_FFI_RET_CHECK_GE(x, y, InternalError)
+/*! \brief Check x == y, returning InternalError on failure. */
+#define TVM_FFI_RET_ICHECK_EQ(x, y) TVM_FFI_RET_CHECK_EQ(x, y, InternalError)
+/*! \brief Check x != y, returning InternalError on failure. */
+#define TVM_FFI_RET_ICHECK_NE(x, y) TVM_FFI_RET_CHECK_NE(x, y, InternalError)
+
} // namespace ffi
} // namespace tvm
#endif // TVM_FFI_EXPECTED_H_
diff --git a/include/tvm/ffi/extra/structural_mutate.h
b/include/tvm/ffi/extra/structural_mutate.h
index 62a741d7..34ee6c03 100644
--- a/include/tvm/ffi/extra/structural_mutate.h
+++ b/include/tvm/ffi/extra/structural_mutate.h
@@ -927,56 +927,27 @@ namespace details {
Type, Name, ResultExpr)
/// \cond Doxygen_Suppress
-#define TVM_FFI_S_MUTATE_UNSAFE_ASSIGN_OR_RETURN_UNCHECKED_IMPL_(Result, Type,
Name, ResultExpr) \
- auto Result = (ResultExpr); /* NOLINT(bugprone-macro-parentheses) */
\
- TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(Result);
\
- Type Name = /* NOLINT(bugprone-macro-parentheses) */
\
- ::tvm::ffi::details::AnyUnsafe::MoveFromAnyAfterCheck<Type>(
\
+#define TVM_FFI_UNSAFE_S_MUTATE_ASSIGN_OR_RETURN_IMPL_(Result, Type, Name,
ResultExpr) \
+ auto Result = (ResultExpr); /* NOLINT(bugprone-macro-parentheses) */
\
+ TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(Result);
\
+ Type Name = /* NOLINT(bugprone-macro-parentheses) */
\
+ ::tvm::ffi::details::AnyUnsafe::MoveFromAnyAfterCheck<Type>(
\
::std::move(::tvm::ffi::details::ExpectedUnsafe::GetData(Result)))
/// \endcond
/*!
* \brief Unwrap a successful mutation result when the caller guarantees it to
be ``Type``.
*
- * This is an unsafe form that can only be used when the mutation contract
guarantees the result
- * type.
+ * The caller must guarantee the successful result has the declared type. No
runtime type check
+ * is performed, including in debug builds; violating this contract is
undefined behavior.
*
* \param Type The guaranteed concrete type of the successful value.
* \param Name The name of the value declared in the enclosing scope.
* \param ResultExpr An expression producing the ``Expected`` value to unwrap.
* \sa TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN
*/
-#define TVM_FFI_S_MUTATE_UNSAFE_ASSIGN_OR_RETURN_UNCHECKED(Type, Name,
ResultExpr) \
- TVM_FFI_S_MUTATE_UNSAFE_ASSIGN_OR_RETURN_UNCHECKED_IMPL_(
\
- TVM_FFI_STR_CONCAT(tvm_ffi_mutate_result_, __COUNTER__), Type, Name,
ResultExpr)
-
-/// \cond Doxygen_Suppress
-#define TVM_FFI_UNSAFE_S_MUTATE_ASSIGN_OR_RETURN_SKIP_CHECK_IMPL_(Result,
Type, Name, ResultExpr) \
- auto Result = (ResultExpr); /* NOLINT(bugprone-macro-parentheses) */
\
- TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(Result);
\
- TVM_FFI_DCHECK(::tvm::ffi::details::AnyUnsafe::CheckAnyStrict<Type>(
\
- ::tvm::ffi::details::ExpectedUnsafe::GetData(Result)))
\
- << "unchecked structural-mutate assign: result is not of the declared
type"; \
- Type Name = /* NOLINT(bugprone-macro-parentheses) */
\
- ::tvm::ffi::details::AnyUnsafe::MoveFromAnyAfterCheck<Type>(
\
- ::std::move(::tvm::ffi::details::ExpectedUnsafe::GetData(Result)))
-/// \endcond
-
-/*!
- * \brief \ref TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN without the type check.
- *
- * Same signature, raw-or-same-T early-return support, and error propagation;
the difference is
- * only what happens to a successful result that is not of type \p Type.
- *
- * The caller must guarantee the result has the declared type; a mismatch is
undefined behavior
- * in a release build, and debug builds catch it with ``TVM_FFI_DCHECK``.
- *
- * \param Type The concrete type of the successful value.
- * \param Name The name of the value declared in the enclosing scope.
- * \param ResultExpr An expression producing the ``Expected`` value to unwrap.
- */
-#define TVM_FFI_UNSAFE_S_MUTATE_ASSIGN_OR_RETURN_SKIP_CHECK(Type, Name,
ResultExpr) \
- TVM_FFI_UNSAFE_S_MUTATE_ASSIGN_OR_RETURN_SKIP_CHECK_IMPL_(
\
+#define TVM_FFI_UNSAFE_S_MUTATE_ASSIGN_OR_RETURN(Type, Name, ResultExpr) \
+ TVM_FFI_UNSAFE_S_MUTATE_ASSIGN_OR_RETURN_IMPL_( \
TVM_FFI_STR_CONCAT(tvm_ffi_mutate_result_, __COUNTER__), Type, Name,
ResultExpr)
} // namespace details
diff --git a/tests/cpp/test_expected_checks.cc
b/tests/cpp/test_expected_checks.cc
new file mode 100644
index 00000000..6797652e
--- /dev/null
+++ b/tests/cpp/test_expected_checks.cc
@@ -0,0 +1,128 @@
+/*
+ * 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/expected.h>
+#include <tvm/ffi/extra/structural_mutate.h>
+
+#include <algorithm>
+#include <iomanip>
+#include <string>
+#include <type_traits>
+#include <utility>
+
+namespace {
+
+using namespace tvm::ffi;
+
+TEST(ExpectedChecks, Builder) {
+ using Builder = details::UnexpectedBuilder;
+ static_assert(!std::is_convertible_v<Builder&, Unexpected<Error>>);
+ static_assert(!std::is_convertible_v<Builder&, Expected<int>>);
+
+ int source_line = __LINE__ + 1;
+ Unexpected<Error> raw = TVM_FFI_UNEXPECTED(ValueError) << std::hex << 31 <<
std::endl;
+ EXPECT_EQ(raw.error().kind(), "ValueError");
+ EXPECT_EQ(raw.error().message(), "1f\n");
+ std::string backtrace = raw.error().backtrace();
+ EXPECT_NE(backtrace.find(__FILE__), std::string::npos);
+ EXPECT_NE(backtrace.find("line " + std::to_string(source_line) + ", in "),
std::string::npos);
+ EXPECT_NE(backtrace.find("Builder"), std::string::npos);
+ EXPECT_EQ(std::count(backtrace.begin(), backtrace.end(), '\n'), 1);
+
+ Expected<void> empty = TVM_FFI_UNEXPECTED(ValueError);
+ EXPECT_TRUE(empty.is_err());
+}
+
+TEST(ExpectedChecks, ReturnChecks) {
+ // Unbraced bodies test that the macros cannot capture the caller's else.
+ // NOLINTBEGIN(google-readability-braces-around-statements)
+ int conditions = 0;
+ int streamed = 0;
+ auto boolean = [&](bool outer, bool pass) noexcept -> Expected<int> {
+ if (outer)
+ TVM_FFI_RET_CHECK((++conditions, pass), ValueError) << " detail " <<
++streamed;
+ else
+ return 2;
+ return 1;
+ };
+ EXPECT_EQ(boolean(false, false).value(), 2);
+ EXPECT_EQ(conditions, 0);
+ EXPECT_EQ(boolean(true, true).value(), 1);
+ EXPECT_EQ(conditions, 1);
+ EXPECT_EQ(streamed, 0);
+ auto failure = boolean(true, false);
+ ASSERT_TRUE(failure.is_err());
+ EXPECT_EQ(failure.error().kind(), "ValueError");
+ EXPECT_EQ(failure.error().message(), "Check failed: ((++conditions, pass))
is false: detail 1");
+ EXPECT_EQ(conditions, 2);
+
+ int left = 0;
+ int right = 0;
+ streamed = 0;
+ auto binary = [&](bool outer, int x) noexcept -> Expected<UnchangedOr<Any>> {
+ if (outer)
+ TVM_FFI_RET_ICHECK_EQ((++left, x), (++right, 3)) << " detail " <<
++streamed;
+ else
+ return UnchangedOr<Any>(Unchanged());
+ return UnchangedOr<Any>(Any(42));
+ };
+ EXPECT_TRUE(binary(false, 4).value().IsUnchanged());
+ EXPECT_EQ(left + right, 0);
+ EXPECT_EQ(binary(true, 3).value().ValueOrUnchanged(Any(0)).cast<int>(), 42);
+ EXPECT_EQ(left, 1);
+ EXPECT_EQ(right, 1);
+ EXPECT_EQ(streamed, 0);
+ auto binary_failure = binary(true, 4);
+ ASSERT_TRUE(binary_failure.is_err());
+ EXPECT_EQ(binary_failure.error().kind(), "InternalError");
+ EXPECT_EQ(binary_failure.error().message(),
+ "Check failed: (++left, x) == (++right, 3) (4 vs. 3) : detail 1");
+ EXPECT_EQ(left, 2);
+ EXPECT_EQ(right, 2);
+ // NOLINTEND(google-readability-braces-around-statements)
+}
+
+template <typename Return>
+Return UnsafeMutateAssign(Expected<Any> result, int* evaluations) noexcept {
+ TVM_FFI_UNSAFE_S_MUTATE_ASSIGN_OR_RETURN(int, value, (++*evaluations,
std::move(result)));
+ if constexpr (std::is_same_v<Return, TVMFFIAny>) {
+ return AnyView(value + 1).CopyToTVMFFIAny();
+ } else {
+ return value + 1;
+ }
+}
+
+TEST(ExpectedChecks, UnsafeAssignment) {
+ int evaluations = 0;
+ EXPECT_EQ(UnsafeMutateAssign<Expected<int>>(Any(41), &evaluations).value(),
42);
+ auto raw_ok = details::ExpectedUnsafe::MoveFromTVMFFIAny<int>(
+ UnsafeMutateAssign<TVMFFIAny>(Any(41), &evaluations));
+ EXPECT_EQ(raw_ok.value(), 42);
+ Error error("ValueError", "unsafe assign", "");
+ auto typed_error = UnsafeMutateAssign<Expected<int>>(error, &evaluations);
+ ASSERT_TRUE(typed_error.is_err());
+ EXPECT_EQ(typed_error.error().message(), "unsafe assign");
+ auto raw_error = details::ExpectedUnsafe::MoveFromTVMFFIAny<int>(
+ UnsafeMutateAssign<TVMFFIAny>(error, &evaluations));
+ ASSERT_TRUE(raw_error.is_err());
+ EXPECT_EQ(raw_error.error().message(), "unsafe assign");
+ EXPECT_EQ(evaluations, 4);
+}
+
+} // namespace