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.git


The following commit(s) were added to refs/heads/main by this push:
     new 4a6c989175 [REFACTOR][S-TIR] Preserve schedule error payloads across 
FFI bridges (#20351)
4a6c989175 is described below

commit 4a6c989175801a5f161f802e825f9e7137f9ad85
Author: Tianqi Chen <[email protected]>
AuthorDate: Tue Sep 15 19:15:27 2026 -0400

    [REFACTOR][S-TIR] Preserve schedule error payloads across FFI bridges 
(#20351)
    
    Move scheduling diagnostic state into a registered object carried by
    `ffi::Error` through `extra_context`, so the payload survives structural
    FFI bridges.
    
    Recover the payload directly or through a visit context, retain it on
    rendered errors for outer scheduling handlers, and preserve the original
    diagnostic when nested handlers encounter an already rendered error.
---
 src/s_tir/analysis/oob_checker.cc                  |  2 +-
 .../postproc/rewrite_parallel_vectorize_unroll.cc  |  5 +-
 src/s_tir/meta_schedule/utils.h                    | 10 +++-
 src/s_tir/schedule/analysis/analysis.cc            | 66 ++++++++++++----------
 src/s_tir/schedule/analysis/reducer.cc             |  9 +--
 src/s_tir/schedule/concrete_schedule.cc            | 60 ++++++++++++--------
 src/s_tir/schedule/error.cc                        | 20 ++++++-
 src/s_tir/schedule/error.h                         | 30 ++++++++--
 src/s_tir/schedule/ir_comparator.cc                |  5 +-
 src/s_tir/schedule/primitive/block_annotate.cc     | 16 +++---
 src/s_tir/schedule/primitive/blockize_tensorize.cc |  8 ++-
 src/s_tir/schedule/primitive/cache_read_write.cc   | 40 ++++++-------
 src/s_tir/schedule/primitive/compute_at.cc         | 14 ++---
 src/s_tir/schedule/primitive/compute_inline.cc     | 36 ++++++------
 src/s_tir/schedule/primitive/decompose_padding.cc  |  9 +--
 src/s_tir/schedule/primitive/for_kind.cc           |  4 +-
 src/s_tir/schedule/primitive/hide_buffer_access.cc |  8 +--
 .../schedule/primitive/layout_transformation.cc    | 40 +++++++------
 .../schedule/primitive/loop_transformation.cc      | 61 +++++++++++---------
 src/s_tir/schedule/primitive/pad_einsum.cc         | 20 +++----
 src/s_tir/schedule/primitive/reduction.cc          | 29 +++++-----
 .../schedule/primitive/reorder_block_iter_var.cc   |  5 +-
 src/s_tir/schedule/primitive/rolling_buffer.cc     | 17 +++---
 src/s_tir/schedule/transform.cc                    |  5 +-
 24 files changed, 303 insertions(+), 216 deletions(-)

diff --git a/src/s_tir/analysis/oob_checker.cc 
b/src/s_tir/analysis/oob_checker.cc
index b193d0b1ba..127c1f5a47 100644
--- a/src/s_tir/analysis/oob_checker.cc
+++ b/src/s_tir/analysis/oob_checker.cc
@@ -40,7 +40,7 @@ struct OOBLocation {
   arith::IntSet shape_bounds;
 };
 
-class OOBError : public s_tir::ScheduleError {
+class OOBError : public s_tir::ScheduleErrorContextObj {
  public:
   OOBError(IRModule mod, std::vector<OOBLocation> locations) : mod_(mod), 
locations_(locations) {}
   ffi::String FastErrorString() const final { return "Out of bound memory 
access"; }
diff --git 
a/src/s_tir/meta_schedule/postproc/rewrite_parallel_vectorize_unroll.cc 
b/src/s_tir/meta_schedule/postproc/rewrite_parallel_vectorize_unroll.cc
index fe12d71fef..19ebfb1a85 100644
--- a/src/s_tir/meta_schedule/postproc/rewrite_parallel_vectorize_unroll.cc
+++ b/src/s_tir/meta_schedule/postproc/rewrite_parallel_vectorize_unroll.cc
@@ -477,7 +477,10 @@ class RewriteParallelVectorizeUnrollNode : public 
PostprocNode {
             int max_step = parsed.unroll_explicit + parsed.unroll_implicit + 1;
             s_tir::RewriteUnroll(sch, unroll_explicit, max_step, block_rv, 
loop_rvs[0]);
           }
-        } catch (const s_tir::ScheduleError& e) {
+        } catch (const ffi::Error& e) {
+          if (s_tir::GetScheduleErrorContext(e) == nullptr) {
+            throw;
+          }
           DLOG(WARNING) << "Failed to apply parallelization/vectorization: " 
<< e.what();
           return false;
         }
diff --git a/src/s_tir/meta_schedule/utils.h b/src/s_tir/meta_schedule/utils.h
index bceab91110..b45dd2cd87 100644
--- a/src/s_tir/meta_schedule/utils.h
+++ b/src/s_tir/meta_schedule/utils.h
@@ -341,7 +341,10 @@ struct ThreadedTraceApply {
                                     s_tir::ScheduleErrorRenderLevel::kNone);
       trace->ApplyToSchedule(sch, /*remove_postproc=*/true);
       sch->EnterPostproc();
-    } catch (const s_tir::ScheduleError& e) {
+    } catch (const ffi::Error& e) {
+      if (s_tir::GetScheduleErrorContext(e) == nullptr) {
+        throw;
+      }
       TVM_PY_LOG(WARNING, nullptr) << "Trace replay failed with ScheduleError: 
" << e.what();
       this->trace_fail_counter_++;
       return std::nullopt;
@@ -358,7 +361,10 @@ struct ThreadedTraceApply {
         if (!item.postproc->Apply(sch)) {
           success = false;
         }
-      } catch (const s_tir::ScheduleError& e) {
+      } catch (const ffi::Error& e) {
+        if (s_tir::GetScheduleErrorContext(e) == nullptr) {
+          throw;
+        }
         DLOG(WARNING) << "Postproc #" << i << " failed with ScheduleError: " 
<< e.what();
         success = false;
       } catch (const std::exception& e) {
diff --git a/src/s_tir/schedule/analysis/analysis.cc 
b/src/s_tir/schedule/analysis/analysis.cc
index 8e715c83fd..00af05480c 100644
--- a/src/s_tir/schedule/analysis/analysis.cc
+++ b/src/s_tir/schedule/analysis/analysis.cc
@@ -64,7 +64,7 @@ const PrimFuncNode* GetRootPrimFunc(const IRModule& mod, 
const StmtNode* root_bl
 
 StmtSRef GetScopeRoot(const ScheduleState& self, const StmtSRef& sref,
                       bool require_stage_pipeline) {
-  class RootBlockError : public ScheduleError {
+  class RootBlockError : public ScheduleErrorContextObj {
    public:
     explicit RootBlockError(IRModule mod) : mod_(mod) {}
     IRModule mod() const final { return mod_; }
@@ -78,7 +78,7 @@ StmtSRef GetScopeRoot(const ScheduleState& self, const 
StmtSRef& sref,
     IRModule mod_;
   };
 
-  class NotStagePipelineError : public ScheduleError {
+  class NotStagePipelineError : public ScheduleErrorContextObj {
    public:
     explicit NotStagePipelineError(IRModule mod, SBlock block) : mod_(mod), 
block_(block) {}
     IRModule mod() const final { return mod_; }
@@ -113,7 +113,7 @@ Definition of a scope that is a stage pipeline:
       }
     }
     if (p == nullptr) {
-      throw RootBlockError(self->mod);
+      throw MakeScheduleError<RootBlockError>(self->mod);
     }
   }
   // Step 2. Handle `require_stage_pipeline`
@@ -121,7 +121,7 @@ Definition of a scope that is a stage pipeline:
     bool stage_pipeline = self->GetSBlockInfo(scope_root_sref).stage_pipeline;
     if (stage_pipeline == false) {
       const SBlockNode* block = TVM_SREF_TO_SBLOCK(scope_root_sref);
-      throw NotStagePipelineError(self->mod, ffi::GetRef<SBlock>(block));
+      throw MakeScheduleError<NotStagePipelineError>(self->mod, 
ffi::GetRef<SBlock>(block));
     }
   }
   return scope_root_sref;
@@ -282,7 +282,7 @@ bool IsCompleteBlock(const ScheduleState& self, const 
StmtSRef& block_sref,
 
 void CheckCompleteBlock(const ScheduleState& self, const StmtSRef& block_sref,
                         const StmtSRef& scope_root_sref) {
-  class IncompleteBlockError : public ScheduleError {
+  class IncompleteBlockError : public ScheduleErrorContextObj {
    public:
     explicit IncompleteBlockError(IRModule mod, SBlock block, int 
violated_cond)
         : mod_(std::move(mod)), block_(std::move(block)), 
violated_cond_(violated_cond) {}
@@ -303,7 +303,8 @@ void CheckCompleteBlock(const ScheduleState& self, const 
StmtSRef& block_sref,
   int error_code = CheckCompleteBlockErrorCode(self, block_sref, 
scope_root_sref);
   if (error_code != 0) {
     const SBlockNode* block = TVM_SREF_TO_SBLOCK(block_sref);
-    throw IncompleteBlockError(self->mod, ffi::GetRef<SBlock>(block), 
error_code);
+    throw MakeScheduleError<IncompleteBlockError>(self->mod, 
ffi::GetRef<SBlock>(block),
+                                                  error_code);
   }
 }
 
@@ -356,7 +357,7 @@ TVM_FFI_STATIC_INIT_BLOCK() {
 
 void CheckReductionBlock(const ScheduleState& self, const StmtSRef& block_sref,
                          const StmtSRef& scope_root_sref) {
-  class NotReductionBlockError : public ScheduleError {
+  class NotReductionBlockError : public ScheduleErrorContextObj {
    public:
     explicit NotReductionBlockError(IRModule mod, SBlock block, int 
violated_cond)
         : mod_(std::move(mod)), block_(std::move(block)), 
violated_cond_(violated_cond) {}
@@ -377,13 +378,14 @@ void CheckReductionBlock(const ScheduleState& self, const 
StmtSRef& block_sref,
   int error_code = CheckReductionBlockErrorCode(self, block_sref, 
scope_root_sref);
   if (error_code != 0) {
     const SBlockNode* block = TVM_SREF_TO_SBLOCK(block_sref);
-    throw NotReductionBlockError(self->mod, ffi::GetRef<SBlock>(block), 
error_code);
+    throw MakeScheduleError<NotReductionBlockError>(self->mod, 
ffi::GetRef<SBlock>(block),
+                                                    error_code);
   }
 }
 
 void CheckCompleteOrReductionBlock(const ScheduleState& self, const StmtSRef& 
block_sref,
                                    const StmtSRef& scope_root_sref) {
-  class NotCompleteOrReductionBlockError : public ScheduleError {
+  class NotCompleteOrReductionBlockError : public ScheduleErrorContextObj {
    public:
     explicit NotCompleteOrReductionBlockError(IRModule mod, SBlock block,
                                               int complete_block_error_code,
@@ -424,12 +426,12 @@ void CheckCompleteOrReductionBlock(const ScheduleState& 
self, const StmtSRef& bl
     return;
   }
   const SBlockNode* block = TVM_SREF_TO_SBLOCK(block_sref);
-  throw NotCompleteOrReductionBlockError(self->mod, ffi::GetRef<SBlock>(block),
-                                         complete_block_error_code, 
reduction_block_error_code);
+  throw MakeScheduleError<NotCompleteOrReductionBlockError>(
+      self->mod, ffi::GetRef<SBlock>(block), complete_block_error_code, 
reduction_block_error_code);
 }
 
 void CheckSubtreeCompactDataflow(const ScheduleState& self, const StmtSRef& 
subtree_root) {
-  class NotCompactDataFlowError : public ScheduleError {
+  class NotCompactDataFlowError : public ScheduleErrorContextObj {
    public:
     explicit NotCompactDataFlowError(IRModule mod, Stmt subtree_root, SBlock 
violate_block,
                                      int local_complete_block_code, int 
local_reduction_block_code)
@@ -477,9 +479,9 @@ void CheckSubtreeCompactDataflow(const ScheduleState& self, 
const StmtSRef& subt
         local_reduction_block_code = CheckReductionBlockErrorCode(self, 
block_sref, subtree_root);
     if (local_complete_block_code != 0 && local_reduction_block_code != 0) {
       const SBlockNode* block = TVM_SREF_TO_SBLOCK(block_sref);
-      throw NotCompactDataFlowError(self->mod, 
ffi::GetRef<Stmt>(subtree_root->stmt),
-                                    ffi::GetRef<SBlock>(block), 
local_complete_block_code,
-                                    local_reduction_block_code);
+      throw MakeScheduleError<NotCompactDataFlowError>(
+          self->mod, ffi::GetRef<Stmt>(subtree_root->stmt), 
ffi::GetRef<SBlock>(block),
+          local_complete_block_code, local_reduction_block_code);
     }
   }
 }
@@ -503,7 +505,7 @@ bool IsOutputBlock(const ScheduleState& self, const 
StmtSRef& block_sref,
 
 void CheckNotOutputBlock(const ScheduleState& self, const StmtSRef& block_sref,
                          const StmtSRef& scope_root_sref) {
-  class OutputBlockError : public ScheduleError {
+  class OutputBlockError : public ScheduleErrorContextObj {
    public:
     explicit OutputBlockError(IRModule mod, SBlock block) : mod_(mod), 
block_(block) {}
     ffi::String FastErrorString() const final {
@@ -518,7 +520,7 @@ void CheckNotOutputBlock(const ScheduleState& self, const 
StmtSRef& block_sref,
   };
   if (IsOutputBlock(self, block_sref, scope_root_sref)) {
     const SBlockNode* block = TVM_SREF_TO_SBLOCK(block_sref);
-    throw OutputBlockError(self->mod, ffi::GetRef<SBlock>(block));
+    throw MakeScheduleError<OutputBlockError>(self->mod, 
ffi::GetRef<SBlock>(block));
   }
 }
 
@@ -589,7 +591,7 @@ bool IsAffineBinding(const SBlockRealize& realize, const 
ffi::Map<Var, Range>& l
 
 void CheckPartialAffineBinding(const ScheduleState& self, SBlock block,
                                const ffi::Optional<StmtSRef>& high_exclusive) {
-  class NotAffineBindingError : public ScheduleError {
+  class NotAffineBindingError : public ScheduleErrorContextObj {
    public:
     explicit NotAffineBindingError(IRModule mod, SBlock block,
                                    ffi::Optional<StmtSRef> high_exclusive)
@@ -639,7 +641,7 @@ void CheckPartialAffineBinding(const ScheduleState& self, 
SBlock block,
       return;
     }
   }
-  throw NotAffineBindingError(self->mod, std::move(block), high_exclusive);
+  throw MakeScheduleError<NotAffineBindingError>(self->mod, std::move(block), 
high_exclusive);
 }
 
 void CheckAffineBinding(const ScheduleState& self, SBlock block) {
@@ -647,7 +649,7 @@ void CheckAffineBinding(const ScheduleState& self, SBlock 
block) {
 }
 
 void CheckBlockHasTrivialBinding(const ScheduleState& self, const StmtSRef& 
block_sref) {
-  class NotTrivialBindingError : public ScheduleError {
+  class NotTrivialBindingError : public ScheduleErrorContextObj {
    public:
     explicit NotTrivialBindingError(IRModule mod, SBlock block)
         : mod_(std::move(mod)), block_(std::move(block)) {}
@@ -671,7 +673,8 @@ void CheckBlockHasTrivialBinding(const ScheduleState& self, 
const StmtSRef& bloc
   };
 
   if (!IsTrivialBinding(self, block_sref)) {
-    throw NotTrivialBindingError(self->mod, 
ffi::GetRef<SBlock>(block_sref->StmtAs<SBlockNode>()));
+    throw MakeScheduleError<NotTrivialBindingError>(
+        self->mod, ffi::GetRef<SBlock>(block_sref->StmtAs<SBlockNode>()));
   }
 }
 
@@ -756,7 +759,7 @@ bool GetVarsTouchedByBlockIters(const SBlockRealize& 
block_realize,
 
 void CheckLoopStartsWithZero(const ScheduleState& self, const StmtSRef& 
loop_sref,
                              arith::AnalyzerObj* analyzer) {
-  class LoopNotStartWithZeroError : public ScheduleError {
+  class LoopNotStartWithZeroError : public ScheduleErrorContextObj {
    public:
     explicit LoopNotStartWithZeroError(IRModule mod, For loop)
         : mod_(mod), loop_(std::move(loop)) {}
@@ -777,7 +780,7 @@ void CheckLoopStartsWithZero(const ScheduleState& self, 
const StmtSRef& loop_sre
   };
   const ForNode* loop = TVM_SREF_TO_FOR(loop_sref);
   if (!analyzer->CanProve(loop->min == 0)) {
-    throw LoopNotStartWithZeroError(self->mod, ffi::GetRef<For>(loop));
+    throw MakeScheduleError<LoopNotStartWithZeroError>(self->mod, 
ffi::GetRef<For>(loop));
   }
 }
 
@@ -823,7 +826,7 @@ ffi::Array<SBlockRealize> 
GetChildBlockRealizeOnSRefTree(const StmtSRef& parent_
 
 SBlockRealize CheckGetSingleChildBlockRealizeOnSRefTree(const ScheduleState& 
self,
                                                         const StmtSRef& 
parent_sref) {
-  class NonSingleChildBlockError : public ScheduleError {
+  class NonSingleChildBlockError : public ScheduleErrorContextObj {
    public:
     explicit NonSingleChildBlockError(IRModule mod, const StmtSRef& sref)
         : mod_(std::move(mod)), stmt_(ffi::GetRef<Stmt>(sref->stmt)) {
@@ -852,7 +855,7 @@ SBlockRealize 
CheckGetSingleChildBlockRealizeOnSRefTree(const ScheduleState& sel
 
   ffi::Array<SBlockRealize> child_block_realize = 
GetChildBlockRealizeOnSRefTree(parent_sref);
   if (child_block_realize.size() != 1) {
-    throw NonSingleChildBlockError(self->mod, parent_sref);
+    throw MakeScheduleError<NonSingleChildBlockError>(self->mod, parent_sref);
   }
   return child_block_realize[0];
 }
@@ -1117,7 +1120,7 @@ ProducerConsumerSplit ProducerConsumerSplit::Find(
     const ffi::Array<StmtSRef>& producer_block_srefs,
     const ffi::Array<StmtSRef>& consumer_block_srefs,
     std::unordered_map<const SBlockNode*, const SBlockRealizeNode*>* 
block2realize) {
-  class InsertionPointNotFoundError : public ScheduleError {
+  class InsertionPointNotFoundError : public ScheduleErrorContextObj {
    public:
     explicit InsertionPointNotFoundError(IRModule mod, int 
last_producer_position,
                                          int first_consumer_position)
@@ -1202,7 +1205,8 @@ ProducerConsumerSplit ProducerConsumerSplit::Find(
     }
   }
   if (last_producer_position >= first_consumer_position) {
-    throw InsertionPointNotFoundError(self->mod, last_producer_position, 
first_consumer_position);
+    throw MakeScheduleError<InsertionPointNotFoundError>(self->mod, 
last_producer_position,
+                                                         
first_consumer_position);
   }
   return ProducerConsumerSplit{last_producer_position,       //
                                first_consumer_position,      //
@@ -1214,7 +1218,7 @@ ProducerConsumerSplit ProducerConsumerSplit::Find(
 
 BufferRegion GetNthAccessBufferRegion(const ScheduleState& self, const SBlock& 
block, int n,
                                       BufferIndexType index_type) {
-  class BufferIndexOutOfRangeError : public ScheduleError {
+  class BufferIndexOutOfRangeError : public ScheduleErrorContextObj {
    public:
     explicit BufferIndexOutOfRangeError(IRModule mod, SBlock block, int 
buffer_index,
                                         BufferIndexType index_type)
@@ -1262,7 +1266,7 @@ BufferRegion GetNthAccessBufferRegion(const 
ScheduleState& self, const SBlock& b
       index_type == BufferIndexType::kWrite ? block->writes : block->reads;
 
   if (n < 0 || static_cast<int>(access_region.size()) <= n) {
-    throw BufferIndexOutOfRangeError(self->mod, block, n, index_type);
+    throw MakeScheduleError<BufferIndexOutOfRangeError>(self->mod, block, n, 
index_type);
   }
   return access_region[n];
 }
@@ -1455,7 +1459,7 @@ AnalyzeReadWritePattern(const BufferRegion& read_region, 
const BufferRegion& wri
 /******** Storage Scope ********/
 
 void CheckStorageScope(const ScheduleState& self, ffi::String storage_scope) {
-  class InvalidStorageScopeError : public ScheduleError {
+  class InvalidStorageScopeError : public ScheduleErrorContextObj {
    public:
     explicit InvalidStorageScopeError(IRModule mod, ffi::String storage_scope)
         : mod_(std::move(mod)), storage_scope_(std::move(storage_scope)) {}
@@ -1479,7 +1483,7 @@ void CheckStorageScope(const ScheduleState& self, 
ffi::String storage_scope) {
   try {
     runtime::StorageScope::Create(std::string(storage_scope));
   } catch (...) {
-    throw InvalidStorageScopeError(self->mod, std::move(storage_scope));
+    throw MakeScheduleError<InvalidStorageScopeError>(self->mod, 
std::move(storage_scope));
   }
 }
 
diff --git a/src/s_tir/schedule/analysis/reducer.cc 
b/src/s_tir/schedule/analysis/reducer.cc
index bff5bec91e..cf54f1976d 100644
--- a/src/s_tir/schedule/analysis/reducer.cc
+++ b/src/s_tir/schedule/analysis/reducer.cc
@@ -315,7 +315,7 @@ static const char* 
kRFactorCrossThreadReductionApplicableBlockDef =
 
 void ErrorRFactorCrossThreadReductionNotApplicable(const 
ffi::Optional<ScheduleState>& self,
                                                    SBlock block, int 
violated_cond) {
-  class RFactorNotApplicableError : public ScheduleError {
+  class RFactorNotApplicableError : public ScheduleErrorContextObj {
    public:
     explicit RFactorNotApplicableError(IRModule mod, SBlock block, int 
violated_cond)
         : mod_(std::move(mod)), block_(std::move(block)), 
violated_cond_(violated_cond) {}
@@ -342,7 +342,8 @@ void ErrorRFactorCrossThreadReductionNotApplicable(const 
ffi::Optional<ScheduleS
   };
 
   if (self.has_value()) {
-    throw RFactorNotApplicableError(self.value()->mod, std::move(block), 
violated_cond);
+    throw MakeScheduleError<RFactorNotApplicableError>(self.value()->mod, 
std::move(block),
+                                                       violated_cond);
   } else {
     TVM_FFI_THROW(ValueError) << "Cross-thread reduction cannot be applied to 
the block "
                               << block->name_hint << " because the block 
violates the condition #"
@@ -603,7 +604,7 @@ bool ReductionIterNotIndexOutputBuffer(const SBlock& block) 
{
   return result.has_value() ? result.value()->value.cast<bool>() : true;
 }
 
-class NoMatchedReducerError : public ScheduleError {
+class NoMatchedReducerError : public ScheduleErrorContextObj {
  public:
   explicit NoMatchedReducerError(IRModule mod, ffi::Array<PrimExpr> identities,
                                  ffi::Array<BufferStore> combiners)
@@ -641,7 +642,7 @@ std::tuple<te::CommReducer, ffi::Array<PrimExpr>, 
ffi::Array<PrimExpr>> GetReduc
       FromIdentityCombiner(identities, combiners, &reducer, &combiner_lhs, 
&combiner_rhs);
   if (!matched) {
     if (self.has_value()) {
-      throw NoMatchedReducerError(self.value()->mod, identities, combiners);
+      throw MakeScheduleError<NoMatchedReducerError>(self.value()->mod, 
identities, combiners);
     } else {
       TVM_FFI_THROW(ValueError)
           << "No matched reducer for the identity and the combiner of the "
diff --git a/src/s_tir/schedule/concrete_schedule.cc 
b/src/s_tir/schedule/concrete_schedule.cc
index f407ed2bed..4feb99b1e6 100644
--- a/src/s_tir/schedule/concrete_schedule.cc
+++ b/src/s_tir/schedule/concrete_schedule.cc
@@ -209,22 +209,32 @@ Schedule ConcreteScheduleNode::Copy() {
 
 /*! \brief Macro that guards the beginning of each invocation of TensorIR 
schedule primitive */
 #define TVM_TIR_SCHEDULE_BEGIN() try {
+/*! \brief Keep the payload on rendered errors for outer scheduling error 
handlers. */
+#define TVM_TIR_SCHEDULE_THROW(error)                                          
  \
+  ffi::details::ErrorBuilder(                                                  
  \
+      "ScheduleError", TVMFFIBacktrace(__FILE__, __LINE__, TVM_FFI_FUNC_SIG, 
0), \
+      TVM_FFI_ALWAYS_LOG_BEFORE_THROW, std::nullopt, (error).extra_context())  
  \
+      .stream()
 /*!
  * \brief Macro that pairs with `TVM_TIR_SCHEDULE_BEGIN`, handling potential 
errors and error
- * message rendering
+ * message rendering. Already rendered errors retain their original diagnostic.
  * \param level An ScheduleErrorRenderLevel enum, level of error rendering
  * \sa ScheduleErrorRenderLevel
  */
-#define TVM_TIR_SCHEDULE_END(primitive, level)                       \
-  }                                                                  \
-  catch (const ScheduleError& error) {                               \
-    if ((level) == ScheduleErrorRenderLevel::kDetail) {              \
-      TVM_FFI_THROW(ScheduleError) << error.RenderReport(primitive); \
-    } else if ((level) == ScheduleErrorRenderLevel::kFast) {         \
-      TVM_FFI_THROW(ScheduleError) << error.FastErrorString();       \
-    } else if ((level) == ScheduleErrorRenderLevel::kNone) {         \
-      TVM_FFI_THROW(ScheduleError) << "(not rendered)";              \
-    }                                                                \
+#define TVM_TIR_SCHEDULE_END(primitive, level)                           \
+  }                                                                      \
+  catch (const ffi::Error& error) {                                      \
+    const auto* context = GetScheduleErrorContext(error);                \
+    if (context == nullptr || !error.message().empty()) {                \
+      throw;                                                             \
+    }                                                                    \
+    if ((level) == ScheduleErrorRenderLevel::kDetail) {                  \
+      TVM_TIR_SCHEDULE_THROW(error) << context->RenderReport(primitive); \
+    } else if ((level) == ScheduleErrorRenderLevel::kFast) {             \
+      TVM_TIR_SCHEDULE_THROW(error) << context->FastErrorString();       \
+    } else if ((level) == ScheduleErrorRenderLevel::kNone) {             \
+      TVM_TIR_SCHEDULE_THROW(error) << "(not rendered)";                 \
+    }                                                                    \
   }
 
 /******** Schedule: Schedule: Sampling ********/
@@ -281,7 +291,7 @@ LoopRV ConcreteScheduleNode::SampleComputeLocation(const 
SBlockRV& block_rv,
 
 SBlockRV ConcreteScheduleNode::GetSBlock(const ffi::String& name,
                                          const ffi::Optional<ffi::String>& 
func_name) {
-  class NotSingleResult : public ScheduleError {
+  class NotSingleResult : public ScheduleErrorContextObj {
    public:
     explicit NotSingleResult(ffi::String name, IRModule mod, const 
ffi::Array<StmtSRef>& blocks)
         : name_(name), mod_(mod), blocks_{} {
@@ -331,7 +341,7 @@ SBlockRV ConcreteScheduleNode::GetSBlock(const ffi::String& 
name,
   ffi::Array<StmtSRef> blocks = s_tir::GetSBlocks(this->state_, name, gv);
   if (blocks.size() != 1) {
     TVM_TIR_SCHEDULE_BEGIN();
-    throw NotSingleResult(name, this->state_->mod, blocks);
+    throw MakeScheduleError<NotSingleResult>(name, this->state_->mod, blocks);
     TVM_TIR_SCHEDULE_END("get-block", this->error_render_level_);
   }
   return CreateRV<SBlockRV>(blocks[0]);
@@ -404,7 +414,7 @@ LoopRV ConcreteScheduleNode::Fuse(const ffi::Array<LoopRV>& 
loop_rvs, bool prese
   return CreateRV<LoopRV>(result);
 }
 
-class NotSingleInferFactorError : public ScheduleError {
+class NotSingleInferFactorError : public ScheduleErrorContextObj {
  public:
   explicit NotSingleInferFactorError(IRModule mod) : mod_(mod) {}
 
@@ -422,7 +432,7 @@ class NotSingleInferFactorError : public ScheduleError {
   IRModule mod_;
 };
 
-class WrongFactorError : public ScheduleError {
+class WrongFactorError : public ScheduleErrorContextObj {
  public:
   explicit WrongFactorError(IRModule mod, For loop, bool product)
       : mod_(mod), loop_(std::move(loop)), product_(product) {}
@@ -450,7 +460,7 @@ class WrongFactorError : public ScheduleError {
   bool product_;
 };
 
-class NonPositiveFactorError : public ScheduleError {
+class NonPositiveFactorError : public ScheduleErrorContextObj {
  public:
   explicit NonPositiveFactorError(IRModule mod, int64_t factor, size_t idx)
       : mod_(std::move(mod)), factor_(factor), idx_(idx) {}
@@ -491,13 +501,14 @@ ffi::Array<LoopRV> ConcreteScheduleNode::Split(const 
LoopRV& loop_rv,
     if (!factor_rvs[i].has_value()) {
       factors.push_back(IntImm::Int32(-1));
       if (infer_index != -1) {
-        throw NotSingleInferFactorError(state_->mod);
+        throw MakeScheduleError<NotSingleInferFactorError>(state_->mod);
       }
       infer_index = i;
     } else {
       PrimExpr factor = this->Get(factor_rvs[i].value());
       if (is_const_int(factor) && !is_positive_const(factor)) {
-        throw NonPositiveFactorError(state_->mod, 
factor.as<IntImmNode>()->value, i);
+        throw MakeScheduleError<NonPositiveFactorError>(state_->mod, 
factor.as<IntImmNode>()->value,
+                                                        i);
       }
       if (factor.ty().bits() > loop->extent.ty().bits()) {
         factor = cast(loop->extent.ty(), factor);
@@ -510,7 +521,7 @@ ffi::Array<LoopRV> ConcreteScheduleNode::Split(const 
LoopRV& loop_rv,
     factors.Set(infer_index,
                 this->analyzer_->Simplify(floordiv(loop->extent + tot_length - 
1, tot_length)));
   } else if (!this->analyzer_->CanProve(tot_length >= loop->extent)) {
-    throw WrongFactorError(state_->mod, ffi::GetRef<For>(loop), true);
+    throw MakeScheduleError<WrongFactorError>(state_->mod, 
ffi::GetRef<For>(loop), true);
   }
   results = s_tir::Split(state_, loop_sref, factors, preserve_unit_iters, 
disable_predication);
   TVM_TIR_SCHEDULE_END("split", this->error_render_level_);
@@ -521,7 +532,7 @@ ffi::Array<LoopRV> ConcreteScheduleNode::Split(const 
LoopRV& loop_rv,
 ffi::Array<LoopRV> ConcreteScheduleNode::LoopPartition(
     const LoopRV& loop_rv, const ffi::Array<ffi::Optional<ExprRV>>& factor_rvs,
     bool preserve_unit_iters) {
-  class SymbolicShapeError : public ScheduleError {
+  class SymbolicShapeError : public ScheduleErrorContextObj {
    public:
     explicit SymbolicShapeError(IRModule mod, For loop) : mod_(mod), 
loop_(std::move(loop)) {}
 
@@ -551,20 +562,21 @@ ffi::Array<LoopRV> ConcreteScheduleNode::LoopPartition(
   ffi::Array<StmtSRef> results;
   TVM_TIR_SCHEDULE_BEGIN();
   if (!is_const_number(loop->min) || !is_const_number(loop->extent)) {
-    throw SymbolicShapeError(state_->mod, ffi::GetRef<For>(loop));
+    throw MakeScheduleError<SymbolicShapeError>(state_->mod, 
ffi::GetRef<For>(loop));
   }
   // infer factor if needed and check validity of factors
   for (size_t i = 0; i < factor_rvs.size(); i++) {
     if (!factor_rvs[i].has_value()) {
       factors.push_back(IntImm::Int32(-1));
       if (infer_index != -1) {
-        throw NotSingleInferFactorError(state_->mod);
+        throw MakeScheduleError<NotSingleInferFactorError>(state_->mod);
       }
       infer_index = i;
     } else {
       PrimExpr factor = this->Get(factor_rvs[i].value());
       if (is_const_int(factor) && !is_positive_const(factor)) {
-        throw NonPositiveFactorError(state_->mod, 
factor.as<IntImmNode>()->value, i);
+        throw MakeScheduleError<NonPositiveFactorError>(state_->mod, 
factor.as<IntImmNode>()->value,
+                                                        i);
       }
       if (factor.ty().bits() > loop->extent.ty().bits()) {
         factor = cast(loop->extent.ty(), factor);
@@ -574,7 +586,7 @@ ffi::Array<LoopRV> ConcreteScheduleNode::LoopPartition(
     }
   }
   if (this->analyzer_->CanProve(tot_length >= loop->extent)) {
-    throw WrongFactorError(state_->mod, ffi::GetRef<For>(loop), false);
+    throw MakeScheduleError<WrongFactorError>(state_->mod, 
ffi::GetRef<For>(loop), false);
   }
   if (infer_index != -1) {
     // if there is a 'None' in the factor list, 'None' becomes the difference 
between the extent and
diff --git a/src/s_tir/schedule/error.cc b/src/s_tir/schedule/error.cc
index 09a1a323a3..bce875a2a3 100644
--- a/src/s_tir/schedule/error.cc
+++ b/src/s_tir/schedule/error.cc
@@ -16,6 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+#include <tvm/ffi/extra/visit_error_context.h>
+#include <tvm/ffi/reflection/registry.h>
 #include <tvm/script/printer/printer.h>
 
 #include "./utils.h"
@@ -25,7 +27,21 @@ namespace s_tir {
 using namespace tvm::prim;
 using namespace tvm::tirx;
 
-ffi::String ScheduleError::RenderReport(const ffi::String& primitive) const {
+const ScheduleErrorContextObj* GetScheduleErrorContext(const ffi::Error& 
error) {
+  if (auto context = error.extra_context()) {
+    if (const auto* payload = context->as<ScheduleErrorContextObj>()) {
+      return payload;
+    }
+    if (const auto* visit_context = context->as<ffi::VisitErrorContextObj>()) {
+      if (visit_context->prev_error_context) {
+        return 
visit_context->prev_error_context.value().as<ScheduleErrorContextObj>();
+      }
+    }
+  }
+  return nullptr;
+}
+
+ffi::String ScheduleErrorContextObj::RenderReport(const ffi::String& 
primitive) const {
   IRModule mod = this->mod();
   std::ostringstream os;
 
@@ -57,5 +73,7 @@ ffi::String ScheduleError::RenderReport(const ffi::String& 
primitive) const {
   return os.str();
 }
 
+TVM_FFI_STATIC_INIT_BLOCK() { 
ffi::reflection::ObjectDef<ScheduleErrorContextObj>(); }
+
 }  // namespace s_tir
 }  // namespace tvm
diff --git a/src/s_tir/schedule/error.h b/src/s_tir/schedule/error.h
index 0062edcffb..ca00fa7c9d 100644
--- a/src/s_tir/schedule/error.h
+++ b/src/s_tir/schedule/error.h
@@ -20,10 +20,12 @@
 #define TVM_S_TIR_SCHEDULE_ERROR_H_
 
 #include <tvm/ffi/error.h>
+#include <tvm/ffi/memory.h>
 #include <tvm/ir/prim/expr.h>
 #include <tvm/s_tir/schedule/state.h>
 
 #include <string>
+#include <type_traits>
 #include <utility>
 
 namespace tvm {
@@ -31,11 +33,10 @@ namespace s_tir {
 using namespace tvm::prim;
 using namespace tvm::tirx;
 
-/*! \brief Error that happens during TensorIR scheduling */
-class ScheduleError : public tvm::ffi::Error {
+/*! \brief Diagnostic payload for an error that happens during TensorIR 
scheduling. */
+class ScheduleErrorContextObj : public ffi::Object {
  public:
-  /*! \brief Base constructor */
-  ScheduleError() : tvm::ffi::Error("ScheduleError", "", 
TVMFFIBacktrace(nullptr, 0, nullptr, 0)) {}
+  virtual ~ScheduleErrorContextObj() = default;
   /*! \brief The error occurred in this IRModule */
   virtual IRModule mod() const = 0;
   /*! \brief The locations of interest that we want to point out */
@@ -57,9 +58,28 @@ class ScheduleError : public tvm::ffi::Error {
   virtual ffi::String FastErrorString() const = 0;
   /*! \brief Render the ScheduleError with the template provided by 
`DetailRenderTemplate` */
   ffi::String RenderReport(const ffi::String& primitive) const;
+
+  static constexpr TVMFFISEqHashKind _type_s_eq_hash_kind = 
kTVMFFISEqHashKindUnsupported;
+  TVM_FFI_DECLARE_OBJECT_INFO("s_tir.ScheduleErrorContext", 
ScheduleErrorContextObj, ffi::Object);
 };
 
-class LoopPositionError : public ScheduleError {
+/*! \brief Create a plain FFI error carrying a lazily rendered scheduling 
diagnostic. */
+template <typename Context, typename... Args>
+ffi::Error MakeScheduleError(Args&&... args) {
+  static_assert(std::is_base_of_v<ScheduleErrorContextObj, Context>);
+  const TVMFFIByteArray* backtrace = TVMFFIBacktrace(nullptr, 0, nullptr, 0);
+  return ffi::Error("ScheduleError", "", std::string(backtrace->data, 
backtrace->size),
+                    std::nullopt,
+                    
ffi::ObjectRef(ffi::make_object<Context>(std::forward<Args>(args)...)));
+}
+
+/*!
+ * \brief Get the scheduling payload, directly or behind one visit-context 
wrapper.
+ * \return The payload owned by error, or nullptr if the error has no 
scheduling payload.
+ */
+const ScheduleErrorContextObj* GetScheduleErrorContext(const ffi::Error& 
error);
+
+class LoopPositionError : public ScheduleErrorContextObj {
  public:
   explicit LoopPositionError(IRModule mod, For loop, SBlock block, const 
std::string& primitive)
       : mod_(std::move(mod)),
diff --git a/src/s_tir/schedule/ir_comparator.cc 
b/src/s_tir/schedule/ir_comparator.cc
index 2f7621d111..3d742e59a6 100644
--- a/src/s_tir/schedule/ir_comparator.cc
+++ b/src/s_tir/schedule/ir_comparator.cc
@@ -47,7 +47,7 @@ using namespace tvm::tirx;
 
 /******** Tensorize Comparator ********/
 
-class TensorIntrinMismatchError : public ScheduleError {
+class TensorIntrinMismatchError : public ScheduleErrorContextObj {
  public:
   explicit TensorIntrinMismatchError(IRModule lhs_mod, Stmt lhs_stmt, Stmt 
rhs_stmt,
                                      std::vector<std::string> error_messages)
@@ -89,7 +89,8 @@ bool TensorizeComparator::VisitStmt(const Stmt& n, const 
Stmt& other) {
   bool equal = n.same_as(other) ||
                ((n->type_index() == other->type_index()) && 
StmtComparator::VisitStmt(n, other));
   if (!equal && assert_mode_ && (n->IsInstance<ForNode>() || 
n->IsInstance<SBlockNode>())) {
-    throw TensorIntrinMismatchError(lhs_mod_, n, other, 
std::move(error_messages_));
+    throw MakeScheduleError<TensorIntrinMismatchError>(lhs_mod_, n, other,
+                                                       
std::move(error_messages_));
   }
   return equal;
 }
diff --git a/src/s_tir/schedule/primitive/block_annotate.cc 
b/src/s_tir/schedule/primitive/block_annotate.cc
index 34fa04e1af..1a937a1809 100644
--- a/src/s_tir/schedule/primitive/block_annotate.cc
+++ b/src/s_tir/schedule/primitive/block_annotate.cc
@@ -29,7 +29,7 @@ namespace s_tir {
 using namespace tvm::prim;
 using namespace tvm::tirx;
 
-class StorageAlignAxisOutOfRangeError : public ScheduleError {
+class StorageAlignAxisOutOfRangeError : public ScheduleErrorContextObj {
  public:
   explicit StorageAlignAxisOutOfRangeError(IRModule mod, BufferVar buffer, int 
axis)
       : mod_(std::move(mod)), buffer_(std::move(buffer)), axis_(axis) {}
@@ -56,7 +56,7 @@ class StorageAlignAxisOutOfRangeError : public ScheduleError {
   static int CheckAndUpdate(const IRModule& mod, const BufferVar& buffer, int 
axis) {
     int ndim = static_cast<int>(buffer->shape.size());
     if (axis < -ndim || axis >= ndim) {
-      throw StorageAlignAxisOutOfRangeError(mod, buffer, axis);
+      throw MakeScheduleError<StorageAlignAxisOutOfRangeError>(mod, buffer, 
axis);
     }
     // If axis is negative, convert it to a non-negative one.
     if (axis < 0) {
@@ -71,7 +71,7 @@ class StorageAlignAxisOutOfRangeError : public ScheduleError {
   int axis_;
 };
 
-class NonAllocatedBufferError : public ScheduleError {
+class NonAllocatedBufferError : public ScheduleErrorContextObj {
  public:
   explicit NonAllocatedBufferError(IRModule mod, BufferVar buffer) : 
mod_(mod), buffer_(buffer) {}
 
@@ -92,7 +92,7 @@ class NonAllocatedBufferError : public ScheduleError {
                                                   const BufferVar& buffer) {
     auto [defining_site_sref, is_alloc] = GetBufferDefiningSite(block_sref, 
buffer);
     if (!defining_site_sref.has_value() || !is_alloc) {
-      throw NonAllocatedBufferError(mod, buffer);
+      throw MakeScheduleError<NonAllocatedBufferError>(mod, buffer);
     }
 
     return defining_site_sref.value();
@@ -106,7 +106,7 @@ class NonAllocatedBufferError : public ScheduleError {
   BufferVar buffer_;
 };
 
-class StorageAlignInvalidFactorError : public ScheduleError {
+class StorageAlignInvalidFactorError : public ScheduleErrorContextObj {
  public:
   explicit StorageAlignInvalidFactorError(IRModule mod, int factor)
       : mod_(std::move(mod)), factor_(factor) {}
@@ -126,7 +126,7 @@ class StorageAlignInvalidFactorError : public ScheduleError 
{
 
   static void Check(const IRModule& mod, int factor) {
     if (factor <= 0) {
-      throw StorageAlignInvalidFactorError(mod, factor);
+      throw MakeScheduleError<StorageAlignInvalidFactorError>(mod, factor);
     }
   }
 
@@ -138,7 +138,7 @@ class StorageAlignInvalidFactorError : public ScheduleError 
{
   int factor_;
 };
 
-class StorageAlignInvalidAnnotationError : public ScheduleError {
+class StorageAlignInvalidAnnotationError : public ScheduleErrorContextObj {
  public:
   explicit StorageAlignInvalidAnnotationError(IRModule mod, SBlock block)
       : mod_(std::move(mod)), block_(std::move(block)) {}
@@ -162,7 +162,7 @@ class StorageAlignInvalidAnnotationError : public 
ScheduleError {
     auto it = block->annotations.find(s_tir::attr::buffer_dim_align);
     if (it != block->annotations.end()) {
       if (!IsValidAnnotation(block, (*it).second)) {
-        throw StorageAlignInvalidAnnotationError(mod, block);
+        throw MakeScheduleError<StorageAlignInvalidAnnotationError>(mod, 
block);
       }
       return (*it).second.as_or_throw<StorageAlignAnnotation>();
     }
diff --git a/src/s_tir/schedule/primitive/blockize_tensorize.cc 
b/src/s_tir/schedule/primitive/blockize_tensorize.cc
index c12baa5862..8d6475bf56 100644
--- a/src/s_tir/schedule/primitive/blockize_tensorize.cc
+++ b/src/s_tir/schedule/primitive/blockize_tensorize.cc
@@ -47,7 +47,7 @@ T DeepCopy(const T& stmt) {
  * \brief ScheduleError that the bindings of the inner block are not divisible 
by the subspace
  * represented by the outer loops.
  */
-class SubspaceNotDivisibleError : public ScheduleError {
+class SubspaceNotDivisibleError : public ScheduleErrorContextObj {
  public:
   explicit SubspaceNotDivisibleError(IRModule mod, For scope_loop, SBlock 
inner_block)
       : mod_(std::move(mod)),
@@ -523,7 +523,8 @@ SBlockRealize BlockizeImpl(const ScheduleState& self, const 
StmtSRef& loop_sref,
   ffi::Array<ffi::Array<arith::IterMark>> division =
       SubspaceDivide(block_realize, block_sref, loop_sref, &loops, analyzer, 
preserve_unit_iters);
   if (division.empty()) {
-    throw SubspaceNotDivisibleError(self->mod, ffi::GetRef<For>(loops.back()), 
block);
+    throw MakeScheduleError<SubspaceNotDivisibleError>(self->mod, 
ffi::GetRef<For>(loops.back()),
+                                                       block);
   }
   PrimExpr outer_predicate = division.back()[0]->extent;
   PrimExpr inner_predicate = division.back()[1]->extent;
@@ -614,7 +615,8 @@ SBlockRealize BlockizeBlocks(const ScheduleState& self, 
const ffi::Array<StmtSRe
     ffi::Array<ffi::Array<arith::IterMark>> division = SubspaceDivide(
         block_realize, block_sref, lca, &loops, analyzer.get(), 
preserve_unit_iters, true);
     if (division.empty()) {
-      throw SubspaceNotDivisibleError(self->mod, 
ffi::GetRef<For>(loops.back()), block);
+      throw MakeScheduleError<SubspaceNotDivisibleError>(self->mod, 
ffi::GetRef<For>(loops.back()),
+                                                         block);
     }
     outer_predicate = division.back()[0]->extent;
     PrimExpr inner_predicate = division.back()[1]->extent;
diff --git a/src/s_tir/schedule/primitive/cache_read_write.cc 
b/src/s_tir/schedule/primitive/cache_read_write.cc
index 4eee15145f..198e01fbe8 100644
--- a/src/s_tir/schedule/primitive/cache_read_write.cc
+++ b/src/s_tir/schedule/primitive/cache_read_write.cc
@@ -34,7 +34,7 @@ using namespace tvm::tirx;
 
 /******** Error Classes ********/
 
-class NotSingleWriteBlock : public ScheduleError {
+class NotSingleWriteBlock : public ScheduleErrorContextObj {
  public:
   explicit NotSingleWriteBlock(IRModule mod, BufferVar buffer, 
ffi::Array<StmtSRef> write_blocks)
       : mod_(std::move(mod)), buffer_(std::move(buffer)) {
@@ -117,7 +117,7 @@ struct ReindexCacheStageInfo : CacheStageInfo {
 
 /* \brief The schedule error that accessed buffer region is not a single point 
for
  * reindex_cache_read/write. */
-class NotSinglePointAccess : public ScheduleError {
+class NotSinglePointAccess : public ScheduleErrorContextObj {
  public:
   explicit NotSinglePointAccess(IRModule mod, SBlock block, BufferRegion 
cache_region,
                                 bool is_cache_read)
@@ -522,7 +522,7 @@ ffi::Optional<StmtSRef> GetOnlyWriteBlock(ScheduleState 
self, const StmtSRef& sc
     const ffi::Array<StmtSRef>& block_srefs = it->second;
     TVM_FFI_ICHECK(!block_srefs.empty());
     if (block_srefs.size() > 1) {
-      throw NotSingleWriteBlock(self->mod, buffer, block_srefs);
+      throw MakeScheduleError<NotSingleWriteBlock>(self->mod, buffer, 
block_srefs);
     }
     return block_srefs[0];
   }
@@ -1492,7 +1492,7 @@ BufferVar CreateReindexBuffer(const BufferVar& buffer, 
const ffi::Array<IterVar>
 /*!
  * \brief The schedule error that the target is not a leaf block.
  */
-class NotLeafBlockError : public ScheduleError {
+class NotLeafBlockError : public ScheduleErrorContextObj {
  public:
   NotLeafBlockError(IRModule mod, SBlock block) : mod_(std::move(mod)), 
block_(std::move(block)) {}
   ffi::String FastErrorString() const final {
@@ -1510,7 +1510,7 @@ class NotLeafBlockError : public ScheduleError {
 };
 
 /*! \brief The schedule error that the buffer access is invalid for reindex. */
-class InvalidBufferAccessError : public ScheduleError {
+class InvalidBufferAccessError : public ScheduleErrorContextObj {
  public:
   enum class ErrorKind {
     kNoAccess,         // buffer access not found
@@ -1557,8 +1557,8 @@ class ReIndexCollector : public StmtExprVisitor {
     ReIndexCollector collector(mod, buffer, block);
     collector(block->body);
     if (!collector.buffer_access_indices_.has_value()) {
-      throw InvalidBufferAccessError(mod, buffer, block,
-                                     
InvalidBufferAccessError::ErrorKind::kNoAccess);
+      throw MakeScheduleError<InvalidBufferAccessError>(
+          mod, buffer, block, InvalidBufferAccessError::ErrorKind::kNoAccess);
     }
     return collector.buffer_access_indices_.value();
   }
@@ -1576,7 +1576,7 @@ class ReIndexCollector : public StmtExprVisitor {
 
   void VisitStmt_(const SBlockNode* block) final {
     // no sub-blocks under this block
-    throw NotLeafBlockError(mod_, block_);
+    throw MakeScheduleError<NotLeafBlockError>(mod_, block_);
   }
 
   void VisitStmt_(const BufferStoreNode* store) final {
@@ -1593,15 +1593,15 @@ class ReIndexCollector : public StmtExprVisitor {
     } else if (!std::equal(buffer_access_indices_.value().begin(),
                            buffer_access_indices_.value().end(), 
indices.begin(), indices.end(),
                            ExprDeepEqual())) {
-      throw InvalidBufferAccessError(mod_, buffer_, block_,
-                                     
InvalidBufferAccessError::ErrorKind::kNonUniqueAccess);
+      throw MakeScheduleError<InvalidBufferAccessError>(
+          mod_, buffer_, block_, 
InvalidBufferAccessError::ErrorKind::kNonUniqueAccess);
     }
   }
 
   void VisitExpr_(const VarNode* var) final {
     if (var == buffer_.get()) {
-      throw InvalidBufferAccessError(mod_, buffer_, block_,
-                                     
InvalidBufferAccessError::ErrorKind::kOpaqueAccess);
+      throw MakeScheduleError<InvalidBufferAccessError>(
+          mod_, buffer_, block_, 
InvalidBufferAccessError::ErrorKind::kOpaqueAccess);
     }
   }
   /*! \brief The IR module */
@@ -1720,7 +1720,7 @@ class ReIndexRewriter : public StmtExprMutator {
 };
 
 void CheckRegionCover(const ScheduleState& self, StmtSRef scope_root, 
BufferVar read_buffer) {
-  class NotRegionCoverError : public ScheduleError {
+  class NotRegionCoverError : public ScheduleErrorContextObj {
    public:
     explicit NotRegionCoverError(IRModule mod, SBlock block) : mod_(mod), 
block_(block) {}
     IRModule mod() const final { return mod_; }
@@ -1743,7 +1743,7 @@ The region cover property require to hold for every of 
its child blocks
       if (region->buffer.same_as(read_buffer)) {
         if (!self->block_info.at(child_block_sref).region_cover) {
           const SBlockNode* block = TVM_SREF_TO_SBLOCK(scope_root);
-          throw NotRegionCoverError(self->mod, ffi::GetRef<SBlock>(block));
+          throw MakeScheduleError<NotRegionCoverError>(self->mod, 
ffi::GetRef<SBlock>(block));
         }
       }
     }
@@ -1968,7 +1968,7 @@ ffi::Array<StmtSRef> GetLoopsUnderScope(const StmtSRef& 
block_sref, const StmtSR
  * \brief The schedule error that block iter vars appears in old buffer and new
  * allocated cache buffer does not match.
  */
-class ReindexCacheReadWriteNotMatchError : public ScheduleError {
+class ReindexCacheReadWriteNotMatchError : public ScheduleErrorContextObj {
  public:
   ReindexCacheReadWriteNotMatchError(IRModule mod, SBlock block, Var var,
                                      ffi::Array<PrimExpr> old_indices,
@@ -2054,8 +2054,8 @@ void CollectReindexCacheStageInfoAndCreateBuffer(
     bool appears_in_new = 
collector_new.use_count_.count(block_iter_var->var.get());
     bool appears_in_old = 
collector_old.use_count_.count(block_iter_var->var.get());
     if (appears_in_new != appears_in_old) {
-      throw ReindexCacheReadWriteNotMatchError(mod, block, 
block_iter_var->var, old_indices,
-                                               new_indices, is_cache_read, 
appears_in_old);
+      throw MakeScheduleError<ReindexCacheReadWriteNotMatchError>(
+          mod, block, block_iter_var->var, old_indices, new_indices, 
is_cache_read, appears_in_old);
     }
     if (appears_in_new) {
       info->block_iter_vars.push_back(block_iter_var);
@@ -2099,7 +2099,7 @@ void CheckSinglePoint(ScheduleState self, const SBlock& 
block, const BufferRegio
     }
   }
   if (!single_point) {
-    throw NotSinglePointAccess(self->mod, block, cache_region, is_cache_read);
+    throw MakeScheduleError<NotSinglePointAccess>(self->mod, block, 
cache_region, is_cache_read);
   }
 }
 
@@ -2243,7 +2243,7 @@ StmtSRef ReindexCacheWrite(ScheduleState self, const 
StmtSRef& block_sref, int w
 }
 
 /*! \brief The schedule error that the target block doesn't both read&write 
target buffer. */
-class NotReadWriteError : public ScheduleError {
+class NotReadWriteError : public ScheduleErrorContextObj {
  public:
   NotReadWriteError(IRModule mod, SBlock block, BufferVar buffer)
       : mod_(std::move(mod)), block_(std::move(block)), 
buffer_(std::move(buffer)) {}
@@ -2285,7 +2285,7 @@ ffi::Array<StmtSRef> CacheInplace(ScheduleState self, 
const StmtSRef& block_sref
   ffi::Optional<BufferRegion> read_region = 
GetBufferRegionFromBuffer(rw_block->reads, buffer);
   ffi::Optional<BufferRegion> write_region = 
GetBufferRegionFromBuffer(rw_block->writes, buffer);
   if (!read_region.has_value() || !write_region.has_value()) {
-    throw NotReadWriteError(self->mod, ffi::GetRef<SBlock>(rw_block), buffer);
+    throw MakeScheduleError<NotReadWriteError>(self->mod, 
ffi::GetRef<SBlock>(rw_block), buffer);
   }
 
   ffi::Array<StmtSRef> results_block_sref;
diff --git a/src/s_tir/schedule/primitive/compute_at.cc 
b/src/s_tir/schedule/primitive/compute_at.cc
index ca1d8d114f..d69e08bbb1 100644
--- a/src/s_tir/schedule/primitive/compute_at.cc
+++ b/src/s_tir/schedule/primitive/compute_at.cc
@@ -35,7 +35,7 @@ using support::NDIntSet;
  * \tparam is_consumer Indicates if all the required blocks are consumers or 
producers
  */
 template <bool is_consumer>
-class NotAllRequiredBlocksAreVisitedError : public ScheduleError {
+class NotAllRequiredBlocksAreVisitedError : public ScheduleErrorContextObj {
  public:
   explicit NotAllRequiredBlocksAreVisitedError(IRModule mod, int 
num_not_visited,
                                                const ffi::Array<StmtSRef>& 
required)
@@ -80,7 +80,7 @@ class NotAllRequiredBlocksAreVisitedError : public 
ScheduleError {
  * \brief An error raised when the given block is not in the same block scope 
as the given loop,
  * or the given loop is the ancestor of the given block.
  */
-class NotInSameScopeError : public ScheduleError {
+class NotInSameScopeError : public ScheduleErrorContextObj {
  public:
   static void CheckAndBindLoopDomain(const ScheduleState& self, const 
StmtSRef& block_sref,
                                      const StmtSRef& loop_sref, const 
StmtSRef& scope_root_sref,
@@ -89,14 +89,14 @@ class NotInSameScopeError : public ScheduleError {
       if (const ForNode* loop = p->StmtAs<ForNode>()) {
         analyzer->Bind(loop->loop_var, Range::FromMinExtent(loop->min, 
loop->extent));
       } else if (p != scope_root_sref.get()) {
-        throw NotInSameScopeError(self->mod, block_sref, loop_sref);
+        throw MakeScheduleError<NotInSameScopeError>(self->mod, block_sref, 
loop_sref);
       } else {
         break;
       }
     }
     for (const StmtSRefNode* p = block_sref->parent; p != 
scope_root_sref.get(); p = p->parent) {
       if (p == loop_sref.get()) {
-        throw NotInSameScopeError(self->mod, block_sref, loop_sref);
+        throw MakeScheduleError<NotInSameScopeError>(self->mod, block_sref, 
loop_sref);
       }
     }
   }
@@ -112,12 +112,12 @@ class NotInSameScopeError : public ScheduleError {
   IRModule mod() const final { return mod_; }
   ffi::Array<ffi::ObjectRef> LocationsOfInterest() const final { return 
{block_, loop_}; }
 
- private:
   explicit NotInSameScopeError(IRModule mod, const StmtSRef& block_sref, const 
StmtSRef& loop_sref)
       : mod_(mod),
         block_(ffi::GetRef<SBlock>(block_sref->StmtAs<SBlockNode>())),
         loop_(ffi::GetRef<For>(loop_sref->StmtAs<ForNode>())) {}
 
+ private:
   IRModule mod_;
   SBlock block_;
   For loop_;
@@ -153,7 +153,7 @@ int FindInsertionPoint(
   if (require_all_producers_visited) {
     int num_producers = producer_srefs.size();
     if (split.n_producers_visited < num_producers) {
-      throw NotAllRequiredBlocksAreVisitedError<false>(
+      throw MakeScheduleError<NotAllRequiredBlocksAreVisitedError<false>>(
           self->mod, num_producers - split.n_producers_visited, 
producer_srefs);
     }
   }
@@ -161,7 +161,7 @@ int FindInsertionPoint(
   if (require_all_consumers_visited) {
     int num_consumers = consumer_srefs.size();
     if (split.n_consumers_visited < num_consumers) {
-      throw NotAllRequiredBlocksAreVisitedError<true>(
+      throw MakeScheduleError<NotAllRequiredBlocksAreVisitedError<true>>(
           self->mod, num_consumers - split.n_consumers_visited, 
consumer_srefs);
     }
   }
diff --git a/src/s_tir/schedule/primitive/compute_inline.cc 
b/src/s_tir/schedule/primitive/compute_inline.cc
index 72fd161430..ae339cf471 100644
--- a/src/s_tir/schedule/primitive/compute_inline.cc
+++ b/src/s_tir/schedule/primitive/compute_inline.cc
@@ -38,7 +38,7 @@ and there should be no variables other than the index 
variables), and f is a bij
 mapping and there should not be predicates in the inlined block. The iter 
domains of the inlined
 block should be covered by the producer block.)";
 
-class HasInitBlock : public ScheduleError {
+class HasInitBlock : public ScheduleErrorContextObj {
  public:
   explicit HasInitBlock(IRModule mod, SBlock block) : mod_(mod), block_(block) 
{}
 
@@ -55,7 +55,7 @@ class HasInitBlock : public ScheduleError {
 
   static void Check(const IRModule& mod, const SBlock& block) {
     if (block->init.has_value()) {
-      throw HasInitBlock(mod, block);
+      throw MakeScheduleError<HasInitBlock>(mod, block);
     }
   }
 
@@ -64,7 +64,7 @@ class HasInitBlock : public ScheduleError {
   SBlock block_;
 };
 
-class NotSingleReadWriteBuffer : public ScheduleError {
+class NotSingleReadWriteBuffer : public ScheduleErrorContextObj {
  public:
   explicit NotSingleReadWriteBuffer(IRModule mod, bool is_read, SBlock block)
       : mod_(mod), is_read_(is_read), block_(std::move(block)) {}
@@ -106,26 +106,26 @@ class NotSingleReadWriteBuffer : public ScheduleError {
       }
       if (buffer_writers.count(BufferVar(ffi::GetRef<Var>(buffer))) > 0) {
         if (read_buffer != nullptr) {
-          throw NotSingleReadWriteBuffer(self->mod, true, block);
+          throw MakeScheduleError<NotSingleReadWriteBuffer>(self->mod, true, 
block);
         }
         read_buffer = buffer;
       }
     }
     if (read_buffer == nullptr) {
-      throw NotSingleReadWriteBuffer(self->mod, true, block);
+      throw MakeScheduleError<NotSingleReadWriteBuffer>(self->mod, true, 
block);
     }
     return BufferVar(ffi::GetRef<Var>(read_buffer));
   }
 
   static BufferVar GetSingleWrite(const ScheduleState& self, const SBlock& 
block) {
     if (block->writes.size() != 1) {
-      throw NotSingleReadWriteBuffer(self->mod, false, block);
+      throw MakeScheduleError<NotSingleReadWriteBuffer>(self->mod, false, 
block);
     }
     return block->writes[0]->buffer;
   }
 };
 
-class BodyAnalysisError : public ScheduleError {
+class BodyAnalysisError : public ScheduleErrorContextObj {
  public:
   explicit BodyAnalysisError(bool is_reverse, IRModule mod, SBlock block)
       : is_reverse_(is_reverse), mod_(mod), block_(std::move(block)) {}
@@ -147,7 +147,7 @@ class BodyAnalysisError : public ScheduleError {
   SBlock block_;
 };
 
-class NonSingleProducerError : public ScheduleError {
+class NonSingleProducerError : public ScheduleErrorContextObj {
  public:
   explicit NonSingleProducerError(IRModule mod, SBlock block)
       : mod_(mod), block_(std::move(block)) {}
@@ -220,7 +220,8 @@ class NonSingleProducerError : public ScheduleError {
             // Check if the producer block is a complete block
             StmtSRef producer_block_sref = self_->stmt2ref.at(node);
             if (!IsCompleteBlock(self_, producer_block_sref, 
scope_root_sref_)) {
-              throw NonSingleProducerError(self_->mod, 
ffi::GetRef<SBlock>(node));
+              throw MakeScheduleError<NonSingleProducerError>(self_->mod,
+                                                              
ffi::GetRef<SBlock>(node));
             }
             producer_across_scope_.back().push_back(ffi::GetRef<SBlock>(node));
             break;
@@ -235,13 +236,14 @@ class NonSingleProducerError : public ScheduleError {
     std::vector<SBlock> producer_across_scope = ProducerFinder::GetProducer(
         self, scope_root_sref, consumer_buffer, 
ffi::GetRef<SBlock>(scope_block));
     if (producer_across_scope.size() != 1) {
-      throw NonSingleProducerError(self->mod, 
ffi::GetRef<SBlock>(consumer_block));
+      throw MakeScheduleError<NonSingleProducerError>(self->mod,
+                                                      
ffi::GetRef<SBlock>(consumer_block));
     }
     return self->stmt2ref.at(producer_across_scope[0].get());
   }
 };
 
-class OpaqueAccessError : public ScheduleError {
+class OpaqueAccessError : public ScheduleErrorContextObj {
  public:
   explicit OpaqueAccessError(IRModule mod, StmtSRef scope_root_sref)
       : mod_(mod), scope_root_(nullptr) {
@@ -266,7 +268,7 @@ class OpaqueAccessError : public ScheduleError {
   SBlock scope_root_;
 };
 
-class ProducerHasNonTrivialPredicateError : public ScheduleError {
+class ProducerHasNonTrivialPredicateError : public ScheduleErrorContextObj {
  public:
   explicit ProducerHasNonTrivialPredicateError(IRModule mod, SBlockRealize 
producer,
                                                PrimExpr new_predicate)
@@ -924,7 +926,7 @@ void ComputeInlineImpl(ScheduleState self, const StmtSRef& 
producer_block_sref,
   // Step 3. Analyze the block body
   ComputeInliner inliner(inlined_buffer, producer_block, scope_root_sref);
   if (!inliner.BodyPatternAllowInline(producer_block)) {
-    throw BodyAnalysisError(false, self->mod, producer_block);
+    throw MakeScheduleError<BodyAnalysisError>(false, self->mod, 
producer_block);
   }
   // Step 4. Create a plan that removes the leaf block to be inlined
   LeafBlockRemovalPlan(self, producer_block_sref, &inliner.src_stmt, 
&inliner.tgt_stmt);
@@ -932,7 +934,7 @@ void ComputeInlineImpl(ScheduleState self, const StmtSRef& 
producer_block_sref,
   // and update other blocks who read from the removed block
   Stmt tgt_stmt = inliner(ffi::GetRef<Stmt>(scope_root_sref->stmt));
   if (inliner.has_opaque_access) {
-    throw OpaqueAccessError(self->mod, scope_root_sref);
+    throw MakeScheduleError<OpaqueAccessError>(self->mod, scope_root_sref);
   }
   // Step 6. Do the real mutation on the AST and the sref tree in the schedule 
state
   if (check_only) {
@@ -976,7 +978,7 @@ void ReverseComputeInlineImpl(ScheduleState self, const 
StmtSRef& consumer_block
   ReverseComputeInliner inliner(inlined_buffer, 
producer_block_sref->StmtAs<SBlockNode>(),
                                 consumer_block_realize, scope_root_sref, 
self->mod);
   if (!inliner.BodyPatternAllowInline(consumer_block_realize)) {
-    throw BodyAnalysisError(true, self->mod, consumer_block);
+    throw MakeScheduleError<BodyAnalysisError>(true, self->mod, 
consumer_block);
   }
   // Step 5. Create a plan that removes the leaf block to be inlined
   LeafBlockRemovalPlan(self, consumer_block_sref, &inliner.src_stmt, 
&inliner.tgt_stmt);
@@ -984,7 +986,7 @@ void ReverseComputeInlineImpl(ScheduleState self, const 
StmtSRef& consumer_block
   // and update other blocks who read from the removed block
   Stmt tgt_stmt = inliner(ffi::GetRef<Stmt>(scope_root_sref->stmt));
   if (inliner.has_opaque_access) {
-    throw OpaqueAccessError(self->mod, scope_root_sref);
+    throw MakeScheduleError<OpaqueAccessError>(self->mod, scope_root_sref);
   }
   // Step 7. Do the real mutation on the AST and the sref tree in the schedule 
state
   if (check_only) {
@@ -1736,7 +1738,7 @@ void FuseReductionEpilogueImpl(ScheduleState self, const 
StmtSRef& reduction_blo
   ReductionEpilogueFuser fuser(reduction_buffer, _reduction_block, 
epilogue_block_realize,
                                scope_root_sref);
   if (!fuser.BodyPatternAllowFusion(epilogue_block_realize)) {
-    throw BodyAnalysisError(true, self->mod, epilogue_block);
+    throw MakeScheduleError<BodyAnalysisError>(true, self->mod, 
epilogue_block);
   }
 
   if (check_only) {
diff --git a/src/s_tir/schedule/primitive/decompose_padding.cc 
b/src/s_tir/schedule/primitive/decompose_padding.cc
index c9ebd17444..c40ee24acc 100644
--- a/src/s_tir/schedule/primitive/decompose_padding.cc
+++ b/src/s_tir/schedule/primitive/decompose_padding.cc
@@ -40,7 +40,7 @@ struct PaddingSBlockInfo {
   PrimExpr pad_value;
 };
 
-class PaddingPatternMatchError : public ScheduleError {
+class PaddingPatternMatchError : public ScheduleErrorContextObj {
  public:
   PaddingPatternMatchError(IRModule mod, SBlock block, const std::string& 
error_msg)
       : mod_(std::move(mod)), block_(std::move(block)), error_msg_(error_msg) 
{}
@@ -76,7 +76,8 @@ class PaddingInfoAnalyzer {
                                                   arith::AnalyzerObj* 
analyzer) {
     PaddingInfoAnalyzer padding_analyzer(analyzer);
     if (!padding_analyzer.MatchPadding(realize, dom_map)) {
-      throw PaddingPatternMatchError(mod, realize->block, 
padding_analyzer.error_msg_);
+      throw MakeScheduleError<PaddingPatternMatchError>(mod, realize->block,
+                                                        
padding_analyzer.error_msg_);
     }
     return padding_analyzer.info_;
   }
@@ -480,8 +481,8 @@ StmtSRef DecomposePaddingImpl(ScheduleState self, const 
StmtSRef& block_sref,
   }
   TVM_FFI_ICHECK(in_bound_filling_pos.defined());
   if (!found_const_filling_pos) {
-    throw LoopPositionError(self->mod, const_filling_pos, 
ffi::GetRef<SBlock>(block),
-                            "decompose_padding");
+    throw MakeScheduleError<LoopPositionError>(self->mod, const_filling_pos,
+                                               ffi::GetRef<SBlock>(block), 
"decompose_padding");
   }
 
   // Check 3. match padding pattern and return padding operation info.
diff --git a/src/s_tir/schedule/primitive/for_kind.cc 
b/src/s_tir/schedule/primitive/for_kind.cc
index 58e19d8496..28cb9194b2 100644
--- a/src/s_tir/schedule/primitive/for_kind.cc
+++ b/src/s_tir/schedule/primitive/for_kind.cc
@@ -26,7 +26,7 @@ namespace s_tir {
 using namespace tvm::prim;
 using namespace tvm::tirx;
 
-class WrongBlockIterTypeError : public ScheduleError {
+class WrongBlockIterTypeError : public ScheduleErrorContextObj {
  public:
   explicit WrongBlockIterTypeError(IRModule mod, ForKind for_kind, Var 
loop_var, SBlock block)
       : mod_(std::move(mod)), loop_var_(std::move(loop_var)), 
block_(std::move(block)) {
@@ -112,7 +112,7 @@ void CheckLoopParallelizableInBlock(const ScheduleState& 
self, ForKind for_kind,
     IterVarType iter_type = iter_var->iter_type;
     if (!(iter_type == kDataPar ||
           (iter_type == kCommReduce && thread_scope.rank == 1 && 
thread_scope.dim_index != -1))) {
-      throw WrongBlockIterTypeError(self->mod, for_kind, loop_var, block);
+      throw MakeScheduleError<WrongBlockIterTypeError>(self->mod, for_kind, 
loop_var, block);
     }
   }
 }
diff --git a/src/s_tir/schedule/primitive/hide_buffer_access.cc 
b/src/s_tir/schedule/primitive/hide_buffer_access.cc
index 0065afe169..fce9329dbd 100644
--- a/src/s_tir/schedule/primitive/hide_buffer_access.cc
+++ b/src/s_tir/schedule/primitive/hide_buffer_access.cc
@@ -31,7 +31,7 @@ using namespace tvm::tirx;
 /******** Error Classes ********/
 
 namespace {
-class BufTypeError : public ScheduleError {
+class BufTypeError : public ScheduleErrorContextObj {
  public:
   explicit BufTypeError(IRModule mod, const ffi::String& buf_type)
       : mod_(std::move(mod)), buf_type_(buf_type) {}
@@ -54,7 +54,7 @@ class BufTypeError : public ScheduleError {
   ffi::String buf_type_;
 };
 
-class InvalidIndexError : public ScheduleError {
+class InvalidIndexError : public ScheduleErrorContextObj {
  public:
   explicit InvalidIndexError(IRModule mod, int num_access_regions, int buf_idx)
       : mod_(std::move(mod)), num_access_regions_(num_access_regions), 
buf_idx_(buf_idx) {}
@@ -99,7 +99,7 @@ void UnsafeHideBufferAccess(ScheduleState self, const 
StmtSRef& block_sref,
   } else if (buf_type == "write") {
     num_access_regions = block->writes.size();
   } else {
-    throw BufTypeError(self->mod, buf_type);
+    throw MakeScheduleError<BufTypeError>(self->mod, buf_type);
   }
 
   std::set<int> buf_indices;
@@ -108,7 +108,7 @@ void UnsafeHideBufferAccess(ScheduleState self, const 
StmtSRef& block_sref,
     if (buf_idx_val >= 0 && buf_idx_val < num_access_regions) {
       buf_indices.insert(buf_idx_val);
     } else {
-      throw InvalidIndexError(self->mod, num_access_regions, buf_idx_val);
+      throw MakeScheduleError<InvalidIndexError>(self->mod, 
num_access_regions, buf_idx_val);
     }
   }
 
diff --git a/src/s_tir/schedule/primitive/layout_transformation.cc 
b/src/s_tir/schedule/primitive/layout_transformation.cc
index 6e09125efe..0a6c231cc9 100644
--- a/src/s_tir/schedule/primitive/layout_transformation.cc
+++ b/src/s_tir/schedule/primitive/layout_transformation.cc
@@ -978,7 +978,7 @@ class TransformLayoutRewriter : private 
tirx::IRMutatorWithAnalyzer {
   arith::Analyzer index_simplifier_;
 };
 
-class BufferIsSubregionError : public ScheduleError {
+class BufferIsSubregionError : public ScheduleErrorContextObj {
  public:
   explicit BufferIsSubregionError(IRModule mod, BufferVar buffer) : mod_(mod), 
buffer_(buffer) {}
 
@@ -1003,7 +1003,7 @@ class BufferIsSubregionError : public ScheduleError {
   BufferVar buffer_;
 };
 
-class TransformationPaddingIndexMapError : public ScheduleError {
+class TransformationPaddingIndexMapError : public ScheduleErrorContextObj {
  public:
   TransformationPaddingIndexMapError(IRModule mod, IndexMap pad_value)
       : mod_(mod), pad_value_(pad_value) {}
@@ -1030,7 +1030,7 @@ class TransformationPaddingIndexMapError : public 
ScheduleError {
   IndexMap pad_value_;
 };
 
-class TransformationPaddingTypeError : public ScheduleError {
+class TransformationPaddingTypeError : public ScheduleErrorContextObj {
  public:
   TransformationPaddingTypeError(IRModule mod, BufferVar buffer, IndexMap 
pad_value)
       : mod_(mod), buffer_(buffer), pad_value_(pad_value) {
@@ -1062,7 +1062,7 @@ class TransformationPaddingTypeError : public 
ScheduleError {
   DLDataType pad_value_dtype_;
 };
 
-class TransformationPaddingExpressionError : public ScheduleError {
+class TransformationPaddingExpressionError : public ScheduleErrorContextObj {
  public:
   static void Check(IRModule mod, BufferVar buffer, IndexMap pad_value) {
     Visitor visitor(buffer);
@@ -1070,8 +1070,8 @@ class TransformationPaddingExpressionError : public 
ScheduleError {
         << "Internal error: Should be caught by ScheduleError checks prior to 
this point";
     visitor(pad_value->final_indices[0]);
     if (visitor.illegal_load) {
-      throw TransformationPaddingExpressionError(mod, buffer, pad_value,
-                                                 visitor.illegal_load.value());
+      throw MakeScheduleError<TransformationPaddingExpressionError>(mod, 
buffer, pad_value,
+                                                                    
visitor.illegal_load.value());
     }
   }
 
@@ -1090,10 +1090,12 @@ class TransformationPaddingExpressionError : public 
ScheduleError {
     ffi::Optional<TensorLoad> illegal_load;
   };
 
+ public:
   TransformationPaddingExpressionError(IRModule mod, BufferVar buffer, 
IndexMap pad_value,
                                        TensorLoad illegal_load)
       : mod_(mod), buffer_(buffer), pad_value_(pad_value), 
illegal_load_(illegal_load) {}
 
+ private:
   ffi::String FastErrorString() const final {
     std::ostringstream ss;
     ss << "ScheduleError: Pad value may not contain load from "
@@ -1118,7 +1120,7 @@ class TransformationPaddingExpressionError : public 
ScheduleError {
   TensorLoad illegal_load_;
 };
 
-class TransformationIntroducesPaddingError : public ScheduleError {
+class TransformationIntroducesPaddingError : public ScheduleErrorContextObj {
  public:
   TransformationIntroducesPaddingError(IRModule mod, BufferVar buffer, 
IndexMap index_map,
                                        PrimExpr padding_predicate)
@@ -1220,14 +1222,15 @@ void TransformLayout(ScheduleState self, const 
StmtSRef& block_sref, int buffer_
 
   auto [defining_site_sref, is_alloc] = GetBufferDefiningSite(block_sref, 
old_buffer);
   if (defining_site_sref.has_value() && !is_alloc) {
-    throw BufferIsSubregionError(self->mod, old_buffer);
+    throw MakeScheduleError<BufferIsSubregionError>(self->mod, old_buffer);
   }
   if (pad_value) {
     if (pad_value.value()->final_indices.size() != 1) {
-      throw TransformationPaddingIndexMapError(self->mod, pad_value.value());
+      throw MakeScheduleError<TransformationPaddingIndexMapError>(self->mod, 
pad_value.value());
     }
     if (pad_value.value()->final_indices[0].ty() != old_buffer->dtype) {
-      throw TransformationPaddingTypeError(self->mod, old_buffer, 
pad_value.value());
+      throw MakeScheduleError<TransformationPaddingTypeError>(self->mod, 
old_buffer,
+                                                              
pad_value.value());
     }
 
     TransformationPaddingExpressionError::Check(self->mod, old_buffer, 
pad_value.value());
@@ -1252,7 +1255,8 @@ void TransformLayout(ScheduleState self, const StmtSRef& 
block_sref, int buffer_
 
   bool has_padding = !is_zero(padding_predicate);
   if (has_padding && !pad_value.has_value()) {
-    throw TransformationIntroducesPaddingError(self->mod, old_buffer, 
index_map, padding_predicate);
+    throw MakeScheduleError<TransformationIntroducesPaddingError>(self->mod, 
old_buffer, index_map,
+                                                                  
padding_predicate);
   }
 
   // Step 2: Infer the shape of the new buffer
@@ -1325,7 +1329,7 @@ IterVarType DetectNewBlockIterType(
   return result;
 }
 
-class NotBijectiveAffineIndexMapError : public ScheduleError {
+class NotBijectiveAffineIndexMapError : public ScheduleErrorContextObj {
  public:
   NotBijectiveAffineIndexMapError(IRModule mod, IndexMap index_map)
       : mod_(std::move(mod)), index_map_(std::move(index_map)) {}
@@ -1348,11 +1352,11 @@ class NotBijectiveAffineIndexMapError : public 
ScheduleError {
   IndexMap index_map_;
 };
 
-class IndexMapNotApplicableToBlockIterError : public ScheduleError {
+class IndexMapNotApplicableToBlockIterError : public ScheduleErrorContextObj {
  public:
   static void Check(const IRModule mod, const SBlock& block, const IndexMap& 
index_map) {
     if (index_map->initial_indices.size() != block->iter_vars.size()) {
-      throw IndexMapNotApplicableToBlockIterError(mod, block, index_map);
+      throw MakeScheduleError<IndexMapNotApplicableToBlockIterError>(mod, 
block, index_map);
     }
   }
   explicit IndexMapNotApplicableToBlockIterError(IRModule mod, SBlock block, 
IndexMap index_map)
@@ -1382,7 +1386,7 @@ class IndexMapNotApplicableToBlockIterError : public 
ScheduleError {
   IndexMap index_map_;
 };
 
-class OpaqueNewIterTypeError : public ScheduleError {
+class OpaqueNewIterTypeError : public ScheduleErrorContextObj {
  public:
   explicit OpaqueNewIterTypeError(IRModule mod, SBlock block, PrimExpr 
iter_value)
       : mod_(std::move(mod)), block_(std::move(block)), 
iter_value_(std::move(iter_value)) {}
@@ -1472,8 +1476,8 @@ void TransformBlockLayout(ScheduleState self, const 
StmtSRef& block_sref,
       iter_type = DetectNewBlockIterType(transformed_block_iters[i], 
block_iter_type);
     }
     if (iter_type == kOpaque) {
-      throw OpaqueNewIterTypeError(self->mod, ffi::GetRef<SBlock>(block_ptr),
-                                   transformed_block_iters[i]);
+      throw MakeScheduleError<OpaqueNewIterTypeError>(self->mod, 
ffi::GetRef<SBlock>(block_ptr),
+                                                      
transformed_block_iters[i]);
     }
     PrimType dtype = new_block_var->ty.as_or_throw<PrimType>();
     new_block_iters.push_back(IterVar(
@@ -1494,7 +1498,7 @@ void TransformBlockLayout(ScheduleState self, const 
StmtSRef& block_sref,
     try {
       inverse_index_map = index_map.Inverse(initial_ranges, analyzer);
     } catch (...) {
-      throw NotBijectiveAffineIndexMapError(self->mod, index_map);
+      throw MakeScheduleError<NotBijectiveAffineIndexMapError>(self->mod, 
index_map);
     }
     // old block vars written in terms of new block vars
     ffi::Array<PrimExpr> inversed_new_block_vars =
diff --git a/src/s_tir/schedule/primitive/loop_transformation.cc 
b/src/s_tir/schedule/primitive/loop_transformation.cc
index 63ed21ac1f..91b03fde7f 100644
--- a/src/s_tir/schedule/primitive/loop_transformation.cc
+++ b/src/s_tir/schedule/primitive/loop_transformation.cc
@@ -149,7 +149,7 @@ class IterMapSimplifyBlockBinding : public StmtExprMutator {
   bool preserve_unit_iters_;
 };
 
-class BlockPropertyError : public ScheduleError {
+class BlockPropertyError : public ScheduleErrorContextObj {
  public:
   /*!
    * \brief Check that all the blocks under the specific stmt have affine 
bindings
@@ -169,7 +169,7 @@ class BlockPropertyError : public ScheduleError {
       void VisitStmt_(const SBlockNode* op) final {
         for (const IterVar& iter_var : op->iter_vars) {
           if (iter_var->iter_type != kDataPar && iter_var->iter_type != 
kCommReduce) {
-            throw BlockPropertyError(state_->mod, ffi::GetRef<SBlock>(op));
+            throw MakeScheduleError<BlockPropertyError>(state_->mod, 
ffi::GetRef<SBlock>(op));
           }
           ffi::Optional<StmtSRef> high_exclusive = top_->parent
                                                        ? 
ffi::GetRef<StmtSRef>(top_->parent)
@@ -204,7 +204,7 @@ class BlockPropertyError : public ScheduleError {
   SBlock block_;
 };
 
-class HasAnnotationOrThreadBindingError : public ScheduleError {
+class HasAnnotationOrThreadBindingError : public ScheduleErrorContextObj {
  public:
   explicit HasAnnotationOrThreadBindingError(IRModule mod, For loop)
       : mod_(mod), loop_(std::move(loop)) {}
@@ -225,7 +225,7 @@ class HasAnnotationOrThreadBindingError : public 
ScheduleError {
   For loop_;
 };
 
-class OuterNotInnerParent : public ScheduleError {
+class OuterNotInnerParent : public ScheduleErrorContextObj {
  public:
   explicit OuterNotInnerParent(IRModule mod, For outer, For inner)
       : mod_(mod), outer_(std::move(outer)), inner_(std::move(inner)) {}
@@ -247,7 +247,7 @@ class OuterNotInnerParent : public ScheduleError {
   For inner_;
 };
 
-class NotOnlyChildError : public ScheduleError {
+class NotOnlyChildError : public ScheduleErrorContextObj {
  public:
   explicit NotOnlyChildError(IRModule mod, For outer, For inner)
       : mod_(mod), outer_(std::move(outer)), inner_(std::move(inner)) {}
@@ -269,7 +269,7 @@ class NotOnlyChildError : public ScheduleError {
   For inner_;
 };
 
-class NotSingleInferFactorError : public ScheduleError {
+class NotSingleInferFactorError : public ScheduleErrorContextObj {
  public:
   explicit NotSingleInferFactorError(IRModule mod) : mod_(mod) {}
 
@@ -287,7 +287,7 @@ class NotSingleInferFactorError : public ScheduleError {
   IRModule mod_;
 };
 
-class WrongFactorProductError : public ScheduleError {
+class WrongFactorProductError : public ScheduleErrorContextObj {
  public:
   explicit WrongFactorProductError(IRModule mod, For loop) : mod_(mod), 
loop_(std::move(loop)) {}
 
@@ -307,7 +307,7 @@ class WrongFactorProductError : public ScheduleError {
   For loop_;
 };
 
-class LoopMultiAppearanceError : public ScheduleError {
+class LoopMultiAppearanceError : public ScheduleErrorContextObj {
  public:
   explicit LoopMultiAppearanceError(IRModule mod, For loop) : mod_(mod), 
loop_(std::move(loop)) {}
 
@@ -326,7 +326,7 @@ class LoopMultiAppearanceError : public ScheduleError {
   For loop_;
 };
 
-class LoopsNotAChainError : public ScheduleError {
+class LoopsNotAChainError : public ScheduleErrorContextObj {
  public:
   enum class ProblemKind { kNotUnderAScope, kHaveNonSingleBranchStmt };
 
@@ -363,7 +363,7 @@ class LoopsNotAChainError : public ScheduleError {
   ProblemKind kind_;
 };
 
-class DependentLoopError : public ScheduleError {
+class DependentLoopError : public ScheduleErrorContextObj {
  public:
   enum class PrimitiveKind { kFuse, kReorder };
   explicit DependentLoopError(IRModule mod, For loop, ffi::String inner_var, 
PrimitiveKind kind)
@@ -406,7 +406,7 @@ ffi::Array<StmtSRef> Split(ScheduleState self, const 
StmtSRef& loop_sref,
   // Step 1. Check correctness
   const ForNode* loop = TVM_SREF_TO_FOR(loop_sref);
   if (!loop->annotations.empty() || loop->thread_binding.has_value()) {
-    throw HasAnnotationOrThreadBindingError(self->mod, ffi::GetRef<For>(loop));
+    throw MakeScheduleError<HasAnnotationOrThreadBindingError>(self->mod, 
ffi::GetRef<For>(loop));
   }
   // Currently, loops not starting with 0 are not supported
   arith::Analyzer analyzer;
@@ -673,7 +673,7 @@ ffi::Array<StmtSRef> LoopPartition(ScheduleState self, 
const StmtSRef& loop_sref
                                    const ffi::Array<PrimExpr>& factors, bool 
preserve_unit_iters) {
   const ForNode* loop = TVM_SREF_TO_FOR(loop_sref);
   if (!loop->annotations.empty() || loop->thread_binding.has_value()) {
-    throw HasAnnotationOrThreadBindingError(self->mod, ffi::GetRef<For>(loop));
+    throw MakeScheduleError<HasAnnotationOrThreadBindingError>(self->mod, 
ffi::GetRef<For>(loop));
   }
 
   arith::Analyzer analyzer;
@@ -867,7 +867,8 @@ StmtSRef Merge(ScheduleState self, const 
ffi::Array<StmtSRef>& loop_srefs) {
     for (auto p = sref.get(); p != lca.get(); p = p->parent) {
       if (auto loop = p->StmtAs<ForNode>()) {
         if (!loop->annotations.empty() || loop->thread_binding.has_value()) {
-          throw HasAnnotationOrThreadBindingError(self->mod, 
ffi::GetRef<For>(loop));
+          throw MakeScheduleError<HasAnnotationOrThreadBindingError>(self->mod,
+                                                                     
ffi::GetRef<For>(loop));
         }
         CheckLoopStartsWithZero(self, ffi::GetRef<StmtSRef>(p), 
analyzer.get());
         nest_loop_i_loops.push_back(ffi::GetRef<For>(loop));
@@ -878,7 +879,7 @@ StmtSRef Merge(ScheduleState self, const 
ffi::Array<StmtSRef>& loop_srefs) {
     const ForNode* outer_loop = nullptr;
     for (auto iter = nest_loop_i_loops.rbegin(); iter != 
nest_loop_i_loops.rend(); ++iter) {
       if (outer_loop && !outer_loop->body.same_as(*iter)) {
-        throw NotOnlyChildError(self->mod, ffi::GetRef<For>(outer_loop), 
*iter);
+        throw MakeScheduleError<NotOnlyChildError>(self->mod, 
ffi::GetRef<For>(outer_loop), *iter);
       }
       outer_loop = (*iter).get();
     }
@@ -932,14 +933,16 @@ StmtSRef Fuse(ScheduleState self, const 
ffi::Array<StmtSRef>& loop_srefs,
   for (const StmtSRef& sref : loop_srefs) {
     const ForNode* loop = TVM_SREF_TO_FOR(sref);
     if (!loop->annotations.empty() || loop->thread_binding.has_value()) {
-      throw HasAnnotationOrThreadBindingError(self->mod, 
ffi::GetRef<For>(loop));
+      throw MakeScheduleError<HasAnnotationOrThreadBindingError>(self->mod, 
ffi::GetRef<For>(loop));
     }
     if (outer_loop_sref.defined()) {
       if (sref->parent != outer_loop_sref.get()) {
-        throw OuterNotInnerParent(self->mod, ffi::GetRef<For>(outer_loop), 
ffi::GetRef<For>(loop));
+        throw MakeScheduleError<OuterNotInnerParent>(self->mod, 
ffi::GetRef<For>(outer_loop),
+                                                     ffi::GetRef<For>(loop));
       }
       if (!outer_loop->body.same_as(ffi::GetRef<For>(loop))) {
-        throw NotOnlyChildError(self->mod, ffi::GetRef<For>(outer_loop), 
ffi::GetRef<For>(loop));
+        throw MakeScheduleError<NotOnlyChildError>(self->mod, 
ffi::GetRef<For>(outer_loop),
+                                                   ffi::GetRef<For>(loop));
       }
     }
     outer_loop_sref = sref;
@@ -952,8 +955,8 @@ StmtSRef Fuse(ScheduleState self, const 
ffi::Array<StmtSRef>& loop_srefs,
     auto result = ffi::StructuralWalk<ffi::WalkOrder::kPreOrder>(loop->extent, 
walkfn);
     if (result.has_value()) {
       Var used_var = result.value()->value.cast<Var>();
-      throw DependentLoopError(self->mod, ffi::GetRef<For>(loop), 
used_var->name,
-                               DependentLoopError::PrimitiveKind::kFuse);
+      throw MakeScheduleError<DependentLoopError>(self->mod, 
ffi::GetRef<For>(loop), used_var->name,
+                                                  
DependentLoopError::PrimitiveKind::kFuse);
     }
     outer_loop_vars.insert(loop->loop_var.get());
     loops.push_back(loop);
@@ -1024,7 +1027,7 @@ std::unordered_set<const StmtSRefNode*> 
CollectLoopsIntoSet(
     auto inserted = loop_srefs.insert(loop_sref.get());
     if (!inserted.second) {
       const ForNode* loop = TVM_SREF_TO_FOR(loop_sref);
-      throw LoopMultiAppearanceError(self->mod, ffi::GetRef<For>(loop));
+      throw MakeScheduleError<LoopMultiAppearanceError>(self->mod, 
ffi::GetRef<For>(loop));
     }
   }
   return loop_srefs;
@@ -1052,8 +1055,8 @@ std::pair<const StmtSRefNode*, const StmtSRefNode*> 
GetBoundaryOfReorderRange(
       // Case 1. If `v` corresponds to a block, stop traversal.
       if (v->stmt->IsInstance<SBlockNode>()) {
         if (scope_block_visited) {
-          throw LoopsNotAChainError(self->mod, std::nullopt,
-                                    
LoopsNotAChainError::ProblemKind::kNotUnderAScope);
+          throw MakeScheduleError<LoopsNotAChainError>(
+              self->mod, std::nullopt, 
LoopsNotAChainError::ProblemKind::kNotUnderAScope);
         }
         scope_block_visited = true;
         break;
@@ -1062,8 +1065,9 @@ std::pair<const StmtSRefNode*, const StmtSRefNode*> 
GetBoundaryOfReorderRange(
       // `bottom`.
       if (visited.count(v)) {
         if (v != bottom) {
-          throw LoopsNotAChainError(self->mod, ffi::GetRef<Stmt>(v->stmt),
-                                    
LoopsNotAChainError::ProblemKind::kHaveNonSingleBranchStmt);
+          throw MakeScheduleError<LoopsNotAChainError>(
+              self->mod, ffi::GetRef<Stmt>(v->stmt),
+              LoopsNotAChainError::ProblemKind::kHaveNonSingleBranchStmt);
         }
         bottom = loop_sref;
         break;
@@ -1099,8 +1103,9 @@ std::vector<const StmtSRefNode*> 
GetLoopsInReorderRange(const ScheduleState& sel
     const ForNode* inner = loop_sref->StmtAs<ForNode>();
     TVM_FFI_ICHECK(outer != nullptr && inner != nullptr);
     if (outer->body.get() != inner) {
-      throw LoopsNotAChainError(self->mod, ffi::GetRef<For>(outer),
-                                
LoopsNotAChainError::ProblemKind::kHaveNonSingleBranchStmt);
+      throw MakeScheduleError<LoopsNotAChainError>(
+          self->mod, ffi::GetRef<For>(outer),
+          LoopsNotAChainError::ProblemKind::kHaveNonSingleBranchStmt);
     }
     chain.push_back(loop_sref);
     loop_sref = parent_loop_sref;
@@ -1151,8 +1156,8 @@ For ConstructNewLoopChain(const ScheduleState& self, 
std::vector<const StmtSRefN
     }
     if (result.has_value()) {
       Var used_var = result.value()->value.cast<Var>();
-      throw DependentLoopError(self->mod, ffi::GetRef<For>(copy), 
used_var->name,
-                               DependentLoopError::PrimitiveKind::kReorder);
+      throw MakeScheduleError<DependentLoopError>(self->mod, 
ffi::GetRef<For>(copy), used_var->name,
+                                                  
DependentLoopError::PrimitiveKind::kReorder);
     }
     inner_vars.insert(copy->loop_var.get());
     new_loop = For(std::move(n));
diff --git a/src/s_tir/schedule/primitive/pad_einsum.cc 
b/src/s_tir/schedule/primitive/pad_einsum.cc
index 6f08e80fb4..569f5fee8f 100644
--- a/src/s_tir/schedule/primitive/pad_einsum.cc
+++ b/src/s_tir/schedule/primitive/pad_einsum.cc
@@ -69,7 +69,7 @@ ffi::Optional<ffi::Array<Var>> CheckTrivialBufferAccess(const 
BufferRegion& buff
 }
 
 /*! \brief The schedule error class when the padding size is invalid. */
-class InvalidPaddingError : public ScheduleError {
+class InvalidPaddingError : public ScheduleErrorContextObj {
  public:
   InvalidPaddingError(IRModule mod, SBlock block, ffi::Array<int64_t> padding)
       : mod_(std::move(mod)), block_(std::move(block)), 
padding_(std::move(padding)) {}
@@ -87,11 +87,11 @@ class InvalidPaddingError : public ScheduleError {
 
   static void Check(const ScheduleState& self, const SBlock& block, 
ffi::Array<int64_t> padding) {
     if (padding.size() != block->iter_vars.size()) {
-      throw InvalidPaddingError(self->mod, block, padding);
+      throw MakeScheduleError<InvalidPaddingError>(self->mod, block, padding);
     }
     for (int64_t pad : padding) {
       if (pad <= 0) {
-        throw InvalidPaddingError(self->mod, block, padding);
+        throw MakeScheduleError<InvalidPaddingError>(self->mod, block, 
padding);
       }
     }
   }
@@ -103,7 +103,7 @@ class InvalidPaddingError : public ScheduleError {
 };
 
 /*! \brief The schedule error class when the block body is not an Einsum 
pattern. */
-class NonEinsumError : public ScheduleError {
+class NonEinsumError : public ScheduleErrorContextObj {
  public:
   explicit NonEinsumError(IRModule mod, SBlock block)
       : mod_(std::move(mod)), block_(std::move(block)) {}
@@ -224,34 +224,34 @@ Einsum ExtractEinsum(const ScheduleState& self, const 
SBlock& block) {
   for (int i = 0; i < n_reads; ++i) {
     const BufferVar& buffer = block->reads[i]->buffer;
     if (buffer_used.count(buffer.get()) != 0) {
-      throw NonEinsumError(self->mod, block);
+      throw MakeScheduleError<NonEinsumError>(self->mod, block);
     }
     buffer_used.insert(buffer.get());
     if (ffi::Optional<ffi::Array<Var>> opt_indices = 
CheckTrivialBufferAccess(block->reads[i])) {
       result.input_buffers.push_back(buffer);
       result.input_indices.Set(buffer, opt_indices.value());
     } else {
-      throw NonEinsumError(self->mod, block);
+      throw MakeScheduleError<NonEinsumError>(self->mod, block);
     }
   }
   int n_writes = block->writes.size();
   for (int i = 0; i < n_writes; ++i) {
     const BufferVar& buffer = block->writes[i]->buffer;
     if (buffer_used.count(buffer.get()) != 0) {
-      throw NonEinsumError(self->mod, block);
+      throw MakeScheduleError<NonEinsumError>(self->mod, block);
     }
     buffer_used.insert(buffer.get());
     if (ffi::Optional<ffi::Array<Var>> opt_indices = 
CheckTrivialBufferAccess(block->writes[i])) {
       result.output_buffers.push_back(buffer);
       result.output_indices.Set(buffer, opt_indices.value());
     } else {
-      throw NonEinsumError(self->mod, block);
+      throw MakeScheduleError<NonEinsumError>(self->mod, block);
     }
   }
   return result;
 }
 
-class BufferNotAllocatedInScopeError : public ScheduleError {
+class BufferNotAllocatedInScopeError : public ScheduleErrorContextObj {
  public:
   explicit BufferNotAllocatedInScopeError(IRModule mod, BufferVar buffer)
       : mod_(std::move(mod)), buffer_(std::move(buffer)) {}
@@ -277,7 +277,7 @@ class BufferNotAllocatedInScopeError : public ScheduleError 
{
 };
 
 /*! \brief The schedule error class when the producer block cannot be padded. 
*/
-class InvalidProducerError : public ScheduleError {
+class InvalidProducerError : public ScheduleErrorContextObj {
  public:
   explicit InvalidProducerError(IRModule mod, SBlock producer)
       : mod_(std::move(mod)), producer_(std::move(producer)) {}
diff --git a/src/s_tir/schedule/primitive/reduction.cc 
b/src/s_tir/schedule/primitive/reduction.cc
index f2d473fc9f..440810fa2e 100644
--- a/src/s_tir/schedule/primitive/reduction.cc
+++ b/src/s_tir/schedule/primitive/reduction.cc
@@ -110,7 +110,7 @@ class DecomposeReductionBlockReplacer : public StmtMutator {
   SBlock new_reduction_block_;
 };
 
-class LoopHeightError : public ScheduleError {
+class LoopHeightError : public ScheduleErrorContextObj {
  public:
   static void CheckLoopHigherThanReduceLoops(const IRModule& mod, const 
SBlockNode* block,
                                              const SBlockRealizeNode* realize,
@@ -136,7 +136,8 @@ class LoopHeightError : public ScheduleError {
         };
         if (ffi::StructuralWalk<ffi::WalkOrder::kPreOrder>(binding, 
walkfn).has_value()) {
           const ForNode* loop = TVM_SREF_TO_FOR(loop_sref);
-          throw LoopHeightError(mod, ffi::GetRef<For>(loop), 
ffi::GetRef<SBlock>(block));
+          throw MakeScheduleError<LoopHeightError>(mod, ffi::GetRef<For>(loop),
+                                                   ffi::GetRef<SBlock>(block));
         }
       }
     }
@@ -205,8 +206,8 @@ StmtSRef DecomposeReduction(ScheduleState self, const 
StmtSRef& block_sref,
   if (self->enable_check) {
     // Cond 0. Check loop_sref is an ancestor of block_sref
     if (std::find(loops.begin(), loops.end(), loop_sref) == loops.end()) {
-      throw LoopPositionError(self->mod, ffi::GetRef<For>(loop), 
ffi::GetRef<SBlock>(block),
-                              "decompose_reduction");
+      throw MakeScheduleError<LoopPositionError>(self->mod, 
ffi::GetRef<For>(loop),
+                                                 ffi::GetRef<SBlock>(block), 
"decompose_reduction");
     }
     // Cond 1. Check block is reduction
     CheckReductionBlock(self, block_sref, scope_root_sref);
@@ -553,7 +554,7 @@ GetReducerGetters() {
   return ReducerRegistry::Global()->reducer_getters;
 }
 
-class NotSerialLoopKindError : public ScheduleError {
+class NotSerialLoopKindError : public ScheduleErrorContextObj {
  public:
   explicit NotSerialLoopKindError(IRModule mod, For loop)
       : mod_(std::move(mod)), loop_(std::move(loop)) {}
@@ -578,7 +579,7 @@ class NotSerialLoopKindError : public ScheduleError {
   For loop_;
 };
 
-class FactorAxisOutOfRangeError : public ScheduleError {
+class FactorAxisOutOfRangeError : public ScheduleErrorContextObj {
  public:
   explicit FactorAxisOutOfRangeError(IRModule mod, BufferVar buffer, int 
factor_axis)
       : mod_(std::move(mod)), buffer_(std::move(buffer)), 
factor_axis_(factor_axis) {}
@@ -604,7 +605,7 @@ class FactorAxisOutOfRangeError : public ScheduleError {
   static int CheckAndUpdate(const IRModule& mod, const BufferVar& buffer, int 
factor_axis) {
     int ndim = static_cast<int>(buffer->shape.size());
     if (factor_axis < -(ndim + 1) || factor_axis > ndim) {
-      throw FactorAxisOutOfRangeError(mod, buffer, factor_axis);
+      throw MakeScheduleError<FactorAxisOutOfRangeError>(mod, buffer, 
factor_axis);
     }
     // If factor_axis is negative, convert it to a non-negative one.
     if (factor_axis < 0) {
@@ -618,7 +619,7 @@ class FactorAxisOutOfRangeError : public ScheduleError {
   int factor_axis_;
 };
 
-class LoopPropertyError : public ScheduleError {
+class LoopPropertyError : public ScheduleErrorContextObj {
  public:
   enum ErrorType {
     kDataParIterTouchRFactorLoop = 0,
@@ -678,7 +679,8 @@ class LoopPropertyError : public ScheduleError {
     ffi::Array<SBlockRealize> children_of_outermost_loop =
         GetChildBlockRealizeOnSRefTree(self->stmt2ref.at(loops[0].get()));
     if (!children_of_outermost_loop[0]->block.same_as(block)) {
-      throw LoopPropertyError(self->mod, loops[0], 
kNotFirstChildBlockOfOutermostLoop);
+      throw MakeScheduleError<LoopPropertyError>(self->mod, loops[0],
+                                                 
kNotFirstChildBlockOfOutermostLoop);
     }
 
     bool meet_reduction_loop = false;
@@ -687,10 +689,11 @@ class LoopPropertyError : public ScheduleError {
       bool reduction_touched = reduce_loop_vars.count(loop->loop_var.get());
 
       if (data_par_touched && reduction_touched) {
-        throw LoopPropertyError(self->mod, loop, 
kLoopTouchedByBothKindsOfBlockIters);
+        throw MakeScheduleError<LoopPropertyError>(self->mod, loop,
+                                                   
kLoopTouchedByBothKindsOfBlockIters);
       } else if (data_par_touched) {
         if (loop.get() == rf_loop) {
-          throw LoopPropertyError(self->mod, loop, 
kDataParIterTouchRFactorLoop);
+          throw MakeScheduleError<LoopPropertyError>(self->mod, loop, 
kDataParIterTouchRFactorLoop);
         }
         continue;
       } else if (reduction_touched) {
@@ -700,7 +703,7 @@ class LoopPropertyError : public ScheduleError {
         }
         continue;
       } else if (meet_reduction_loop && !is_one(loop->extent)) {
-        throw LoopPropertyError(self->mod, loop, 
kUnboundLoopUnderReductionLoop);
+        throw MakeScheduleError<LoopPropertyError>(self->mod, loop, 
kUnboundLoopUnderReductionLoop);
       }
     }
   }
@@ -1368,7 +1371,7 @@ StmtSRef RFactor(ScheduleState self, const StmtSRef& 
rf_loop_sref, int factor_ax
   }
   const ForNode* rf_loop = TVM_SREF_TO_FOR(rf_loop_sref);
   if (rf_loop->kind != ForKind::kSerial) {
-    throw NotSerialLoopKindError(self->mod, ffi::GetRef<For>(rf_loop));
+    throw MakeScheduleError<NotSerialLoopKindError>(self->mod, 
ffi::GetRef<For>(rf_loop));
   }
 
   // Step 2. Collect loop vars that are touched by data parallel block iters 
and reduction block
diff --git a/src/s_tir/schedule/primitive/reorder_block_iter_var.cc 
b/src/s_tir/schedule/primitive/reorder_block_iter_var.cc
index 6f6994c5ae..9dd28f0586 100644
--- a/src/s_tir/schedule/primitive/reorder_block_iter_var.cc
+++ b/src/s_tir/schedule/primitive/reorder_block_iter_var.cc
@@ -31,7 +31,7 @@ using namespace tvm::tirx;
  * \brief The reorder index is not a valid permutation of
  *   [0, 1, ..., n-1] where n is the number of block iter vars.
  */
-class InvalidReorderIndex : public ScheduleError {
+class InvalidReorderIndex : public ScheduleErrorContextObj {
  public:
   explicit InvalidReorderIndex(IRModule mod, SBlock block, ffi::Array<int64_t> 
new_order)
       : mod_(mod), block_(block), new_order_(new_order) {}
@@ -101,7 +101,8 @@ void ReorderBlockIterVar(ScheduleState self, const 
StmtSRef& block_sref,
     return x >= 0 && x < static_cast<int>(num_block_itervars);
   });
   if (!is_full || !is_unique || !is_within_boundary) {
-    throw InvalidReorderIndex(self->mod, ffi::GetRef<SBlock>(block_n), 
new_order);
+    throw MakeScheduleError<InvalidReorderIndex>(self->mod, 
ffi::GetRef<SBlock>(block_n),
+                                                 new_order);
   }
 
   // find parent block
diff --git a/src/s_tir/schedule/primitive/rolling_buffer.cc 
b/src/s_tir/schedule/primitive/rolling_buffer.cc
index f831ee10e6..d2ab7ec186 100644
--- a/src/s_tir/schedule/primitive/rolling_buffer.cc
+++ b/src/s_tir/schedule/primitive/rolling_buffer.cc
@@ -66,7 +66,7 @@ BufferRegion GetRelaxedBufferRegion(const SBlockRealize& 
realize, const BufferRe
   return BufferRegion(buffer_region->buffer, relaxed_region);
 }
 
-class RollingBufferDependencyError : public ScheduleError {
+class RollingBufferDependencyError : public ScheduleErrorContextObj {
  public:
   explicit RollingBufferDependencyError(IRModule mod, SBlock block)
       : mod_(mod), block_(std::move(block)) {}
@@ -95,13 +95,15 @@ class RollingBufferDependencyError : public ScheduleError {
     for (const Dependency& producers : scope->GetDepsByDst(block_sref)) {
       if (!(producers->kind == DepKind::kRAW)) {
         const SBlockNode* block = TVM_SREF_TO_SBLOCK(block_sref);
-        throw RollingBufferDependencyError(self->mod, 
ffi::GetRef<SBlock>(block));
+        throw MakeScheduleError<RollingBufferDependencyError>(self->mod,
+                                                              
ffi::GetRef<SBlock>(block));
       }
     }
     for (const Dependency& consumers : scope->GetDepsBySrc(block_sref)) {
       if (!(consumers->kind == DepKind::kRAW)) {
         const SBlockNode* block = TVM_SREF_TO_SBLOCK(block_sref);
-        throw RollingBufferDependencyError(self->mod, 
ffi::GetRef<SBlock>(block));
+        throw MakeScheduleError<RollingBufferDependencyError>(self->mod,
+                                                              
ffi::GetRef<SBlock>(block));
       }
     }
   }
@@ -111,7 +113,7 @@ class RollingBufferDependencyError : public ScheduleError {
   SBlock block_;
 };
 
-class RollingBufferMatchError : public ScheduleError {
+class RollingBufferMatchError : public ScheduleErrorContextObj {
  public:
   RollingBufferMatchError(IRModule mod, SBlock block, BufferRegion 
buffer_region)
       : mod_(mod), block_(block), buffer_region_(buffer_region) {}
@@ -137,7 +139,7 @@ class RollingBufferMatchError : public ScheduleError {
   BufferRegion buffer_region_;
 };
 
-class RollingBufferInsertionError : public ScheduleError {
+class RollingBufferInsertionError : public ScheduleErrorContextObj {
  public:
   RollingBufferInsertionError(IRModule mod, BufferVar buffer, SBlock block)
       : mod_(mod), buffer_(std::move(buffer)), block_(block) {}
@@ -170,7 +172,8 @@ class RollingBufferInfoCollector {
     RollingBufferInfoCollector collector;
     if (!collector.MatchRollingBuffer(block_sref, buffer_region)) {
       const SBlockNode* block = TVM_SREF_TO_SBLOCK(block_sref);
-      throw RollingBufferMatchError(mod, ffi::GetRef<SBlock>(block), 
buffer_region);
+      throw MakeScheduleError<RollingBufferMatchError>(mod, 
ffi::GetRef<SBlock>(block),
+                                                       buffer_region);
     }
     return collector.info_;
   }
@@ -438,7 +441,7 @@ void RollingBuffer(ScheduleState self, const StmtSRef& 
block_sref, int write_buf
   consumers_sref.push_back(block_sref);
   StmtSRef lca = GetSRefLowestCommonAncestor(consumers_sref);
   if (!lca->StmtAs<ForNode>()) {
-    throw RollingBufferInsertionError(self->mod, buffer_region->buffer, block);
+    throw MakeScheduleError<RollingBufferInsertionError>(self->mod, 
buffer_region->buffer, block);
   }
 
   for (auto it = loop_srefs.rbegin(); it != loop_srefs.rend(); ++it) {
diff --git a/src/s_tir/schedule/transform.cc b/src/s_tir/schedule/transform.cc
index 424a0128bd..4e7d6042d4 100644
--- a/src/s_tir/schedule/transform.cc
+++ b/src/s_tir/schedule/transform.cc
@@ -237,7 +237,7 @@ Stmt ReplaceBufferMutator::VisitStmt_(const SBlockNode* 
block) {
 
 void LeafBlockRemovalPlan(const ScheduleState& self, const StmtSRef& 
leaf_block_sref,
                           Stmt* src_stmt, Stmt* tgt_stmt) {
-  class OnlyLeafError : public ScheduleError {
+  class OnlyLeafError : public ScheduleErrorContextObj {
    public:
     explicit OnlyLeafError(IRModule mod, SBlock leaf_block, SBlock scope_root)
         : mod_(mod), leaf_block_(leaf_block), scope_root_(scope_root) {}
@@ -300,7 +300,8 @@ void LeafBlockRemovalPlan(const ScheduleState& self, const 
StmtSRef& leaf_block_
   TVM_FFI_ICHECK(sref != nullptr && sref->stmt != nullptr);
   const auto* leaf_block = TVM_SREF_TO_SBLOCK(leaf_block_sref);
   const auto* scope_block = TVM_SREF_TO_SBLOCK(sref);
-  throw OnlyLeafError(self->mod, ffi::GetRef<SBlock>(leaf_block), 
ffi::GetRef<SBlock>(scope_block));
+  throw MakeScheduleError<OnlyLeafError>(self->mod, 
ffi::GetRef<SBlock>(leaf_block),
+                                         ffi::GetRef<SBlock>(scope_block));
 }
 
 ffi::Optional<LoopRV> TileWithTensorIntrin(const s_tir::Schedule& sch,

Reply via email to