diff --git a/mlir/include/mlir-c/BuiltinAttributes.h b/mlir/include/mlir-c/BuiltinAttributes.h index 63198192453efb89aa5e56831e662750b9114765..01d1b6008f5e215fd4d232f51f4085697f74a838 100644 --- a/mlir/include/mlir-c/BuiltinAttributes.h +++ b/mlir/include/mlir-c/BuiltinAttributes.h @@ -558,6 +558,23 @@ mlirDenseElementsAttrGetRawData(MlirAttribute attr); // Resource blob attributes. //===----------------------------------------------------------------------===// +MLIR_CAPI_EXPORTED bool +mlirAttributeIsADenseResourceElements(MlirAttribute attr); + +/// Unlike the typed accessors below, constructs the attribute with a raw +/// data buffer and no type/alignment checking. Use a more strongly typed +/// accessor if possible. If dataIsMutable is false, then an immutable +/// AsmResourceBlob will be created and that passed data contents will be +/// treated as const. +/// If the deleter is non NULL, then it will be called when the data buffer +/// can no longer be accessed (passing userData to it). +MLIR_CAPI_EXPORTED MlirAttribute mlirUnmanagedDenseResourceElementsAttrGet( + MlirType shapedType, MlirStringRef name, void *data, size_t dataLength, + size_t dataAlignment, bool dataIsMutable, + void (*deleter)(void *userData, const void *data, size_t size, + size_t align), + void *userData); + MLIR_CAPI_EXPORTED MlirAttribute mlirUnmanagedDenseBoolResourceElementsAttrGet( MlirType shapedType, MlirStringRef name, intptr_t numElements, const int *elements); diff --git a/mlir/include/mlir/IR/BuiltinAttributes.td b/mlir/include/mlir/IR/BuiltinAttributes.td index 075eee456a7b58f72ead5801f236192556ec2e5a..893ded074ed4d3d70443bd798c9455888ffe79a7 100644 --- a/mlir/include/mlir/IR/BuiltinAttributes.td +++ b/mlir/include/mlir/IR/BuiltinAttributes.td @@ -466,21 +466,20 @@ def Builtin_DenseResourceElementsAttr : Builtin_Attr<"DenseResourceElements", [ let builders = [ AttrBuilderWithInferredContext<(ins "ShapedType":$type, "DenseResourceElementsHandle":$handle - )> - ]; - let extraClassDeclaration = [{ - protected: + )>, /// A builder that inserts a new resource into the builtin dialect's blob /// manager using the provided blob. The handle of the inserted blob is used /// when building the attribute. The provided `blobName` is used as a hint /// for the key of the new handle for the `blob` resource, but may be /// changed if necessary to ensure uniqueness during insertion. - static DenseResourceElementsAttr get( - ShapedType type, StringRef blobName, AsmResourceBlob blob - ); + /// This base class builder does no element type specific size or alignment + /// checking. Use the typed subclasses for more safety unless if performing + /// generic operations. + AttrBuilderWithInferredContext<(ins + "ShapedType":$type, "StringRef":$blobName, "AsmResourceBlob":$blob + )> + ]; - public: - }]; let skipDefaultBuilders = 1; } diff --git a/mlir/lib/Bindings/Python/IRAttributes.cpp b/mlir/lib/Bindings/Python/IRAttributes.cpp index 75d743f3a3962a6262a12bd662f0108d5176219a..220c04f4a2aeaa8f590102df1fcaf638d99f0216 100644 --- a/mlir/lib/Bindings/Python/IRAttributes.cpp +++ b/mlir/lib/Bindings/Python/IRAttributes.cpp @@ -72,6 +72,32 @@ Raises: type or if the buffer does not meet expectations. )"; +static const char kDenseResourceElementsAttrGetFromBufferDocstring[] = + R"(Gets a DenseResourceElementsAttr from a Python buffer or array. + +This function does minimal validation or massaging of the data, and it is +up to the caller to ensure that the buffer meets the characteristics +implied by the shape. + +The backing buffer and any user objects will be retained for the lifetime +of the resource blob. This is typically bounded to the context but the +resource can have a shorter lifespan depending on how it is used in +subsequent processing. + +Args: + buffer: The array or buffer to convert. + name: Name to provide to the resource (may be changed upon collision). + type: The explicit ShapedType to construct the attribute with. + context: Explicit context, if not from context manager. + +Returns: + DenseResourceElementsAttr on success. + +Raises: + ValueError: If the type of the buffer or array cannot be matched to an MLIR + type or if the buffer does not meet expectations. +)"; + namespace { static MlirStringRef toMlirStringRef(const std::string &s) { @@ -985,6 +1011,82 @@ public: } }; +class PyDenseResourceElementsAttribute + : public PyConcreteAttribute { +public: + static constexpr IsAFunctionTy isaFunction = + mlirAttributeIsADenseResourceElements; + static constexpr const char *pyClassName = "DenseResourceElementsAttr"; + using PyConcreteAttribute::PyConcreteAttribute; + + static PyDenseResourceElementsAttribute + getFromBuffer(py::buffer buffer, std::string name, PyType type, + std::optional alignment, bool isMutable, + DefaultingPyMlirContext contextWrapper) { + if (!mlirTypeIsAShaped(type)) { + throw std::invalid_argument( + "Constructing a DenseResourceElementsAttr requires a ShapedType."); + } + + // Do not request any conversions as we must ensure to use caller + // managed memory. + int flags = PyBUF_STRIDES; + std::unique_ptr view = std::make_unique(); + if (PyObject_GetBuffer(buffer.ptr(), view.get(), flags) != 0) { + throw py::error_already_set(); + } + + // This scope releaser will only release if we haven't yet transferred + // ownership. + auto freeBuffer = llvm::make_scope_exit([&]() { + if (view) + PyBuffer_Release(view.get()); + }); + + if (!PyBuffer_IsContiguous(view.get(), 'A')) { + throw std::invalid_argument("Contiguous buffer is required."); + } + + // Infer alignment to be the stride of one element if not explicit. + size_t inferredAlignment; + if (alignment) + inferredAlignment = *alignment; + else + inferredAlignment = view->strides[view->ndim - 1]; + + // The userData is a Py_buffer* that the deleter owns. + auto deleter = [](void *userData, const void *data, size_t size, + size_t align) { + Py_buffer *ownedView = static_cast(userData); + PyBuffer_Release(ownedView); + delete ownedView; + }; + + size_t rawBufferSize = view->len; + MlirAttribute attr = mlirUnmanagedDenseResourceElementsAttrGet( + type, toMlirStringRef(name), view->buf, rawBufferSize, + inferredAlignment, isMutable, deleter, static_cast(view.get())); + if (mlirAttributeIsNull(attr)) { + throw std::invalid_argument( + "DenseResourceElementsAttr could not be constructed from the given " + "buffer. " + "This may mean that the Python buffer layout does not match that " + "MLIR expected layout and is a bug."); + } + view.release(); + return PyDenseResourceElementsAttribute(contextWrapper->getRef(), attr); + } + + static void bindDerived(ClassTy &c) { + c.def_static("get_from_buffer", + PyDenseResourceElementsAttribute::getFromBuffer, + py::arg("array"), py::arg("name"), py::arg("type"), + py::arg("alignment") = py::none(), + py::arg("is_mutable") = false, py::arg("context") = py::none(), + kDenseResourceElementsAttrGetFromBufferDocstring); + } +}; + class PyDictAttribute : public PyConcreteAttribute { public: static constexpr IsAFunctionTy isaFunction = mlirAttributeIsADictionary; @@ -1261,6 +1363,7 @@ void mlir::python::populateIRAttributes(py::module &m) { PyGlobals::get().registerTypeCaster( mlirDenseIntOrFPElementsAttrGetTypeID(), pybind11::cpp_function(denseIntOrFPElementsAttributeCaster)); + PyDenseResourceElementsAttribute::bind(m); PyDictAttribute::bind(m); PySymbolRefAttribute::bind(m); diff --git a/mlir/lib/CAPI/IR/BuiltinAttributes.cpp b/mlir/lib/CAPI/IR/BuiltinAttributes.cpp index de221ddbfa7a922f1fc900fb7544dc9a1f2ba55f..b3066ee0c28bdc82194a4808b140103da6edc017 100644 --- a/mlir/lib/CAPI/IR/BuiltinAttributes.cpp +++ b/mlir/lib/CAPI/IR/BuiltinAttributes.cpp @@ -770,6 +770,30 @@ const void *mlirDenseElementsAttrGetRawData(MlirAttribute attr) { // Resource blob attributes. //===----------------------------------------------------------------------===// +bool mlirAttributeIsADenseResourceElements(MlirAttribute attr) { + return llvm::isa(unwrap(attr)); +} + +MlirAttribute mlirUnmanagedDenseResourceElementsAttrGet( + MlirType shapedType, MlirStringRef name, void *data, size_t dataLength, + size_t dataAlignment, bool dataIsMutable, + void (*deleter)(void *userData, const void *data, size_t size, + size_t align), + void *userData) { + AsmResourceBlob::DeleterFn cppDeleter = {}; + if (deleter) { + cppDeleter = [deleter, userData](void *data, size_t size, size_t align) { + deleter(userData, data, size, align); + }; + } + AsmResourceBlob blob( + llvm::ArrayRef(static_cast(data), dataLength), + dataAlignment, std::move(cppDeleter), dataIsMutable); + return wrap( + DenseResourceElementsAttr::get(llvm::cast(unwrap(shapedType)), + unwrap(name), std::move(blob))); +} + template static MlirAttribute getDenseResource(MlirType shapedType, MlirStringRef name, intptr_t numElements, const T *elements) { @@ -778,131 +802,122 @@ static MlirAttribute getDenseResource(MlirType shapedType, MlirStringRef name, llvm::ArrayRef(elements, numElements)))); } -MLIR_CAPI_EXPORTED MlirAttribute mlirUnmanagedDenseBoolResourceElementsAttrGet( +MlirAttribute mlirUnmanagedDenseBoolResourceElementsAttrGet( MlirType shapedType, MlirStringRef name, intptr_t numElements, const int *elements) { return getDenseResource(shapedType, name, numElements, elements); } -MLIR_CAPI_EXPORTED MlirAttribute mlirUnmanagedDenseUInt8ResourceElementsAttrGet( +MlirAttribute mlirUnmanagedDenseUInt8ResourceElementsAttrGet( MlirType shapedType, MlirStringRef name, intptr_t numElements, const uint8_t *elements) { return getDenseResource(shapedType, name, numElements, elements); } -MLIR_CAPI_EXPORTED MlirAttribute -mlirUnmanagedDenseUInt16ResourceElementsAttrGet(MlirType shapedType, - MlirStringRef name, - intptr_t numElements, - const uint16_t *elements) { +MlirAttribute mlirUnmanagedDenseUInt16ResourceElementsAttrGet( + MlirType shapedType, MlirStringRef name, intptr_t numElements, + const uint16_t *elements) { return getDenseResource(shapedType, name, numElements, elements); } -MLIR_CAPI_EXPORTED MlirAttribute -mlirUnmanagedDenseUInt32ResourceElementsAttrGet(MlirType shapedType, - MlirStringRef name, - intptr_t numElements, - const uint32_t *elements) { +MlirAttribute mlirUnmanagedDenseUInt32ResourceElementsAttrGet( + MlirType shapedType, MlirStringRef name, intptr_t numElements, + const uint32_t *elements) { return getDenseResource(shapedType, name, numElements, elements); } -MLIR_CAPI_EXPORTED MlirAttribute -mlirUnmanagedDenseUInt64ResourceElementsAttrGet(MlirType shapedType, - MlirStringRef name, - intptr_t numElements, - const uint64_t *elements) { +MlirAttribute mlirUnmanagedDenseUInt64ResourceElementsAttrGet( + MlirType shapedType, MlirStringRef name, intptr_t numElements, + const uint64_t *elements) { return getDenseResource(shapedType, name, numElements, elements); } -MLIR_CAPI_EXPORTED MlirAttribute mlirUnmanagedDenseInt8ResourceElementsAttrGet( +MlirAttribute mlirUnmanagedDenseInt8ResourceElementsAttrGet( MlirType shapedType, MlirStringRef name, intptr_t numElements, const int8_t *elements) { return getDenseResource(shapedType, name, numElements, elements); } -MLIR_CAPI_EXPORTED MlirAttribute mlirUnmanagedDenseInt16ResourceElementsAttrGet( +MlirAttribute mlirUnmanagedDenseInt16ResourceElementsAttrGet( MlirType shapedType, MlirStringRef name, intptr_t numElements, const int16_t *elements) { return getDenseResource(shapedType, name, numElements, elements); } -MLIR_CAPI_EXPORTED MlirAttribute mlirUnmanagedDenseInt32ResourceElementsAttrGet( +MlirAttribute mlirUnmanagedDenseInt32ResourceElementsAttrGet( MlirType shapedType, MlirStringRef name, intptr_t numElements, const int32_t *elements) { return getDenseResource(shapedType, name, numElements, elements); } -MLIR_CAPI_EXPORTED MlirAttribute mlirUnmanagedDenseInt64ResourceElementsAttrGet( +MlirAttribute mlirUnmanagedDenseInt64ResourceElementsAttrGet( MlirType shapedType, MlirStringRef name, intptr_t numElements, const int64_t *elements) { return getDenseResource(shapedType, name, numElements, elements); } -MLIR_CAPI_EXPORTED MlirAttribute mlirUnmanagedDenseFloatResourceElementsAttrGet( +MlirAttribute mlirUnmanagedDenseFloatResourceElementsAttrGet( MlirType shapedType, MlirStringRef name, intptr_t numElements, const float *elements) { return getDenseResource(shapedType, name, numElements, elements); } -MLIR_CAPI_EXPORTED MlirAttribute -mlirUnmanagedDenseDoubleResourceElementsAttrGet(MlirType shapedType, - MlirStringRef name, - intptr_t numElements, - const double *elements) { +MlirAttribute mlirUnmanagedDenseDoubleResourceElementsAttrGet( + MlirType shapedType, MlirStringRef name, intptr_t numElements, + const double *elements) { return getDenseResource(shapedType, name, numElements, elements); } - template static T getDenseResourceVal(MlirAttribute attr, intptr_t pos) { return (*llvm::cast(unwrap(attr)).tryGetAsArrayRef())[pos]; } -MLIR_CAPI_EXPORTED bool -mlirDenseBoolResourceElementsAttrGetValue(MlirAttribute attr, intptr_t pos) { +bool mlirDenseBoolResourceElementsAttrGetValue(MlirAttribute attr, + intptr_t pos) { return getDenseResourceVal(attr, pos); } -MLIR_CAPI_EXPORTED uint8_t -mlirDenseUInt8ResourceElementsAttrGetValue(MlirAttribute attr, intptr_t pos) { +uint8_t mlirDenseUInt8ResourceElementsAttrGetValue(MlirAttribute attr, + intptr_t pos) { return getDenseResourceVal(attr, pos); } -MLIR_CAPI_EXPORTED uint16_t -mlirDenseUInt16ResourceElementsAttrGetValue(MlirAttribute attr, intptr_t pos) { +uint16_t mlirDenseUInt16ResourceElementsAttrGetValue(MlirAttribute attr, + intptr_t pos) { return getDenseResourceVal(attr, pos); } -MLIR_CAPI_EXPORTED uint32_t -mlirDenseUInt32ResourceElementsAttrGetValue(MlirAttribute attr, intptr_t pos) { +uint32_t mlirDenseUInt32ResourceElementsAttrGetValue(MlirAttribute attr, + intptr_t pos) { return getDenseResourceVal(attr, pos); } -MLIR_CAPI_EXPORTED uint64_t -mlirDenseUInt64ResourceElementsAttrGetValue(MlirAttribute attr, intptr_t pos) { +uint64_t mlirDenseUInt64ResourceElementsAttrGetValue(MlirAttribute attr, + intptr_t pos) { return getDenseResourceVal(attr, pos); } -MLIR_CAPI_EXPORTED int8_t -mlirDenseInt8ResourceElementsAttrGetValue(MlirAttribute attr, intptr_t pos) { +int8_t mlirDenseInt8ResourceElementsAttrGetValue(MlirAttribute attr, + intptr_t pos) { return getDenseResourceVal(attr, pos); } -MLIR_CAPI_EXPORTED int16_t -mlirDenseInt16ResourceElementsAttrGetValue(MlirAttribute attr, intptr_t pos) { +int16_t mlirDenseInt16ResourceElementsAttrGetValue(MlirAttribute attr, + intptr_t pos) { return getDenseResourceVal(attr, pos); } -MLIR_CAPI_EXPORTED int32_t -mlirDenseInt32ResourceElementsAttrGetValue(MlirAttribute attr, intptr_t pos) { +int32_t mlirDenseInt32ResourceElementsAttrGetValue(MlirAttribute attr, + intptr_t pos) { return getDenseResourceVal(attr, pos); } -MLIR_CAPI_EXPORTED int64_t -mlirDenseInt64ResourceElementsAttrGetValue(MlirAttribute attr, intptr_t pos) { +int64_t mlirDenseInt64ResourceElementsAttrGetValue(MlirAttribute attr, + intptr_t pos) { return getDenseResourceVal(attr, pos); } -MLIR_CAPI_EXPORTED float -mlirDenseFloatResourceElementsAttrGetValue(MlirAttribute attr, intptr_t pos) { +float mlirDenseFloatResourceElementsAttrGetValue(MlirAttribute attr, + intptr_t pos) { return getDenseResourceVal(attr, pos); } -MLIR_CAPI_EXPORTED double -mlirDenseDoubleResourceElementsAttrGetValue(MlirAttribute attr, intptr_t pos) { +double mlirDenseDoubleResourceElementsAttrGetValue(MlirAttribute attr, + intptr_t pos) { return getDenseResourceVal(attr, pos); } diff --git a/mlir/test/CAPI/ir.c b/mlir/test/CAPI/ir.c index c3b78fe1762c005c6454c7983803d38c6c39478d..5725d05a3e132f7494567c7477c0eb4ab2d70177 100644 --- a/mlir/test/CAPI/ir.c +++ b/mlir/test/CAPI/ir.c @@ -35,6 +35,17 @@ static void registerAllUpstreamDialects(MlirContext ctx) { mlirDialectRegistryDestroy(registry); } +struct ResourceDeleteUserData { + const char *name; +}; +static struct ResourceDeleteUserData resourceI64BlobUserData = { + "resource_i64_blob"}; +static void reportResourceDelete(void *userData, const void *data, size_t size, + size_t align) { + fprintf(stderr, "reportResourceDelete: %s\n", + ((struct ResourceDeleteUserData *)userData)->name); +} + void populateLoopBody(MlirContext ctx, MlirBlock loopBody, MlirLocation location, MlirBlock funcBody) { MlirValue iv = mlirBlockGetArgument(loopBody, 0); @@ -1118,7 +1129,8 @@ int printBuiltinAttributes(MlirContext ctx) { const uint8_t *uint8RawData = (const uint8_t *)mlirDenseElementsAttrGetRawData(uint8Elements); - const int8_t *int8RawData = (const int8_t *)mlirDenseElementsAttrGetRawData(int8Elements); + const int8_t *int8RawData = + (const int8_t *)mlirDenseElementsAttrGetRawData(int8Elements); const uint32_t *uint32RawData = (const uint32_t *)mlirDenseElementsAttrGetRawData(uint32Elements); const int32_t *int32RawData = @@ -1127,7 +1139,8 @@ int printBuiltinAttributes(MlirContext ctx) { (const uint64_t *)mlirDenseElementsAttrGetRawData(uint64Elements); const int64_t *int64RawData = (const int64_t *)mlirDenseElementsAttrGetRawData(int64Elements); - const float *floatRawData = (const float *)mlirDenseElementsAttrGetRawData(floatElements); + const float *floatRawData = + (const float *)mlirDenseElementsAttrGetRawData(floatElements); const double *doubleRawData = (const double *)mlirDenseElementsAttrGetRawData(doubleElements); const uint16_t *bf16RawData = @@ -1268,6 +1281,14 @@ int printBuiltinAttributes(MlirContext ctx) { MlirAttribute doublesBlob = mlirUnmanagedDenseDoubleResourceElementsAttrGet( mlirRankedTensorTypeGet(2, shape, mlirF64TypeGet(ctx), encoding), mlirStringRefCreateFromCString("resource_f64"), 2, doubles); + MlirAttribute blobBlob = mlirUnmanagedDenseResourceElementsAttrGet( + mlirRankedTensorTypeGet(2, shape, mlirIntegerTypeGet(ctx, 64), encoding), + mlirStringRefCreateFromCString("resource_i64_blob"), /*data=*/uints64, + /*dataLength=*/sizeof(uints64), + /*dataAlignment=*/_Alignof(uint64_t), + /*dataIsMutable=*/false, + /*deleter=*/reportResourceDelete, + /*userData=*/(void *)&resourceI64BlobUserData); mlirAttributeDump(uint8Blob); mlirAttributeDump(uint16Blob); @@ -1279,6 +1300,7 @@ int printBuiltinAttributes(MlirContext ctx) { mlirAttributeDump(int64Blob); mlirAttributeDump(floatsBlob); mlirAttributeDump(doublesBlob); + mlirAttributeDump(blobBlob); // CHECK: dense_resource : tensor<1x2xui8> // CHECK: dense_resource : tensor<1x2xui16> // CHECK: dense_resource : tensor<1x2xui32> @@ -1289,6 +1311,7 @@ int printBuiltinAttributes(MlirContext ctx) { // CHECK: dense_resource : tensor<1x2xi64> // CHECK: dense_resource : tensor<1x2xf32> // CHECK: dense_resource : tensor<1x2xf64> + // CHECK: dense_resource : tensor<1x2xi64> if (mlirDenseUInt8ResourceElementsAttrGetValue(uint8Blob, 1) != 1 || mlirDenseUInt16ResourceElementsAttrGetValue(uint16Blob, 1) != 1 || @@ -1302,7 +1325,8 @@ int printBuiltinAttributes(MlirContext ctx) { fabsf(mlirDenseFloatResourceElementsAttrGetValue(floatsBlob, 1) - 1.0f) > 1e-6 || fabs(mlirDenseDoubleResourceElementsAttrGetValue(doublesBlob, 1) - 1.0f) > - 1e-6) + 1e-6 || + mlirDenseUInt64ResourceElementsAttrGetValue(blobBlob, 1) != 1) return 23; MlirLocation loc = mlirLocationUnknownGet(ctx); @@ -2320,9 +2344,13 @@ int main(void) { if (testDialectRegistry()) return 15; - mlirContextDestroy(ctx); - testExplicitThreadPools(); testDiagnostics(); + + // CHECK: DESTROY MAIN CONTEXT + // CHECK: reportResourceDelete: resource_i64_blob + fprintf(stderr, "DESTROY MAIN CONTEXT\n"); + mlirContextDestroy(ctx); + return 0; } diff --git a/mlir/test/python/ir/array_attributes.py b/mlir/test/python/ir/array_attributes.py index 452d860861d783a9ae5f50399626111fb775cd24..9251588a4c48a6e3611a688fd8f9ec8a41060f0a 100644 --- a/mlir/test/python/ir/array_attributes.py +++ b/mlir/test/python/ir/array_attributes.py @@ -5,6 +5,7 @@ import gc from mlir.ir import * import numpy as np +import weakref def run(f): @@ -162,7 +163,7 @@ def testGetDenseElementsBF16(): @run def testGetDenseElementsInteger4(): with Context(): - array = np.array([[2, 4, 7], [-2, -4, -8]], dtype=np.uint8) + array = np.array([[2, 4, 7], [-2, -4, -8]], dtype=np.int8) attr = DenseElementsAttr.get(array, type=IntegerType.get_signless(4)) # Note: These values don't mean much since just bit-casting. But they # shouldn't change. @@ -417,3 +418,44 @@ def testGetDenseElementsIndex(): print(arr) # CHECK: True print(arr.dtype == np.int64) + + +# CHECK-LABEL: TEST: testGetDenseResourceElementsAttr +@run +def testGetDenseResourceElementsAttr(): + def on_delete(_): + print("BACKING MEMORY DELETED") + + context = Context() + mview = memoryview(np.array([[1, 2, 3], [4, 5, 6]], dtype=np.int32)) + ref = weakref.ref(mview, on_delete) + + def test_attribute(context, mview): + with context, Location.unknown(): + element_type = IntegerType.get_signless(32) + tensor_type = RankedTensorType.get((2, 3), element_type) + resource = DenseResourceElementsAttr.get_from_buffer( + mview, "from_py", tensor_type + ) + module = Module.parse("module {}") + module.operation.attributes["test.resource"] = resource + # CHECK: test.resource = dense_resource : tensor<2x3xi32> + # CHECK: from_py: "0x04000000010000000200000003000000040000000500000006000000" + print(module) + + # Verifies type casting. + # CHECK: dense_resource : tensor<2x3xi32> + print( + DenseResourceElementsAttr(module.operation.attributes["test.resource"]) + ) + + test_attribute(context, mview) + mview = None + gc.collect() + # CHECK: FREEING CONTEXT + print("FREEING CONTEXT") + context = None + gc.collect() + # CHECK: BACKING MEMORY DELETED + # CHECK: EXIT FUNCTION + print("EXIT FUNCTION")