[llvm-branch-commits] [mlir] [MLIR][ODS] Print prop-dict fields with custom printers (PR #217589)

Mehdi Amini via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Thu Aug 20 03:51:55 PDT 2026


https://github.com/joker-eph created https://github.com/llvm/llvm-project/pull/217589

Generate a key-value prop-dict printer that dispatches each field to its ODS printer while retaining attribute conversion for non-compositional default parsers.

Keep operation-specific property printer hooks ahead of the generated implementation and preserve legacy input compatibility.

See #155475

Assisted-by: Codex

>From 7988edac7e207afc08aa3fa120b07dc40e1c10ec Mon Sep 17 00:00:00 2001
From: Mehdi Amini <joker.eph at gmail.com>
Date: Wed, 19 Aug 2026 09:04:44 -0700
Subject: [PATCH] [MLIR][ODS] Print prop-dict fields with custom printers

Generate a key-value prop-dict printer that dispatches each field to its
ODS printer while retaining attribute conversion for non-compositional default
parsers.

Keep operation-specific property printer hooks ahead of the generated
implementation and preserve legacy input compatibility.

Assisted-by: Codex
---
 mlir/docs/DefiningDialects/Operations.md      |   5 +-
 mlir/include/mlir/Dialect/LLVMIR/XeVMOps.td   |   2 +
 .../include/mlir/Dialect/XeGPU/IR/XeGPUOps.td |   2 +
 mlir/include/mlir/IR/OpBase.td                |   5 +
 mlir/include/mlir/IR/OpDefinition.h           |  22 ++-
 mlir/include/mlir/TableGen/Operator.h         |   3 +
 mlir/lib/TableGen/Operator.cpp                |   4 +
 .../memref-to-emitc-alloc-copy.mlir           |   8 +-
 .../memref-to-emitc-alloc-dealloc.mlir        |  16 +-
 .../memref-to-emitc-alloc-load-store.mlir     |   8 +-
 .../MemRefToEmitC/memref-to-emitc-copy.mlir   |   2 +-
 .../memref-with-custom-types.mlir             |   3 +-
 .../Dialect/EmitC/member_call_opaque.mlir     |   4 +-
 mlir/test/IR/enum-attr-roundtrip.mlir         |   8 +-
 mlir/test/IR/properties.mlir                  |  38 +++--
 mlir/test/Target/LLVMIR/Import/intrinsic.ll   |   2 +-
 mlir/test/lib/Dialect/Test/TestOps.td         |  28 ++++
 .../op-format-custom-properties-printer.td    |  23 +++
 mlir/test/mlir-tblgen/op-format.mlir          |  12 +-
 mlir/tools/mlir-tblgen/OpFormatGen.cpp        | 138 +++++++++++++++++-
 20 files changed, 278 insertions(+), 55 deletions(-)
 create mode 100644 mlir/test/mlir-tblgen/op-format-custom-properties-printer.td

diff --git a/mlir/docs/DefiningDialects/Operations.md b/mlir/docs/DefiningDialects/Operations.md
index 419041a52b0da..a6e119727c906 100644
--- a/mlir/docs/DefiningDialects/Operations.md
+++ b/mlir/docs/DefiningDialects/Operations.md
@@ -768,7 +768,10 @@ The available directives are as follows:
         `FieldParser` specialization is available or when the selected
         specialization declares `isKeyValueCompositional = false`.
     -   The legacy `<{key = attribute, ...}>` dictionary spelling is also
-        accepted when parsing and is used by the generated printer.
+        accepted when parsing. The generated printer uses the key-value
+        spelling and the same custom-printer or attribute-conversion choice.
+        Operations that provide a custom `printProperties` hook should set
+        `hasCustomPropertiesPrinter` to suppress the shadowed generated helper.
     -   Any property or inherent attribute that is not used elsewhere in the
         format is parsed and printed as part of this list.
     -   If present, the `attr-dict` will not contain any inherent attributes.
diff --git a/mlir/include/mlir/Dialect/LLVMIR/XeVMOps.td b/mlir/include/mlir/Dialect/LLVMIR/XeVMOps.td
index c52a1c0c94402..f9c80235aa1ca 100644
--- a/mlir/include/mlir/Dialect/LLVMIR/XeVMOps.td
+++ b/mlir/include/mlir/Dialect/LLVMIR/XeVMOps.td
@@ -48,6 +48,8 @@ class XeVM_Attr<string attrName, string attrMnemonic, list<Trait> traits = []>
 class XeVM_Op<string mnemonic, list<Trait> traits = []>
     : LLVM_OpBase<XeVM_Dialect, mnemonic, traits> {
 
+  let hasCustomPropertiesPrinter = 1;
+
   code extraBaseClassDeclaration = [{
     void printProperties(::mlir::MLIRContext *ctx,
             ::mlir::OpAsmPrinter &p, const Properties &prop,
diff --git a/mlir/include/mlir/Dialect/XeGPU/IR/XeGPUOps.td b/mlir/include/mlir/Dialect/XeGPU/IR/XeGPUOps.td
index 49b98922cee4c..b6b60b252eec8 100644
--- a/mlir/include/mlir/Dialect/XeGPU/IR/XeGPUOps.td
+++ b/mlir/include/mlir/Dialect/XeGPU/IR/XeGPUOps.td
@@ -26,6 +26,8 @@ include "mlir/Interfaces/ViewLikeInterface.td"
 class XeGPU_Op<string mnemonic, list<Trait> traits = []>:
           Op<XeGPU_Dialect, mnemonic, traits> {
 
+  let hasCustomPropertiesPrinter = 1;
+
   code extraBaseClassDeclaration = [{
     void printProperties(::mlir::MLIRContext *ctx,
             ::mlir::OpAsmPrinter &p, const Properties &prop,
diff --git a/mlir/include/mlir/IR/OpBase.td b/mlir/include/mlir/IR/OpBase.td
index 0d0669e90c3f7..a130d5847a1c7 100644
--- a/mlir/include/mlir/IR/OpBase.td
+++ b/mlir/include/mlir/IR/OpBase.td
@@ -408,6 +408,11 @@ class Op<Dialect dialect, string mnemonic, list<Trait> props = []> {
   ///   * void print(OpAsmPrinter &p)
   bit hasCustomAssemblyFormat = 0;
 
+  /// This field indicates that the operation provides a custom
+  /// `printProperties` hook. Setting it avoids generating the default
+  /// per-field `prop-dict` printer that the hook would shadow.
+  bit hasCustomPropertiesPrinter = 0;
+
   // A bit indicating if the operation has additional invariants that need to
   // verified (aside from those verified by other ODS constructs). If set to `1`,
   // an additional `LogicalResult verify()` declaration will be generated on the
diff --git a/mlir/include/mlir/IR/OpDefinition.h b/mlir/include/mlir/IR/OpDefinition.h
index 075d39194ed97..d92a93746bf54 100644
--- a/mlir/include/mlir/IR/OpDefinition.h
+++ b/mlir/include/mlir/IR/OpDefinition.h
@@ -1864,6 +1864,18 @@ class Op : public OpState, public Traits<ConcreteType>... {
   using detect_has_print_properties =
       llvm::is_detected<has_print_properties, T>;
 
+  /// Trait to check if T provides a generated printer for the key-value
+  /// spelling of `prop-dict`.
+  template <typename T, typename... Args>
+  using has_print_properties_as_key_value_list =
+      decltype(T::_odsPrintPropertiesAsKeyValueList(
+          std::declval<MLIRContext *>(), std::declval<OpAsmPrinter &>(),
+          std::declval<const typename PropertiesSelector<T>::type &>(),
+          std::declval<ArrayRef<StringRef>>()));
+  template <typename T>
+  using detect_has_print_properties_as_key_value_list =
+      llvm::is_detected<has_print_properties_as_key_value_list, T>;
+
   /// Trait to check if parseProperties(OpAsmParser, T) exist
   template <typename T, typename... Args>
   using has_parse_properties = decltype(parseProperties(
@@ -2036,15 +2048,19 @@ class Op : public OpState, public Traits<ConcreteType>... {
                                         InferredProperties<T> &properties) {}
 
   /// Print the operation properties with names not included within
-  /// 'elidedProps'. Unless overridden, this method will try to dispatch to a
-  /// `printProperties` free-function if it exists, and otherwise by converting
-  /// the properties to an Attribute.
+  /// 'elidedProps'. Unless overridden, this method first tries to dispatch to a
+  /// `printProperties` free-function, then to the generated per-field printer,
+  /// and finally converts the properties to an Attribute.
   template <typename T>
   static void printProperties(MLIRContext *ctx, OpAsmPrinter &p,
                               const T &properties,
                               ArrayRef<StringRef> elidedProps = {}) {
     if constexpr (detect_has_print_properties<T>::value)
       return printProperties(p, properties, elidedProps);
+    if constexpr (detect_has_print_properties_as_key_value_list<
+                      ConcreteType>::value)
+      return ConcreteType::_odsPrintPropertiesAsKeyValueList(ctx, p, properties,
+                                                             elidedProps);
     genericPrintProperties(
         p, ConcreteType::getPropertiesAsAttr(ctx, properties), elidedProps);
   }
diff --git a/mlir/include/mlir/TableGen/Operator.h b/mlir/include/mlir/TableGen/Operator.h
index f0514d8e61748..4c0ba2a1db9ec 100644
--- a/mlir/include/mlir/TableGen/Operator.h
+++ b/mlir/include/mlir/TableGen/Operator.h
@@ -143,6 +143,9 @@ class Operator {
   /// Returns true if default builders should not be generated.
   bool skipDefaultBuilders() const;
 
+  /// Returns true if the operation provides a custom properties printer.
+  bool hasCustomPropertiesPrinter() const;
+
   /// Op result iterators.
   const_value_iterator result_begin() const;
   const_value_iterator result_end() const;
diff --git a/mlir/lib/TableGen/Operator.cpp b/mlir/lib/TableGen/Operator.cpp
index 82dfbcbfa4d4f..148c3408b707d 100644
--- a/mlir/lib/TableGen/Operator.cpp
+++ b/mlir/lib/TableGen/Operator.cpp
@@ -186,6 +186,10 @@ bool Operator::skipDefaultBuilders() const {
   return def.getValueAsBit("skipDefaultBuilders");
 }
 
+bool Operator::hasCustomPropertiesPrinter() const {
+  return def.getValueAsBit("hasCustomPropertiesPrinter");
+}
+
 auto Operator::result_begin() const -> const_value_iterator {
   return results.begin();
 }
diff --git a/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-copy.mlir b/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-copy.mlir
index be0b9baf502bc..8f5b93bffe27e 100644
--- a/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-copy.mlir
+++ b/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-copy.mlir
@@ -18,7 +18,7 @@ func.func @alloc_copy(%arg0: memref<999xi32>) {
 // CHECK-LABEL:   func.func @alloc_copy(
 // CHECK-SAME:      %[[ARG0:.*]]: memref<999xi32>) {
 // CHECK:           %[[UNREALIZED_CONVERSION_CAST_0:.*]] = builtin.unrealized_conversion_cast %[[ARG0]] : memref<999xi32> to !emitc.array<999xi32>
-// CHECK:           %[[CALL_OPAQUE_0:.*]] = emitc.call_opaque "sizeof"() <{args = [i32]}> : () -> !emitc.size_t
+// CHECK:           %[[CALL_OPAQUE_0:.*]] = emitc.call_opaque "sizeof"() <args = [i32]> : () -> !emitc.size_t
 // CHECK:           %[[VAL_0:.*]] = "emitc.constant"() <{value = 999 : index}> : () -> index
 // CHECK:           %[[MUL_0:.*]] = emitc.mul %[[CALL_OPAQUE_0]], %[[VAL_0]] : (!emitc.size_t, index) -> !emitc.size_t
 // CHECK:           %[[CALL_OPAQUE_1:.*]] = emitc.call_opaque "malloc"(%[[MUL_0]]) : (!emitc.size_t) -> !emitc.ptr<!emitc.opaque<"void">>
@@ -30,11 +30,11 @@ func.func @alloc_copy(%arg0: memref<999xi32>) {
 // CHECK:           %[[VAL_2:.*]] = "emitc.constant"() <{value = 0 : index}> : () -> index
 // CHECK:           %[[SUBSCRIPT_1:.*]] = emitc.subscript %[[UNREALIZED_CONVERSION_CAST_1]]{{\[}}%[[VAL_2]]] : (!emitc.array<999xi32>, index) -> !emitc.lvalue<i32>
 // CHECK:           %[[ADDRESS_OF_1:.*]] = emitc.address_of %[[SUBSCRIPT_1]] : !emitc.lvalue<i32>
-// CHECK:           %[[CALL_OPAQUE_2:.*]] = emitc.call_opaque "sizeof"() <{args = [i32]}> : () -> !emitc.size_t
+// CHECK:           %[[CALL_OPAQUE_2:.*]] = emitc.call_opaque "sizeof"() <args = [i32]> : () -> !emitc.size_t
 // CHECK:           %[[VAL_3:.*]] = "emitc.constant"() <{value = 999 : index}> : () -> index
 // CHECK:           %[[MUL_1:.*]] = emitc.mul %[[CALL_OPAQUE_2]], %[[VAL_3]] : (!emitc.size_t, index) -> !emitc.size_t
 // CHECK:           emitc.call_opaque "memcpy"(%[[ADDRESS_OF_1]], %[[ADDRESS_OF_0]], %[[MUL_1]]) : (!emitc.ptr<i32>, !emitc.ptr<i32>, !emitc.size_t) -> ()
-// CHECK:           %[[CALL_OPAQUE_3:.*]] = emitc.call_opaque "sizeof"() <{args = [i32]}> : () -> !emitc.size_t
+// CHECK:           %[[CALL_OPAQUE_3:.*]] = emitc.call_opaque "sizeof"() <args = [i32]> : () -> !emitc.size_t
 // CHECK:           %[[VAL_4:.*]] = "emitc.constant"() <{value = 999 : index}> : () -> index
 // CHECK:           %[[MUL_2:.*]] = emitc.mul %[[CALL_OPAQUE_3]], %[[VAL_4]] : (!emitc.size_t, index) -> !emitc.size_t
 // CHECK:           %[[CALL_OPAQUE_4:.*]] = emitc.call_opaque "malloc"(%[[MUL_2]]) : (!emitc.size_t) -> !emitc.ptr<!emitc.opaque<"void">>
@@ -46,7 +46,7 @@ func.func @alloc_copy(%arg0: memref<999xi32>) {
 // CHECK:           %[[VAL_6:.*]] = "emitc.constant"() <{value = 0 : index}> : () -> index
 // CHECK:           %[[SUBSCRIPT_3:.*]] = emitc.subscript %[[UNREALIZED_CONVERSION_CAST_2]]{{\[}}%[[VAL_6]]] : (!emitc.array<999xi32>, index) -> !emitc.lvalue<i32>
 // CHECK:           %[[ADDRESS_OF_3:.*]] = emitc.address_of %[[SUBSCRIPT_3]] : !emitc.lvalue<i32>
-// CHECK:           %[[CALL_OPAQUE_5:.*]] = emitc.call_opaque "sizeof"() <{args = [i32]}> : () -> !emitc.size_t
+// CHECK:           %[[CALL_OPAQUE_5:.*]] = emitc.call_opaque "sizeof"() <args = [i32]> : () -> !emitc.size_t
 // CHECK:           %[[VAL_7:.*]] = "emitc.constant"() <{value = 999 : index}> : () -> index
 // CHECK:           %[[MUL_3:.*]] = emitc.mul %[[CALL_OPAQUE_5]], %[[VAL_7]] : (!emitc.size_t, index) -> !emitc.size_t
 // CHECK:           emitc.call_opaque "memcpy"(%[[ADDRESS_OF_3]], %[[ADDRESS_OF_2]], %[[MUL_3]]) : (!emitc.ptr<i32>, !emitc.ptr<i32>, !emitc.size_t) -> ()
diff --git a/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-dealloc.mlir b/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-dealloc.mlir
index 3194a40c16eeb..ad2329b922c81 100644
--- a/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-dealloc.mlir
+++ b/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-dealloc.mlir
@@ -15,7 +15,7 @@ func.func @alloc_and_dealloc() {
 // CPP:      module {
 // CPP-NEXT:   emitc.include <"cstdlib">
 // CPP-LABEL:  alloc_and_dealloc()
-// CPP-NEXT:   %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() <{args = [i32]}> : () -> !emitc.size_t
+// CPP-NEXT:   %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() <args = [i32]> : () -> !emitc.size_t
 // CPP-NEXT:   %[[ALLOC_SIZE:.*]] = "emitc.constant"() <{value = 999 : index}> : () -> index
 // CPP-NEXT:   %[[ALLOC_TOTAL_SIZE:.*]] = emitc.mul %[[ALLOC]], %[[ALLOC_SIZE]] : (!emitc.size_t, index) -> !emitc.size_t
 // CPP-NEXT:   %[[ALLOC_PTR:.*]] = emitc.call_opaque "malloc"(%[[ALLOC_TOTAL_SIZE]]) : (!emitc.size_t) -> !emitc.ptr<!emitc.opaque<"void">>
@@ -27,7 +27,7 @@ func.func @alloc_and_dealloc() {
 // NOCPP:      module {
 // NOCPP-NEXT:   emitc.include <"stdlib.h">
 // NOCPP-LABEL:  alloc_and_dealloc()
-// NOCPP-NEXT:   %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() <{args = [i32]}> : () -> !emitc.size_t
+// NOCPP-NEXT:   %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() <args = [i32]> : () -> !emitc.size_t
 // NOCPP-NEXT:   %[[ALLOC_SIZE:.*]] = "emitc.constant"() <{value = 999 : index}> : () -> index
 // NOCPP-NEXT:   %[[ALLOC_TOTAL_SIZE:.*]] = emitc.mul %[[ALLOC]], %[[ALLOC_SIZE]] : (!emitc.size_t, index) -> !emitc.size_t
 // NOCPP-NEXT:   %[[ALLOC_PTR:.*]] = emitc.call_opaque "malloc"(%[[ALLOC_TOTAL_SIZE]]) : (!emitc.size_t) -> !emitc.ptr<!emitc.opaque<"void">>
@@ -43,7 +43,7 @@ func.func @alloc_and_dealloc_aligned() {
 }
 
 // CPP-LABEL: alloc_and_dealloc_aligned
-// CPP-NEXT: %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() <{args = [f32]}> : () -> !emitc.size_t
+// CPP-NEXT: %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() <args = [f32]> : () -> !emitc.size_t
 // CPP-NEXT: %[[ALLOC_SIZE:.*]] = "emitc.constant"() <{value = 999 : index}> : () -> index
 // CPP-NEXT: %[[ALLOC_TOTAL_SIZE:.*]] = emitc.mul %[[ALLOC]], %[[ALLOC_SIZE]] : (!emitc.size_t, index) -> !emitc.size_t
 // CPP-NEXT: %[[ALIGNMENT:.*]] = "emitc.constant"() <{value = 64 : index}> : () -> !emitc.size_t 
@@ -54,7 +54,7 @@ func.func @alloc_and_dealloc_aligned() {
 // CPP-NEXT: return
 
 // NOCPP-LABEL: alloc_and_dealloc_aligned
-// NOCPP-NEXT: %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() <{args = [f32]}> : () -> !emitc.size_t
+// NOCPP-NEXT: %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() <args = [f32]> : () -> !emitc.size_t
 // NOCPP-NEXT: %[[ALLOC_SIZE:.*]] = "emitc.constant"() <{value = 999 : index}> : () -> index
 // NOCPP-NEXT: %[[ALLOC_TOTAL_SIZE:.*]] = emitc.mul %[[ALLOC]], %[[ALLOC_SIZE]] : (!emitc.size_t, index) -> !emitc.size_t
 // NOCPP-NEXT: %[[ALIGNMENT:.*]] = "emitc.constant"() <{value = 64 : index}> : () -> !emitc.size_t 
@@ -71,7 +71,7 @@ func.func @allocating_and_deallocating_multi() {
 }
 
 // CPP-LABEL: allocating_and_deallocating_multi
-// CPP-NEXT: %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() <{args = [i32]}> : () -> !emitc.size_t
+// CPP-NEXT: %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() <args = [i32]> : () -> !emitc.size_t
 // CPP-NEXT: %[[ALLOC_SIZE:.*]] = "emitc.constant"() <{value = 6993 : index}> : () -> index
 // CPP-NEXT: %[[ALLOC_TOTAL_SIZE:.*]] = emitc.mul %[[ALLOC]], %[[ALLOC_SIZE]] : (!emitc.size_t, index) -> !emitc.size_t
 // CPP-NEXT: %[[ALLOC_PTR:.*]] = emitc.call_opaque "malloc"(%[[ALLOC_TOTAL_SIZE]]) : (!emitc.size_t) -> !emitc.ptr<!emitc.opaque<"void">
@@ -81,7 +81,7 @@ func.func @allocating_and_deallocating_multi() {
 // CPP-NEXT: return
 
 // NOCPP-LABEL: allocating_and_deallocating_multi
-// NOCPP-NEXT: %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() <{args = [i32]}> : () -> !emitc.size_t
+// NOCPP-NEXT: %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() <args = [i32]> : () -> !emitc.size_t
 // NOCPP-NEXT: %[[ALLOC_SIZE:.*]] = "emitc.constant"() <{value = 6993 : index}> : () -> index
 // NOCPP-NEXT: %[[ALLOC_TOTAL_SIZE:.*]] = emitc.mul %[[ALLOC]], %[[ALLOC_SIZE]] : (!emitc.size_t, index) -> !emitc.size_t
 // NOCPP-NEXT: %[[ALLOC_PTR:.*]] = emitc.call_opaque "malloc"(%[[ALLOC_TOTAL_SIZE]]) : (!emitc.size_t) -> !emitc.ptr<!emitc.opaque<"void">>
@@ -97,7 +97,7 @@ func.func @alloc_and_dealloc_rank0() {
 }
 
 // CPP-LABEL: alloc_and_dealloc_rank0
-// CPP-NEXT: %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() <{args = [i32]}> : () -> !emitc.size_t
+// CPP-NEXT: %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() <args = [i32]> : () -> !emitc.size_t
 // CPP-NEXT: %[[ALLOC_SIZE:.*]] = "emitc.constant"() <{value = 1 : index}> : () -> index
 // CPP-NEXT: %[[ALLOC_TOTAL_SIZE:.*]] = emitc.mul %[[ALLOC]], %[[ALLOC_SIZE]] : (!emitc.size_t, index) -> !emitc.size_t
 // CPP-NEXT: %[[ALLOC_PTR:.*]] = emitc.call_opaque "malloc"(%[[ALLOC_TOTAL_SIZE]]) : (!emitc.size_t) -> !emitc.ptr<!emitc.opaque<"void">>
@@ -107,7 +107,7 @@ func.func @alloc_and_dealloc_rank0() {
 // CPP-NEXT: return
 
 // NOCPP-LABEL: alloc_and_dealloc_rank0
-// NOCPP-NEXT: %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() <{args = [i32]}> : () -> !emitc.size_t
+// NOCPP-NEXT: %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() <args = [i32]> : () -> !emitc.size_t
 // NOCPP-NEXT: %[[ALLOC_SIZE:.*]] = "emitc.constant"() <{value = 1 : index}> : () -> index
 // NOCPP-NEXT: %[[ALLOC_TOTAL_SIZE:.*]] = emitc.mul %[[ALLOC]], %[[ALLOC_SIZE]] : (!emitc.size_t, index) -> !emitc.size_t
 // NOCPP-NEXT: %[[ALLOC_PTR:.*]] = emitc.call_opaque "malloc"(%[[ALLOC_TOTAL_SIZE]]) : (!emitc.size_t) -> !emitc.ptr<!emitc.opaque<"void">>
diff --git a/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-load-store.mlir b/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-load-store.mlir
index 653220470bb5a..43c5733baccb3 100644
--- a/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-load-store.mlir
+++ b/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-load-store.mlir
@@ -21,7 +21,7 @@
 // CHECK-SAME:  %[[ARG_J:.*]]: !emitc.size_t)
 func.func private @memref_alloc_store(%v : f32, %i: index, %j: index) {
   /// Allocation size  computation
-  // CHECK:     %[[SIZEOF_F32:.*]] = call_opaque "sizeof"() <{args = [f32]}> : () -> !emitc.size_t
+  // CHECK:     %[[SIZEOF_F32:.*]] = call_opaque "sizeof"() <args = [f32]> : () -> !emitc.size_t
   // CHECK:     %[[NUM_ELEMS:.*]] = "emitc.constant"() <{value = 32 : index}> : () -> index
   // CHECK:     %[[TOTAL_BYTES:.*]] = mul %[[SIZEOF_F32]], %[[NUM_ELEMS]] : (!emitc.size_t, index) -> !emitc.size_t
   /// Alloc
@@ -42,7 +42,7 @@ func.func private @memref_alloc_store(%v : f32, %i: index, %j: index) {
 // CHECK-SAME:  %[[ARG_I:.*]]: !emitc.size_t,
 // CHECK-SAME:  %[[ARG_J:.*]]: !emitc.size_t) -> f32
 func.func private @memref_alloc_load(%i: index, %j: index) -> f32 {
-  // CHECK:     %[[SIZEOF_F32:.*]] = call_opaque "sizeof"() <{args = [f32]}> : () -> !emitc.size_t
+  // CHECK:     %[[SIZEOF_F32:.*]] = call_opaque "sizeof"() <args = [f32]> : () -> !emitc.size_t
   // CHECK:     %[[NUM_ELEMS:.*]] = "emitc.constant"() <{value = 32 : index}> : () -> index
   // CHECK:     %[[TOTAL_BYTES:.*]] = mul %[[SIZEOF_F32]], %[[NUM_ELEMS]] : (!emitc.size_t, index) -> !emitc.size_t
   // CHECK:     %[[MALLOC_PTR:.*]] = call_opaque "malloc"(%[[TOTAL_BYTES]]) : (!emitc.size_t) -> !emitc.ptr<!emitc.opaque<"void">>
@@ -77,7 +77,7 @@ func.func @memref_load_store(%buff0: memref<2xf32>,
 // CHECK-LABEL: emitc.func private @memref_alloc_store_rank0(
 // CHECK-SAME:  %[[VAL:.*]]: i32)
 func.func private @memref_alloc_store_rank0(%v : i32) {
-  // CHECK:     %[[SIZEOF_I32:.*]] = call_opaque "sizeof"() <{args = [i32]}> : () -> !emitc.size_t
+  // CHECK:     %[[SIZEOF_I32:.*]] = call_opaque "sizeof"() <args = [i32]> : () -> !emitc.size_t
   // CHECK:     %[[NUM_ELEMS:.*]] = "emitc.constant"() <{value = 1 : index}> : () -> index
   // CHECK:     %[[TOTAL_BYTES:.*]] = mul %[[SIZEOF_I32]], %[[NUM_ELEMS]] : (!emitc.size_t, index) -> !emitc.size_t
   // CHECK:     %[[MALLOC_PTR:.*]] = call_opaque "malloc"(%[[TOTAL_BYTES]]) : (!emitc.size_t) -> !emitc.ptr<!emitc.opaque<"void">>
@@ -92,7 +92,7 @@ func.func private @memref_alloc_store_rank0(%v : i32) {
 
 // CHECK-LABEL: emitc.func private @memref_alloc_load_rank0() -> i32
 func.func private @memref_alloc_load_rank0() -> i32 {
-  // CHECK:     %[[SIZEOF_I32:.*]] = call_opaque "sizeof"() <{args = [i32]}> : () -> !emitc.size_t
+  // CHECK:     %[[SIZEOF_I32:.*]] = call_opaque "sizeof"() <args = [i32]> : () -> !emitc.size_t
   // CHECK:     %[[NUM_ELEMS:.*]] = "emitc.constant"() <{value = 1 : index}> : () -> index
   // CHECK:     %[[TOTAL_BYTES:.*]] = mul %[[SIZEOF_I32]], %[[NUM_ELEMS]] : (!emitc.size_t, index) -> !emitc.size_t
   // CHECK:     %[[MALLOC_PTR:.*]] = call_opaque "malloc"(%[[TOTAL_BYTES]]) : (!emitc.size_t) -> !emitc.ptr<!emitc.opaque<"void">>
diff --git a/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-copy.mlir b/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-copy.mlir
index 6105521d9326d..828aa68ea9f1c 100644
--- a/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-copy.mlir
+++ b/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-copy.mlir
@@ -21,7 +21,7 @@ func.func @copying(%arg0 : memref<9x4x5x7xf32>, %arg1 : memref<9x4x5x7xf32>) {
 // CHECK:           %[[VAL_1:.*]] = "emitc.constant"() <{value = 0 : index}> : () -> index
 // CHECK:           %[[SUBSCRIPT_1:.*]] = emitc.subscript %[[UNREALIZED_CONVERSION_CAST_0]]{{\[}}%[[VAL_1]], %[[VAL_1]], %[[VAL_1]], %[[VAL_1]]] : (!emitc.array<9x4x5x7xf32>, index, index, index, index) -> !emitc.lvalue<f32>
 // CHECK:           %[[ADDRESS_OF_1:.*]] = emitc.address_of %[[SUBSCRIPT_1]] : !emitc.lvalue<f32>
-// CHECK:           %[[CALL_OPAQUE_0:.*]] = emitc.call_opaque "sizeof"() <{args = [f32]}> : () -> !emitc.size_t
+// CHECK:           %[[CALL_OPAQUE_0:.*]] = emitc.call_opaque "sizeof"() <args = [f32]> : () -> !emitc.size_t
 // CHECK:           %[[VAL_2:.*]] = "emitc.constant"() <{value = 1260 : index}> : () -> index
 // CHECK:           %[[MUL_0:.*]] = emitc.mul %[[CALL_OPAQUE_0]], %[[VAL_2]] : (!emitc.size_t, index) -> !emitc.size_t
 // CHECK:           emitc.call_opaque "memcpy"(%[[ADDRESS_OF_1]], %[[ADDRESS_OF_0]], %[[MUL_0]]) : (!emitc.ptr<f32>, !emitc.ptr<f32>, !emitc.size_t) -> ()
diff --git a/mlir/test/Conversion/MemRefToEmitC/memref-with-custom-types.mlir b/mlir/test/Conversion/MemRefToEmitC/memref-with-custom-types.mlir
index 4cc874633d456..bdb41baa4f743 100644
--- a/mlir/test/Conversion/MemRefToEmitC/memref-with-custom-types.mlir
+++ b/mlir/test/Conversion/MemRefToEmitC/memref-with-custom-types.mlir
@@ -5,7 +5,7 @@
 
 // CHECK-LABEL: emitc.func @alloc_with_custom_element_type()
 func.func @alloc_with_custom_element_type() {
-  // CHECK:       call_opaque "sizeof"() <{args = [!emitc.opaque<"TestElementT">]}> : () -> !emitc.size_t
+  // CHECK:       call_opaque "sizeof"() <args = [!emitc.opaque<"TestElementT">]> : () -> !emitc.size_t
   // CHECK:       cast
   // CHECK-SAME:  !emitc.ptr<!emitc.opaque<"void">> to !emitc.ptr<!emitc.opaque<"TestElementT">>
   %0 = memref.alloc() : memref<10x!test.memref_element>
@@ -43,4 +43,3 @@ func.func @load_with_custom_element_type(%i: index) -> !test.memref_element {
   %v = memref.load %alloc[%i] : memref<4x!test.memref_element>
   return %v : !test.memref_element
 }
-
diff --git a/mlir/test/Dialect/EmitC/member_call_opaque.mlir b/mlir/test/Dialect/EmitC/member_call_opaque.mlir
index 5971d7826b270..bc4e0d55ddb48 100644
--- a/mlir/test/Dialect/EmitC/member_call_opaque.mlir
+++ b/mlir/test/Dialect/EmitC/member_call_opaque.mlir
@@ -19,14 +19,14 @@ func.func @member_call_template_args(%arg0 : !emitc.opaque<"MyClass">) {
   return
 }
 // CHECK-LABEL: func @member_call_template_args
-// CHECK: emitc.member_call_opaque %arg0 "method"() <{template_args = [i32]}> : !emitc.opaque<"MyClass">, () -> i32
+// CHECK: emitc.member_call_opaque %arg0 "method"() <template_args = [i32]> : !emitc.opaque<"MyClass">, () -> i32
 
 func.func @member_call_reorder(%arg0 : !emitc.opaque<"MyClass">, %arg1 : i32, %arg2 : i32) {
   %0 = emitc.member_call_opaque %arg0 "method" (%arg1, %arg2) <{args = [1 : index, 0 : index]}> : !emitc.opaque<"MyClass">, (i32, i32) -> i32
   return
 }
 // CHECK-LABEL: func @member_call_reorder
-// CHECK: emitc.member_call_opaque %arg0 "method"(%arg1, %arg2) <{args = [1 : index, 0 : index]}> : !emitc.opaque<"MyClass">, (i32, i32) -> i32
+// CHECK: emitc.member_call_opaque %arg0 "method"(%arg1, %arg2) <args = [1 : index, 0 : index]> : !emitc.opaque<"MyClass">, (i32, i32) -> i32
 
 func.func @member_call_lvalue_arg(%arg0 : !emitc.opaque<"MyClass">, %arg1 : !emitc.lvalue<i32>) {
   %0 = emitc.member_call_opaque %arg0 "method" (%arg1) : !emitc.opaque<"MyClass">, (!emitc.lvalue<i32>) -> i32
diff --git a/mlir/test/IR/enum-attr-roundtrip.mlir b/mlir/test/IR/enum-attr-roundtrip.mlir
index d02776e226c80..113b5f07ee6f3 100644
--- a/mlir/test/IR/enum-attr-roundtrip.mlir
+++ b/mlir/test/IR/enum-attr-roundtrip.mlir
@@ -44,14 +44,14 @@ func.func @test_enum_prop() -> () {
   // CHECK: test.op_with_enum_prop first
   "test.op_with_enum_prop"() <{value = 0 : i32}> {} : () -> ()
 
-  // CHECK: test.op_with_enum_prop_attr_form <{value = 0 : i32}>
+  // CHECK: test.op_with_enum_prop_attr_form <value = first>
   test.op_with_enum_prop_attr_form <{value = 0 : i32}>
-  // CHECK: test.op_with_enum_prop_attr_form <{value = 1 : i32}>
+  // CHECK: test.op_with_enum_prop_attr_form <value = second>
   test.op_with_enum_prop_attr_form <{value = #test<enum second>}>
 
-  // CHECK: test.op_with_enum_prop_attr_form_always <{value = #test<enum first>}>
+  // CHECK: test.op_with_enum_prop_attr_form_always <value = first>
   test.op_with_enum_prop_attr_form_always <{value = #test<enum first>}>
-  // CHECK: test.op_with_enum_prop_attr_form_always  <{value = #test<enum second>}
+  // CHECK: test.op_with_enum_prop_attr_form_always <value = second>
   test.op_with_enum_prop_attr_form_always <{value = #test<enum second>}>
 
   return
diff --git a/mlir/test/IR/properties.mlir b/mlir/test/IR/properties.mlir
index 64548e41dc111..de37707a79d4d 100644
--- a/mlir/test/IR/properties.mlir
+++ b/mlir/test/IR/properties.mlir
@@ -14,7 +14,7 @@ test.with_properties a = 32, b = "foo", c = "bar", flag = true, array = [1, 2, 3
 test.with_nice_properties "foo bar" is -3
 
 // CHECK:   test.with_wrapped_properties
-// CHECK-SAME:    <{prop = "content for properties"}>{{$}}
+// CHECK-SAME:    <prop = "content for properties">{{$}}
 // GENERIC: "test.with_wrapped_properties"()
 // GENERIC-SAME:  <{prop = "content for properties"}> : () -> ()
 test.with_wrapped_properties <{prop = "content for properties"}>
@@ -28,44 +28,50 @@ test.empty_properties
 // GENERIC: "test.empty_properties"()
 test.empty_properties <>
 
-// The key-value spelling uses the custom parsers for both attributes and
-// properties. Until the custom printer is enabled, it round-trips to the
-// generic DictionaryAttr spelling.
-// CHECK: test.with_custom_prop_dict <{attr = 1 : i32, prop = 2 : i64}>
+// The key-value spelling uses the custom parsers and printers for both
+// attributes and properties.
+// CHECK: test.with_custom_prop_dict <prop = 2, attr = 1>
 // GENERIC: "test.with_custom_prop_dict"()
 // GENERIC-SAME: <{attr = 1 : i32, defaulted = 42 : i64, prop = 2 : i64, unit = false}>
 test.with_custom_prop_dict <attr = 1, prop = 2>
 
 // The generic DictionaryAttr spelling remains accepted for compatibility.
-// CHECK: test.with_custom_prop_dict <{attr = 3 : i32, prop = 4 : i64}>
+// CHECK: test.with_custom_prop_dict <prop = 4, attr = 3>
 // GENERIC: "test.with_custom_prop_dict"()
 // GENERIC-SAME: <{attr = 3 : i32, defaulted = 42 : i64, prop = 4 : i64, unit = false}>
 test.with_custom_prop_dict <{attr = 3 : i32, prop = 4 : i64}>
 
 // Entries are order-independent, and optional/default-valued entries use
 // their custom parsers when present.
-// CHECK: test.with_custom_prop_dict <{attr = 5 : i32, defaulted = 43 : i64, optional = "set", prop = 6 : i64}>
+// CHECK: test.with_custom_prop_dict <prop = 6, defaulted = 43, attr = 5, optional = "set">
 // GENERIC: "test.with_custom_prop_dict"()
 // GENERIC-SAME: <{attr = 5 : i32, defaulted = 43 : i64, optional = "set", prop = 6 : i64, unit = false}>
 test.with_custom_prop_dict <optional = "set", defaulted = 43, prop = 6, attr = 5>
 
 // A field name that is also the start of an attribute must not be consumed by
 // the legacy DictionaryAttr compatibility probe.
-// CHECK: test.with_custom_prop_dict <{attr = 7 : i32, prop = 8 : i64, unit}>
+// CHECK: test.with_custom_prop_dict <prop = 8, unit = unit, attr = 7>
 // GENERIC: "test.with_custom_prop_dict"()
 // GENERIC-SAME: <{attr = 7 : i32, defaulted = 42 : i64, prop = 8 : i64, unit}>
 test.with_custom_prop_dict <unit = unit, attr = 7, prop = 8>
 
+// Inherent attributes use their custom assembly printer in the key-value
+// spelling. Optional enum attributes compile and are omitted when absent.
+// CHECK: test.with_custom_attr_prop_dict <prop = 9, attr = first>
+test.with_custom_attr_prop_dict <attr = first, prop = 9>
+// CHECK: test.with_custom_attr_prop_dict <prop = 10, attr = first, optionalAttr = second>
+test.with_custom_attr_prop_dict <optionalAttr = second, prop = 10, attr = first>
+
 // Properties bound elsewhere in the assembly format are excluded from the
 // key-value list.
-// CHECK: test.with_properties_and_attr 7 <{rhs = 8 : i64}>
+// CHECK: test.with_properties_and_attr 7 <rhs = 8>
 // GENERIC: "test.with_properties_and_attr"()
 // GENERIC-SAME: <{lhs = 7 : i32, rhs = 8 : i64}>
 test.with_properties_and_attr 7 <rhs = 8>
 
 // A property without a usable custom parser falls back to its attribute
 // conversion for this compatibility spelling.
-// CHECK: test.with_wrapped_properties <{prop = "custom spelling"}>
+// CHECK: test.with_wrapped_properties <prop = "custom spelling">
 // GENERIC: "test.with_wrapped_properties"()
 // GENERIC-SAME: <{prop = "custom spelling"}>
 test.with_wrapped_properties <prop = "custom spelling">
@@ -89,7 +95,8 @@ test.with_wrapped_array_properties <prop = ["first", "second"]>
 // following scalar key also checks that the container does not consume the
 // outer comma.
 // CHECK: test.with_key_value_parser_boundaries
-// CHECK-SAME: <{maybe = [], maybeEnum = [], next = 9 : i64, specializedMaybe = [7 : i16], specializedValues = array<i32: 3, 4>, values = array<i64: 1, 2>}>
+// CHECK-SAME: <values = array<i64: 1, 2>, maybe = [], maybeEnum = [],
+// CHECK-SAME: specializedValues = [3, 4], specializedMaybe = some<7>, next = 9>
 // GENERIC: "test.with_key_value_parser_boundaries"()
 // GENERIC-SAME: <{maybe = [], maybeEnum = [], next = 9 : i64, specializedMaybe = [7 : i16], specializedValues = array<i32: 3, 4>, values = array<i64: 1, 2>}>
 test.with_key_value_parser_boundaries <specializedValues = [3, 4], specializedMaybe = some<7>, values = array<i64: 1, 2>, maybe = [], maybeEnum = [], next = 9>
@@ -97,7 +104,7 @@ test.with_key_value_parser_boundaries <specializedValues = [3, 4], specializedMa
 // A comma-separated bit-enum FieldParser is not compositional with the outer
 // list, so prop-dict uses its attribute conversion before parsing another key.
 // CHECK: test.op_with_bit_enum_prop_dict
-// CHECK-SAME: <{flags = 3 : i32, next = 9 : i64}>
+// CHECK-SAME: <flags = 3 : i32, next = 9>
 // GENERIC: "test.op_with_bit_enum_prop_dict"()
 // GENERIC-SAME: <{flags = 3 : i32, next = 9 : i64}>
 test.op_with_bit_enum_prop_dict <flags = 3 : i32, next = 9>
@@ -132,7 +139,7 @@ test.variadic_segment_prop %ci64, %ci64 : %ci64 : i64, i64 : i64 end
 // `<{...}>`. Without the parser-side fix, re-parsing the CHECK line below
 // (which is exactly what the printer emits) fails with "duplicate or unknown
 // key 'operandSegmentSizes' in dictionary attribute".
-// CHECK: test.variadic_segment_prop_bulk_type(%[[CI64]], %[[CI64]], %[[CI64]]) : (i64, i64, i64) -> (i64, i64, i64) <{operandSegmentSizes = array<i32: 2, 1>, resultSegmentSizes = array<i32: 2, 1>}>
+// CHECK: test.variadic_segment_prop_bulk_type(%[[CI64]], %[[CI64]], %[[CI64]]) : (i64, i64, i64) -> (i64, i64, i64) <operandSegmentSizes = [2, 1], resultSegmentSizes = [2, 1]>
 // GENERIC: "test.variadic_segment_prop_bulk_type"(%[[CI64]], %[[CI64]], %[[CI64]]) <{operandSegmentSizes = array<i32: 2, 1>, resultSegmentSizes = array<i32: 2, 1>}> : (i64, i64, i64) -> (i64, i64, i64)
 test.variadic_segment_prop_bulk_type(%ci64, %ci64, %ci64) : (i64, i64, i64) -> (i64, i64, i64) <operandSegmentSizes = [2, 1], resultSegmentSizes = [2, 1]>
 
@@ -190,7 +197,8 @@ test.with_array_properties ints = [1, 2] strings = ["a", "b"] nested = [[1, 2],
 
 // Tests that DefaultValuedProp is elided from prop-dict when value equals default.
 // CHECK: test.op_with_property_predicates
-// CHECK-SAME: <{array = [], more_constrained = 1 : i64, non_empty_constrained = [1], non_empty_unconstrained = [1], scalar = 1 : i64, unconstrained = 0 : i64}>
+// CHECK-SAME: <scalar = 1, more_constrained = 1, array = [],
+// CHECK-SAME: non_empty_unconstrained = [1], non_empty_constrained = [1], unconstrained = 0>
 // CHECK-NOT: defaulted
 test.op_with_property_predicates <{
   scalar = 1 : i64,
@@ -203,8 +211,8 @@ test.op_with_property_predicates <{
 // Keyed parsing composes optional and aggregate property parsers with a
 // following outer dictionary entry.
 // CHECK: test.op_with_property_predicates
+// CHECK-SAME: optional = 2
 // CHECK-SAME: array = [3, 4]
-// CHECK-SAME: optional = [2]
 test.op_with_property_predicates <
   scalar = 1,
   optional = 2,
diff --git a/mlir/test/Target/LLVMIR/Import/intrinsic.ll b/mlir/test/Target/LLVMIR/Import/intrinsic.ll
index 5bbb920e423c8..070ffa654129b 100644
--- a/mlir/test/Target/LLVMIR/Import/intrinsic.ll
+++ b/mlir/test/Target/LLVMIR/Import/intrinsic.ll
@@ -582,7 +582,7 @@ define void @trap_intrinsics() {
   call void @llvm.trap()
   ; CHECK: llvm.intr.debugtrap
   call void @llvm.debugtrap()
-  ; CHECK: llvm.intr.ubsantrap <{failureKind = 1 : i8}>
+  ; CHECK: llvm.intr.ubsantrap <failureKind = 1>
   call void @llvm.ubsantrap(i8 1)
   ret void
 }
diff --git a/mlir/test/lib/Dialect/Test/TestOps.td b/mlir/test/lib/Dialect/Test/TestOps.td
index 0bd7fe401c3c9..2908d743f0ece 100644
--- a/mlir/test/lib/Dialect/Test/TestOps.td
+++ b/mlir/test/lib/Dialect/Test/TestOps.td
@@ -3645,6 +3645,16 @@ def TestOpWithCustomPropDict : TEST_Op<"with_custom_prop_dict"> {
   );
 }
 
+def TestOpWithCustomAttrPropDict
+    : TEST_Op<"with_custom_attr_prop_dict"> {
+  let assemblyFormat = "prop-dict attr-dict";
+  let arguments = (ins
+    TestEnumAttr:$attr,
+    OptionalAttr<TestEnumAttr>:$optionalAttr,
+    I64Prop:$prop
+  );
+}
+
 def TestOpWithPropertiesAndInferredType
   : TEST_Op<"with_properties_and_inferred_type", [
     DeclareOpInterfaceMethods<InferTypeOpInterface>
@@ -3842,6 +3852,13 @@ def KeyValueOptionalEnumProperty
 
 def KeyValueSpecializedListProperty
     : Property<"::llvm::SmallVector<int32_t>"> {
+  let printer = [{
+    [&]() {
+      $_printer << "[";
+      ::llvm::interleaveComma($_storage, $_printer);
+      $_printer << "]";
+    }()
+  }];
   let convertToAttribute =
       "return ::mlir::DenseI32ArrayAttr::get($_ctxt, $_storage);";
   let convertFromAttribute = [{
@@ -3860,6 +3877,15 @@ def KeyValueSpecializedListProperty
 
 def KeyValueSpecializedOptionalProperty
     : Property<"std::optional<int16_t>"> {
+  let printer = [{
+    [&]() {
+      if (!$_storage) {
+        $_printer << "none";
+        return;
+      }
+      $_printer << "some<" << *$_storage << ">";
+    }()
+  }];
   let convertToAttribute = [{
     if (!$_storage)
       return ::mlir::ArrayAttr::get($_ctxt, {});
@@ -4005,6 +4031,7 @@ def PropertiesWithCustomPrint : Property<"PropertiesWithCustomPrint"> {
 }
 
 def TestOpWithNiceProperties : TEST_Op<"with_nice_properties"> {
+  let hasCustomPropertiesPrinter = 1;
   let assemblyFormat = "prop-dict attr-dict";
   let arguments = (ins
      PropertiesWithCustomPrint:$prop
@@ -4069,6 +4096,7 @@ def VersionedProperties : Property<"VersionedProperties"> {
 }
 
 def TestOpWithVersionedProperties : TEST_Op<"with_versioned_properties"> {
+  let hasCustomPropertiesPrinter = 1;
   let assemblyFormat = "prop-dict attr-dict";
   let arguments = (ins
      VersionedProperties:$prop
diff --git a/mlir/test/mlir-tblgen/op-format-custom-properties-printer.td b/mlir/test/mlir-tblgen/op-format-custom-properties-printer.td
new file mode 100644
index 0000000000000..18743e41feb64
--- /dev/null
+++ b/mlir/test/mlir-tblgen/op-format-custom-properties-printer.td
@@ -0,0 +1,23 @@
+// RUN: mlir-tblgen -gen-op-defs -I %S/../../include %s | FileCheck %s
+
+include "mlir/IR/OpBase.td"
+
+def TestDialect : Dialect {
+  let name = "test";
+  let cppNamespace = "::test";
+}
+
+// A custom properties printer shadows the generated per-field helper.
+// CHECK-NOT: CustomPropertiesPrinterOp::_odsPrintPropertiesAsKeyValueList
+// CHECK: void CustomPropertiesPrinterOp::print
+// CHECK-NOT: CustomPropertiesPrinterOp::_odsPrintPropertiesAsKeyValueList
+def CustomPropertiesPrinterOp : Op<TestDialect, "custom_properties_printer"> {
+  let arguments = (ins I64Attr:$attr);
+  let assemblyFormat = "prop-dict attr-dict";
+  let hasCustomPropertiesPrinter = 1;
+  let extraClassDeclaration = [{
+    void printProperties(::mlir::MLIRContext *, ::mlir::OpAsmPrinter &,
+                         const Properties &,
+                         ::mlir::ArrayRef<::llvm::StringRef>);
+  }];
+}
diff --git a/mlir/test/mlir-tblgen/op-format.mlir b/mlir/test/mlir-tblgen/op-format.mlir
index 5f44e5cffe675..315b4e57beea0 100644
--- a/mlir/test/mlir-tblgen/op-format.mlir
+++ b/mlir/test/mlir-tblgen/op-format.mlir
@@ -297,13 +297,13 @@ test.format_optional_prop_dict <{a = [], b = 1 : i32}>
 // CHECK: test.format_optional_prop_dict {{$}}
 test.format_optional_prop_dict <{}>
 
-// CHECK: test.format_optional_prop_dict <{a = ["foo"]}>
+// CHECK: test.format_optional_prop_dict <a = "foo">
 test.format_optional_prop_dict <{a = ["foo"]}>
 
-// CHECK: test.format_optional_prop_dict <{b = 2 : i32}>
+// CHECK: test.format_optional_prop_dict <b = 2>
 test.format_optional_prop_dict <{b = 2 : i32}>
 
-// CHECK: test.format_optional_prop_dict <{a = ["foo"], b = 2 : i32}>
+// CHECK: test.format_optional_prop_dict <a = "foo", b = 2>
 test.format_optional_prop_dict <{a = ["foo"], b = 2 : i32}>
 
 //===----------------------------------------------------------------------===//
@@ -532,15 +532,15 @@ test.format_optional_operand_type(%i64) : i64
 // CHECK: test.format_infer_type_variadic_operands(%[[I32]], %[[I32]] : i32, i32) (%[[I64]], %[[I64]] : i64, i64)
 %ignored_res13:4 = test.format_infer_type_variadic_operands(%i32, %i32 : i32, i32) (%i64, %i64 : i64, i64)
 
-// CHECK: test.with_properties_and_attr 16 <{rhs = 16 : i64}>
+// CHECK: test.with_properties_and_attr 16 <rhs = 16>
 test.with_properties_and_attr 16 <{rhs = 16 : i64}>
 
-// CHECK: test.with_properties_and_inferred_type 16 <{packed, rhs = 16 : i64}>
+// CHECK: test.with_properties_and_inferred_type 16 <rhs = 16, packed = unit>
 %should_be_i32 = test.with_properties_and_inferred_type 16 <{packed, rhs = 16 : i64}>
 // Assert through the verifier that its inferred as i32.
 test.format_all_types_match_var %should_be_i32, %i32 : i32
 
-// CHECK: test.using_property_in_custom_and_other [1, 4, 20] <{other = 16 : i64}>
+// CHECK: test.using_property_in_custom_and_other [1, 4, 20] <other = 16>
 test.using_property_in_custom_and_other [1, 4, 20] <{other = 16 : i64}>
 
 //===----------------------------------------------------------------------===//
diff --git a/mlir/tools/mlir-tblgen/OpFormatGen.cpp b/mlir/tools/mlir-tblgen/OpFormatGen.cpp
index 815c51c91dddd..e503482ba5b71 100644
--- a/mlir/tools/mlir-tblgen/OpFormatGen.cpp
+++ b/mlir/tools/mlir-tblgen/OpFormatGen.cpp
@@ -2297,7 +2297,7 @@ static const char *regionSingleBlockImplicitTerminatorPrinterCode = R"(
 /// {1}: The name of the enum attributes symbolToString function.
 static const char *enumAttrBeginPrinterCode = R"(
   {
-    auto caseValue = {0}();
+    auto caseValue = {0};
     auto caseValueStr = {1}(caseValue);
 )";
 
@@ -2348,6 +2348,129 @@ static void genVariadicSegmentElision(OperationFormat &fmt, Operator &op,
     body << "  " << elidedStorage << ".push_back(\"resultSegmentSizes\");\n";
 }
 
+static void genEnumAttrPrinter(const NamedAttribute *var, const Operator &op,
+                               MethodBody &body, StringRef valueExpression);
+
+/// Generate the key-value printer used by the default `prop-dict` printer.
+static void genKeyValuePropDictPrinter(OperationFormat &fmt, Operator &op,
+                                       OpClass &opClass) {
+  if (!fmt.hasPropDict || !fmt.useProperties || op.hasCustomPropertiesPrinter())
+    return;
+
+  bool hasPrintableField =
+      !op.getProperties().empty() ||
+      llvm::any_of(
+          op.getAttributes(),
+          [](const auto &attr) { return !attr.attr.isDerivedAttr(); }) ||
+      (op.getTrait("::mlir::OpTrait::AttrSizedOperandSegments") &&
+       fmt.allOperands) ||
+      (op.getTrait("::mlir::OpTrait::AttrSizedResultSegments") &&
+       fmt.allResultTypes);
+  if (!hasPrintableField)
+    return;
+
+  SmallVector<MethodParameter> paramList;
+  paramList.emplace_back("::mlir::MLIRContext *", "_odsContext");
+  paramList.emplace_back("::mlir::OpAsmPrinter &", "_odsPrinter");
+  paramList.emplace_back("const Properties &", "prop");
+  paramList.emplace_back("::mlir::ArrayRef<::llvm::StringRef>", "elidedProps");
+  Method *method = opClass.addStaticMethod(
+      "void", "_odsPrintPropertiesAsKeyValueList", std::move(paramList));
+  MethodBody &body = method->body().indent();
+
+  body << R"decl(
+bool first = true;
+auto printKey = [&](::llvm::StringRef name) {
+  _odsPrinter << (first ? "<" : ", ") << name << " = ";
+  first = false;
+};
+auto shouldPrint = [&](::llvm::StringRef name) {
+  return !::llvm::is_contained(elidedProps, name);
+};
+)decl";
+
+  auto genSegmentSizesPrinter = [&](StringRef name) {
+    body << "if (shouldPrint(\"" << name << "\")) {\n"
+         << "  printKey(\"" << name << "\");\n"
+         << "  _odsPrinter << \"[\";\n"
+         << "  ::llvm::interleaveComma(prop." << name << ", _odsPrinter);\n"
+         << "  _odsPrinter << \"]\";\n"
+         << "}\n";
+  };
+  if (op.getTrait("::mlir::OpTrait::AttrSizedOperandSegments") &&
+      fmt.allOperands)
+    genSegmentSizesPrinter("operandSegmentSizes");
+  if (op.getTrait("::mlir::OpTrait::AttrSizedResultSegments") &&
+      fmt.allResultTypes)
+    genSegmentSizesPrinter("resultSegmentSizes");
+
+  for (const NamedProperty &namedProperty : op.getProperties()) {
+    const Property &property = namedProperty.prop;
+    body << "if (shouldPrint(\"" << namedProperty.name << "\")) {\n"
+         << "  printKey(\"" << namedProperty.name << "\");\n";
+    FmtContext printerContext;
+    printerContext.addSubst("_printer", "_odsPrinter");
+    printerContext.addSubst("_ctxt", "_odsContext");
+    printerContext.addSubst("_storage", "prop." + namedProperty.name);
+    if (property.usesDefaultParser()) {
+      body << "  if constexpr (::mlir::detail::HasKeyValueFieldParser<"
+              "std::remove_cv_t<std::remove_reference_t<decltype(prop."
+           << namedProperty.name << ")>>>::value) {\n"
+           << "    " << tgfmt(property.getPrinterCall(), &printerContext)
+           << ";\n"
+           << "  } else {\n"
+           << "    auto propertyAttr = [&]() -> ::mlir::Attribute {\n";
+      FmtContext conversionContext;
+      conversionContext.addSubst("_ctxt", "_odsContext");
+      conversionContext.addSubst("_storage", "prop." + namedProperty.name);
+      body << tgfmt(property.getConvertToAttributeCall(), &conversionContext)
+           << "\n"
+           << "    }();\n"
+           << "    _odsPrinter.printAttribute(propertyAttr);\n"
+           << "  }\n";
+    } else {
+      body << "  " << tgfmt(property.getPrinterCall(), &printerContext)
+           << ";\n";
+    }
+    body << "}\n";
+  }
+
+  for (const NamedAttribute &namedAttr : op.getAttributes()) {
+    if (namedAttr.attr.isDerivedAttr())
+      continue;
+    StringRef name = namedAttr.name;
+    body << "if (shouldPrint(\"" << name << "\")";
+    if (namedAttr.attr.isOptional())
+      body << " && prop." << name;
+    body << ") {\n"
+         << "  printKey(\"" << name << "\");\n";
+
+    if (canFormatEnumAttr(&namedAttr)) {
+      FmtContext conversionContext;
+      conversionContext.withSelf("prop." + name);
+      std::string valueExpression = std::string(tgfmt(
+          namedAttr.attr.getConvertFromStorageCall(), &conversionContext));
+      genEnumAttrPrinter(&namedAttr, op, body, valueExpression);
+    } else if (shouldFormatSymbolNameAttr(&namedAttr)) {
+      body << "  _odsPrinter.printSymbolName(prop." << name
+           << ".getValue());\n";
+    } else {
+      AttributeVariable attrVariable(&namedAttr);
+      if (attrVariable.getTypeBuilder())
+        body << "  _odsPrinter.printAttributeWithoutType(prop." << name
+             << ");\n";
+      else if (attrVariable.shouldBeQualified() ||
+               namedAttr.attr.getStorageType() == "::mlir::Attribute")
+        body << "  _odsPrinter.printAttribute(prop." << name << ");\n";
+      else
+        body << "  _odsPrinter.printStrippedAttrOrType(prop." << name << ");\n";
+    }
+    body << "}\n";
+  }
+  body << "if (!first)\n"
+          "  _odsPrinter << \">\";\n";
+}
+
 /// Generate the printer for the 'prop-dict' directive.
 static void genPropDictPrinter(OperationFormat &fmt, Operator &op,
                                MethodBody &body) {
@@ -2586,15 +2709,20 @@ static MethodBody &genTypeOperandPrinter(FormatElement *arg, const Operator &op,
 
 /// Generate the printer for an enum attribute.
 static void genEnumAttrPrinter(const NamedAttribute *var, const Operator &op,
-                               MethodBody &body) {
+                               MethodBody &body,
+                               StringRef valueExpression = {}) {
   Attribute baseAttr = var->attr.getBaseAttr();
   const EnumInfo enumInfo(getEnumInfoRecord(baseAttr));
   std::vector<EnumCase> cases = enumInfo.getAllCases();
   bool dereferenceGetter =
       var->attr.isOptional() && !var->attr.hasDefaultValue();
 
-  body << formatv(enumAttrBeginPrinterCode,
-                  (dereferenceGetter ? "*" : "") + op.getGetterName(var->name),
+  std::string caseValue = valueExpression.empty()
+                              ? op.getGetterName(var->name) + "()"
+                              : valueExpression.str();
+  if (dereferenceGetter)
+    caseValue = "*(" + caseValue + ")";
+  body << formatv(enumAttrBeginPrinterCode, caseValue,
                   enumInfo.getSymbolToStringFnName());
 
   // Get a string containing all of the cases that can't be represented with a
@@ -2990,6 +3118,8 @@ void OperationFormat::genPrinter(Operator &op, OpClass &opClass) {
   bool shouldEmitSpace = true, lastWasPunctuation = false;
   for (FormatElement *element : elements)
     genElementPrinter(element, body, op, shouldEmitSpace, lastWasPunctuation);
+
+  genKeyValuePropDictPrinter(*this, op, opClass);
 }
 
 //===----------------------------------------------------------------------===//



More information about the llvm-branch-commits mailing list