[Mlir-commits] [mlir] [mlir] Refactor opaque properties to make them type-safe (PR #185157)

Krzysztof Drewniak llvmlistbot at llvm.org
Fri Mar 6 23:05:18 PST 2026


https://github.com/krzysz00 created https://github.com/llvm/llvm-project/pull/185157

At its core, this commit changes `OpaqueProperties` (aka a void*) to `PropertyRef`, which is a {TypeID, void*}, where the TypeID is the ID of the storage type of the given property (which can, as is often the case for operations, be a struct of other properties).

Long-term, this change will allow for
1) Some sort of getFooPropertyRef() on property structs, allowing individual members to be extracted generically
2) By having a property kind that is an OwningProprtyRef, generic parsing (in combination with a bunch of other changes) 3) Probably a safer C/Python API because we'll be able to indicate what's supposed to be under a given void*

While I was here, Claude noticed that the dynamic dialect code was such that no two operations would ever be deemed equivalent because their (zero-sized, nonexistent) properties would compare false. That's been fixed.

>From b909aee95d552f0103b88635fc3df9da1e858f0a Mon Sep 17 00:00:00 2001
From: Krzysztof Drewniak <krzysdrewniak at gmail.com>
Date: Mon, 2 Mar 2026 20:44:16 -0800
Subject: [PATCH] [mlir] Refactor opaque properties to make them type-safe

At its core, this commit changes `OpaqueProperties` (aka a void*) to
`PropertyRef`, which is a {TypeID, void*}, where the TypeID is the ID
of the storage type of the given property (which can, as is often the
case for operations, be a struct of other properties).

Long-term, this change will allow for
1) Some sort of getFooPropertyRef() on property structs, allowing
individual members to be extracted generically
2) By having a property kind that is an OwningProprtyRef, generic
parsing (in combination with a bunch of other changes)
3) Probably a safer C/Python API because we'll be able to indicate
what's supposed to be under a given void*

While I was here, Claude noticed that the dynamic dialect code was
such that no two operations would ever be deemed equivalent because
their (zero-sized, nonexistent) properties would compare false. That's
been fixed.

Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
 mlir/include/mlir/IR/ExtensibleDialect.h      |  18 +--
 mlir/include/mlir/IR/OpDefinition.h           |  11 +-
 mlir/include/mlir/IR/Operation.h              |  45 +++---
 mlir/include/mlir/IR/OperationSupport.h       | 149 ++++++++++--------
 .../mlir/Interfaces/InferTypeOpInterface.td   |  14 +-
 mlir/lib/CAPI/IR/IR.cpp                       |   6 +-
 mlir/lib/CAPI/Interfaces/Interfaces.cpp       |  15 +-
 mlir/lib/Dialect/AMDGPU/IR/AMDGPUOps.cpp      |   2 +-
 .../Bufferization/IR/BufferizationOps.cpp     |   2 +-
 mlir/lib/Dialect/MemRef/IR/MemRefOps.cpp      |   2 +-
 mlir/lib/Dialect/Ptr/IR/PtrDialect.cpp        |   2 +-
 mlir/lib/Dialect/SMT/IR/SMTOps.cpp            |   4 +-
 .../SparseTensor/IR/SparseTensorDialect.cpp   |  14 +-
 mlir/lib/Dialect/Tosa/IR/TosaOps.cpp          |  10 +-
 mlir/lib/Dialect/WasmSSA/IR/WasmSSAOps.cpp    |   6 +-
 mlir/lib/IR/ExtensibleDialect.cpp             |   2 +
 mlir/lib/IR/MLIRContext.cpp                   |  22 +--
 mlir/lib/IR/Operation.cpp                     |  21 ++-
 mlir/lib/IR/OperationSupport.cpp              |   2 +
 .../IRDLToCpp/Templates/PerOperationDecl.txt  |   2 +-
 .../Transforms/Utils/DialectConversion.cpp    |   8 +-
 mlir/test/lib/Dialect/Test/TestOpDefs.cpp     |  12 +-
 mlir/test/lib/Dialect/Test/TestOps.td         |   4 +-
 mlir/test/lib/Dialect/Test/TestOpsSyntax.cpp  |   2 +-
 mlir/test/lib/Dialect/Test/TestOpsSyntax.td   |   8 +-
 mlir/test/lib/Dialect/Test/TestPatterns.cpp   |   7 +-
 mlir/test/mlir-tblgen/op-decl-and-defs.td     |   2 +-
 mlir/test/python/python_test_ops.td           |   6 +-
 mlir/tools/mlir-tblgen/OpDefinitionsGen.cpp   |  43 +++--
 .../FileLineColLocBreakpointManagerTest.cpp   |   2 +-
 30 files changed, 246 insertions(+), 197 deletions(-)

diff --git a/mlir/include/mlir/IR/ExtensibleDialect.h b/mlir/include/mlir/IR/ExtensibleDialect.h
index e52306f79378a..f6b82bffcfffc 100644
--- a/mlir/include/mlir/IR/ExtensibleDialect.h
+++ b/mlir/include/mlir/IR/ExtensibleDialect.h
@@ -550,25 +550,23 @@ class DynamicOpDefinition : public OperationName::Impl {
     return success();
   }
   int getOpPropertyByteSize() final { return 0; }
-  void initProperties(OperationName opName, OpaqueProperties storage,
-                      OpaqueProperties init) final {}
-  void deleteProperties(OpaqueProperties prop) final {}
+  void initProperties(OperationName opName, PropertyRef storage,
+                      PropertyRef init) final {}
+  void deleteProperties(PropertyRef prop) final {}
   void populateDefaultProperties(OperationName opName,
-                                 OpaqueProperties properties) final {}
+                                 PropertyRef properties) final {}
 
   LogicalResult
-  setPropertiesFromAttr(OperationName opName, OpaqueProperties properties,
+  setPropertiesFromAttr(OperationName opName, PropertyRef properties,
                         Attribute attr,
                         function_ref<InFlightDiagnostic()> emitError) final {
     emitError() << "extensible Dialects don't support properties";
     return failure();
   }
   Attribute getPropertiesAsAttr(Operation *op) final { return {}; }
-  void copyProperties(OpaqueProperties lhs, OpaqueProperties rhs) final {}
-  bool compareProperties(OpaqueProperties, OpaqueProperties) final {
-    return false;
-  }
-  llvm::hash_code hashProperties(OpaqueProperties prop) final { return {}; }
+  void copyProperties(PropertyRef lhs, PropertyRef rhs) final {}
+  bool compareProperties(PropertyRef, PropertyRef) final { return true; }
+  llvm::hash_code hashProperties(PropertyRef prop) final { return {}; }
 
 private:
   DynamicOpDefinition(
diff --git a/mlir/include/mlir/IR/OpDefinition.h b/mlir/include/mlir/IR/OpDefinition.h
index be92fe0a6c7e3..e886ac45675e2 100644
--- a/mlir/include/mlir/IR/OpDefinition.h
+++ b/mlir/include/mlir/IR/OpDefinition.h
@@ -243,9 +243,10 @@ class OpState {
   /// so we can cast it away here.
   explicit OpState(Operation *state) : state(state) {}
 
-  /// For all op which don't have properties, we keep a single instance of
-  /// `EmptyProperties` to be used where a reference to a properties is needed:
-  /// this allow to bind a pointer to the reference without triggering UB.
+  /// For all ops which don't have properties, we keep a single instance of
+  /// `EmptyProperties` to be used where a pointer to a struct of properties
+  /// is needed: this allows binding a pointer to the reference without
+  /// triggering UB.
   static EmptyProperties &getEmptyProperties() {
     static EmptyProperties emptyProperties;
     return emptyProperties;
@@ -1978,7 +1979,7 @@ class Op : public OpState, public Traits<ConcreteType>... {
     if constexpr (!hasProperties())
       return getEmptyProperties();
     return *getOperation()
-                ->getPropertiesStorageUnsafe()
+                ->getPropertiesStorage()
                 .template as<InferredProperties<T> *>();
   }
 
@@ -2152,4 +2153,6 @@ struct DenseMapInfo<T,
 };
 } // namespace llvm
 
+MLIR_DECLARE_EXPLICIT_TYPE_ID(mlir::EmptyProperties)
+
 #endif
diff --git a/mlir/include/mlir/IR/Operation.h b/mlir/include/mlir/IR/Operation.h
index ea9fbab4acf9c..cb4b0e4688296 100644
--- a/mlir/include/mlir/IR/Operation.h
+++ b/mlir/include/mlir/IR/Operation.h
@@ -92,17 +92,15 @@ class alignas(8) Operation final
   /// necessary.
   static Operation *create(Location location, OperationName name,
                            TypeRange resultTypes, ValueRange operands,
-                           NamedAttrList &&attributes,
-                           OpaqueProperties properties, BlockRange successors,
-                           unsigned numRegions);
+                           NamedAttrList &&attributes, PropertyRef properties,
+                           BlockRange successors, unsigned numRegions);
 
   /// Create a new Operation with the specific fields. This constructor uses an
   /// existing attribute dictionary to avoid uniquing a list of attributes.
   static Operation *create(Location location, OperationName name,
                            TypeRange resultTypes, ValueRange operands,
-                           DictionaryAttr attributes,
-                           OpaqueProperties properties, BlockRange successors,
-                           unsigned numRegions);
+                           DictionaryAttr attributes, PropertyRef properties,
+                           BlockRange successors, unsigned numRegions);
 
   /// Create a new Operation from the fields stored in `state`.
   static Operation *create(const OperationState &state);
@@ -110,8 +108,7 @@ class alignas(8) Operation final
   /// Create a new Operation with the specific fields.
   static Operation *create(Location location, OperationName name,
                            TypeRange resultTypes, ValueRange operands,
-                           NamedAttrList &&attributes,
-                           OpaqueProperties properties,
+                           NamedAttrList &&attributes, PropertyRef properties,
                            BlockRange successors = {},
                            RegionRange regions = {});
 
@@ -904,23 +901,29 @@ class alignas(8) Operation final
   int getPropertiesStorageSize() const {
     return ((int)propertiesStorageSize) * 8;
   }
