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


The following commit(s) were added to refs/heads/main by this push:
     new 043ace5f1 [REFACTOR] Require sequence bodies and mark function 
declarations (#153)
043ace5f1 is described below

commit 043ace5f1045097ff122606df80e022db62ef15d
Author: Tianqi Chen <[email protected]>
AuthorDate: Sun Nov 1 13:25:05 2026 -0500

    [REFACTOR] Require sequence bodies and mark function declarations (#153)
---
 include/tvm/ir/function.h                          |  9 +++++++-
 include/tvm/script/ir_builder/frame.h              |  4 ++--
 include/tvm/script/ir_builder/ir.h                 |  2 +-
 include/tvm/tirx/function.h                        | 14 +++++++------
 include/tvm/tirx/script/ir_builder/frame.h         |  2 +-
 include/tvm/tirx/stmt_functor.h                    |  2 +-
 python/tvm_tirx/tirx/function.py                   | 17 +++++++--------
 rust/src/tirx/function.rs                          | 12 +++++------
 rust/tests/tirx/function.rs                        | 12 +++++++----
 rust/tests/tirx/main.rs                            |  2 +-
 src/backend/cuda/codegen/codegen_cuda.cc           |  6 +++---
 src/backend/cuda/codegen/codegen_cuda_host.cc      |  4 ++--
 src/backend/cuda/transform/bind_backend_config.cc  | 14 ++++++-------
 src/backend/cuda/transform/lower_iket.cc           | 16 +++++++--------
 src/backend/metal/codegen/codegen_metal.cc         |  4 ++--
 src/backend/opencl/codegen/codegen_opencl.cc       |  4 ++--
 src/backend/trn/codegen/codegen_trn.cc             |  4 ++--
 src/backend/trn/transform/lower_trainium_layout.cc |  4 ++--
 src/backend/vulkan/codegen/codegen_spirv.cc        |  4 ++--
 src/backend/webgpu/codegen/codegen_webgpu.cc       |  6 +++---
 src/codegen/build_common.h                         |  1 +
 src/codegen/c/codegen_c.cc                         |  7 +++++--
 src/codegen/c/codegen_c_host.cc                    |  4 ++++
 src/codegen/llvm/codegen_aarch64.cc                |  4 ++--
 src/codegen/llvm/codegen_cpu.cc                    |  4 ++++
 src/codegen/llvm/codegen_llvm.cc                   |  7 +++++--
 src/codegen/llvm/llvm_module.cc                    |  1 +
 src/script/ir_builder/frame.cc                     |  4 +++-
 src/script/ir_builder/ir.cc                        |  8 +++++++-
 src/tirx/analysis/check_well_formed.cc             |  5 +++++
 src/tirx/ir/function.cc                            | 24 +++++++++-------------
 src/tirx/script/ir_builder/frame.cc                | 21 ++++++++-----------
 src/tirx/script/printer/function.cc                |  5 ++---
 src/tirx/tile/lower.cc                             |  6 +++---
 src/tirx/transform/common_subexpr_elim.cc          |  6 +++---
 src/tirx/transform/decorate_device_scope.cc        |  4 ++--
 src/tirx/transform/flatten_tensor.cc               |  8 ++++----
 src/tirx/transform/hoist_if.cc                     |  6 +++---
 src/tirx/transform/lower_intrin.cc                 |  2 +-
 src/tirx/transform/lower_tensor_layouts.cc         |  4 ++--
 src/tirx/transform/lower_tvm_builtin.cc            |  4 ++--
 src/tirx/transform/make_packed_api.cc              | 12 +++++------
 src/tirx/transform/narrow_datatype.cc              |  4 ++--
 src/tirx/transform/remove_no_op.cc                 |  7 +++----
 src/tirx/transform/renormalize_split_pattern.cc    |  2 +-
 src/tirx/transform/split_host_device.cc            |  2 +-
 src/tirx/transform/stmt_simplify.cc                |  4 ++--
 src/tirx/transform/storage_rewrite.cc              |  9 ++++----
 src/tirx/transform/unroll_loop.cc                  |  4 ++--
 tests/cpp/tirx/thread_scope_test.cc                |  4 +---
 tests/python/tirx/ir/test_function.py              |  4 ++--
 tests/python/tirx/test_constructor.py              |  6 +++---
 52 files changed, 181 insertions(+), 154 deletions(-)

diff --git a/include/tvm/ir/function.h b/include/tvm/ir/function.h
index a6d01fb11..cf4e763bb 100644
--- a/include/tvm/ir/function.h
+++ b/include/tvm/ir/function.h
@@ -209,9 +209,16 @@ class BaseFunc : public Expr {
 /*!
  * \brief Generic attribute names that can be attached to any function.
  *
- * \sa tvm::tirx::attr, tvm::relax::attr
+ * \sa tvm::tirx::attr
  */
 namespace attr {
+/*!
+ * \brief Whether this function is a declaration.
+ *
+ * Type: bool
+ */
+inline constexpr ffi::reflection::AttrKey<bool> kDeclaration{"declaration"};
+
 /*!
  * \brief Indicates the special calling convention.
  *
diff --git a/include/tvm/script/ir_builder/frame.h 
b/include/tvm/script/ir_builder/frame.h
index cb078bf18..e0d00ad47 100644
--- a/include/tvm/script/ir_builder/frame.h
+++ b/include/tvm/script/ir_builder/frame.h
@@ -47,8 +47,8 @@ class IRModuleFrameObj : public IRBuilderFrameObj {
   /*! \brief A map from string names to global variables that ensures global 
uniqueness. */
   ffi::Map<ffi::String, GlobalVar> global_var_map;
   /*!
-   * \brief A map from GlobalVar to all global functions.
-   * \note Only defined functions are in the map, while declared functions are 
not included.
+   * \brief A map from GlobalVar to global functions and pending declarations.
+   * \note Declarations carry attr::kDeclaration until replaced by a 
definition.
    */
   ffi::Map<GlobalVar, BaseFunc> functions;
   /*! \brief IRModule's attributes. */
diff --git a/include/tvm/script/ir_builder/ir.h 
b/include/tvm/script/ir_builder/ir.h
index 924c82d89..90d464c3c 100644
--- a/include/tvm/script/ir_builder/ir.h
+++ b/include/tvm/script/ir_builder/ir.h
@@ -46,7 +46,7 @@ TVM_TIRX_DLL IRModuleFrame IRModule();
  * \brief Declare a Function without given the specific function 
implementation.
  * \note It is usually used in cross-function call. And we can specify the 
function by `DefFunction`
  * \param func_name The function unique name.
- * \param func_signature A Function w/o body, which used to specify the 
function signature
+ * \param func_signature A Function marked with attr::kDeclaration, specifying 
the signature
  *                       (i.e. func params and func return type/shape).
  * \return The corresponding GlobalVar.
  */
diff --git a/include/tvm/tirx/function.h b/include/tvm/tirx/function.h
index 8fd9de61c..224b1798f 100644
--- a/include/tvm/tirx/function.h
+++ b/include/tvm/tirx/function.h
@@ -53,8 +53,10 @@ class FunctionObj : public BaseFuncObj {
   ffi::Array<tvm::Var> params;
   /*! \brief The return type of the function. */
   Type ret_type = Type::Missing();
-  /*! \brief The body of the function, absent for a declaration. */
-  ffi::Optional<SeqStmt> body;
+  /*! \brief The body of the function, empty for a marked declaration. */
+  SeqStmt body;
+
+  explicit FunctionObj(ffi::UnsafeInit tag) : body(tag) {}
 
   static void RegisterReflection() {
     namespace refl = tvm::ffi::reflection;
@@ -108,7 +110,8 @@ class Function : public BaseFunc {
    *
    * \param params The parameters of the function.
    *
-   * \param body The body of the function, or std::nullopt for a declaration.
+   * \param body The body of the function. Declarations use an empty sequence 
and
+   * tvm::attr::kDeclaration.
    *
    * \param ret_type The return type of the function.
    *
@@ -116,9 +119,8 @@ class Function : public BaseFunc {
    *
    * \param loc The location of this object in the source code.
    */
-  TVM_TIRX_DLL Function(ffi::Array<tvm::Var> params, ffi::Optional<SeqStmt> 
body,
-                        Type ret_type = VoidType(), DictAttrs attrs = 
DictAttrs(),
-                        Location loc = UnknownLoc());
+  TVM_TIRX_DLL Function(ffi::Array<tvm::Var> params, SeqStmt body, Type 
ret_type = VoidType(),
+                        DictAttrs attrs = DictAttrs(), Location loc = 
UnknownLoc());
 
   explicit Function(ffi::ObjectPtr<FunctionObj> node) : 
BaseFunc(std::move(node)) {}
 
diff --git a/include/tvm/tirx/script/ir_builder/frame.h 
b/include/tvm/tirx/script/ir_builder/frame.h
index 01427e4eb..2f7af4994 100644
--- a/include/tvm/tirx/script/ir_builder/frame.h
+++ b/include/tvm/tirx/script/ir_builder/frame.h
@@ -55,7 +55,7 @@ class TVM_TIRX_DLL FunctionFrameObj : public ir::StmtFrameObj 
{
   ffi::Map<ffi::String, Any> attrs;
   /*! \brief Whether it is a persistent kernel. */
   bool persistent;
-  /*! \brief Whether this frame declares a bodyless signature. */
+  /*! \brief Whether this frame collects a signature for later definition. */
   bool is_declaration{false};
   /*! \brief Finalized function and its module identity. */
   ffi::Optional<tvm::tirx::Function> function;
diff --git a/include/tvm/tirx/stmt_functor.h b/include/tvm/tirx/stmt_functor.h
index 54279a832..60e97354a 100644
--- a/include/tvm/tirx/stmt_functor.h
+++ b/include/tvm/tirx/stmt_functor.h
@@ -258,7 +258,7 @@ class TVM_TIRX_DLL StmtExprMutator : public 
tvm::ExprMutator {
     return ffi::Optional<SeqStmt>(std::move(result).ValueUnchecked());
   }
 
-  /*! \brief Mutate an optional statement, preserving an absent function body. 
*/
+  /*! \brief Mutate an optional statement, preserving absence. */
   TVM_FFI_INLINE UnchangedOr<ffi::Optional<Stmt>> Mutate(
       const ffi::Optional<Stmt>& stmt, InplaceMode inplace_mode = 
InplaceMode::kDisallow) {
     if (!stmt.has_value()) return ffi::Unchanged();
diff --git a/python/tvm_tirx/tirx/function.py b/python/tvm_tirx/tirx/function.py
index d3c6fae0c..071fc90d3 100644
--- a/python/tvm_tirx/tirx/function.py
+++ b/python/tvm_tirx/tirx/function.py
@@ -34,14 +34,15 @@ class Function(BaseFunc, Scriptable):
     """A TIRx function definition or declaration.
 
     The function's ``ty`` is a FuncType derived from parameter types and
-    ``ret_type``. A present body is normalized to a SeqStmt.
+    ``ret_type``. The body is normalized to a SeqStmt.
+    A declaration has an empty body and the ``declaration`` attribute set to 
True.
 
     Parameters
     ----------
     params : list[tvm_tirx.ir.Var]
         Input parameters, in calling order.
-    body : Stmt or Sequence[Stmt] or None
-        The function body, or None for a declaration without an implementation.
+    body : Stmt or Sequence[Stmt]
+        The function body. An empty sequence is a valid body.
     ret_type : tvm_tirx.ir.Type, optional
         The declared return type. Omitting it or supplying MissingType produces
         an empty TupleType, the type returned by the C++ VoidType() helper.
@@ -52,12 +53,12 @@ class Function(BaseFunc, Scriptable):
         The source location of the function.
     """
 
-    body: SeqStmt | None
+    body: SeqStmt
 
     def __init__(
         self,
         params,
-        body: Stmt | Sequence[Stmt] | None,
+        body: Stmt | Sequence[Stmt],
         ret_type=None,
         attrs=None,
         loc: Location = UNKNOWN_LOC,
@@ -83,13 +84,13 @@ class Function(BaseFunc, Scriptable):
             loc,
         )  # type: ignore
 
-    def with_body(self, new_body: Stmt | Sequence[Stmt] | None, loc: Location 
= UNKNOWN_LOC):
+    def with_body(self, new_body: Stmt | Sequence[Stmt], loc: Location = 
UNKNOWN_LOC):
         """Create a function with the same parameters, return type, and 
attributes but a new body.
 
         Parameters
         ----------
-        new_body : Stmt | Sequence[Stmt] | None
-            The new body, or None for a declaration.
+        new_body : Stmt | Sequence[Stmt]
+            The new body.
 
         loc : Location, optional
             The source location of the new function. Defaults to UNKNOWN_LOC;
diff --git a/rust/src/tirx/function.rs b/rust/src/tirx/function.rs
index 289e4628b..e74fcf037 100644
--- a/rust/src/tirx/function.rs
+++ b/rust/src/tirx/function.rs
@@ -21,7 +21,7 @@
 //! Tensor functions.
 //!
 //! The objects `python/tvm_tirx/tirx/function.py` defines. A [`Function`] is a
-//! [`BaseFunc`] with parameters, a return type and an optional body, built 
with [`Function::new`];
+//! [`BaseFunc`] with parameters, a return type and a required sequence body, 
built with [`Function::new`];
 //! [`renew_def`] gives its variables fresh definitions.
 
 // `custom-new` reserves `new` for a constructor with the C++ one's semantics 
(`ty` derived from the
@@ -67,7 +67,7 @@ pub struct FunctionObj {
     base: BaseFuncObj,
     pub params: Array<Var>,
     pub ret_type: Type,
-    pub body: Option<SeqStmt>,
+    pub body: SeqStmt,
 }
 
 const _: () = {
@@ -102,7 +102,7 @@ impl FunctionObj {
         attrs: DictAttrs,
         params: Array<Var>,
         ret_type: Type,
-        body: Option<SeqStmt>,
+        body: SeqStmt,
     ) -> Self {
         let base = BaseFuncObj::new(loc, ty, attrs);
         Self { base, params, ret_type, body }
@@ -117,7 +117,7 @@ impl Function {
         attrs: impl Into<DictAttrs>,
         params: impl Into<Array<Var>>,
         ret_type: impl Into<Type>,
-        body: impl Into<Option<SeqStmt>>,
+        body: impl Into<SeqStmt>,
     ) -> Self {
         let obj = FunctionObj::new(
             loc.into(),
@@ -140,7 +140,7 @@ impl Function {
     /// (C++ `Function(params, body, ret_type, attrs, loc)`).
     pub fn new(
         params: impl IntoIterator<Item = impl Into<Var>>,
-        body: Option<&SeqStmt>,
+        body: &SeqStmt,
         ret_type: impl Into<Type>,
         attrs: impl Into<DictAttrs>,
         loc: impl Into<Location>,
@@ -153,7 +153,7 @@ impl Function {
         };
         let param_types = params.iter().map(|param| 
param.ty.clone()).collect::<Vec<Type>>();
         let ty = FuncType::new(param_types, ret_type.clone());
-        Self::from_fields(loc.into(), ty, attrs.into(), params, ret_type, 
body.cloned())
+        Self::from_fields(loc.into(), ty, attrs.into(), params, ret_type, 
body.clone())
     }
 }
 
diff --git a/rust/tests/tirx/function.rs b/rust/tests/tirx/function.rs
index 882e353cd..794f3eb2d 100644
--- a/rust/tests/tirx/function.rs
+++ b/rust/tests/tirx/function.rs
@@ -52,9 +52,13 @@ fn functions_agree_with_cpp() {
         [(FfiString::from("note"), Any::from(1i64))].into_iter().collect();
     let attrs = DictAttrs::new(note);
     for (what, body, ret_type) in [
-        ("Function", Some(body.clone()), &int32),
-        ("Function(void)", Some(body.clone()), &missing),
-        ("Function(no body)", None, &int32),
+        ("Function", body.clone(), &int32),
+        ("Function(void)", body.clone(), &missing),
+        (
+            "Function(empty body)",
+            SeqStmt::new(Vec::<tvm_tirx::ir::Stmt>::new(), UNKNOWN_LOC),
+            &int32,
+        ),
     ] {
         let native_function = native(
             "tirx.Function",
@@ -68,7 +72,7 @@ fn functions_agree_with_cpp() {
         );
         agree(
             what,
-            Ok(Function::new(params.iter(), body.as_ref(), ret_type, &attrs, 
UNKNOWN_LOC)),
+            Ok(Function::new(params.iter(), &body, ret_type, &attrs, 
UNKNOWN_LOC)),
             native_function,
         );
     }
diff --git a/rust/tests/tirx/main.rs b/rust/tests/tirx/main.rs
index 7f2b95b3e..7a58fe011 100644
--- a/rust/tests/tirx/main.rs
+++ b/rust/tests/tirx/main.rs
@@ -98,7 +98,7 @@ fn void_function(params: Vec<Var>) -> Function {
         .try_into()
         .unwrap();
     let missing: Type = 
global("ir.MissingType").call_tuple(()).unwrap().try_into().unwrap();
-    Function::new(params, Some(&body), missing, DictAttrs::new(Map::new()), 
UNKNOWN_LOC)
+    Function::new(params, &body, missing, DictAttrs::new(Map::new()), 
UNKNOWN_LOC)
 }
 
 fn module_of(name: &str, func: &Function) -> IRModule {
diff --git a/src/backend/cuda/codegen/codegen_cuda.cc 
b/src/backend/cuda/codegen/codegen_cuda.cc
index 6d4f6b228..8cb871814 100644
--- a/src/backend/cuda/codegen/codegen_cuda.cc
+++ b/src/backend/cuda/codegen/codegen_cuda.cc
@@ -307,7 +307,7 @@ void CodeGenCUDA::InitFuncState(const Function& func) {
 void CodeGenCUDA::DeclareFunction(const GlobalVar& gvar, const Function& func) 
{
   if (!RegisterFunctionName(gvar, func)) return;
   // Definitions supply their body-derived qualifiers in AddFunction.
-  if (!func->body.has_value()) {
+  if (func->GetAttr(tvm::attr::kDeclaration).value_or(false)) {
     InitFuncState(func);
     PrintFunctionSignature(GetFunctionName(gvar), func, fwd_decl_stream);
     fwd_decl_stream << ";\n";
@@ -316,7 +316,7 @@ void CodeGenCUDA::DeclareFunction(const GlobalVar& gvar, 
const Function& func) {
 
 void CodeGenCUDA::AddFunction(const GlobalVar& gvar, const Function& func) {
   DeclareFunction(gvar, func);
-  if (!func->body.has_value()) return;
+  if (func->GetAttr(tvm::attr::kDeclaration).value_or(false)) return;
   InitFuncState(func);
   std::ostringstream parameters;
   PrintFunctionParameters(func, parameters);
@@ -327,7 +327,7 @@ void CodeGenCUDA::AddFunction(const GlobalVar& gvar, const 
Function& func) {
   stream << " {\n";
   PreFunctionBody(func);
   int func_scope = BeginScope();
-  PrintStmt(func->body.value());
+  PrintStmt(func->body);
   EndScope(func_scope);
   PrintIndent();
   stream << "}\n\n";
diff --git a/src/backend/cuda/codegen/codegen_cuda_host.cc 
b/src/backend/cuda/codegen/codegen_cuda_host.cc
index ba19c9018..32e06edc0 100644
--- a/src/backend/cuda/codegen/codegen_cuda_host.cc
+++ b/src/backend/cuda/codegen/codegen_cuda_host.cc
@@ -138,7 +138,7 @@ class CodeGenCUDAHost : public CodeGenCHost {
   }
 
   void AddFunction(const GlobalVar& gvar, const Function& func) override {
-    if (!func->body.has_value()) {
+    if (func->GetAttr(tvm::attr::kDeclaration).value_or(false)) {
       DeclareFunction(gvar, func);
       return;
     }
@@ -148,7 +148,7 @@ class CodeGenCUDAHost : public CodeGenCHost {
     PrintFunctionSignature(GetFunctionName(gvar), func, stream);
     stream << " {\n  TVM_FFI_SAFE_CALL_BEGIN();\n  try {\n";
     int scope = BeginScope();
-    PrintStmt(func->body.value());
+    PrintStmt(func->body);
     EndScope(scope);
     // CUDADeviceGuard reports errors by throwing. Clear the reported CUDA
     // last-error state so a subsequent successful launch is not blamed for it.
diff --git a/src/backend/cuda/transform/bind_backend_config.cc 
b/src/backend/cuda/transform/bind_backend_config.cc
index 9d87b3ba2..13223a4ce 100644
--- a/src/backend/cuda/transform/bind_backend_config.cc
+++ b/src/backend/cuda/transform/bind_backend_config.cc
@@ -50,9 +50,8 @@ class BackendConfigBinder : public StmtExprMutator {
             .cast<ffi::Array<ffi::Any>>();
     target_ = resolved[0].cast<Target>();
     defaults_ = resolved[1].cast<ffi::String>();
-    if (!func->body.has_value()) return func;
-    auto body =
-        Mutate(func->body.value(), 
InplaceMode::kDisallow).ValueOrUnchanged(func->body.value());
+    if (func->GetAttr(tvm::attr::kDeclaration).value_or(false)) return func;
+    auto body = Mutate(func->body, 
InplaceMode::kDisallow).ValueOrUnchanged(func->body);
     func.CopyOnWrite()->body = body;
     return WithAttrs(func, {{tvm::attr::kTarget.name, target_}, 
{"backend_config", defaults_}});
   }
@@ -102,9 +101,8 @@ class DeviceHelperSpecializer : public StmtExprMutator {
         active_(active) {}
 
   Function Rewrite(Function func) {
-    if (!func->body.has_value()) return func;
-    auto body =
-        Mutate(func->body.value(), 
InplaceMode::kDisallow).ValueOrUnchanged(func->body.value());
+    if (func->GetAttr(tvm::attr::kDeclaration).value_or(false)) return func;
+    auto body = Mutate(func->body, 
InplaceMode::kDisallow).ValueOrUnchanged(func->body);
     func.CopyOnWrite()->body = body;
     return func;
   }
@@ -219,9 +217,9 @@ Pass SpecializeEntryHelpers() {
       pending.pop_back();
       if (!reachable.insert(gv.get()).second) continue;
       auto func = result->Lookup(gv).as<Function>();
-      if (!func || !func.value()->body.has_value()) continue;
+      if (!func || 
func.value()->GetAttr(tvm::attr::kDeclaration).value_or(false)) continue;
       auto collector = ffi::make_object<HelperCallCollector>();
-      collector->Visit(func.value()->body.value());
+      collector->Visit(func.value()->body);
       for (auto callee : collector->calls) {
         if (result->functions.count(callee)) pending.push_back(callee);
       }
diff --git a/src/backend/cuda/transform/lower_iket.cc 
b/src/backend/cuda/transform/lower_iket.cc
index dae1cdadb..ec8792501 100644
--- a/src/backend/cuda/transform/lower_iket.cc
+++ b/src/backend/cuda/transform/lower_iket.cc
@@ -1199,24 +1199,24 @@ IRModule LowerIketImpl(IRModule module) {
   if (!IketEnabled(module)) {
     for (const auto& [global_var, base_function] : module->functions) {
       const auto* function_node = base_function.as<FunctionObj>();
-      if (!function_node || !function_node->body.has_value()) continue;
+      if (!function_node || 
function_node->GetAttr(tvm::attr::kDeclaration).value_or(false))
+        continue;
       Function function = ffi::GetRef<Function>(function_node);
       auto collector = ffi::make_object<AnnotationCollector>();
       collector->Visit(function->body);
-      TokenTensorSet tokens = CollectTokenTensors(function->body.value());
+      TokenTensorSet tokens = CollectTokenTensors(function->body);
       if (!collector->has_annotations && tokens.empty()) continue;
       if (collector->has_annotations) {
         auto verifier = ffi::make_object<TokenVerifier>(tokens);
         verifier->Visit(function->body);
-        TokenDeclarationMap token_declarations = 
CollectTokenDeclarations(function->body.value());
+        TokenDeclarationMap token_declarations = 
CollectTokenDeclarations(function->body);
         auto schema_verifier =
             ffi::make_object<RangeEndSchemaVerifier>(token_declarations, 
&collector->declarations);
         schema_verifier->Visit(function->body);
         ValidateRangeSchemas(collector->declarations);
       }
       auto strip = ffi::make_object<StripIket>(std::move(tokens));
-      Stmt stripped =
-          
strip->Mutate(function->body.value()).ValueOrUnchanged(function->body.value());
+      Stmt stripped = 
strip->Mutate(function->body).ValueOrUnchanged(function->body);
       Stmt body =
           
ffi::make_object<RemoveStrippedIketNoOps>()->Mutate(stripped).ValueOrUnchanged(stripped);
       if (!function->body.same_as(body)) {
@@ -1230,7 +1230,7 @@ IRModule LowerIketImpl(IRModule module) {
   std::vector<KernelIketInfo> kernels;
   for (const auto& [global_var, base_function] : module->functions) {
     const auto* function_node = base_function.as<FunctionObj>();
-    if (!function_node || !function_node->body.has_value()) continue;
+    if (!function_node || 
function_node->GetAttr(tvm::attr::kDeclaration).value_or(false)) continue;
     Function function = ffi::GetRef<Function>(function_node);
     auto collector = ffi::make_object<AnnotationCollector>();
     collector->Visit(function->body);
@@ -1240,10 +1240,10 @@ IRModule LowerIketImpl(IRModule module) {
     TVM_FFI_CHECK(IsCudaDeviceFunction(function), ValueError)
         << "IKET annotations are only valid in a split CUDA device kernel";
 
-    TokenTensorSet tokens = CollectTokenTensors(function->body.value());
+    TokenTensorSet tokens = CollectTokenTensors(function->body);
     auto verifier = ffi::make_object<TokenVerifier>(tokens);
     verifier->Visit(function->body);
-    TokenDeclarationMap token_declarations = 
CollectTokenDeclarations(function->body.value());
+    TokenDeclarationMap token_declarations = 
CollectTokenDeclarations(function->body);
     auto schema_verifier =
         ffi::make_object<RangeEndSchemaVerifier>(token_declarations, 
&collector->declarations);
     schema_verifier->Visit(function->body);
diff --git a/src/backend/metal/codegen/codegen_metal.cc 
b/src/backend/metal/codegen/codegen_metal.cc
index 62e5ae2a9..130de7f7c 100644
--- a/src/backend/metal/codegen/codegen_metal.cc
+++ b/src/backend/metal/codegen/codegen_metal.cc
@@ -91,7 +91,7 @@ CodeGenMetal::CodeGenMetal(Target target) : target_(target) {
 }
 
 void CodeGenMetal::AddFunction(const GlobalVar& gvar, const Function& func) {
-  TVM_FFI_CHECK(func->body.has_value(), ValueError)
+  TVM_FFI_CHECK(!func->GetAttr(tvm::attr::kDeclaration).value_or(false), 
ValueError)
       << "Kernel code generation requires a function body";
   // NOTE: There is no inter-function calls among Metal kernels.
   // For now we keep the metal codegen without inter-function call
@@ -208,7 +208,7 @@ void CodeGenMetal::AddFunction(const GlobalVar& gvar, const 
Function& func) {
   // the function scope.
   stream << ") {\n";
   int func_scope = this->BeginScope();
-  this->PrintStmt(func->body.value());
+  this->PrintStmt(func->body);
   this->EndScope(func_scope);
   this->PrintIndent();
   this->stream << "}\n\n";
diff --git a/src/backend/opencl/codegen/codegen_opencl.cc 
b/src/backend/opencl/codegen/codegen_opencl.cc
index 9b3a577bd..fd249318e 100644
--- a/src/backend/opencl/codegen/codegen_opencl.cc
+++ b/src/backend/opencl/codegen/codegen_opencl.cc
@@ -132,8 +132,8 @@ CodeGenOpenCL::CodeGenOpenCL() {
 void CodeGenOpenCL::InitFuncState(const Function& f) {
   CodeGenC::InitFuncState(f);
   texture_access_qualifiers_.clear();
-  if (f->body.has_value()) {
-    
this->SetTextureScope(ffi::make_object<InferTextureAccess>()->Infer(f->body.value()));
+  if (!f->GetAttr(tvm::attr::kDeclaration).value_or(false)) {
+    
this->SetTextureScope(ffi::make_object<InferTextureAccess>()->Infer(f->body));
   }
   for (Var arg : f->params) {
     auto ptr_type = arg->ty.as<PtrTypeObj>();
diff --git a/src/backend/trn/codegen/codegen_trn.cc 
b/src/backend/trn/codegen/codegen_trn.cc
index f0613bcc6..f14ace278 100644
--- a/src/backend/trn/codegen/codegen_trn.cc
+++ b/src/backend/trn/codegen/codegen_trn.cc
@@ -81,7 +81,7 @@ CodeGenTrainium::CodeGenTrainium(Target target) : 
target_(target) {
 }
 
 void CodeGenTrainium::AddFunction(const GlobalVar& gvar, const Function& func) 
{
-  TVM_FFI_CHECK(func->body.has_value(), ValueError)
+  TVM_FFI_CHECK(!func->GetAttr(tvm::attr::kDeclaration).value_or(false), 
ValueError)
       << "Kernel code generation requires a function body";
   // NOTE: There is no inter-function calls among Trainium kernels.
   // For now we keep the Trainium codegen without inter-function call
@@ -141,7 +141,7 @@ void CodeGenTrainium::AddFunction(const GlobalVar& gvar, 
const Function& func) {
   // the function scope.
   stream << "):\n";
   int func_scope = this->BeginScope();
-  this->PrintStmt(func->body.value());
+  this->PrintStmt(func->body);
   this->PrintIndent();
   stream << "return ";
   for (size_t i = 0; i < output_vids.size(); i++) {
diff --git a/src/backend/trn/transform/lower_trainium_layout.cc 
b/src/backend/trn/transform/lower_trainium_layout.cc
index 1f1bcd664..44db1f01d 100644
--- a/src/backend/trn/transform/lower_trainium_layout.cc
+++ b/src/backend/trn/transform/lower_trainium_layout.cc
@@ -355,9 +355,9 @@ namespace transform {
 
 Pass LowerTrainiumLayout() {
   auto pass_func = [](Function f, IRModule m, PassContext ctx) {
-    if (!f->body.has_value()) return f;
+    if (f->GetAttr(tvm::attr::kDeclaration).value_or(false)) return f;
     auto* n = f.CopyOnWrite();
-    auto [body, params] = TrainiumLayoutApplier::Lower(n->body.value(), 
n->params);
+    auto [body, params] = TrainiumLayoutApplier::Lower(n->body, n->params);
     n->body = std::move(body);
     n->params = std::move(params);
     return f;
diff --git a/src/backend/vulkan/codegen/codegen_spirv.cc 
b/src/backend/vulkan/codegen/codegen_spirv.cc
index 89d85c986..8e0fba03b 100644
--- a/src/backend/vulkan/codegen/codegen_spirv.cc
+++ b/src/backend/vulkan/codegen/codegen_spirv.cc
@@ -114,7 +114,7 @@ const VarObj* AsBufferVarObj(const Expr& expr) {
 CodeGenSPIRV::CodeGenSPIRV(Target target) : spirv_support_(target) {}
 
 runtime::SPIRVShader CodeGenSPIRV::BuildFunction(const Function& f, const 
std::string& name) {
-  TVM_FFI_CHECK(f->body.has_value(), ValueError)
+  TVM_FFI_CHECK(!f->GetAttr(tvm::attr::kDeclaration).value_or(false), 
ValueError)
       << "Kernel code generation requires a function body";
   this->InitFuncState();
   TVM_FFI_ICHECK(f->HasNonzeroAttr(tvm::tirx::attr::kNoAlias.name))
@@ -178,7 +178,7 @@ runtime::SPIRVShader CodeGenSPIRV::BuildFunction(const 
Function& f, const std::s
       }
     }
   }
-  this->Dispatch(f->body.value());
+  this->Dispatch(f->body);
   builder_->SetLocalSize(func_ptr, workgroup_size_);
   builder_->MakeInst(spv::OpReturn);
   builder_->MakeInst(spv::OpFunctionEnd);
diff --git a/src/backend/webgpu/codegen/codegen_webgpu.cc 
b/src/backend/webgpu/codegen/codegen_webgpu.cc
index 44069f57e..306e0f483 100644
--- a/src/backend/webgpu/codegen/codegen_webgpu.cc
+++ b/src/backend/webgpu/codegen/codegen_webgpu.cc
@@ -201,7 +201,7 @@ CodeGenWebGPU::CodeGenWebGPU(Target target) : 
target_(target) {
 }
 
 runtime::FunctionInfo CodeGenWebGPU::AddFunction(const Function& f, bool 
skip_readonly_decl) {
-  TVM_FFI_CHECK(f->body.has_value(), ValueError)
+  TVM_FFI_CHECK(!f->GetAttr(tvm::attr::kDeclaration).value_or(false), 
ValueError)
       << "Kernel code generation requires a function body";
   // clear previous generated state.
   this->InitFuncState(f);
@@ -241,7 +241,7 @@ runtime::FunctionInfo CodeGenWebGPU::AddFunction(const 
Function& f, bool skip_re
   // runtime classifies storage-buffer arguments by the serialized "handle" 
dtype.
   constexpr DLDataType kRuntimeOpaqueHandleType{kDLOpaqueHandle, 64, 1};
 
-  WebGPUWorkGroupInfo info = 
WebGPUWorkgroupInfoCollector::Collect(f->body.value());
+  WebGPUWorkGroupInfo info = WebGPUWorkgroupInfoCollector::Collect(f->body);
 
   std::vector<Var> pod_args;
   int num_buffer = 0;
@@ -354,7 +354,7 @@ runtime::FunctionInfo CodeGenWebGPU::AddFunction(const 
Function& f, bool skip_re
                << val_pod_args << "." << packGridDimX << ") { return; }\n";
   // the function scope.
   int func_scope = this->BeginScope();
-  this->PrintStmt(f->body.value());
+  this->PrintStmt(f->body);
   this->EndScope(func_scope);
   this->PrintIndent();
   this->stream << "}\n\n";
diff --git a/src/codegen/build_common.h b/src/codegen/build_common.h
index 36d555ac1..d039be492 100644
--- a/src/codegen/build_common.h
+++ b/src/codegen/build_common.h
@@ -50,6 +50,7 @@ inline ffi::Map<ffi::String, runtime::FunctionInfo> 
ExtractFuncInfo(const IRModu
     TVM_FFI_ICHECK(kv.second->IsInstance<tirx::FunctionObj>())
         << "Can only lower IR Module with Functions";
     auto f = kv.second.as_or_throw<tirx::Function>();
+    if (f->GetAttr(tvm::attr::kDeclaration).value_or(false)) continue;
 
     ffi::Array<DLDataType> arg_types;
     ffi::Array<runtime::ArgExtraTags> arg_extra_tags;
diff --git a/src/codegen/c/codegen_c.cc b/src/codegen/c/codegen_c.cc
index bbe009df6..668a8dbd6 100644
--- a/src/codegen/c/codegen_c.cc
+++ b/src/codegen/c/codegen_c.cc
@@ -153,6 +153,9 @@ void CodeGenC::PrintFunctionParameters(const Function& 
func, std::ostream& os) {
 }
 
 bool CodeGenC::RegisterFunctionName(const GlobalVar& gvar, const Function& 
func) {
+  TVM_FFI_CHECK(!func->GetAttr(tvm::attr::kDeclaration).value_or(false) || 
func->body->seq.empty(),
+                ValueError)
+      << "Function declaration " << gvar->name_hint << " must have an empty 
body";
   if (internal_functions_.count(gvar)) return false;
 
   auto function_name = [&]() -> ffi::String {
@@ -194,7 +197,7 @@ void CodeGenC::AddFunction(const GlobalVar& gvar, const 
Function& f) {
   // If the function has already been forward-declared, this is a
   // no-op.
   DeclareFunction(gvar, f);
-  if (!f->body.has_value()) return;
+  if (f->GetAttr(tvm::attr::kDeclaration).value_or(false)) return;
   auto function_name = GetFunctionName(gvar);
 
   // clear previous generated state.
@@ -204,7 +207,7 @@ void CodeGenC::AddFunction(const GlobalVar& gvar, const 
Function& f) {
   stream << " {\n";
   this->PreFunctionBody(f);
   int func_scope = this->BeginScope();
-  this->PrintStmt(f->body.value());
+  this->PrintStmt(f->body);
   this->EndScope(func_scope);
   this->PrintIndent();
   this->stream << "}\n\n";
diff --git a/src/codegen/c/codegen_c_host.cc b/src/codegen/c/codegen_c_host.cc
index 55a917f0a..6a88ef3f8 100644
--- a/src/codegen/c/codegen_c_host.cc
+++ b/src/codegen/c/codegen_c_host.cc
@@ -74,6 +74,10 @@ void CodeGenCHost::AddFunction(const GlobalVar& gvar, const 
Function& func) {
 
 void CodeGenCHost::AddFunction(const GlobalVar& gvar, const Function& func,
                                bool emit_fwd_func_decl) {
+  if (func->GetAttr(tvm::attr::kDeclaration).value_or(false)) {
+    DeclareFunction(gvar, func);
+    return;
+  }
   auto global_symbol = func->GetAttr(tvm::attr::kGlobalSymbol);
   if (global_symbol) {
     function_names_.push_back(global_symbol.value());
diff --git a/src/codegen/llvm/codegen_aarch64.cc 
b/src/codegen/llvm/codegen_aarch64.cc
index c3fb8037d..1d9519048 100644
--- a/src/codegen/llvm/codegen_aarch64.cc
+++ b/src/codegen/llvm/codegen_aarch64.cc
@@ -84,8 +84,8 @@ void 
CodeGenAArch64::SetComputeScopeAttributes(llvm::Function* func) {
 void CodeGenAArch64::SetPStateAttributes(llvm::Function* func, const Function& 
f) {
   // These string Function attrs are exposed directly through T.func_attr and 
with_attr.
   if (auto sm = 
f->GetAttr<ffi::String>(tvm::codegen::aarch64::attr::kPStateSM)) {
-    // A locally streaming body does not change a bodyless declaration's 
interface.
-    if (sm.value() != "body" || f->body.has_value()) {
+    // A locally streaming body does not change a declaration's interface.
+    if (sm.value() != "body" || 
!f->GetAttr(tvm::attr::kDeclaration).value_or(false)) {
       func->addFnAttr(MakeStringRef("aarch64_pstate_sm_" + sm.value()));
     }
   }
diff --git a/src/codegen/llvm/codegen_cpu.cc b/src/codegen/llvm/codegen_cpu.cc
index 2f00189f1..84e5e12c7 100644
--- a/src/codegen/llvm/codegen_cpu.cc
+++ b/src/codegen/llvm/codegen_cpu.cc
@@ -232,6 +232,10 @@ llvm::DISubprogram* CodeGenCPU::CreateDebugFunction(const 
GlobalVar& gvar, const
 }
 
 void CodeGenCPU::AddFunction(const GlobalVar& gvar, const Function& func) {
+  if (func->GetAttr(tvm::attr::kDeclaration).value_or(false)) {
+    DeclareFunction(gvar, func);
+    return;
+  }
   di_subprogram_ = CreateDebugFunction(gvar, func);
   EmitDebugLocation(func->loc);
   CodeGenLLVM::AddFunction(gvar, func);
diff --git a/src/codegen/llvm/codegen_llvm.cc b/src/codegen/llvm/codegen_llvm.cc
index 301ca6fa6..1b579bbcf 100644
--- a/src/codegen/llvm/codegen_llvm.cc
+++ b/src/codegen/llvm/codegen_llvm.cc
@@ -294,6 +294,9 @@ std::tuple<std::string, llvm::Function::LinkageTypes> 
CodeGenLLVM::GetLinkage(
 }
 
 llvm::Function* CodeGenLLVM::DeclareFunctionInternal(const GlobalVar& gvar, 
const Function& func) {
+  TVM_FFI_CHECK(!func->GetAttr(tvm::attr::kDeclaration).value_or(false) || 
func->body->seq.empty(),
+                ValueError)
+      << "Function declaration " << gvar->name_hint << " must have an empty 
body";
   if (auto it = functions_.find(gvar.get()); it != functions_.end()) {
     return it->second;
   }
@@ -349,7 +352,7 @@ void CodeGenLLVM::AddFunctionInternal(const GlobalVar& 
gvar, const Function& f)
   this->InitFuncState();
 
   function_ = DeclareFunctionInternal(gvar, f);
-  if (!f->body.has_value()) return;
+  if (f->GetAttr(tvm::attr::kDeclaration).value_or(false)) return;
 
   // set var map and align information
   auto arg_it = function_->arg_begin();
@@ -368,7 +371,7 @@ void CodeGenLLVM::AddFunctionInternal(const GlobalVar& 
gvar, const Function& f)
   llvm::LLVMContext* ctx = llvm_target_->GetContext();
   llvm::BasicBlock* entry = llvm::BasicBlock::Create(*ctx, "entry", function_);
   builder_->SetInsertPoint(entry);
-  this->Dispatch(f->body.value());
+  this->Dispatch(f->body);
 
   // Body-local alignment assumptions need not hold on paths that returned
   // before reaching them. Keep them as llvm.assume instead of strengthening
diff --git a/src/codegen/llvm/llvm_module.cc b/src/codegen/llvm/llvm_module.cc
index d76fa3763..bbdc613c5 100644
--- a/src/codegen/llvm/llvm_module.cc
+++ b/src/codegen/llvm/llvm_module.cc
@@ -263,6 +263,7 @@ void LLVMModuleObj::Init(const IRModule& mod, const Target& 
target) {
       continue;
     }
     auto f = kv.second.as_or_throw<tirx::Function>();
+    if (f->GetAttr(tvm::attr::kDeclaration).value_or(false)) continue;
     auto global_symbol = f->GetAttr(tvm::attr::kGlobalSymbol);
     bool is_entry_func = f->HasNonzeroAttr(tvm::tirx::attr::kIsEntryFunc.name);
 
diff --git a/src/script/ir_builder/frame.cc b/src/script/ir_builder/frame.cc
index 935be5c66..1cd5c7bd6 100644
--- a/src/script/ir_builder/frame.cc
+++ b/src/script/ir_builder/frame.cc
@@ -103,7 +103,9 @@ void IRModuleFrameObj::ExitWithScope() {
   for (const auto& kv : functions) {
     const GlobalVar& gv = kv.first;
     const BaseFunc& func = kv.second;
-    TVM_FFI_CHECK(func.defined(), ValueError) << "function " << gv->name_hint 
<< " is not defined";
+    TVM_FFI_CHECK(func.defined() && 
!func->GetAttr(tvm::attr::kDeclaration).value_or(false),
+                  ValueError)
+        << "function " << gv->name_hint << " is declared but not defined";
     func_map.Set(gv, func);
   }
   IRBuilder builder = IRBuilder::Current();
diff --git a/src/script/ir_builder/ir.cc b/src/script/ir_builder/ir.cc
index e3c50d320..d332944d7 100644
--- a/src/script/ir_builder/ir.cc
+++ b/src/script/ir_builder/ir.cc
@@ -98,12 +98,18 @@ void DefFunction(const ffi::String& func_name, const 
BaseFunc& func) {
   TVM_FFI_CHECK(it != frame->global_var_map.end(), ValueError)
       << "function " << func_name << " does not exist, please declare it 
first.";
   const GlobalVar& gv = (*it).second;
-  frame->functions.Set(gv, func);
+  TVM_FFI_CHECK(!func->GetAttr(tvm::attr::kDeclaration).value_or(false), 
ValueError)
+      << "function " << func_name << " requires a definition";
+  auto previous = frame->functions.Get(gv);
+  TVM_FFI_CHECK(!previous || 
previous.value()->GetAttr(tvm::attr::kDeclaration).value_or(false),
+                ValueError)
+      << "function " << func_name << " has already been defined";
   if (auto ty = GetGlobalVarType(func)) {
     gv->ty = ty.value();
   } else {
     TVM_FFI_THROW(InternalError) << "Unsupported function type: " << 
func->GetTypeKey();
   }
+  frame->functions.Set(gv, func);
 }
 
 void ModuleAttrs(ffi::Map<ffi::String, Any> attrs, bool allow_overwrite) {
diff --git a/src/tirx/analysis/check_well_formed.cc 
b/src/tirx/analysis/check_well_formed.cc
index faf0bbbbf..7261e9b62 100644
--- a/src/tirx/analysis/check_well_formed.cc
+++ b/src/tirx/analysis/check_well_formed.cc
@@ -290,6 +290,11 @@ class WellFormednessVerifier : public StmtExprVisitor {
   }
 
   Result Visit_(const FunctionObj* op) {
+    if (op->GetAttr(tvm::attr::kDeclaration).value_or(false)) {
+      Verify(false) << "ValueError: Function at <current location> is a 
declaration, "
+                       "but a definition is required.";
+      return std::nullopt;
+    }
     int enclosing_depth = loop_depth_;
     loop_depth_ = 0;
     WithScope([&]() -> Result {
diff --git a/src/tirx/ir/function.cc b/src/tirx/ir/function.cc
index eb346a7e8..513481591 100644
--- a/src/tirx/ir/function.cc
+++ b/src/tirx/ir/function.cc
@@ -54,7 +54,7 @@ TVM_FFI_INLINE ffi::Expected<ffi::UnchangedOr<ffi::Any>> 
FunctionMutate(
                                     }));
   TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Type>, mapped_ret_type,
                                     mutator->MutateExpected(self->ret_type));
-  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Optional<Stmt>>, 
mapped_body,
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Stmt>, mapped_body,
                                     mutator->MutateExpected(self->body));
   if (mapped_params.UnchangedOrSameAs(self->params) &&
       mapped_ret_type.UnchangedOrSameAs(self->ret_type) &&
@@ -66,9 +66,7 @@ TVM_FFI_INLINE ffi::Expected<ffi::UnchangedOr<ffi::Any>> 
FunctionMutate(
   copy->ret_type = 
std::move(mapped_ret_type).ValueOrUnchanged(std::move(copy->ret_type));
   if (!mapped_body.IsUnchanged()) {
     auto replacement = std::move(mapped_body).ValueUnchecked();
-    copy->body = replacement.has_value()
-                     ? 
ffi::Optional<SeqStmt>(SeqStmt(std::move(replacement).value()))
-                     : std::nullopt;
+    copy->body = SeqStmt(std::move(replacement));
   }
   copy->ty = copy->func_type_annotation();
   return ffi::Any(std::move(copy));
@@ -87,7 +85,7 @@ TVM_FFI_INLINE ffi::Expected<ffi::UnchangedOr<ffi::Any>> 
FunctionMaybeInplaceMut
   TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
       ffi::UnchangedOr<Type>, mapped_ret_type,
       mutator->MutateExpected(self->ret_type, ffi::InplaceMode::kAllow));
-  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Optional<Stmt>>, 
mapped_body,
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Stmt>, mapped_body,
                                     mutator->MutateExpected(self->body, 
ffi::InplaceMode::kAllow));
   if (!mapped_params.UnchangedOrSameAs(self->params)) {
     self->params = std::move(mapped_params).ValueUnchecked();
@@ -97,9 +95,7 @@ TVM_FFI_INLINE ffi::Expected<ffi::UnchangedOr<ffi::Any>> 
FunctionMaybeInplaceMut
   }
   if (!(mapped_body.IsUnchanged() || 
ffi::AnyView(mapped_body).same_as(self->body))) {
     auto replacement = std::move(mapped_body).ValueUnchecked();
-    self->body = replacement.has_value()
-                     ? 
ffi::Optional<SeqStmt>(SeqStmt(std::move(replacement).value()))
-                     : std::nullopt;
+    self->body = SeqStmt(std::move(replacement));
   }
   // A nested signature node may change in place without replacing params or 
ret_type.
   self->ty = self->func_type_annotation();
@@ -109,14 +105,15 @@ TVM_FFI_INLINE ffi::Expected<ffi::UnchangedOr<ffi::Any>> 
FunctionMaybeInplaceMut
 }  // namespace
 
 // Get the function type of a Function
-Function::Function(ffi::Array<tvm::Var> params, ffi::Optional<SeqStmt> body, 
Type ret_type,
-                   DictAttrs attrs, Location loc)
+Function::Function(ffi::Array<tvm::Var> params, SeqStmt body, Type ret_type, 
DictAttrs attrs,
+                   Location loc)
     : BaseFunc(ffi::UnsafeInit{}) {
+  TVM_FFI_ICHECK(body.defined());
   if (ret_type.as<MissingType>().has_value()) {
     ret_type = VoidType();
   }
 
-  auto n = ffi::make_object<FunctionObj>();
+  auto n = ffi::make_object<FunctionObj>(ffi::UnsafeInit{});
   n->params = std::move(params);
   n->body = std::move(body);
   n->ret_type = std::move(ret_type);
@@ -161,9 +158,8 @@ TVM_FFI_STATIC_INIT_BLOCK() {
             ffi::FStructuralMutate::FromNative<&FunctionMaybeInplaceMutate>());
 
   refl::GlobalDef().def(
-      "tirx.Function",
-      [](ffi::Array<tvm::Var> params, ffi::Optional<SeqStmt> body, Type 
ret_type, DictAttrs attrs,
-         Location loc) { return Function(params, body, ret_type, attrs, loc); 
});
+      "tirx.Function", [](ffi::Array<tvm::Var> params, SeqStmt body, Type 
ret_type, DictAttrs attrs,
+                          Location loc) { return Function(params, body, 
ret_type, attrs, loc); });
   refl::GlobalDef().def("tirx.RenewDef", RenewDef);
 }
 
diff --git a/src/tirx/script/ir_builder/frame.cc 
b/src/tirx/script/ir_builder/frame.cc
index 46362e910..31d83fd9d 100644
--- a/src/tirx/script/ir_builder/frame.cc
+++ b/src/tirx/script/ir_builder/frame.cc
@@ -88,13 +88,14 @@ void FunctionFrameObj::ExitWithScope() {
   }
   TVM_FFI_CHECK(!is_declaration || stmts.empty(), ValueError)
       << "A function declaration cannot contain body statements";
-  ffi::Optional<tvm::SeqStmt> body = std::nullopt;
-  if (!is_declaration) body = AsStmt(stmts);
+  // The marker belongs to this signature, not the frame reused for its body.
+  auto function_attrs = attrs;
+  if (is_declaration) function_attrs.Set(tvm::attr::kDeclaration.name, true);
   tvm::tirx::Function func(
       /*params=*/args,
-      /*body=*/body,
+      /*body=*/AsStmt(stmts),
       /*ret_type=*/ret_type.value_or(TupleType::Empty()),
-      /*attrs=*/attrs.defined() ? DictAttrs(attrs) : DictAttrs(),
+      /*attrs=*/DictAttrs(function_attrs),
       /*loc=*/loc);
   func = FinalizeFunction(std::move(func));
   function = func;
@@ -109,18 +110,12 @@ void FunctionFrameObj::ExitWithScope() {
            "function scope, if it's defined in a Module";
     const ir::IRModuleFrame& frame = opt_frame.value();
     const ffi::String& func_name = name.value_or("");
-    if (!frame->global_var_map.count(func_name) ||
-        !frame->functions.count(frame->global_var_map.at(func_name))) {
-      // Case. First time visiting the function.
+    if (!frame->global_var_map.count(func_name)) {
+      // A complete function can also be registered on its first visit.
       global_var = ir::DeclFunction(func_name, func);
-    }
-    // Define the function.
-    // Note we do checks to disallow redefinition of functions inside the 
`DefFunction`.
-    if (!global_var.has_value()) {
+    } else {
       TVM_FFI_CHECK(!is_declaration, ValueError) << "function " << func_name 
<< " already exists";
       global_var = frame->global_var_map.at(func_name);
-    }
-    if (!is_declaration) {
       ir::DefFunction(func_name, func);
     }
   } else {
diff --git a/src/tirx/script/printer/function.cc 
b/src/tirx/script/printer/function.cc
index 96271e0fe..3f7bf5b3c 100644
--- a/src/tirx/script/printer/function.cc
+++ b/src/tirx/script/printer/function.cc
@@ -94,9 +94,8 @@ void PrintFunction(DocTranslatorObj* d, const 
tirx::FunctionObj* func, ExprDoc d
         }
         if (!decorator_keys.empty())
           decorator = decorator->Call({}, decorator_keys, decorator_values);
-        ffi::Array<StmtDoc> body;
-        if (func->body.has_value())
-          body = ToStmtDocArray(d->WithDocScope([&]() { 
d->Translate(func->body.value()); }));
+        ffi::Array<StmtDoc> body =
+            ToStmtDocArray(d->WithDocScope([&]() { d->Translate(func->body); 
}));
         std::vector<std::pair<ffi::String, ffi::Any>> attrs;
         for (const auto& [key, value] : func->attrs->dict) {
           if (key != tvm::attr::kGlobalSymbol.name &&
diff --git a/src/tirx/tile/lower.cc b/src/tirx/tile/lower.cc
index e6504b709..fa0b4c00a 100644
--- a/src/tirx/tile/lower.cc
+++ b/src/tirx/tile/lower.cc
@@ -632,11 +632,11 @@ namespace transform {
 
 Pass TileDispatch() {
   auto pass_func = [](Function f, IRModule m, PassContext ctx) {
-    if (!f->body.has_value()) return f;
+    if (f->GetAttr(tvm::attr::kDeclaration).value_or(false)) return f;
     Target target = ResolveTarget(f);
     auto* n = f.CopyOnWrite();
-    n->body = TileDispatcher::LowerOpCalls(n->body.value(), target);
-    if (!NoOpCallVerifier::Verify(n->body.value())) {
+    n->body = TileDispatcher::LowerOpCalls(n->body, target);
+    if (!NoOpCallVerifier::Verify(n->body)) {
       LOG(FATAL) << "Failed to lower the TIRx program: " << f;
     }
     return f;
diff --git a/src/tirx/transform/common_subexpr_elim.cc 
b/src/tirx/transform/common_subexpr_elim.cc
index 850080b23..bc3e02114 100644
--- a/src/tirx/transform/common_subexpr_elim.cc
+++ b/src/tirx/transform/common_subexpr_elim.cc
@@ -882,12 +882,12 @@ namespace transform {
  */
 Pass CommonSubexprElim() {
   auto pass_func = [](Function f, IRModule m, PassContext ctx) {
-    if (!f->body.has_value()) return f;
-    auto [insert_before, expr_remap] = CSEPlanner::Plan(f->body.value());
+    if (f->GetAttr(tvm::attr::kDeclaration).value_or(false)) return f;
+    auto [insert_before, expr_remap] = CSEPlanner::Plan(f->body);
     if (!insert_before.empty()) {
       auto* n = f.CopyOnWrite();
       n->body = ffi::make_object<CSERewriter>(std::move(insert_before), 
std::move(expr_remap))
-                    ->Rewrite(f->body.value());
+                    ->Rewrite(f->body);
     }
     return f;
   };
diff --git a/src/tirx/transform/decorate_device_scope.cc 
b/src/tirx/transform/decorate_device_scope.cc
index 2cbad33c0..5d6164907 100644
--- a/src/tirx/transform/decorate_device_scope.cc
+++ b/src/tirx/transform/decorate_device_scope.cc
@@ -33,10 +33,10 @@ namespace transform {
 
 Pass DecorateDeviceScope() {
   auto pass_func = [](Function f, IRModule m, PassContext ctx) {
-    if (!f->body.has_value()) return f;
+    if (f->GetAttr(tvm::attr::kDeclaration).value_or(false)) return f;
     static const Op device_scope = Op::Get("tirx.device_scope");
     auto* n = f.CopyOnWrite();
-    n->body = RegionStmt(device_scope, {}, {}, DictAttrs(), 
std::move(n->body).value());
+    n->body = RegionStmt(device_scope, {}, {}, DictAttrs(), 
std::move(n->body));
     return f;
   };
   return CreateFunctionPass(pass_func, 0, "tirx.DecorateDeviceScope");
diff --git a/src/tirx/transform/flatten_tensor.cc 
b/src/tirx/transform/flatten_tensor.cc
index ddb761dcb..0b4e58573 100644
--- a/src/tirx/transform/flatten_tensor.cc
+++ b/src/tirx/transform/flatten_tensor.cc
@@ -65,7 +65,7 @@ class TensorFlattener : public 
StmtExprMutatorWithConstraintContext {
   using StmtExprMutatorWithConstraintContext::Mutate;
   using StmtExprMutatorWithConstraintContext::Mutate_;
   static Function Flatten(Function func) {
-    if (!func->body.has_value()) return func;
+    if (func->GetAttr(tvm::attr::kDeclaration).value_or(false)) return func;
     auto pass = ffi::make_object<TensorFlattener>(sym::ConstraintContext());
     pass->AddParamConstraints(func);
     for (const Var& param : func->params) {
@@ -74,8 +74,8 @@ class TensorFlattener : public 
StmtExprMutatorWithConstraintContext {
         pass->Define(tensor.value());
       }
     }
-    auto body_result = pass->Mutate(func->body, InplaceMode::kDisallow)
-                           .as_or_throw<UnchangedOr<ffi::Optional<SeqStmt>>>();
+    auto body_result =
+        pass->Mutate(func->body, 
InplaceMode::kDisallow).as_or_throw<UnchangedOr<SeqStmt>>();
     bool body_unchanged = body_result.UnchangedOrSameAs(func->body);
     auto body = std::move(body_result).ValueOrUnchanged(func->body);
 
@@ -89,7 +89,7 @@ class TensorFlattener : public 
StmtExprMutatorWithConstraintContext {
           if (!old_buf.value().same_as(new_buf)) {
             body = SeqStmt({Bind(new_buf, Call(new_buf.ty(), decl_tensor_op(),
                                                {old_buf.value().data()}, {}, 
{new_buf.ty()})),
-                            std::move(body).value()});
+                            std::move(body)});
             body_unchanged = false;
           }
         }
diff --git a/src/tirx/transform/hoist_if.cc b/src/tirx/transform/hoist_if.cc
index 94be8ae59..7dcd9c418 100644
--- a/src/tirx/transform/hoist_if.cc
+++ b/src/tirx/transform/hoist_if.cc
@@ -240,11 +240,11 @@ namespace transform {
 
 Pass HoistIf() {
   auto pass_func = [](Function f, IRModule m, PassContext ctx) {
-    if (!f->body.has_value()) return f;
+    if (f->GetAttr(tvm::attr::kDeclaration).value_or(false)) return f;
     auto* n = f.CopyOnWrite();
     Stmt body = ffi::make_object<IfHoister>()
-                    ->Mutate(n->body.value(), InplaceMode::kAllow)
-                    .ValueOrUnchanged(n->body.value());
+                    ->Mutate(n->body, InplaceMode::kAllow)
+                    .ValueOrUnchanged(n->body);
     n->body = tirx::ConvertSSA(std::move(body));
     return f;
   };
diff --git a/src/tirx/transform/lower_intrin.cc 
b/src/tirx/transform/lower_intrin.cc
index 645e58170..9a37aa325 100644
--- a/src/tirx/transform/lower_intrin.cc
+++ b/src/tirx/transform/lower_intrin.cc
@@ -411,7 +411,7 @@ Pass LowerIntrin() {
     n->body =
         ffi::make_object<IntrinInjecter>(sym::ConstraintContext(), 
target.value(), enable_fast_math)
             ->Mutate(n->body, InplaceMode::kAllow)
-            .as_or_throw<UnchangedOr<ffi::Optional<SeqStmt>>>()
+            .as_or_throw<UnchangedOr<SeqStmt>>()
             .ValueOrUnchanged(n->body);
     return f;
   };
diff --git a/src/tirx/transform/lower_tensor_layouts.cc 
b/src/tirx/transform/lower_tensor_layouts.cc
index ebeb8c70a..fbe259110 100644
--- a/src/tirx/transform/lower_tensor_layouts.cc
+++ b/src/tirx/transform/lower_tensor_layouts.cc
@@ -390,10 +390,10 @@ namespace transform {
 
 Pass LowerTensorLayouts() {
   auto pass_func = [](Function f, IRModule m, PassContext ctx) {
-    if (!f->body.has_value()) return f;
+    if (f->GetAttr(tvm::attr::kDeclaration).value_or(false)) return f;
     Target target = ResolveTarget(f);
     auto* n = f.CopyOnWrite();
-    auto [body, params] = LayoutApplier::Flatten(n->body.value(), n->params, 
target);
+    auto [body, params] = LayoutApplier::Flatten(n->body, n->params, target);
     n->body = std::move(body);
     n->params = std::move(params);
     return f;
diff --git a/src/tirx/transform/lower_tvm_builtin.cc 
b/src/tirx/transform/lower_tvm_builtin.cc
index 6d8af98c6..bf2bc0272 100644
--- a/src/tirx/transform/lower_tvm_builtin.cc
+++ b/src/tirx/transform/lower_tvm_builtin.cc
@@ -61,7 +61,7 @@ class BuiltinLower : public StmtExprMutator {
   using StmtExprMutator::Mutate;
   using StmtExprMutator::Mutate_;
   static Function Build(Function func) {
-    if (!func->body.has_value()) return func;
+    if (func->GetAttr(tvm::attr::kDeclaration).value_or(false)) return func;
     ffi::Optional<PrimExpr> device_type = std::nullopt;
     bool preserve_ffi_kernel = false;
     if (auto target = func->GetAttr(tvm::attr::kTarget)) {
@@ -71,7 +71,7 @@ class BuiltinLower : public StmtExprMutator {
     }
 
     auto mutator = ffi::make_object<BuiltinLower>(device_type, 
preserve_ffi_kernel);
-    func.CopyOnWrite()->body = 
mutator->VisitBodyAndRealizeAlloca(func->body.value());
+    func.CopyOnWrite()->body = mutator->VisitBodyAndRealizeAlloca(func->body);
     return func;
   }
 
diff --git a/src/tirx/transform/make_packed_api.cc 
b/src/tirx/transform/make_packed_api.cc
index c95beaf70..f98a4ad1f 100644
--- a/src/tirx/transform/make_packed_api.cc
+++ b/src/tirx/transform/make_packed_api.cc
@@ -221,7 +221,7 @@ ffi::Optional<ffi::String> RequiresPackedAPI(const 
Function& func) {
 }
 
 Function MakePackedAPI(Function func) {
-  if (!func->body.has_value()) return func;
+  if (func->GetAttr(tvm::attr::kDeclaration).value_or(false)) return func;
   auto global_symbol = RequiresPackedAPI(func);
   if (!global_symbol.has_value()) {
     return func;
@@ -272,8 +272,8 @@ Function MakePackedAPI(Function func) {
                                       ffi::symbol::tvm_ffi_symbol_prefix + 
global_symbol.value()}});
 
   Stmt body = ffi::make_object<ReturnRewriter>(v_result)
-                  ->Mutate(func_ptr->body.value(), InplaceMode::kAllow)
-                  .ValueOrUnchanged(func_ptr->body.value());
+                  ->Mutate(func_ptr->body, InplaceMode::kAllow)
+                  .ValueOrUnchanged(func_ptr->body);
   body = RegionStmt(tirx::compute_scope_op(), {StringImm(name_hint + 
"_compute_")}, {}, DictAttrs(),
                     body);
   // Set device context
@@ -311,7 +311,7 @@ Function MakePackedAPI(Function func) {
   func_ptr->body = body;
   func_ptr->params = args;
 
-  ffi::Array<Var> undefined = UndefinedVars(func_ptr->body.value(), 
func_ptr->params);
+  ffi::Array<Var> undefined = UndefinedVars(func_ptr->body, func_ptr->params);
   TVM_FFI_ICHECK_EQ(undefined.size(), 0)
       << "In Function " << name_hint << " variables " << undefined
       << " are used, but are not passed in as API arguments";
@@ -342,10 +342,10 @@ Pass MakePackedAPI() {
     for (const auto& [gvar, base_func] : mptr->functions) {
       if (auto opt = base_func.as<Function>()) {
         auto func = opt.value();
-        if (!func->body.has_value()) continue;
+        if (func->GetAttr(tvm::attr::kDeclaration).value_or(false)) continue;
         auto orig_func = func;
 
-        if (auto body = SubroutineCallRewriter::Apply(packed_func_methods, 
func->body.value())) {
+        if (auto body = SubroutineCallRewriter::Apply(packed_func_methods, 
func->body)) {
           func.CopyOnWrite()->body = body.value();
         }
 
diff --git a/src/tirx/transform/narrow_datatype.cc 
b/src/tirx/transform/narrow_datatype.cc
index 8dabe1b45..11abd4d43 100644
--- a/src/tirx/transform/narrow_datatype.cc
+++ b/src/tirx/transform/narrow_datatype.cc
@@ -320,9 +320,9 @@ namespace transform {
 
 Pass NarrowDataType(int target_bits) {
   auto pass_func = [target_bits](Function f, IRModule m, PassContext ctx) {
-    if (!f->body.has_value()) return f;
+    if (f->GetAttr(tvm::attr::kDeclaration).value_or(false)) return f;
     auto* n = f.CopyOnWrite();
-    n->body = 
ffi::make_object<NarrowDataTypeRewriter>(target_bits)->Rewrite(n->body.value());
+    n->body = 
ffi::make_object<NarrowDataTypeRewriter>(target_bits)->Rewrite(n->body);
     return f;
   };
   return CreateFunctionPass(pass_func, 0, "tirx.NarrowDataType");
diff --git a/src/tirx/transform/remove_no_op.cc 
b/src/tirx/transform/remove_no_op.cc
index 0692ec16b..ec021dd27 100644
--- a/src/tirx/transform/remove_no_op.cc
+++ b/src/tirx/transform/remove_no_op.cc
@@ -251,16 +251,15 @@ namespace transform {
 
 Pass RemoveNoOp() {
   auto pass_func = [](Function f, IRModule m, PassContext ctx) {
-    if (!f->body.has_value()) return f;
+    if (f->GetAttr(tvm::attr::kDeclaration).value_or(false)) return f;
     RemoveNoOpConfig config = 
ctx->GetConfigOrDefault<RemoveNoOpConfig>("tirx.RemoveNoOp");
 
     bool ignore_profiler_call = config->ignore_profiler_call;
 
     {
       auto* write_ptr = f.CopyOnWrite();
-      write_ptr->body =
-          NoOpRemover::Apply(std::move(write_ptr->body).value(), 
sym::ConstraintContext(),
-                             ignore_profiler_call, 
config->max_simplification_steps);
+      write_ptr->body = NoOpRemover::Apply(std::move(write_ptr->body), 
sym::ConstraintContext(),
+                                           ignore_profiler_call, 
config->max_simplification_steps);
     }
     return f;
   };
diff --git a/src/tirx/transform/renormalize_split_pattern.cc 
b/src/tirx/transform/renormalize_split_pattern.cc
index 1565acb29..a1ac65e08 100644
--- a/src/tirx/transform/renormalize_split_pattern.cc
+++ b/src/tirx/transform/renormalize_split_pattern.cc
@@ -215,7 +215,7 @@ Pass RenormalizeSplitPattern() {
     auto* n = f.CopyOnWrite();
     n->body = 
ffi::make_object<SplitPatternReNormalizer>(sym::ConstraintContext())
                   ->Mutate(n->body, InplaceMode::kAllow)
-                  .as_or_throw<UnchangedOr<ffi::Optional<SeqStmt>>>()
+                  .as_or_throw<UnchangedOr<SeqStmt>>()
                   .ValueOrUnchanged(std::move(n->body));
     return f;
   };
diff --git a/src/tirx/transform/split_host_device.cc 
b/src/tirx/transform/split_host_device.cc
index da85655db..121fafe96 100644
--- a/src/tirx/transform/split_host_device.cc
+++ b/src/tirx/transform/split_host_device.cc
@@ -691,7 +691,7 @@ class DeviceKernelMutator : public StmtExprMutator {
         write_ptr->ret_type = VoidType();
         Target target = func->GetAttr(tvm::attr::kTarget).value();
         bool preserve_early_returns = target->kind->name == "cuda";
-        write_ptr->body = ReturnRemover::Apply(write_ptr->body.value(), 
!preserve_early_returns);
+        write_ptr->body = ReturnRemover::Apply(write_ptr->body, 
!preserve_early_returns);
       }
 
       func = WithAttrs(std::move(func),
diff --git a/src/tirx/transform/stmt_simplify.cc 
b/src/tirx/transform/stmt_simplify.cc
index d63b98ce6..3a6b45007 100644
--- a/src/tirx/transform/stmt_simplify.cc
+++ b/src/tirx/transform/stmt_simplify.cc
@@ -98,9 +98,9 @@ Function StmtSimplifier::Run(Function func) {
                   if (auto value = string_bindings.Get(var)) return 
ffi::Any(*value);
                   return ffi::Unchanged();
                 })
-                .as_or_throw<ffi::Optional<SeqStmt>>();
+                .as_or_throw<SeqStmt>();
   n->body = Mutate(n->body, InplaceMode::kAllow)
-                .as_or_throw<UnchangedOr<ffi::Optional<SeqStmt>>>()
+                .as_or_throw<UnchangedOr<SeqStmt>>()
                 .ValueOrUnchanged(n->body);
   return func;
 }
diff --git a/src/tirx/transform/storage_rewrite.cc 
b/src/tirx/transform/storage_rewrite.cc
index 4d3e92190..aaa09d6c7 100644
--- a/src/tirx/transform/storage_rewrite.cc
+++ b/src/tirx/transform/storage_rewrite.cc
@@ -2269,7 +2269,7 @@ namespace transform {
 
 Pass StorageRewrite() {
   auto pass_func = [](Function f, IRModule m, PassContext ctx) {
-    if (!f->body.has_value()) return f;
+    if (f->GetAttr(tvm::attr::kDeclaration).value_or(false)) return f;
     bool enable_reuse = true;
     bool reuse_require_exact_matched_dtype = false;
     bool merge_static_smem = 
ctx->GetConfig<bool>("tirx.merge_static_smem").value_or(false);
@@ -2287,9 +2287,8 @@ Pass StorageRewrite() {
       reuse_require_exact_matched_dtype = true;
     }
     auto* n = f.CopyOnWrite();
-    n->body = 
ffi::make_object<StoragePlanRewriter>()->Rewrite(std::move(n->body).value(),
-                                                               n->params, 
true, enable_reuse,
-                                                               
reuse_require_exact_matched_dtype);
+    n->body = ffi::make_object<StoragePlanRewriter>()->Rewrite(
+        std::move(n->body), n->params, true, enable_reuse, 
reuse_require_exact_matched_dtype);
     // Parameters may not be rewritten, but internal allocations may.
     return PointerValueTypeRewrite(std::move(f), true, false, false, true, 
true, true, false);
   };
@@ -2303,7 +2302,7 @@ TVM_FFI_STATIC_INIT_BLOCK() {
 
 Pass PointerValueTypeRewrite() {
   auto pass_func = [](Function f, IRModule m, PassContext ctx) {
-    if (!f->body.has_value()) return f;
+    if (f->GetAttr(tvm::attr::kDeclaration).value_or(false)) return f;
     return PointerValueTypeRewrite(std::move(f));
   };
   return CreateFunctionPass(pass_func, 0, "tirx.PointerValueTypeRewrite");
diff --git a/src/tirx/transform/unroll_loop.cc 
b/src/tirx/transform/unroll_loop.cc
index 53d09f43b..833b41d9b 100644
--- a/src/tirx/transform/unroll_loop.cc
+++ b/src/tirx/transform/unroll_loop.cc
@@ -368,10 +368,10 @@ namespace transform {
 
 Pass UnrollLoop() {
   auto pass_func = [=](Function f, IRModule m, PassContext ctx) {
-    if (!f->body.has_value()) return f;
+    if (f->GetAttr(tvm::attr::kDeclaration).value_or(false)) return f;
     auto* n = f.CopyOnWrite();
     auto cfg = ctx->GetConfigOrDefault<UnrollLoopConfig>("tirx.UnrollLoop");
-    n->body = UnrollLoop(std::move(f->body).value(), cfg);
+    n->body = UnrollLoop(std::move(f->body), cfg);
     return f;
   };
   return CreateFunctionPass(pass_func, 0, "tirx.UnrollLoop");
diff --git a/tests/cpp/tirx/thread_scope_test.cc 
b/tests/cpp/tirx/thread_scope_test.cc
index c3adc3959..1e2e86e6f 100644
--- a/tests/cpp/tirx/thread_scope_test.cc
+++ b/tests/cpp/tirx/thread_scope_test.cc
@@ -97,9 +97,7 @@ TEST(ThreadScope, TypedCallAndDispatchWithoutPython) {
   auto result = transform::TileDispatch()(IRModule({{main, func}}));
   auto lowered = result->Lookup(main).as_or_throw<Function>();
   EXPECT_EQ(*invoked, 1);
-  EXPECT_EQ(
-      
lowered->body.value()->seq[0].as_or_throw<Evaluate>()->value.as_or_throw<IntImm>()->value,
-      37);
+  
EXPECT_EQ(lowered->body->seq[0].as_or_throw<Evaluate>()->value.as_or_throw<IntImm>()->value,
 37);
   EXPECT_THROW(refl::ObjectCreator("tirx.tensor.ScopeAttrs")({}), ffi::Error);
   EXPECT_THROW(refl::ObjectCreator("tirx.tensor.ScopeAttrs")({{"scope", 
ffi::String("inner")}}),
                ffi::Error);
diff --git a/tests/python/tirx/ir/test_function.py 
b/tests/python/tirx/ir/test_function.py
index 5ddc96633..f5bf5a7b2 100644
--- a/tests/python/tirx/ir/test_function.py
+++ b/tests/python/tirx/ir/test_function.py
@@ -53,8 +53,8 @@ def test_structural_map_specializes_function_signature():
     # place while a nested TensorType changes. The cached FuncType must follow.
     def declaration():
         return Function(
-            [Var("tensor", TensorType((n,), "float32"))], None, 
TensorType((n,), "float32")
-        )
+            [Var("tensor", TensorType((n,), "float32"))], [], TensorType((n,), 
"float32")
+        ).with_attr("declaration", True)
 
     inplace = declaration()
     inplace = tvm_ffi.structural_map(
diff --git a/tests/python/tirx/test_constructor.py 
b/tests/python/tirx/test_constructor.py
index 04c507700..40aad8975 100644
--- a/tests/python/tirx/test_constructor.py
+++ b/tests/python/tirx/test_constructor.py
@@ -352,9 +352,9 @@ def test_math_unary_constructor_preserves_bfloat16():
     assert y.ty == tvm_tirx.ir.PrimType("bfloat16")
 
 
-def test_function_declaration_optional_body_roundtrip():
-    declaration = tvm_tirx.tirx.Function([], None)
-    assert declaration.body is None
+def test_function_declaration_required_body_roundtrip():
+    declaration = tvm_tirx.tirx.Function([], []).with_attr("declaration", True)
+    assert len(declaration.body) == 0
     assert len(declaration.with_body(tvm_tirx.ir.Evaluate(0)).body) == 0
     restored = tvm_tirx.ir.load_json(tvm_tirx.ir.save_json(declaration))
     tvm_tirx.ir.assert_structural_equal(declaration, restored, 
map_free_vars=True)

Reply via email to