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 1c86f55a88 [IR] Make function attribute updates generic via reflected 
shallow copy (#20366)
1c86f55a88 is described below

commit 1c86f55a88d6862a956454ca26bb90cec1698da4
Author: Tianqi Chen <[email protected]>
AuthorDate: Wed Sep 16 15:21:41 2026 -0400

    [IR] Make function attribute updates generic via reflected shallow copy 
(#20366)
    
    Function attribute updates use the concrete runtime type's reflected
    shallow-copy constructor when the input is shared, while retaining
    unique inputs and dictionary copy-on-write. This lets BaseFunc callers
    preserve concrete function fields without core IR depending on TIRX or
    Relax types.
    
    Replace the core dialect dispatch and immediately redundant Relax
    call-site dispatch with the generic helpers. Copyable BaseFunc subtypes,
    including ExternFunc, support all three attribute operations uniformly.
---
 include/tvm/ir/attrs.h               |  55 +++++++++++--
 src/ir/function.cc                   |  44 +---------
 src/relax/transform/decompose_ops.cc |  25 +-----
 src/relax/transform/run_codegen.cc   |   6 +-
 tests/cpp/function_attrs_test.cc     | 154 +++++++++++++++++++++++++++++++++++
 tests/python/ir/test_ir_attrs.py     |  37 +++++++++
 6 files changed, 245 insertions(+), 76 deletions(-)

diff --git a/include/tvm/ir/attrs.h b/include/tvm/ir/attrs.h
index 0c70f2d9ab..ce4537e7ca 100644
--- a/include/tvm/ir/attrs.h
+++ b/include/tvm/ir/attrs.h
@@ -32,6 +32,7 @@
 #include <tvm/ffi/extra/structural_equal.h>
 #include <tvm/ffi/extra/structural_hash.h>
 #include <tvm/ffi/function.h>
+#include <tvm/ffi/reflection/accessor.h>
 #include <tvm/ffi/reflection/registry.h>
 #include <tvm/ir/cow.h>
 
@@ -222,7 +223,8 @@ class DictAttrs : public Attrs {
  * \param attr_key The attribute key.
  * \param attr_value The value attribute value.
  *
- * \tparam TFunc The corresponding function or module type.
+ * \tparam TFunc The corresponding function or module type, including BaseFunc.
+ *                Shared inputs must register a reflected shallow-copy 
constructor.
  *
  * \returns The new function or module with updated attributes.
  *
@@ -244,8 +246,19 @@ class DictAttrs : public Attrs {
 template <typename TFunc>
 inline TFunc WithAttr(TFunc input, const std::string& attr_key, Any 
attr_value) {
   using TNode = typename TFunc::ContainerType;
-  static_assert(TNode::_type_final, "Can only operate on the leaf nodes");
-  TNode* node = input.CopyOnWrite();
+  if (!input.unique()) {
+    static ffi::reflection::TypeAttrColumn 
shallow_copy(ffi::reflection::type_attr::kShallowCopy);
+    ffi::AnyView copy_func = shallow_copy[input->type_index()];
+    TVM_FFI_CHECK(copy_func.type_index() == ffi::TypeIndex::kTVMFFIFunction, 
TypeError)
+        << "Type " << input->GetTypeKey()
+        << " must register an ffi.Function for __ffi_shallow_copy__";
+    Any copy = copy_func.cast<ffi::Function>()(input);
+    TVM_FFI_CHECK(copy.type_index() == input->type_index() && 
copy.as<ffi::Object>() != input.get(),
+                  TypeError)
+        << "Shallow copy must return a distinct object of type " << 
input->GetTypeKey();
+    input = std::move(copy).cast<TFunc>();
+  }
+  TNode* node = const_cast<TNode*>(input.operator->());
   // node->attrs is NOTNULLABLE by contract, but defend against a caller
   // that left a moved-from DictAttrs in place by re-initializing here.
   if (!node->attrs.defined()) node->attrs = DictAttrs();
@@ -259,16 +272,28 @@ inline TFunc WithAttr(TFunc input, const std::string& 
attr_key, Any attr_value)
  * \param input The thing to annotate (BaseFunc or IRModule)
  * \param attrs Key/values attributes to add to \p input.
  *
- * \tparam TFunc The corresponding function or module type.
+ * \tparam TFunc The corresponding function or module type, including BaseFunc.
+ *                Shared inputs must register a reflected shallow-copy 
constructor.
  *
  * \returns The new function or module with updated attributes.
  */
 template <typename TFunc>
 inline TFunc WithAttrs(TFunc input, ffi::Map<ffi::String, Any> attrs) {
   using TNode = typename TFunc::ContainerType;
-  static_assert(TNode::_type_final, "Can only operate on the leaf nodes");
   if (attrs.empty()) return input;
-  TNode* node = input.CopyOnWrite();
+  if (!input.unique()) {
+    static ffi::reflection::TypeAttrColumn 
shallow_copy(ffi::reflection::type_attr::kShallowCopy);
+    ffi::AnyView copy_func = shallow_copy[input->type_index()];
+    TVM_FFI_CHECK(copy_func.type_index() == ffi::TypeIndex::kTVMFFIFunction, 
TypeError)
+        << "Type " << input->GetTypeKey()
+        << " must register an ffi.Function for __ffi_shallow_copy__";
+    Any copy = copy_func.cast<ffi::Function>()(input);
+    TVM_FFI_CHECK(copy.type_index() == input->type_index() && 
copy.as<ffi::Object>() != input.get(),
+                  TypeError)
+        << "Shallow copy must return a distinct object of type " << 
input->GetTypeKey();
+    input = std::move(copy).cast<TFunc>();
+  }
+  TNode* node = const_cast<TNode*>(input.operator->());
   // node->attrs is NOTNULLABLE by contract, but defend against a caller
   // that left a moved-from DictAttrs in place by re-initializing here.
   if (!node->attrs.defined()) node->attrs = DictAttrs();
@@ -286,7 +311,8 @@ inline TFunc WithAttrs(TFunc input, ffi::Map<ffi::String, 
Any> attrs) {
  * \param input The thing to annotate (BaseFunc or IRModule)
  * \param attr_key The attribute key.
  *
- * \tparam TFunc The corresponding function or module type.
+ * \tparam TFunc The corresponding function or module type, including BaseFunc.
+ *                Shared inputs must register a reflected shallow-copy 
constructor.
  *
  * \returns The new function or module with removed attribute.
  *
@@ -308,8 +334,19 @@ inline TFunc WithAttrs(TFunc input, ffi::Map<ffi::String, 
Any> attrs) {
 template <typename TFunc>
 inline TFunc WithoutAttr(TFunc input, const std::string& attr_key) {
   using TNode = typename TFunc::ContainerType;
-  static_assert(TNode::_type_final, "Can only operate on the leaf nodes");
-  TNode* node = input.CopyOnWrite();
+  if (!input.unique()) {
+    static ffi::reflection::TypeAttrColumn 
shallow_copy(ffi::reflection::type_attr::kShallowCopy);
+    ffi::AnyView copy_func = shallow_copy[input->type_index()];
+    TVM_FFI_CHECK(copy_func.type_index() == ffi::TypeIndex::kTVMFFIFunction, 
TypeError)
+        << "Type " << input->GetTypeKey()
+        << " must register an ffi.Function for __ffi_shallow_copy__";
+    Any copy = copy_func.cast<ffi::Function>()(input);
+    TVM_FFI_CHECK(copy.type_index() == input->type_index() && 
copy.as<ffi::Object>() != input.get(),
+                  TypeError)
+        << "Shallow copy must return a distinct object of type " << 
input->GetTypeKey();
+    input = std::move(copy).cast<TFunc>();
+  }
+  TNode* node = const_cast<TNode*>(input.operator->());
   // node->attrs is NOTNULLABLE by contract, but defend against a caller
   // that left a moved-from DictAttrs in place; nothing to erase from an
   // empty dict.
diff --git a/src/ir/function.cc b/src/ir/function.cc
index c14dfa29de..92a8119157 100644
--- a/src/ir/function.cc
+++ b/src/ir/function.cc
@@ -25,8 +25,6 @@
 #include <tvm/ffi/reflection/registry.h>
 #include <tvm/ffi/rvalue_ref.h>
 #include <tvm/ir/function.h>
-#include <tvm/relax/expr.h>
-#include <tvm/tirx/function.h>
 
 namespace tvm {
 
@@ -37,48 +35,14 @@ TVM_FFI_STATIC_INIT_BLOCK() {
       .def("ir.BaseFuncCopy", [](BaseFunc func) { return func; })
       .def("ir.BaseFuncWithAttr",
            [](ffi::RValueRef<BaseFunc> func_ref, ffi::String key, Any value) 
-> BaseFunc {
-             BaseFunc func = *std::move(func_ref);
-             if (func->IsInstance<tirx::PrimFuncNode>()) {
-               return WithAttr(std::move(func).as_or_throw<tirx::PrimFunc>(), 
key, value);
-             } else if (func->IsInstance<relax::FunctionNode>()) {
-               return WithAttr(std::move(func).as_or_throw<relax::Function>(), 
key, value);
-             } else if (func->IsInstance<relax::ExternFuncNode>()) {
-               return 
WithAttr(std::move(func).as_or_throw<relax::ExternFunc>(), key, value);
-             } else {
-               TVM_FFI_THROW(InternalError)
-                   << "Do not support function type " << func->GetTypeKey();
-             }
+             return WithAttr(*std::move(func_ref), key, std::move(value));
            })
       .def("ir.BaseFuncWithAttrs",
-           [](ffi::RValueRef<BaseFunc> func_ref,
-              ffi::Map<ffi::String, ffi::Any> attr_map) -> BaseFunc {
-             BaseFunc func = *std::move(func_ref);
-             if (func->IsInstance<tirx::PrimFuncNode>()) {
-               return WithAttrs(std::move(func).as_or_throw<tirx::PrimFunc>(), 
attr_map);
-             }
-             if (const auto f = 
tvm::ffi::Function::GetGlobal("relax.FuncWithAttrs")) {
-               if (auto ret = (*f)(func, 
attr_map).cast<ffi::Optional<BaseFunc>>()) {
-                 return ret.value();
-               }
-             }
-             if (func->IsInstance<relax::ExternFuncNode>()) {
-               return 
WithAttrs(std::move(func).as_or_throw<relax::ExternFunc>(), attr_map);
-             }
-             TVM_FFI_THROW(InternalError) << "Do not support function type " 
<< func->GetTypeKey();
-             TVM_FFI_UNREACHABLE();
-           })
+           [](ffi::RValueRef<BaseFunc> func_ref, ffi::Map<ffi::String, 
ffi::Any> attr_map)
+               -> BaseFunc { return WithAttrs(*std::move(func_ref), 
std::move(attr_map)); })
       .def("ir.BaseFuncWithoutAttr",
            [](ffi::RValueRef<BaseFunc> func_ref, ffi::String key) -> BaseFunc {
-             BaseFunc func = *std::move(func_ref);
-             if (func->IsInstance<tirx::PrimFuncNode>()) {
-               return 
WithoutAttr(std::move(func).as_or_throw<tirx::PrimFunc>(), key);
-             } else if (func->IsInstance<relax::FunctionNode>()) {
-               return 
WithoutAttr(std::move(func).as_or_throw<relax::Function>(), key);
-             } else {
-               TVM_FFI_THROW(InternalError)
-                   << "Do not support function type " << func->GetTypeKey();
-               TVM_FFI_UNREACHABLE();
-             }
+             return WithoutAttr(*std::move(func_ref), key);
            });
 }
 
diff --git a/src/relax/transform/decompose_ops.cc 
b/src/relax/transform/decompose_ops.cc
index 8d6da8c046..af0127d7f6 100644
--- a/src/relax/transform/decompose_ops.cc
+++ b/src/relax/transform/decompose_ops.cc
@@ -217,27 +217,6 @@ namespace transform {
 
 namespace {
 
-/*! \brief Helper: add or remove an attribute on a BaseFunc */
-BaseFunc BaseFuncWithAttr(BaseFunc func, const std::string& attr_key, Any 
attr_value) {
-  if (auto tirx = func.as<tirx::PrimFunc>()) {
-    return WithAttr(tirx.value(), attr_key, attr_value);
-  } else if (auto relax_fn = func.as<relax::Function>()) {
-    return WithAttr(relax_fn.value(), attr_key, attr_value);
-  } else {
-    return func;
-  }
-}
-
-BaseFunc BaseFuncWithoutAttr(BaseFunc func, const std::string& attr_key) {
-  if (auto tirx = func.as<tirx::PrimFunc>()) {
-    return WithoutAttr(tirx.value(), attr_key);
-  } else if (auto relax_fn = func.as<relax::Function>()) {
-    return WithoutAttr(relax_fn.value(), attr_key);
-  } else {
-    return func;
-  }
-}
-
 /*!
  * \brief Apply a pass to a single named function within an IRModule.
  *
@@ -258,7 +237,7 @@ Pass ApplyDecomposeToFunction(Pass pass, ffi::String 
func_name) {
           // Mark internal functions as externally-exposed so that
           // call-tracing transforms inside the pass do not remove them.
           internal_functions.insert(gvar->name_hint);
-          func = BaseFuncWithAttr(func, tvm::attr::kGlobalSymbol, 
gvar->name_hint);
+          func = WithAttr(std::move(func), tvm::attr::kGlobalSymbol, 
gvar->name_hint);
         }
       } else {
         // Replace non-target functions with stubs to keep references intact.
@@ -282,7 +261,7 @@ Pass ApplyDecomposeToFunction(Pass pass, ffi::String 
func_name) {
           write_ptr->Remove((*it).second);
         }
         if (internal_functions.count(gvar->name_hint)) {
-          func = BaseFuncWithoutAttr(func, tvm::attr::kGlobalSymbol);
+          func = WithoutAttr(std::move(func), tvm::attr::kGlobalSymbol);
         }
         write_ptr->Add(gvar, func);
       }
diff --git a/src/relax/transform/run_codegen.cc 
b/src/relax/transform/run_codegen.cc
index 162bc33355..75cc23dbda 100644
--- a/src/relax/transform/run_codegen.cc
+++ b/src/relax/transform/run_codegen.cc
@@ -131,10 +131,8 @@ class CodeGenRunner : ExprMutator {
           extern_funcs_[gvar_node] = new_func;
           // Remove the global symbol and codegen attributes from the function 
so that it can be
           // removed the module.
-          const auto RemoveFuncAttrFunc = 
tvm::ffi::Function::GetGlobal("ir.BaseFuncWithoutAttr");
-          TVM_FFI_ICHECK(RemoveFuncAttrFunc.has_value());
-          func = (*RemoveFuncAttrFunc)(func, 
tvm::attr::kGlobalSymbol).cast<Function>();
-          func = (*RemoveFuncAttrFunc)(func, attr::kCodegen).cast<Function>();
+          func = WithoutAttr(std::move(func), tvm::attr::kGlobalSymbol);
+          func = WithoutAttr(std::move(func), attr::kCodegen);
           builder_->UpdateFunction(gvar, func);
           return create_call_dps_packed(new_func, ret_ty);
         }
diff --git a/tests/cpp/function_attrs_test.cc b/tests/cpp/function_attrs_test.cc
new file mode 100644
index 0000000000..bc5edac549
--- /dev/null
+++ b/tests/cpp/function_attrs_test.cc
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include <gtest/gtest.h>
+#include <tvm/ir/function.h>
+#include <tvm/ir/module.h>
+#include <tvm/relax/expr.h>
+#include <tvm/tirx/function.h>
+#include <tvm/tirx/stmt.h>
+
+namespace tvm {
+namespace {
+
+class CustomFuncNode : public BaseFuncNode {
+ public:
+  ffi::String payload = "preserved";
+  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("test.AttrsCustomFunc", CustomFuncNode, 
BaseFuncNode);
+};
+
+class MissingCopyFuncNode : public BaseFuncNode {
+ public:
+  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("test.AttrsMissingCopyFunc", 
MissingCopyFuncNode, BaseFuncNode);
+};
+
+class InvalidCopyFuncNode : public BaseFuncNode {
+ public:
+  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("test.AttrsInvalidCopyFunc", 
InvalidCopyFuncNode, BaseFuncNode);
+};
+
+int invalid_copy_mode = 0;
+TVM_FFI_STATIC_INIT_BLOCK() {
+  ffi::reflection::ObjectDef<CustomFuncNode>().def_ro("payload", 
&CustomFuncNode::payload);
+  ffi::reflection::TypeAttrDef<InvalidCopyFuncNode>().def(
+      ffi::reflection::type_attr::kShallowCopy, [](BaseFunc func) -> Any {
+        if (invalid_copy_mode == 0) return func;
+        if (invalid_copy_mode == 1) return 
BaseFunc(ffi::make_object<CustomFuncNode>());
+        return nullptr;
+      });
+}
+
+TEST(FunctionAttrs, GenericSubtypeAndSharedAttributes) {
+  BaseFunc original(ffi::make_object<CustomFuncNode>());
+  original = WithAttr(std::move(original), "keep", 1);
+  DictAttrs attrs = original->attrs;
+  auto check_copy = [&](const BaseFunc& result) {
+    EXPECT_FALSE(result.same_as(original));
+    EXPECT_EQ(result->type_index(), original->type_index());
+    EXPECT_EQ(result.as<CustomFuncNode>()->payload, "preserved");
+    EXPECT_TRUE(result->ty.same_as(original->ty));
+    EXPECT_TRUE(result->span.same_as(original->span));
+    EXPECT_EQ(original->attrs->dict.size(), 1);
+    EXPECT_TRUE(original->attrs.same_as(attrs));
+    EXPECT_EQ(attrs->dict.at("keep").cast<int>(), 1);
+  };
+  auto added = WithAttr(original, "added", 2);
+  check_copy(added);
+  EXPECT_EQ(added->attrs->dict.at("added").cast<int>(), 2);
+  auto updated = WithAttrs(original, {{"keep", 3}, {"added", 4}});
+  check_copy(updated);
+  EXPECT_EQ(updated->attrs->dict.at("keep").cast<int>(), 3);
+  auto removed = WithoutAttr(original, "keep");
+  check_copy(removed);
+  EXPECT_TRUE(removed->attrs->dict.empty());
+  EXPECT_TRUE(WithAttrs(original, {}).same_as(original));
+}
+
+TEST(FunctionAttrs, UniqueReuseAndSharedDictionary) {
+  BaseFunc func(ffi::make_object<CustomFuncNode>());
+  const auto* ptr = func.get();
+  DictAttrs shared_attrs = func->attrs;
+  func = WithAttr(std::move(func), "key", 1);
+  EXPECT_EQ(func.get(), ptr);
+  EXPECT_TRUE(shared_attrs->dict.empty());
+  func = WithAttrs(std::move(func), {{"key", 2}, {"other", 3}});
+  EXPECT_EQ(func.get(), ptr);
+  func = WithoutAttr(std::move(func), "key");
+  EXPECT_EQ(func.get(), ptr);
+  EXPECT_FALSE(func->attrs->dict.count("key"));
+  EXPECT_EQ(func->attrs->dict.at("other").cast<int>(), 3);
+}
+
+TEST(FunctionAttrs, TypedFunctionsAndModule) {
+  tirx::PrimFunc prim({}, tirx::Evaluate(0));
+  auto prim_copy = WithAttr(prim, "key", 1);
+  EXPECT_FALSE(prim_copy.same_as(prim));
+  EXPECT_TRUE(prim_copy->body.same_as(prim->body));
+  EXPECT_TRUE(prim_copy->params.same_as(prim->params));
+  EXPECT_TRUE(prim_copy->ret_type.same_as(prim->ret_type));
+  BaseFunc base = prim;
+  auto generic_copy = WithAttrs(base, {{"key", 1}});
+  EXPECT_EQ(generic_copy->type_index(), prim->type_index());
+  EXPECT_TRUE(generic_copy.as<tirx::PrimFuncNode>()->body.same_as(prim->body));
+
+  relax::ExternFunc ext("external_symbol");
+  auto ext_copy = WithoutAttr(WithAttr(ext, "key", 1), "key");
+  EXPECT_EQ(ext_copy->global_symbol, ext->global_symbol);
+  EXPECT_TRUE(ext_copy->attrs->dict.empty());
+  IRModule mod = IRModule::FromExpr(prim);
+  auto mod_copy = WithAttrs(mod, {{"key", 1}});
+  EXPECT_TRUE(mod->attrs->dict.empty());
+  EXPECT_EQ(mod_copy->attrs->dict.at("key").cast<int>(), 1);
+  EXPECT_TRUE(WithoutAttr(mod_copy, "key")->attrs->dict.empty());
+}
+
+TEST(FunctionAttrs, MissingAndInvalidHooksPreserveInput) {
+  BaseFunc missing(ffi::make_object<MissingCopyFuncNode>());
+  EXPECT_THROW(WithAttr(missing, "key", 1), ffi::Error);
+  EXPECT_THROW(WithAttrs(missing, {{"key", 1}}), ffi::Error);
+  EXPECT_THROW(WithoutAttr(missing, "key"), ffi::Error);
+  EXPECT_TRUE(WithAttrs(missing, {}).same_as(missing));
+  // Unique input needs no copy hook.
+  const auto* ptr = missing.get();
+  missing = WithAttr(std::move(missing), "key", 1);
+  EXPECT_EQ(missing.get(), ptr);
+
+  BaseFunc invalid(ffi::make_object<InvalidCopyFuncNode>());
+  invalid = WithAttr(std::move(invalid), "key", 1);
+  for (invalid_copy_mode = 0; invalid_copy_mode < 3; ++invalid_copy_mode) {
+    EXPECT_THROW(WithAttr(invalid, "key", 2), ffi::Error);
+    EXPECT_THROW(WithAttrs(invalid, {{"key", 2}}), ffi::Error);
+    EXPECT_THROW(WithoutAttr(invalid, "key"), ffi::Error);
+    EXPECT_EQ(invalid->attrs->dict.at("key").cast<int>(), 1);
+  }
+}
+
+TEST(FunctionAttrs, MovedFromAttributes) {
+  BaseFunc func(ffi::make_object<CustomFuncNode>());
+  // Simulate a caller moving through the base handle, bypassing DictAttrs' 
reset-on-move.
+  ffi::ObjectRef moved =
+      
std::move(static_cast<ffi::ObjectRef&>(const_cast<BaseFuncNode*>(func.operator->())->attrs));
+  EXPECT_EQ(WithAttr(func, "key", 1)->attrs->dict.size(), 1);
+  EXPECT_EQ(WithAttrs(func, {{"key", 1}})->attrs->dict.size(), 1);
+  EXPECT_TRUE(WithoutAttr(func, "key")->attrs->dict.empty());
+  EXPECT_FALSE(func->attrs.defined());
+}
+
+}  // namespace
+}  // namespace tvm
diff --git a/tests/python/ir/test_ir_attrs.py b/tests/python/ir/test_ir_attrs.py
index 25480f7265..5d8171d152 100644
--- a/tests/python/ir/test_ir_attrs.py
+++ b/tests/python/ir/test_ir_attrs.py
@@ -57,6 +57,43 @@ def test_assert_structural_equal_reports_mismatch():
     assert "and rhs at" in message
 
 
[email protected]("kind", ["prim", "relax", "extern"])
+def test_function_attribute_copy_preserves_fields(kind):
+    span = tvm.ir.Span(tvm.ir.SourceName("attrs"), 1, 2, 3, 4)
+    if kind == "prim":
+        var = tvm.tirx.Var("x", "int32")
+        func = tvm.tirx.PrimFunc([var], tvm.tirx.Evaluate(var), span=span)
+        fields = ["params", "body", "ret_type", "ty", "span"]
+    elif kind == "relax":
+        var = tvm.relax.Var("x", tvm.relax.TensorType([2], "float32"))
+        func = tvm.relax.Function([var], var, is_pure=False, span=span)
+        fields = ["params", "body", "ret_ty", "ty", "span"]
+    else:
+        func = tvm.relax.ExternFunc("external_symbol", span=span)
+        fields = ["ty", "span"]
+
+    func = func.with_attr("keep", 1)
+    shared_attrs = func.attrs
+    added = func.with_attr("added", 2)
+    updated = func.with_attr({"keep": 3, "added": 4})
+    removed = func.without_attr("keep")
+    assert func.with_attr({}).same_as(func)
+    for result in [added, updated, removed]:
+        assert type(result) is type(func)
+        assert not result.same_as(func)
+        for field in fields:
+            assert getattr(result, field).same_as(getattr(func, field))
+        if kind == "relax":
+            assert result.is_pure is False
+        if kind == "extern":
+            assert result.global_symbol == "external_symbol"
+        assert func.attrs.same_as(shared_attrs)
+        assert dict(shared_attrs) == {"keep": 1}
+    assert dict(added.attrs) == {"keep": 1, "added": 2}
+    assert dict(updated.attrs) == {"keep": 3, "added": 4}
+    assert not removed.attrs
+
+
 if __name__ == "__main__":
     test_dict_attrs()
     test_attrs_equal()

Reply via email to