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

Reply via email to