-  /// Returns the properties storage.
-  OpaqueProperties getPropertiesStorage() {
+
+  /// Return a geeric (but typed) reference to the property type storage.
+  PropertyRef getPropertiesStorage() {
     if (propertiesStorageSize)
-      return getPropertiesStorageUnsafe();
+      return PropertyRef(name.getOpPropertiesTypeID(),
+                         getRawPropertiesStorageUnsafe());
     return {nullptr};
   }
-  OpaqueProperties getPropertiesStorage() const {
+
+  PropertyRef getPropertiesStorage() const {
     if (propertiesStorageSize)
-      return {reinterpret_cast<void *>(const_cast<detail::OpProperties *>(
-          getTrailingObjects<detail::OpProperties>()))};
+      return PropertyRef(
+          name.getOpPropertiesTypeID(),
+          reinterpret_cast<void *>(const_cast<detail::OpProperties *>(
+              getTrailingObjects<detail::OpProperties>())));
     return {nullptr};
   }
-  /// Returns the properties storage without checking whether properties are
-  /// present.
-  OpaqueProperties getPropertiesStorageUnsafe() {
-    return {
-        reinterpret_cast<void *>(getTrailingObjects<detail::OpProperties>())};
+
+  /// Returns a pointer to the properties storage (if it exists) with no type
+  /// information.
+  void *getRawPropertiesStorageUnsafe() {
+    return reinterpret_cast<void *>(const_cast<detail::OpProperties *>(
+        getTrailingObjects<detail::OpProperties>()));
   }
 
   /// Return the properties converted to an attribute.
@@ -940,7 +943,7 @@ class alignas(8) Operation final
 
   /// Copy properties from an existing other properties object. The two objects
   /// must be the same type.
-  void copyProperties(OpaqueProperties rhs);
+  void copyProperties(PropertyRef rhs);
 
   /// Compute a hash for the op properties (if any).
   llvm::hash_code hashProperties();
@@ -969,7 +972,7 @@ class alignas(8) Operation final
   Operation(Location location, OperationName name, unsigned numResults,
             unsigned numSuccessors, unsigned numRegions,
             int propertiesStorageSize, DictionaryAttr attributes,
-            OpaqueProperties properties, bool hasOperandStorage);
+            PropertyRef properties, bool hasOperandStorage);
 
   // Operations are deleted through the destroy() member because they are
   // allocated with malloc.
diff --git a/mlir/include/mlir/IR/OperationSupport.h b/mlir/include/mlir/IR/OperationSupport.h
index ac069df1d3e7d..f91661bebce75 100644
--- a/mlir/include/mlir/IR/OperationSupport.h
+++ b/mlir/include/mlir/IR/OperationSupport.h
@@ -63,22 +63,34 @@ template <typename ValueRangeT>
 class ValueTypeRange;
 
 //===----------------------------------------------------------------------===//
-// OpaqueProperties
+// PropertyRef
 //===----------------------------------------------------------------------===//
 
-/// Simple wrapper around a void* in order to express generically how to pass
-/// in op properties through APIs.
-class OpaqueProperties {
+/// Type-safe wrapper around a void* for passing properties, including the
+/// properties structs of operations, generically through APIs. Pairs data with
+/// a TypeID for assert-based type checking. Note that the type in the type ID
+/// is the **storage** type of the property, and that the default object has a
+/// null data pointer and a type ID equal to the type ID for `void`.
+class PropertyRef {
 public:
-  OpaqueProperties(void *prop) : properties(prop) {}
-  operator bool() const { return properties != nullptr; }
+  PropertyRef() = default;
+  PropertyRef(std::nullptr_t) {}
+  PropertyRef(TypeID typeID, void *data) : typeID(typeID), data(data) {}
+  operator bool() const { return data != nullptr; }
   template <typename Dest>
   Dest as() const {
-    return static_cast<Dest>(const_cast<void *>(properties));
+    static_assert(std::is_pointer_v<Dest>,
+                  "PropertyRef::as<T>() requires T to be a pointer type");
+    using RawType = std::remove_cv_t<std::remove_pointer_t<Dest>>;
+    assert((!data || typeID == TypeID::get<RawType>()) &&
+           "Property type mismatch: TypeID does not match requested type");
+    return static_cast<Dest>(data);
   }
+  TypeID getTypeID() const { return typeID; }
 
 private:
-  void *properties;
+  TypeID typeID;
+  void *data = nullptr;
 };
 
 //===----------------------------------------------------------------------===//
@@ -130,18 +142,18 @@ class OperationName {
     verifyInherentAttrs(OperationName opName, NamedAttrList &attributes,
                         function_ref<InFlightDiagnostic()> emitError) = 0;
     virtual int getOpPropertyByteSize() = 0;
-    virtual void initProperties(OperationName opName, OpaqueProperties storage,
-                                OpaqueProperties init) = 0;
-    virtual void deleteProperties(OpaqueProperties) = 0;
+    virtual void initProperties(OperationName opName, PropertyRef storage,
+                                PropertyRef init) = 0;
+    virtual void deleteProperties(PropertyRef) = 0;
     virtual void populateDefaultProperties(OperationName opName,
-                                           OpaqueProperties properties) = 0;
+                                           PropertyRef properties) = 0;
     virtual LogicalResult
-    setPropertiesFromAttr(OperationName, OpaqueProperties, Attribute,
+    setPropertiesFromAttr(OperationName, PropertyRef, Attribute,
                           function_ref<InFlightDiagnostic()> emitError) = 0;
     virtual Attribute getPropertiesAsAttr(Operation *) = 0;
-    virtual void copyProperties(OpaqueProperties, OpaqueProperties) = 0;
-    virtual bool compareProperties(OpaqueProperties, OpaqueProperties) = 0;
-    virtual llvm::hash_code hashProperties(OpaqueProperties) = 0;
+    virtual void copyProperties(PropertyRef, PropertyRef) = 0;
+    virtual bool compareProperties(PropertyRef, PropertyRef) = 0;
+    virtual llvm::hash_code hashProperties(PropertyRef) = 0;
   };
 
 public:
@@ -160,6 +172,7 @@ class OperationName {
     Dialect *getDialect() const { return dialect; }
     StringAttr getName() const { return name; }
     TypeID getTypeID() const { return typeID; }
+    TypeID getPropertiesTypeID() const { return propertiesTypeID; }
     ArrayRef<StringAttr> getAttributeNames() const { return attributeNames; }
 
   protected:
@@ -186,13 +199,20 @@ class OperationName {
     /// lookup/creation/etc., as opposed to raw strings.
     ArrayRef<StringAttr> attributeNames;
 
+    /// The TypeID of the Properties struct for this operation.
+    TypeID propertiesTypeID;
+
     friend class RegisteredOperationName;
   };
 
 protected:
   /// Default implementation for unregistered operations.
   struct UnregisteredOpModel : public Impl {
-    using Impl::Impl;
+    UnregisteredOpModel(StringAttr name, Dialect *dialect, TypeID typeID,
+                        detail::InterfaceMap interfaceMap)
+        : Impl(name, dialect, typeID, std::move(interfaceMap)) {
+      propertiesTypeID = TypeID::get<Attribute>();
+    }
     LogicalResult foldHook(Operation *, ArrayRef<Attribute>,
                            SmallVectorImpl<OpFoldResult> &) final;
     void getCanonicalizationPatterns(RewritePatternSet &, MLIRContext *) final;
@@ -211,18 +231,18 @@ class OperationName {
     verifyInherentAttrs(OperationName opName, NamedAttrList &attributes,
                         function_ref<InFlightDiagnostic()> emitError) final;
     int getOpPropertyByteSize() final;
-    void initProperties(OperationName opName, OpaqueProperties storage,
-                        OpaqueProperties init) final;
-    void deleteProperties(OpaqueProperties) final;
+    void initProperties(OperationName opName, PropertyRef storage,
+                        PropertyRef init) final;
+    void deleteProperties(PropertyRef) final;
     void populateDefaultProperties(OperationName opName,
-                                   OpaqueProperties properties) final;
+                                   PropertyRef properties) final;
     LogicalResult
-    setPropertiesFromAttr(OperationName, OpaqueProperties, Attribute,
+    setPropertiesFromAttr(OperationName, PropertyRef, Attribute,
                           function_ref<InFlightDiagnostic()> emitError) final;
     Attribute getPropertiesAsAttr(Operation *) final;
-    void copyProperties(OpaqueProperties, OpaqueProperties) final;
-    bool compareProperties(OpaqueProperties, OpaqueProperties) final;
-    llvm::hash_code hashProperties(OpaqueProperties) final;
+    void copyProperties(PropertyRef, PropertyRef) final;
+    bool compareProperties(PropertyRef, PropertyRef) final;
+    llvm::hash_code hashProperties(PropertyRef) final;
   };
 
 public:
@@ -413,18 +433,23 @@ class OperationName {
     return getImpl()->getOpPropertyByteSize();
   }
 
+  /// Return the TypeID of the op properties.
+  TypeID getOpPropertiesTypeID() const {
+    return getImpl()->getPropertiesTypeID();
+  }
+
   /// This hooks destroy the op properties.
-  void destroyOpProperties(OpaqueProperties properties) const {
+  void destroyOpProperties(PropertyRef properties) const {
     getImpl()->deleteProperties(properties);
   }
 
   /// Initialize the op properties.
-  void initOpProperties(OpaqueProperties storage, OpaqueProperties init) const {
+  void initOpProperties(PropertyRef storage, PropertyRef init) const {
     getImpl()->initProperties(*this, storage, init);
   }
 
   /// Set the default values on the ODS attribute in the properties.
-  void populateDefaultProperties(OpaqueProperties properties) const {
+  void populateDefaultProperties(PropertyRef properties) const {
     getImpl()->populateDefaultProperties(*this, properties);
   }
 
@@ -435,21 +460,21 @@ class OperationName {
 
   /// Define the op properties from the provided Attribute.
   LogicalResult setOpPropertiesFromAttribute(
-      OperationName opName, OpaqueProperties properties, Attribute attr,
+      OperationName opName, PropertyRef properties, Attribute attr,
       function_ref<InFlightDiagnostic()> emitError) const {
     return getImpl()->setPropertiesFromAttr(opName, properties, attr,
                                             emitError);
   }
 
-  void copyOpProperties(OpaqueProperties lhs, OpaqueProperties rhs) const {
+  void copyOpProperties(PropertyRef lhs, PropertyRef rhs) const {
     return getImpl()->copyProperties(lhs, rhs);
   }
 
-  bool compareOpProperties(OpaqueProperties lhs, OpaqueProperties rhs) const {
+  bool compareOpProperties(PropertyRef lhs, PropertyRef rhs) const {
     return getImpl()->compareProperties(lhs, rhs);
   }
 
-  llvm::hash_code hashOpProperties(OpaqueProperties properties) const {
+  llvm::hash_code hashOpProperties(PropertyRef properties) const {
     return getImpl()->hashProperties(properties);
   }
 
@@ -528,9 +553,13 @@ class RegisteredOperationName : public OperationName {
   /// to a concrete op implementation.
   template <typename ConcreteOp>
   struct Model : public Impl {
+    using Properties = std::remove_reference_t<
+        decltype(std::declval<ConcreteOp>().getProperties())>;
     Model(Dialect *dialect)
         : Impl(ConcreteOp::getOperationName(), dialect,
-               TypeID::get<ConcreteOp>(), ConcreteOp::getInterfaceMap()) {}
+               TypeID::get<ConcreteOp>(), ConcreteOp::getInterfaceMap()) {
+      propertiesTypeID = TypeID::get<Properties>();
+    }
     LogicalResult foldHook(Operation *op, ArrayRef<Attribute> attrs,
                            SmallVectorImpl<OpFoldResult> &results) final {
       return ConcreteOp::getFoldHookFn()(op, attrs, results);
@@ -560,9 +589,6 @@ class RegisteredOperationName : public OperationName {
 
     /// Implementation for "Properties"
 
-    using Properties = std::remove_reference_t<
-        decltype(std::declval<ConcreteOp>().getProperties())>;
-
     std::optional<Attribute> getInherentAttr(Operation *op,
                                              StringRef name) final {
       if constexpr (hasProperties) {
@@ -606,8 +632,8 @@ class RegisteredOperationName : public OperationName {
         return sizeof(Properties);
       return 0;
     }
-    void initProperties(OperationName opName, OpaqueProperties storage,
-                        OpaqueProperties init) final {
+    void initProperties(OperationName opName, PropertyRef storage,
+                        PropertyRef init) final {
       using Properties =
           typename ConcreteOp::template InferredProperties<ConcreteOp>;
       if (init)
@@ -618,18 +644,18 @@ class RegisteredOperationName : public OperationName {
         ConcreteOp::populateDefaultProperties(opName,
                                               *storage.as<Properties *>());
     }
-    void deleteProperties(OpaqueProperties prop) final {
+    void deleteProperties(PropertyRef prop) final {
       prop.as<Properties *>()->~Properties();
     }
     void populateDefaultProperties(OperationName opName,
-                                   OpaqueProperties properties) final {
+                                   PropertyRef properties) final {
       if constexpr (hasProperties)
         ConcreteOp::populateDefaultProperties(opName,
                                               *properties.as<Properties *>());
     }
 
     LogicalResult
-    setPropertiesFromAttr(OperationName opName, OpaqueProperties properties,
+    setPropertiesFromAttr(OperationName opName, PropertyRef properties,
                           Attribute attr,
                           function_ref<InFlightDiagnostic()> emitError) final {
       if constexpr (hasProperties) {
@@ -647,15 +673,15 @@ class RegisteredOperationName : public OperationName {
       }
       return {};
     }
-    bool compareProperties(OpaqueProperties lhs, OpaqueProperties rhs) final {
+    bool compareProperties(PropertyRef lhs, PropertyRef rhs) final {
       if constexpr (hasProperties)
         return *lhs.as<Properties *>() == *rhs.as<Properties *>();
       return true;
     }
-    void copyProperties(OpaqueProperties lhs, OpaqueProperties rhs) final {
+    void copyProperties(PropertyRef lhs, PropertyRef rhs) final {
       *lhs.as<Properties *>() = *rhs.as<Properties *>();
     }
-    llvm::hash_code hashProperties(OpaqueProperties prop) final {
+    llvm::hash_code hashProperties(PropertyRef prop) final {
       if constexpr (hasProperties)
         return ConcreteOp::computePropertiesHash(*prop.as<Properties *>());
 
@@ -959,11 +985,9 @@ struct OperationState {
   Attribute propertiesAttr;
 
 private:
-  OpaqueProperties properties = nullptr;
-  TypeID propertiesId;
-  llvm::function_ref<void(OpaqueProperties)> propertiesDeleter;
-  llvm::function_ref<void(OpaqueProperties, const OpaqueProperties)>
-      propertiesSetter;
+  PropertyRef properties;
+  llvm::function_ref<void(PropertyRef)> propertiesDeleter;
+  llvm::function_ref<void(PropertyRef, const PropertyRef)> propertiesSetter;
   friend class Operation;
 
 public:
@@ -984,13 +1008,13 @@ struct OperationState {
   OperationState &operator=(const OperationState &other) = delete;
   ~OperationState();
 
-  /// Get (or create) a properties of the provided type to be set on the
+  /// Get (or create) the properties of the provided type to be set on the
   /// operation on creation.
   template <typename T>
   T &getOrAddProperties() {
     if (!properties) {
       T *p = new T{};
-      properties = p;
+      properties = PropertyRef(TypeID::get<T>(), p);
 #if defined(__clang__)
 #if __has_warning("-Wdangling-assignment-gsl")
 #pragma clang diagnostic push
@@ -998,11 +1022,8 @@ struct OperationState {
 #pragma clang diagnostic ignored "-Wdangling-assignment-gsl"
 #endif
 #endif
-      propertiesDeleter = [](OpaqueProperties prop) {
-        delete prop.as<const T *>();
-      };
-      propertiesSetter = [](OpaqueProperties newProp,
-                            const OpaqueProperties prop) {
+      propertiesDeleter = [](PropertyRef prop) { delete prop.as<const T *>(); };
+      propertiesSetter = [](PropertyRef newProp, const PropertyRef prop) {
         *newProp.as<T *>() = *prop.as<const T *>();
       };
 #if defined(__clang__)
@@ -1010,12 +1031,12 @@ struct OperationState {
 #pragma clang diagnostic pop
 #endif
 #endif
-      propertiesId = TypeID::get<T>();
     }
-    assert(propertiesId == TypeID::get<T>() && "Inconsistent properties");
+    assert(properties.getTypeID() == TypeID::get<T>() &&
+           "Inconsistent properties");
     return *properties.as<T *>();
   }
-  OpaqueProperties getRawProperties() { return properties; }
+  PropertyRef getRawProperties() { return properties; }
 
   // Set the properties defined on this OpState on the given operation,
   // optionally emit diagnostics on error through the provided diagnostic.
@@ -1026,13 +1047,13 @@ struct OperationState {
   // Make `newProperties` the source of the properties that will be copied into
   // the operation. The memory referenced by `newProperties` must remain live
   // until after the `Operation` is created, at which time it may be
-  // deallocated. Calls to `getOrAddProperties<>() will return references to
+  // deallocated. Calls to `getOrAddProperties<>()` will return references to
   // this memory.
   template <typename T>
   void useProperties(T &newProperties) {
     assert(!properties &&
            "Can't provide a properties struct when one has been allocated");
-    properties = &newProperties;
+    properties = PropertyRef(TypeID::get<T>(), &newProperties);
 #if defined(__clang__)
 #if __has_warning("-Wdangling-assignment-gsl")
 #pragma clang diagnostic push
@@ -1040,9 +1061,8 @@ struct OperationState {
 #pragma clang diagnostic ignored "-Wdangling-assignment-gsl"
 #endif
 #endif
-    propertiesDeleter = [](OpaqueProperties) {};
-    propertiesSetter = [](OpaqueProperties newProp,
-                          const OpaqueProperties prop) {
+    propertiesDeleter = [](PropertyRef) {};
+    propertiesSetter = [](PropertyRef newProp, const PropertyRef prop) {
       *newProp.as<T *>() = *prop.as<const T *>();
     };
 #if defined(__clang__)
@@ -1050,7 +1070,6 @@ struct OperationState {
 #pragma clang diagnostic pop
 #endif
 #endif
-    propertiesId = TypeID::get<T>();
   }
 
   void addOperands(ValueRange newOperands);
diff --git a/mlir/include/mlir/Interfaces/InferTypeOpInterface.td b/mlir/include/mlir/Interfaces/InferTypeOpInterface.td
index 67568f731f597..36397d596aed6 100644
--- a/mlir/include/mlir/Interfaces/InferTypeOpInterface.td
+++ b/mlir/include/mlir/Interfaces/InferTypeOpInterface.td
@@ -53,7 +53,7 @@ def InferTypeOpInterface : OpInterface<"InferTypeOpInterface"> {
                     "::std::optional<::mlir::Location>":$location,
                     "::mlir::ValueRange":$operands,
                     "::mlir::DictionaryAttr":$attributes,
-                    "::mlir::OpaqueProperties":$properties,
+                    "::mlir::PropertyRef":$properties,
                     "::mlir::RegionRange":$regions,
                     "::llvm::SmallVectorImpl<::mlir::Type>&":$inferredReturnTypes)
     >,
@@ -87,7 +87,7 @@ def InferTypeOpInterface : OpInterface<"InferTypeOpInterface"> {
                     "::std::optional<::mlir::Location>":$location,
                     "::mlir::ValueRange":$operands,
                     "::mlir::DictionaryAttr":$attributes,
-                    "::mlir::OpaqueProperties":$properties,
+                    "::mlir::PropertyRef":$properties,
                     "::mlir::RegionRange":$regions,
                     "::llvm::SmallVectorImpl<::mlir::Type>&":$returnTypes),
       /*methodBody=*/[{}],
@@ -164,7 +164,7 @@ def InferShapedTypeOpInterface : OpInterface<"InferShapedTypeOpInterface"> {
                     "::std::optional<::mlir::Location>":$location,
                     "::mlir::ValueShapeRange":$operands,
                     "::mlir::DictionaryAttr":$attributes,
-                    "::mlir::OpaqueProperties":$properties,
+                    "::mlir::PropertyRef":$properties,
                     "::mlir::RegionRange":$regions,
                     "::llvm::SmallVectorImpl<::mlir::ShapedTypeComponents>&":
                       $inferredReturnShapes),
@@ -220,7 +220,7 @@ class InferTypeOpAdaptorBase<code additionalDecls = [{}]> : TraitList<
         $cppClass::inferReturnTypes(::mlir::MLIRContext *context,
                           std::optional<::mlir::Location> location,
                           ::mlir::ValueRange operands, ::mlir::DictionaryAttr attributes,
-                          ::mlir::OpaqueProperties properties, ::mlir::RegionRange regions,
+                          ::mlir::PropertyRef properties, ::mlir::RegionRange regions,
                           ::llvm::SmallVectorImpl<::mlir::Type> &inferredReturnTypes) {
           $cppClass::Adaptor adaptor(operands, attributes, properties, regions);
           return $cppClass::inferReturnTypes(context,
@@ -258,7 +258,7 @@ class InferShapedTypeOpAdaptorBase<list<string> overridenMethods = []> : TraitLi
         $cppClass::inferReturnTypeComponents(::mlir::MLIRContext *context,
                           std::optional<::mlir::Location> location,
                           ::mlir::ValueShapeRange operands, ::mlir::DictionaryAttr attributes,
-                          ::mlir::OpaqueProperties properties, ::mlir::RegionRange regions,
+                          ::mlir::PropertyRef properties, ::mlir::RegionRange regions,
                           ::llvm::SmallVectorImpl<::mlir::ShapedTypeComponents> &inferredReturnShapes) {
           $cppClass::Adaptor adaptor(operands, attributes, properties, regions);
           return $cppClass::inferReturnTypeComponents(context,
@@ -293,7 +293,7 @@ class InferTensorTypeBase<list<string> overridenMethods = []> : TraitList<
         $cppClass::inferReturnTypes(::mlir::MLIRContext *context,
                           std::optional<::mlir::Location> location,
                           ::mlir::ValueRange operands, ::mlir::DictionaryAttr attributes,
-                          ::mlir::OpaqueProperties properties, ::mlir::RegionRange regions,
+                          ::mlir::PropertyRef properties, ::mlir::RegionRange regions,
                           ::llvm::SmallVectorImpl<::mlir::Type> &inferredReturnTypes) {
           ::llvm::SmallVector<::mlir::ShapedTypeComponents, 2> retComponents;
           if (failed($cppClass::inferReturnTypeComponents(context, location,
@@ -331,7 +331,7 @@ class InferTensorTypeAdaptorBase<list<string> overridenMethods = []> : TraitList
         $cppClass::inferReturnTypes(::mlir::MLIRContext *context,
                           std::optional<::mlir::Location> location,
                           ::mlir::ValueRange operands, ::mlir::DictionaryAttr attributes,
-                          ::mlir::OpaqueProperties properties, ::mlir::RegionRange regions,
+                          ::mlir::PropertyRef properties, ::mlir::RegionRange regions,
                           ::llvm::SmallVectorImpl<::mlir::Type> &inferredReturnTypes) {
           SmallVector<ShapedTypeComponents, 2> retComponents;
           if (failed($cppClass::inferReturnTypeComponents(context, location,
diff --git a/mlir/lib/CAPI/IR/IR.cpp b/mlir/lib/CAPI/IR/IR.cpp
index 29f9287279b8f..5c5145b0a5acc 100644
--- a/mlir/lib/CAPI/IR/IR.cpp
+++ b/mlir/lib/CAPI/IR/IR.cpp
@@ -552,11 +552,11 @@ static LogicalResult inferOperationTypes(OperationState &state) {
   }
 
   DictionaryAttr attributes = state.attributes.getDictionary(context);
-  OpaqueProperties properties = state.getRawProperties();
+  PropertyRef properties = state.getRawProperties();
 
   if (!properties && info->getOpPropertyByteSize() > 0 && !attributes.empty()) {
-    auto prop = std::make_unique<char[]>(info->getOpPropertyByteSize());
-    properties = OpaqueProperties(prop.get());
+    auto propAlloc = std::make_unique<char[]>(info->getOpPropertyByteSize());
+    properties = PropertyRef(info->getOpPropertiesTypeID(), propAlloc.get());
     if (properties) {
       auto emitError = [&]() {
         return mlir::emitError(state.location)
diff --git a/mlir/lib/CAPI/Interfaces/Interfaces.cpp b/mlir/lib/CAPI/Interfaces/Interfaces.cpp
index 437a1dbab9dae..73b5d2505eac5 100644
--- a/mlir/lib/CAPI/Interfaces/Interfaces.cpp
+++ b/mlir/lib/CAPI/Interfaces/Interfaces.cpp
@@ -107,9 +107,15 @@ MlirLogicalResult mlirInferTypeOpInterfaceInferReturnTypes(
       unwrapRegions(nRegions, regions);
 
   SmallVector<Type> inferredTypes;
+  // The C API passes an opaque void*; we trust the caller to pass the correct
+  // properties type for this operation.
+  // TODO: Create a C API that's more type-safe.
+  PropertyRef propertyRef =
+      properties ? PropertyRef(info->getOpPropertiesTypeID(), properties)
+                 : PropertyRef(nullptr);
   if (failed(info->getInterface<InferTypeOpInterface>()->inferReturnTypes(
           unwrap(context), maybeLocation, unwrappedOperands, attributeDict,
-          properties, unwrappedRegions, inferredTypes)))
+          propertyRef, unwrappedRegions, inferredTypes)))
     return mlirLogicalResultFailure();
 
   SmallVector<MlirType> wrappedInferredTypes;
@@ -141,11 +147,16 @@ MlirLogicalResult mlirInferShapedTypeOpInterfaceInferReturnTypes(
       unwrapRegions(nRegions, regions);
 
   SmallVector<ShapedTypeComponents> inferredTypeComponents;
+  // The C API passes an opaque void*; we trust the caller to pass the correct
+  // properties type for this operation.
+  PropertyRef propertyRef =
+      properties ? PropertyRef(info->getOpPropertiesTypeID(), properties)
+                 : PropertyRef(nullptr);
   if (failed(info->getInterface<InferShapedTypeOpInterface>()
                  ->inferReturnTypeComponents(
                      unwrap(context), maybeLocation,
                      mlir::ValueRange(llvm::ArrayRef(unwrappedOperands)),
-                     attributeDict, properties, unwrappedRegions,
+                     attributeDict, propertyRef, unwrappedRegions,
                      inferredTypeComponents)))
     return mlirLogicalResultFailure();
 
diff --git a/mlir/lib/Dialect/AMDGPU/IR/AMDGPUOps.cpp b/mlir/lib/Dialect/AMDGPU/IR/AMDGPUOps.cpp
index f452d2de15dc8..724c6b3aab951 100644
--- a/mlir/lib/Dialect/AMDGPU/IR/AMDGPUOps.cpp
+++ b/mlir/lib/Dialect/AMDGPU/IR/AMDGPUOps.cpp
@@ -103,7 +103,7 @@ static FailureOr<MemRefType> getFatRawBufferTypeLike(MemRefType source,
 
 LogicalResult FatRawBufferCastOp::inferReturnTypes(
     MLIRContext *context, std::optional<Location> location, ValueRange operands,
-    DictionaryAttr attributes, OpaqueProperties properties, RegionRange regions,
+    DictionaryAttr attributes, PropertyRef properties, RegionRange regions,
     SmallVectorImpl<Type> &inferredReturnTypes) {
   Adaptor adaptor(operands, attributes, properties, regions);
   auto sourceType =
diff --git a/mlir/lib/Dialect/Bufferization/IR/BufferizationOps.cpp b/mlir/lib/Dialect/Bufferization/IR/BufferizationOps.cpp
index 71ad8bbf91c3b..c525ec116f699 100644
--- a/mlir/lib/Dialect/Bufferization/IR/BufferizationOps.cpp
+++ b/mlir/lib/Dialect/Bufferization/IR/BufferizationOps.cpp
@@ -908,7 +908,7 @@ std::optional<Value> CloneOp::buildClone(OpBuilder &builder, Value alloc) {
 
 LogicalResult DeallocOp::inferReturnTypes(
     MLIRContext *context, std::optional<::mlir::Location> location,
-    ValueRange operands, DictionaryAttr attributes, OpaqueProperties properties,
+    ValueRange operands, DictionaryAttr attributes, PropertyRef properties,
     RegionRange regions, SmallVectorImpl<Type> &inferredReturnTypes) {
   DeallocOpAdaptor adaptor(operands, attributes, properties, regions);
   inferredReturnTypes = SmallVector<Type>(adaptor.getRetained().size(),
diff --git a/mlir/lib/Dialect/MemRef/IR/MemRefOps.cpp b/mlir/lib/Dialect/MemRef/IR/MemRefOps.cpp
index 844e6183cff06..4a2028b7ce4f2 100644
--- a/mlir/lib/Dialect/MemRef/IR/MemRefOps.cpp
+++ b/mlir/lib/Dialect/MemRef/IR/MemRefOps.cpp
@@ -634,7 +634,7 @@ LogicalResult DistinctObjectsOp::verify() {
 LogicalResult DistinctObjectsOp::inferReturnTypes(
     MLIRContext * /*context*/, std::optional<Location> /*location*/,
     ValueRange operands, DictionaryAttr /*attributes*/,
-    OpaqueProperties /*properties*/, RegionRange /*regions*/,
+    PropertyRef /*properties*/, RegionRange /*regions*/,
     SmallVectorImpl<Type> &inferredReturnTypes) {
   llvm::copy(operands.getTypes(), std::back_inserter(inferredReturnTypes));
   return success();
diff --git a/mlir/lib/Dialect/Ptr/IR/PtrDialect.cpp b/mlir/lib/Dialect/Ptr/IR/PtrDialect.cpp
index 51f25f755a8a6..444197d12097f 100644
--- a/mlir/lib/Dialect/Ptr/IR/PtrDialect.cpp
+++ b/mlir/lib/Dialect/Ptr/IR/PtrDialect.cpp
@@ -355,7 +355,7 @@ OpFoldResult PtrAddOp::fold(FoldAdaptor adaptor) {
 
 LogicalResult PtrAddOp::inferReturnTypes(
     MLIRContext *context, std::optional<Location> location, ValueRange operands,
-    DictionaryAttr attributes, OpaqueProperties properties, RegionRange regions,
+    DictionaryAttr attributes, PropertyRef properties, RegionRange regions,
     SmallVectorImpl<Type> &inferredReturnTypes) {
   // Get the base pointer and offset types.
   Type baseType = operands[0].getType();
diff --git a/mlir/lib/Dialect/SMT/IR/SMTOps.cpp b/mlir/lib/Dialect/SMT/IR/SMTOps.cpp
index c517ef2b18f6f..06ad4ab51b88b 100644
--- a/mlir/lib/Dialect/SMT/IR/SMTOps.cpp
+++ b/mlir/lib/Dialect/SMT/IR/SMTOps.cpp
@@ -22,7 +22,7 @@ using namespace mlir;
 LogicalResult BVConstantOp::inferReturnTypes(
     mlir::MLIRContext *context, std::optional<mlir::Location> location,
     ::mlir::ValueRange operands, ::mlir::DictionaryAttr attributes,
-    ::mlir::OpaqueProperties properties, ::mlir::RegionRange regions,
+    ::mlir::PropertyRef properties, ::mlir::RegionRange regions,
     ::llvm::SmallVectorImpl<::mlir::Type> &inferredReturnTypes) {
   inferredReturnTypes.push_back(
       properties.as<Properties *>()->getValue().getType());
@@ -173,7 +173,7 @@ LogicalResult ExtractOp::verify() {
 
 LogicalResult ConcatOp::inferReturnTypes(
     MLIRContext *context, std::optional<Location> location, ValueRange operands,
-    DictionaryAttr attributes, OpaqueProperties properties, RegionRange regions,
+    DictionaryAttr attributes, PropertyRef properties, RegionRange regions,
     SmallVectorImpl<Type> &inferredReturnTypes) {
   inferredReturnTypes.push_back(BitVectorType::get(
       context, cast<BitVectorType>(operands[0].getType()).getWidth() +
diff --git a/mlir/lib/Dialect/SparseTensor/IR/SparseTensorDialect.cpp b/mlir/lib/Dialect/SparseTensor/IR/SparseTensorDialect.cpp
index 05b2e07b88ba5..b77a536861d2a 100644
--- a/mlir/lib/Dialect/SparseTensor/IR/SparseTensorDialect.cpp
+++ b/mlir/lib/Dialect/SparseTensor/IR/SparseTensorDialect.cpp
@@ -1605,8 +1605,7 @@ OpFoldResult ReinterpretMapOp::fold(FoldAdaptor adaptor) {
 
 template <typename ToBufferOp>
 static LogicalResult inferSparseBufferType(ValueRange ops, DictionaryAttr attr,
-                                           OpaqueProperties prop,
-                                           RegionRange region,
+                                           PropertyRef prop, RegionRange region,
                                            SmallVectorImpl<mlir::Type> &ret) {
   typename ToBufferOp::Adaptor adaptor(ops, attr, prop, region);
   SparseTensorType stt = getSparseTensorType(adaptor.getTensor());
@@ -1647,7 +1646,7 @@ LogicalResult ToPositionsOp::verify() {
 LogicalResult
 ToPositionsOp::inferReturnTypes(MLIRContext *ctx, std::optional<Location> loc,
                                 ValueRange ops, DictionaryAttr attr,
-                                OpaqueProperties prop, RegionRange region,
+                                PropertyRef prop, RegionRange region,
                                 SmallVectorImpl<mlir::Type> &ret) {
   return inferSparseBufferType<ToPositionsOp>(ops, attr, prop, region, ret);
 }
@@ -1664,7 +1663,7 @@ LogicalResult ToCoordinatesOp::verify() {
 LogicalResult
 ToCoordinatesOp::inferReturnTypes(MLIRContext *ctx, std::optional<Location> loc,
                                   ValueRange ops, DictionaryAttr attr,
-                                  OpaqueProperties prop, RegionRange region,
+                                  PropertyRef prop, RegionRange region,
                                   SmallVectorImpl<mlir::Type> &ret) {
   return inferSparseBufferType<ToCoordinatesOp>(ops, attr, prop, region, ret);
 }
@@ -1678,7 +1677,7 @@ LogicalResult ToCoordinatesBufferOp::verify() {
 
 LogicalResult ToCoordinatesBufferOp::inferReturnTypes(
     MLIRContext *ctx, std::optional<Location> loc, ValueRange ops,
-    DictionaryAttr attr, OpaqueProperties prop, RegionRange region,
+    DictionaryAttr attr, PropertyRef prop, RegionRange region,
     SmallVectorImpl<mlir::Type> &ret) {
   return inferSparseBufferType<ToCoordinatesBufferOp>(ops, attr, prop, region,
                                                       ret);
@@ -1695,8 +1694,7 @@ LogicalResult ToValuesOp::verify() {
 LogicalResult ToValuesOp::inferReturnTypes(MLIRContext *ctx,
                                            std::optional<Location> loc,
                                            ValueRange ops, DictionaryAttr attr,
-                                           OpaqueProperties prop,
-                                           RegionRange region,
+                                           PropertyRef prop, RegionRange region,
                                            SmallVectorImpl<mlir::Type> &ret) {
   return inferSparseBufferType<ToValuesOp>(ops, attr, prop, region, ret);
 }
@@ -2367,7 +2365,7 @@ parseSparseCoIterateLoop(OpAsmParser &parser, OperationState &state,
 
 LogicalResult ExtractIterSpaceOp::inferReturnTypes(
     MLIRContext *ctx, std::optional<Location> loc, ValueRange ops,
-    DictionaryAttr attr, OpaqueProperties prop, RegionRange region,
+    DictionaryAttr attr, PropertyRef prop, RegionRange region,
     SmallVectorImpl<mlir::Type> &ret) {
 
   ExtractIterSpaceOp::Adaptor adaptor(ops, attr, prop, region);
diff --git a/mlir/lib/Dialect/Tosa/IR/TosaOps.cpp b/mlir/lib/Dialect/Tosa/IR/TosaOps.cpp
index 6072aecdf347b..aeb9bd737fa6a 100644
--- a/mlir/lib/Dialect/Tosa/IR/TosaOps.cpp
+++ b/mlir/lib/Dialect/Tosa/IR/TosaOps.cpp
@@ -1831,8 +1831,8 @@ LogicalResult tosa::ConcatOp::verify() {
 
 LogicalResult tosa::EqualOp::inferReturnTypeComponents(
     MLIRContext *context, ::std::optional<Location> location,
-    ValueShapeRange operands, DictionaryAttr attributes,
-    OpaqueProperties properties, RegionRange regions,
+    ValueShapeRange operands, DictionaryAttr attributes, PropertyRef properties,
+    RegionRange regions,
     SmallVectorImpl<ShapedTypeComponents> &inferredReturnShapes) {
   auto elementType = IntegerType::get(context, /*width=*/1);
 
@@ -2332,8 +2332,8 @@ LogicalResult tosa::SliceOp::verify() {
 
 LogicalResult tosa::MulOp::inferReturnTypeComponents(
     MLIRContext *context, ::std::optional<Location> location,
-    ValueShapeRange operands, DictionaryAttr attributes,
-    OpaqueProperties properties, RegionRange regions,
+    ValueShapeRange operands, DictionaryAttr attributes, PropertyRef properties,
+    RegionRange regions,
     SmallVectorImpl<ShapedTypeComponents> &inferredReturnShapes) {
   // mul op's output shape only depend on input1 and input2, not on shift
   ValueShapeRange twoInputs = operands.drop_back();
@@ -3372,7 +3372,7 @@ static LogicalResult NAryInferReturnTypes(
   LogicalResult OP::inferReturnTypeComponents(                                 \
       MLIRContext *context, ::std::optional<Location> location,                \
       ValueShapeRange operands, DictionaryAttr attributes,                     \
-      OpaqueProperties properties, RegionRange regions,                        \
+      PropertyRef properties, RegionRange regions,                             \
       SmallVectorImpl<ShapedTypeComponents> &inferredReturnShapes) {           \
     return NAryInferReturnTypes(operands, inferredReturnShapes);               \
   }
diff --git a/mlir/lib/Dialect/WasmSSA/IR/WasmSSAOps.cpp b/mlir/lib/Dialect/WasmSSA/IR/WasmSSAOps.cpp
index a514ea9218bd7..f3eb601bed5c3 100644
--- a/mlir/lib/Dialect/WasmSSA/IR/WasmSSAOps.cpp
+++ b/mlir/lib/Dialect/WasmSSA/IR/WasmSSAOps.cpp
@@ -362,7 +362,7 @@ Block *IfOp::getLabelTarget() { return getTarget(); }
 
 LogicalResult LocalOp::inferReturnTypes(
     MLIRContext *context, ::std::optional<Location> location,
-    ValueRange operands, DictionaryAttr attributes, OpaqueProperties properties,
+    ValueRange operands, DictionaryAttr attributes, PropertyRef properties,
     RegionRange regions, SmallVectorImpl<Type> &inferredReturnTypes) {
   LocalOp::GenericAdaptor<ValueRange> adaptor{operands, attributes, properties,
                                               regions};
@@ -380,7 +380,7 @@ LogicalResult LocalOp::inferReturnTypes(
 
 LogicalResult LocalGetOp::inferReturnTypes(
     MLIRContext *context, ::std::optional<Location> location,
-    ValueRange operands, DictionaryAttr attributes, OpaqueProperties properties,
+    ValueRange operands, DictionaryAttr attributes, PropertyRef properties,
     RegionRange regions, SmallVectorImpl<Type> &inferredReturnTypes) {
   return inferTeeGetResType(operands, inferredReturnTypes);
 }
@@ -401,7 +401,7 @@ LogicalResult LocalSetOp::verify() {
 
 LogicalResult LocalTeeOp::inferReturnTypes(
     MLIRContext *context, ::std::optional<Location> location,
-    ValueRange operands, DictionaryAttr attributes, OpaqueProperties properties,
+    ValueRange operands, DictionaryAttr attributes, PropertyRef properties,
     RegionRange regions, SmallVectorImpl<Type> &inferredReturnTypes) {
   return inferTeeGetResType(operands, inferredReturnTypes);
 }
diff --git a/mlir/lib/IR/ExtensibleDialect.cpp b/mlir/lib/IR/ExtensibleDialect.cpp
index 3312549ddfcd4..b51d330160646 100644
--- a/mlir/lib/IR/ExtensibleDialect.cpp
+++ b/mlir/lib/IR/ExtensibleDialect.cpp
@@ -304,6 +304,8 @@ DynamicOpDefinition::DynamicOpDefinition(
       getCanonicalizationPatternsFn(std::move(getCanonicalizationPatternsFn)),
       populateDefaultAttrsFn(std::move(populateDefaultAttrsFn)) {
   typeID = dialect->allocateTypeID();
+  // DynamicOpDefinition doesn't support properties, so use the ID of void.
+  propertiesTypeID = TypeID();
 }
 
 std::unique_ptr<DynamicOpDefinition> DynamicOpDefinition::get(
diff --git a/mlir/lib/IR/MLIRContext.cpp b/mlir/lib/IR/MLIRContext.cpp
index 73219c6917061..3fadaa67b1345 100644
--- a/mlir/lib/IR/MLIRContext.cpp
+++ b/mlir/lib/IR/MLIRContext.cpp
@@ -901,20 +901,20 @@ LogicalResult OperationName::UnregisteredOpModel::verifyInherentAttrs(
 int OperationName::UnregisteredOpModel::getOpPropertyByteSize() {
   return sizeof(Attribute);
 }
-void OperationName::UnregisteredOpModel::initProperties(
-    OperationName opName, OpaqueProperties storage, OpaqueProperties init) {
+void OperationName::UnregisteredOpModel::initProperties(OperationName opName,
+                                                        PropertyRef storage,
+                                                        PropertyRef init) {
   new (storage.as<Attribute *>()) Attribute();
   if (init)
     *storage.as<Attribute *>() = *init.as<Attribute *>();
 }
-void OperationName::UnregisteredOpModel::deleteProperties(
-    OpaqueProperties prop) {
+void OperationName::UnregisteredOpModel::deleteProperties(PropertyRef prop) {
   prop.as<Attribute *>()->~Attribute();
 }
 void OperationName::UnregisteredOpModel::populateDefaultProperties(
-    OperationName opName, OpaqueProperties properties) {}
+    OperationName opName, PropertyRef properties) {}
 LogicalResult OperationName::UnregisteredOpModel::setPropertiesFromAttr(
-    OperationName opName, OpaqueProperties properties, Attribute attr,
+    OperationName opName, PropertyRef properties, Attribute attr,
     function_ref<InFlightDiagnostic()> emitError) {
   *properties.as<Attribute *>() = attr;
   return success();
@@ -923,16 +923,16 @@ Attribute
 OperationName::UnregisteredOpModel::getPropertiesAsAttr(Operation *op) {
   return *op->getPropertiesStorage().as<Attribute *>();
 }
-void OperationName::UnregisteredOpModel::copyProperties(OpaqueProperties lhs,
-                                                        OpaqueProperties rhs) {
+void OperationName::UnregisteredOpModel::copyProperties(PropertyRef lhs,
+                                                        PropertyRef rhs) {
   *lhs.as<Attribute *>() = *rhs.as<Attribute *>();
 }
-bool OperationName::UnregisteredOpModel::compareProperties(
-    OpaqueProperties lhs, OpaqueProperties rhs) {
+bool OperationName::UnregisteredOpModel::compareProperties(PropertyRef lhs,
+                                                           PropertyRef rhs) {
   return *lhs.as<Attribute *>() == *rhs.as<Attribute *>();
 }
 llvm::hash_code
-OperationName::UnregisteredOpModel::hashProperties(OpaqueProperties prop) {
+OperationName::UnregisteredOpModel::hashProperties(PropertyRef prop) {
   return llvm::hash_combine(*prop.as<Attribute *>());
 }
 
diff --git a/mlir/lib/IR/Operation.cpp b/mlir/lib/IR/Operation.cpp
index bf8a918641dfb..5e1eb9539f41c 100644
--- a/mlir/lib/IR/Operation.cpp
+++ b/mlir/lib/IR/Operation.cpp
@@ -50,9 +50,8 @@ Operation *Operation::create(const OperationState &state) {
 /// Create a new Operation with the specific fields.
 Operation *Operation::create(Location location, OperationName name,
                              TypeRange resultTypes, ValueRange operands,
-                             NamedAttrList &&attributes,
-                             OpaqueProperties properties, BlockRange successors,
-                             RegionRange regions) {
+                             NamedAttrList &&attributes, PropertyRef properties,
+                             BlockRange successors, RegionRange regions) {
   unsigned numRegions = regions.size();
   Operation *op =
       create(location, name, resultTypes, operands, std::move(attributes),
@@ -66,9 +65,8 @@ Operation *Operation::create(Location location, OperationName name,
 /// Create a new Operation with the specific fields.
 Operation *Operation::create(Location location, OperationName name,
                              TypeRange resultTypes, ValueRange operands,
-                             NamedAttrList &&attributes,
-                             OpaqueProperties properties, BlockRange successors,
-                             unsigned numRegions) {
+                             NamedAttrList &&attributes, PropertyRef properties,
+                             BlockRange successors, unsigned numRegions) {
   // Populate default attributes.
   name.populateDefaultAttrs(attributes);
 
@@ -81,9 +79,8 @@ Operation *Operation::create(Location location, OperationName name,
 /// unnecessarily uniquing a list of attributes.
 Operation *Operation::create(Location location, OperationName name,
                              TypeRange resultTypes, ValueRange operands,
-                             DictionaryAttr attributes,
-                             OpaqueProperties properties, BlockRange successors,
-                             unsigned numRegions) {
+                             DictionaryAttr attributes, PropertyRef properties,
+                             BlockRange successors, unsigned numRegions) {
   assert(llvm::all_of(resultTypes, [](Type t) { return t; }) &&
          "unexpected null result type");
 
@@ -146,7 +143,7 @@ Operation *Operation::create(Location location, OperationName name,
   for (unsigned i = 0; i != numSuccessors; ++i)
     new (&blockOperands[i]) BlockOperand(op, successors[i]);
 
-  // This must be done after properties are initalized.
+  // This must be done after properties are initialized.
   op->setAttrs(attributes);
 
   return op;
@@ -155,7 +152,7 @@ Operation *Operation::create(Location location, OperationName name,
 Operation::Operation(Location location, OperationName name, unsigned numResults,
                      unsigned numSuccessors, unsigned numRegions,
                      int fullPropertiesStorageSize, DictionaryAttr attributes,
-                     OpaqueProperties properties, bool hasOperandStorage)
+                     PropertyRef properties, bool hasOperandStorage)
     : location(location), numResults(numResults), numSuccs(numSuccessors),
       numRegions(numRegions), hasOperandStorage(hasOperandStorage),
       propertiesStorageSize((fullPropertiesStorageSize + 7) / 8), name(name) {
@@ -363,7 +360,7 @@ LogicalResult Operation::setPropertiesFromAttribute(
       this->getName(), this->getPropertiesStorage(), attr, emitError);
 }
 
-void Operation::copyProperties(OpaqueProperties rhs) {
+void Operation::copyProperties(PropertyRef rhs) {
   name.copyOpProperties(getPropertiesStorage(), rhs);
 }
 
diff --git a/mlir/lib/IR/OperationSupport.cpp b/mlir/lib/IR/OperationSupport.cpp
index 3ff61daaac60b..804a825218390 100644
--- a/mlir/lib/IR/OperationSupport.cpp
+++ b/mlir/lib/IR/OperationSupport.cpp
@@ -966,3 +966,5 @@ OperationFingerPrint::OperationFingerPrint(Operation *topOp,
 
   hash = hasher.result();
 }
+
+MLIR_DEFINE_EXPLICIT_TYPE_ID(mlir::EmptyProperties)
diff --git a/mlir/lib/Target/IRDLToCpp/Templates/PerOperationDecl.txt b/mlir/lib/Target/IRDLToCpp/Templates/PerOperationDecl.txt
index 93ce0bef1f269..18edc17f8a28a 100644
--- a/mlir/lib/Target/IRDLToCpp/Templates/PerOperationDecl.txt
+++ b/mlir/lib/Target/IRDLToCpp/Templates/PerOperationDecl.txt
@@ -54,7 +54,7 @@ class __OP_CPP_NAME__GenericAdaptor
   using Base = detail::__OP_CPP_NAME__GenericAdaptorBase;
 public:
   __OP_CPP_NAME__GenericAdaptor(RangeT values, ::mlir::DictionaryAttr attrs,
-                                ::mlir::OpaqueProperties properties,
+                                ::mlir::PropertyRef properties,
                                 ::mlir::RegionRange regions = {})
     : __OP_CPP_NAME__GenericAdaptor(values, attrs,
       (properties ? *properties.as<::mlir::EmptyProperties *>()
diff --git a/mlir/lib/Transforms/Utils/DialectConversion.cpp b/mlir/lib/Transforms/Utils/DialectConversion.cpp
index c5facc32e4461..9dbf5bb8c5449 100644
--- a/mlir/lib/Transforms/Utils/DialectConversion.cpp
+++ b/mlir/lib/Transforms/Utils/DialectConversion.cpp
@@ -687,10 +687,10 @@ class ModifyOperationRewrite : public OperationRewrite {
         name(op->getName()), loc(op->getLoc()), attrs(op->getAttrDictionary()),
         operands(op->operand_begin(), op->operand_end()),
         successors(op->successor_begin(), op->successor_end()) {
-    if (OpaqueProperties prop = op->getPropertiesStorage()) {
+    if (PropertyRef prop = op->getPropertiesStorage()) {
       // Make a copy of the properties.
       propertiesStorage = operator new(op->getPropertiesStorageSize());
-      OpaqueProperties propCopy(propertiesStorage);
+      PropertyRef propCopy(name.getOpPropertiesTypeID(), propertiesStorage);
       name.initOpProperties(propCopy, /*init=*/prop);
     }
   }
@@ -711,7 +711,7 @@ class ModifyOperationRewrite : public OperationRewrite {
       listener->notifyOperationModified(op);
 
     if (propertiesStorage) {
-      OpaqueProperties propCopy(propertiesStorage);
+      PropertyRef propCopy(name.getOpPropertiesTypeID(), propertiesStorage);
       // Note: The operation may have been erased in the mean time, so
       // OperationName must be stored in this object.
       name.destroyOpProperties(propCopy);
@@ -727,7 +727,7 @@ class ModifyOperationRewrite : public OperationRewrite {
     for (const auto &it : llvm::enumerate(successors))
       op->setSuccessor(it.value(), it.index());
     if (propertiesStorage) {
-      OpaqueProperties propCopy(propertiesStorage);
+      PropertyRef propCopy(name.getOpPropertiesTypeID(), propertiesStorage);
       op->copyProperties(propCopy);
       name.destroyOpProperties(propCopy);
       operator delete(propertiesStorage);
diff --git a/mlir/test/lib/Dialect/Test/TestOpDefs.cpp b/mlir/test/lib/Dialect/Test/TestOpDefs.cpp
index 7cf728f933395..39054b5c4d543 100644
--- a/mlir/test/lib/Dialect/Test/TestOpDefs.cpp
+++ b/mlir/test/lib/Dialect/Test/TestOpDefs.cpp
@@ -256,7 +256,7 @@ OpFoldResult TestOpInPlaceFold::fold(FoldAdaptor adaptor) {
 
 LogicalResult OpWithInferTypeInterfaceOp::inferReturnTypes(
     MLIRContext *, std::optional<Location> location, ValueRange operands,
-    DictionaryAttr attributes, OpaqueProperties properties, RegionRange regions,
+    DictionaryAttr attributes, PropertyRef properties, RegionRange regions,
     SmallVectorImpl<Type> &inferredReturnTypes) {
   if (operands[0].getType() != operands[1].getType()) {
     return emitOptionalError(location, "operand type mismatch ",
@@ -273,8 +273,8 @@ LogicalResult OpWithInferTypeInterfaceOp::inferReturnTypes(
 
 LogicalResult OpWithShapedTypeInferTypeInterfaceOp::inferReturnTypeComponents(
     MLIRContext *context, std::optional<Location> location,
-    ValueShapeRange operands, DictionaryAttr attributes,
-    OpaqueProperties properties, RegionRange regions,
+    ValueShapeRange operands, DictionaryAttr attributes, PropertyRef properties,
+    RegionRange regions,
     SmallVectorImpl<ShapedTypeComponents> &inferredReturnShapes) {
   // Create return type consisting of the last element of the first operand.
   auto operandType = operands.front().getType();
@@ -1152,7 +1152,7 @@ LogicalResult OpWithInferTypeAdaptorInterfaceOp::inferReturnTypes(
 // refineReturnType, currently only refineReturnType can be omitted.
 LogicalResult OpWithRefineTypeInterfaceOp::inferReturnTypes(
     MLIRContext *context, std::optional<Location> location, ValueRange operands,
-    DictionaryAttr attributes, OpaqueProperties properties, RegionRange regions,
+    DictionaryAttr attributes, PropertyRef properties, RegionRange regions,
     SmallVectorImpl<Type> &returnTypes) {
   returnTypes.clear();
   return OpWithRefineTypeInterfaceOp::refineReturnTypes(
@@ -1162,7 +1162,7 @@ LogicalResult OpWithRefineTypeInterfaceOp::inferReturnTypes(
 
 LogicalResult OpWithRefineTypeInterfaceOp::refineReturnTypes(
     MLIRContext *, std::optional<Location> location, ValueRange operands,
-    DictionaryAttr attributes, OpaqueProperties properties, RegionRange regions,
+    DictionaryAttr attributes, PropertyRef properties, RegionRange regions,
     SmallVectorImpl<Type> &returnTypes) {
   if (operands[0].getType() != operands[1].getType()) {
     return emitOptionalError(location, "operand type mismatch ",
@@ -1244,7 +1244,7 @@ OpWithShapedTypeInferTypeAdaptorInterfaceOp::reifyReturnTypeShapes(
 
 LogicalResult TestOpWithPropertiesAndInferredType::inferReturnTypes(
     MLIRContext *context, std::optional<Location>, ValueRange operands,
-    DictionaryAttr attributes, OpaqueProperties properties, RegionRange regions,
+    DictionaryAttr attributes, PropertyRef properties, RegionRange regions,
     SmallVectorImpl<Type> &inferredReturnTypes) {
 
   Adaptor adaptor(operands, attributes, properties, regions);
diff --git a/mlir/test/lib/Dialect/Test/TestOps.td b/mlir/test/lib/Dialect/Test/TestOps.td
index bd0b6e25efa53..74dcb0a10abd6 100644
--- a/mlir/test/lib/Dialect/Test/TestOps.td
+++ b/mlir/test/lib/Dialect/Test/TestOps.td
@@ -546,7 +546,7 @@ def VariadicRegionInferredTypesOp : TEST_Op<"variadic_region_inferred",
   let extraClassDeclaration = [{
     static llvm::LogicalResult inferReturnTypes(mlir::MLIRContext *context,
           std::optional<::mlir::Location> location, mlir::ValueRange operands,
-          mlir::DictionaryAttr attributes, mlir::OpaqueProperties properties, mlir::RegionRange regions,
+          mlir::DictionaryAttr attributes, mlir::PropertyRef properties, mlir::RegionRange regions,
           llvm::SmallVectorImpl<mlir::Type> &inferredReturnTypes) {
       inferredReturnTypes.assign({mlir::IntegerType::get(context, 16)});
       return mlir::success();
@@ -2779,7 +2779,7 @@ class TableGenBuildInferReturnTypeBaseOp<string mnemonic,
   let extraClassDeclaration = [{
     static ::llvm::LogicalResult inferReturnTypes(::mlir::MLIRContext *,
           ::std::optional<::mlir::Location> location, ::mlir::ValueRange operands,
-          ::mlir::DictionaryAttr attributes, mlir::OpaqueProperties properties, ::mlir::RegionRange regions,
+          ::mlir::DictionaryAttr attributes, mlir::PropertyRef properties, ::mlir::RegionRange regions,
           ::llvm::SmallVectorImpl<::mlir::Type> &inferredReturnTypes) {
       inferredReturnTypes.assign({operands[0].getType()});
       return ::mlir::success();
diff --git a/mlir/test/lib/Dialect/Test/TestOpsSyntax.cpp b/mlir/test/lib/Dialect/Test/TestOpsSyntax.cpp
index cc131ad9757f9..5880c2a2302b0 100644
--- a/mlir/test/lib/Dialect/Test/TestOpsSyntax.cpp
+++ b/mlir/test/lib/Dialect/Test/TestOpsSyntax.cpp
@@ -285,7 +285,7 @@ void ParseB64BytesOp::print(OpAsmPrinter &p) {
 ::llvm::LogicalResult FormatInferType2Op::inferReturnTypes(
     ::mlir::MLIRContext *context, ::std::optional<::mlir::Location> location,
     ::mlir::ValueRange operands, ::mlir::DictionaryAttr attributes,
-    OpaqueProperties properties, ::mlir::RegionRange regions,
+    PropertyRef properties, ::mlir::RegionRange regions,
     ::llvm::SmallVectorImpl<::mlir::Type> &inferredReturnTypes) {
   inferredReturnTypes.assign({::mlir::IntegerType::get(context, 16)});
   return ::mlir::success();
diff --git a/mlir/test/lib/Dialect/Test/TestOpsSyntax.td b/mlir/test/lib/Dialect/Test/TestOpsSyntax.td
index 6b10ec6173a50..936f744855a08 100644
--- a/mlir/test/lib/Dialect/Test/TestOpsSyntax.td
+++ b/mlir/test/lib/Dialect/Test/TestOpsSyntax.td
@@ -689,7 +689,7 @@ def FormatInferTypeOp : TEST_Op<"format_infer_type", [InferTypeOpInterface]> {
   let extraClassDeclaration = [{
     static ::llvm::LogicalResult inferReturnTypes(::mlir::MLIRContext *context,
           ::std::optional<::mlir::Location> location, ::mlir::ValueRange operands,
-          ::mlir::DictionaryAttr attributes, mlir::OpaqueProperties properties, ::mlir::RegionRange regions,
+          ::mlir::DictionaryAttr attributes, mlir::PropertyRef properties, ::mlir::RegionRange regions,
           ::llvm::SmallVectorImpl<::mlir::Type> &inferredReturnTypes) {
       inferredReturnTypes.assign({::mlir::IntegerType::get(context, 16)});
       return ::mlir::success();
@@ -712,7 +712,7 @@ class FormatInferAllTypesBaseOp<string mnemonic, list<Trait> traits = []>
   let extraClassDeclaration = [{
     static ::llvm::LogicalResult inferReturnTypes(::mlir::MLIRContext *context,
           ::std::optional<::mlir::Location> location, ::mlir::ValueRange operands,
-          ::mlir::DictionaryAttr attributes, mlir::OpaqueProperties properties, ::mlir::RegionRange regions,
+          ::mlir::DictionaryAttr attributes, mlir::PropertyRef properties, ::mlir::RegionRange regions,
           ::llvm::SmallVectorImpl<::mlir::Type> &inferredReturnTypes) {
       ::mlir::TypeRange operandTypes = operands.getTypes();
       inferredReturnTypes.assign(operandTypes.begin(), operandTypes.end());
@@ -759,7 +759,7 @@ def FormatInferTypeRegionsOp
   let extraClassDeclaration = [{
     static ::llvm::LogicalResult inferReturnTypes(::mlir::MLIRContext *context,
           ::std::optional<::mlir::Location> location, ::mlir::ValueRange operands,
-          ::mlir::DictionaryAttr attributes, mlir::OpaqueProperties properties, ::mlir::RegionRange regions,
+          ::mlir::DictionaryAttr attributes, mlir::PropertyRef properties, ::mlir::RegionRange regions,
           ::llvm::SmallVectorImpl<::mlir::Type> &inferredReturnTypes) {
       if (regions.empty())
         return ::mlir::failure();
@@ -780,7 +780,7 @@ def FormatInferTypeVariadicOperandsOp
   let extraClassDeclaration = [{
     static ::llvm::LogicalResult inferReturnTypes(::mlir::MLIRContext *context,
           ::std::optional<::mlir::Location> location, ::mlir::ValueRange operands,
-          ::mlir::DictionaryAttr attributes, mlir::OpaqueProperties properties, ::mlir::RegionRange regions,
+          ::mlir::DictionaryAttr attributes, mlir::PropertyRef properties, ::mlir::RegionRange regions,
           ::llvm::SmallVectorImpl<::mlir::Type> &inferredReturnTypes) {
       FormatInferTypeVariadicOperandsOpAdaptor adaptor(
           operands, attributes, *properties.as<Properties *>(), {});
diff --git a/mlir/test/lib/Dialect/Test/TestPatterns.cpp b/mlir/test/lib/Dialect/Test/TestPatterns.cpp
index 6c44ace831e96..1714289991ceb 100644
--- a/mlir/test/lib/Dialect/Test/TestPatterns.cpp
+++ b/mlir/test/lib/Dialect/Test/TestPatterns.cpp
@@ -744,10 +744,13 @@ static void invokeCreateWithInferredReturnType(Operation *op) {
     for (int j = 0; j < e; ++j) {
       std::array<Value, 2> values = {{fop.getArgument(i), fop.getArgument(j)}};
       SmallVector<Type, 2> inferredReturnTypes;
+      // Only pass properties if the op's properties type matches OpTy's.
+      PropertyRef properties = op->getPropertiesStorage();
+      if (properties.getTypeID() != TypeID::get<typename OpTy::Properties>())
+        properties = PropertyRef(nullptr);
       if (succeeded(OpTy::inferReturnTypes(
               context, std::nullopt, values, op->getDiscardableAttrDictionary(),
-              op->getPropertiesStorage(), op->getRegions(),
-              inferredReturnTypes))) {
+              properties, op->getRegions(), inferredReturnTypes))) {
         OperationState state(location, OpTy::getOperationName());
         // TODO: Expand to regions.
         OpTy::build(b, state, values, op->getAttrs());
diff --git a/mlir/test/mlir-tblgen/op-decl-and-defs.td b/mlir/test/mlir-tblgen/op-decl-and-defs.td
index 80dedb8475b9e..e92b4044668c1 100644
--- a/mlir/test/mlir-tblgen/op-decl-and-defs.td
+++ b/mlir/test/mlir-tblgen/op-decl-and-defs.td
@@ -75,7 +75,7 @@ def NS_AOp : NS_Op<"a_op", [IsolatedFromAbove, IsolatedFromAbove]> {
 // CHECK: class AOpGenericAdaptor : public detail::AOpGenericAdaptorBase {
 // CHECK: public:
 // CHECK:   AOpGenericAdaptor(RangeT values, ::mlir::DictionaryAttr attrs, const Properties &properties, ::mlir::RegionRange regions = {}) : Base(attrs, properties, regions), odsOperands(values) {}
-// CHECK:   AOpGenericAdaptor(RangeT values, ::mlir::DictionaryAttr attrs, ::mlir::OpaqueProperties properties, ::mlir::RegionRange regions = {}) : AOpGenericAdaptor(values, attrs, (properties ? *properties.as<Properties *>() : Properties{}), regions) {}
+// CHECK:   AOpGenericAdaptor(RangeT values, ::mlir::DictionaryAttr attrs, ::mlir::PropertyRef properties, ::mlir::RegionRange regions = {}) : AOpGenericAdaptor(values, attrs, (properties ? *properties.as<Properties *>() : Properties{}), regions) {}
 // CHECK:   AOpGenericAdaptor(RangeT values, ::mlir::DictionaryAttr attrs = nullptr) : AOpGenericAdaptor(values, attrs, Properties{}, {}) {}
 // CHECK:   AOpGenericAdaptor(RangeT values, const AOpGenericAdaptorBase &base) : Base(base), odsOperands(values) {}
 // CHECK:   RangeT getODSOperands(unsigned index) {
diff --git a/mlir/test/python/python_test_ops.td b/mlir/test/python/python_test_ops.td
index cfc1d72bb479d..96e951424ca51 100644
--- a/mlir/test/python/python_test_ops.td
+++ b/mlir/test/python/python_test_ops.td
@@ -135,7 +135,7 @@ def InferResultsOp : TestOp<"infer_results_op", [InferTypeOpInterface]> {
     static ::llvm::LogicalResult inferReturnTypes(
       ::mlir::MLIRContext *context, ::std::optional<::mlir::Location> location,
       ::mlir::ValueRange operands, ::mlir::DictionaryAttr attributes,
-      ::mlir::OpaqueProperties,
+      ::mlir::PropertyRef,
       ::mlir::RegionRange regions,
       ::llvm::SmallVectorImpl<::mlir::Type> &inferredReturnTypes) {
       ::mlir::Builder b(context);
@@ -158,7 +158,7 @@ def InferResultsVariadicInputsOp : TestOp<"infer_results_variadic_inputs_op",
     static ::llvm::LogicalResult inferReturnTypes(
       ::mlir::MLIRContext *context, ::std::optional<::mlir::Location> location,
       ::mlir::ValueRange operands, ::mlir::DictionaryAttr attributes,
-      ::mlir::OpaqueProperties,
+      ::mlir::PropertyRef,
       ::mlir::RegionRange regions,
       ::llvm::SmallVectorImpl<::mlir::Type> &inferredReturnTypes) {
       ::mlir::Builder b(context);
@@ -187,7 +187,7 @@ def InferShapedTypeComponentsOp : TestOp<"infer_shaped_type_components_op",
     ::llvm::LogicalResult $cppClass::inferReturnTypeComponents(
       ::mlir::MLIRContext *context, ::std::optional<::mlir::Location> location,
       ::mlir::ValueShapeRange operands, ::mlir::DictionaryAttr attributes,
-      ::mlir::OpaqueProperties properties, ::mlir::RegionRange regions,
+      ::mlir::PropertyRef properties, ::mlir::RegionRange regions,
       ::llvm::SmallVectorImpl<
         ::mlir::ShapedTypeComponents>& inferredShapedTypeComponents) {
       $cppClass::Adaptor adaptor(operands, attributes, properties, regions);
diff --git a/mlir/tools/mlir-tblgen/OpDefinitionsGen.cpp b/mlir/tools/mlir-tblgen/OpDefinitionsGen.cpp
index e93f91bb540c2..edb009938f005 100644
--- a/mlir/tools/mlir-tblgen/OpDefinitionsGen.cpp
+++ b/mlir/tools/mlir-tblgen/OpDefinitionsGen.cpp
@@ -2852,8 +2852,8 @@ void OpEmitter::genInferredTypeCollectiveParamBuilder(
     // function.
     body << formatv(R"(
   if (!attributes.empty()) {
-    ::mlir::OpaqueProperties properties =
-      &{1}.getOrAddProperties<{0}::Properties>();
+    (void){1}.getOrAddProperties<{0}::Properties>();
+    ::mlir::PropertyRef properties = {1}.getRawProperties();
     std::optional<::mlir::RegisteredOperationName> info =
       {1}.name.getRegisteredInfo();
     if (failed(info->setOpPropertiesFromAttribute({1}.name, properties,
@@ -3145,8 +3145,8 @@ void OpEmitter::genCollectiveParamBuilder(CollectiveBuilderKind kind) {
     // function.
     body << formatv(R"(
   if (!attributes.empty()) {
-    ::mlir::OpaqueProperties properties =
-      &{1}.getOrAddProperties<{0}::Properties>();
+    (void){1}.getOrAddProperties<{0}::Properties>();
+    ::mlir::PropertyRef properties = {1}.getRawProperties();
     std::optional<::mlir::RegisteredOperationName> info =
       {1}.name.getRegisteredInfo();
     if (failed(info->setOpPropertiesFromAttribute({1}.name, properties,
@@ -4430,22 +4430,22 @@ OpOperandAdaptorEmitter::OpOperandAdaptorEmitter(
     constructor->addMemberInitializer("Base", "attrs, properties, regions");
     constructor->addMemberInitializer("odsOperands", "values");
 
-    // Add a forwarding constructor to the previous one that accepts
-    // OpaqueProperties instead and check for null and perform the cast to the
-    // actual properties type.
+    // Add a forwarding constructor that accepts PropertyRef instead of a
+    // concrete properties struct. It checks for null and casts to the actual
+    // properties type.
     paramList[1] = MethodParameter("::mlir::DictionaryAttr", "attrs");
-    paramList[2] = MethodParameter("::mlir::OpaqueProperties", "properties");
-    auto *opaquePropertiesConstructor =
+    paramList[2] = MethodParameter("::mlir::PropertyRef", "properties");
+    auto *propertyRefConstructor =
         genericAdaptor.addConstructor(std::move(paramList));
     if (useProperties) {
-      opaquePropertiesConstructor->addMemberInitializer(
+      propertyRefConstructor->addMemberInitializer(
           genericAdaptor.getClassName(),
           "values, "
           "attrs, "
           "(properties ? *properties.as<Properties *>() : Properties{}), "
           "regions");
     } else {
-      opaquePropertiesConstructor->addMemberInitializer(
+      propertyRefConstructor->addMemberInitializer(
           genericAdaptor.getClassName(),
           "values, "
           "attrs, "
@@ -4702,6 +4702,7 @@ static void emitOpClasses(
 
   for (auto *def : defs) {
     Operator op(*def);
+    OpOrAdaptorHelper emitHelper(op, /*emitForOp=*/true);
     if (emitDecl) {
       {
         NamespaceEmitter emitter(os, op.getCppNamespace());
@@ -4711,9 +4712,15 @@ static void emitOpClasses(
         OpEmitter::emitDecl(op, os, staticVerifierEmitter);
       }
       // Emit the TypeID explicit specialization to have a single definition.
-      if (!op.getCppNamespace().empty())
+      if (!op.getCppNamespace().empty()) {
         os << "MLIR_DECLARE_EXPLICIT_TYPE_ID(" << op.getCppNamespace()
-           << "::" << op.getCppClassName() << ")\n\n";
+           << "::" << op.getCppClassName() << ")\n";
+        if (emitHelper.hasNonEmptyPropertiesStruct())
+          os << "MLIR_DECLARE_EXPLICIT_TYPE_ID(" << op.getCppNamespace()
+             << "::detail::" << op.getCppClassName()
+             << "GenericAdaptorBase::Properties)\n";
+        os << "\n";
+      }
     } else {
       {
         NamespaceEmitter emitter(os, op.getCppNamespace());
@@ -4722,9 +4729,15 @@ static void emitOpClasses(
         OpEmitter::emitDef(op, os, staticVerifierEmitter);
       }
       // Emit the TypeID explicit specialization to have a single definition.
-      if (!op.getCppNamespace().empty())
+      if (!op.getCppNamespace().empty()) {
         os << "MLIR_DEFINE_EXPLICIT_TYPE_ID(" << op.getCppNamespace()
-           << "::" << op.getCppClassName() << ")\n\n";
+           << "::" << op.getCppClassName() << ")\n";
+        if (emitHelper.hasNonEmptyPropertiesStruct())
+          os << "MLIR_DEFINE_EXPLICIT_TYPE_ID(" << op.getCppNamespace()
+             << "::detail::" << op.getCppClassName()
+             << "GenericAdaptorBase::Properties)\n";
+        os << "\n";
+      }
     }
   }
 }
diff --git a/mlir/unittests/Debug/FileLineColLocBreakpointManagerTest.cpp b/mlir/unittests/Debug/FileLineColLocBreakpointManagerTest.cpp
index 6a81422b6b66b..d33aa46b6e48d 100644
--- a/mlir/unittests/Debug/FileLineColLocBreakpointManagerTest.cpp
+++ b/mlir/unittests/Debug/FileLineColLocBreakpointManagerTest.cpp
@@ -24,7 +24,7 @@ static Operation *createOp(MLIRContext *context, Location loc,
                            unsigned int numRegions = 0) {
   context->allowUnregisteredDialects();
   return Operation::create(loc, OperationName(operationName, context), {}, {},
-                           NamedAttrList(), OpaqueProperties(nullptr), {},
+                           NamedAttrList(), PropertyRef(nullptr), {},
                            numRegions);
 }
 



More information about the Mlir-commits mailing list