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()