diff --git a/mlir/include/mlir/Bytecode/BytecodeImplementation.h b/mlir/include/mlir/Bytecode/BytecodeImplementation.h --- a/mlir/include/mlir/Bytecode/BytecodeImplementation.h +++ b/mlir/include/mlir/Bytecode/BytecodeImplementation.h @@ -23,6 +23,17 @@ #include "llvm/ADT/Twine.h" namespace mlir { +//===--------------------------------------------------------------------===// +// Dialect Version Interface. +//===--------------------------------------------------------------------===// + +/// This class is used to represent the version of a dialect, for the purpose +/// of polymorphic destruction. +class DialectVersion { +public: + virtual ~DialectVersion() = default; +}; + //===----------------------------------------------------------------------===// // DialectBytecodeReader //===----------------------------------------------------------------------===// @@ -39,6 +50,16 @@ /// Emit an error to the reader. virtual InFlightDiagnostic emitError(const Twine &msg = {}) = 0; + /// Retrieve the dialect version for the dialect that is in processing. It is + /// responsibility of the caller to make sure that the version is actually + /// available. + std::shared_ptr getDialectVersion() const { return _version; } + + /// Load a dialect version on the reader. + void loadVersion(std::shared_ptr version) { + _version = version; + }; + /// Read out a list of elements, invoking the provided callback for each /// element. The callback function may be in any of the following forms: /// * LogicalResult(T &) @@ -168,6 +189,9 @@ private: /// Read a handle to a dialect resource. virtual FailureOr readResourceHandle() = 0; + + /// Reference to the dialect version we are currently reading. + std::shared_ptr _version; }; //===----------------------------------------------------------------------===// @@ -261,17 +285,6 @@ virtual int64_t getBytecodeVersion() const = 0; }; -//===--------------------------------------------------------------------===// -// Dialect Version Interface. -//===--------------------------------------------------------------------===// - -/// This class is used to represent the version of a dialect, for the purpose -/// of polymorphic destruction. -class DialectVersion { -public: - virtual ~DialectVersion() = default; -}; - //===----------------------------------------------------------------------===// // BytecodeDialectInterface //===----------------------------------------------------------------------===// @@ -353,8 +366,8 @@ virtual void writeVersion(DialectBytecodeWriter &writer) const {} // Read the version of this dialect from the provided reader and return it as - // a `unique_ptr` to a dialect version object. - virtual std::unique_ptr + // a `shared_ptr` to a dialect version object. + virtual std::shared_ptr readVersion(DialectBytecodeReader &reader) const { reader.emitError("Dialect does not support versioning"); return nullptr; diff --git a/mlir/include/mlir/IR/OpBase.td b/mlir/include/mlir/IR/OpBase.td --- a/mlir/include/mlir/IR/OpBase.td +++ b/mlir/include/mlir/IR/OpBase.td @@ -2526,6 +2526,10 @@ // Whether this op has a folder. bit hasFolder = 0; + // Whether to let ops implement their custom `readProperties` and + // `writeProperties` methods to emit bytecode. + bit useCustomPropertiesEncoding = 0; + // Op traits. // Note: The list of traits will be uniqued by ODS. list traits = props; diff --git a/mlir/include/mlir/TableGen/Operator.h b/mlir/include/mlir/TableGen/Operator.h --- a/mlir/include/mlir/TableGen/Operator.h +++ b/mlir/include/mlir/TableGen/Operator.h @@ -352,6 +352,10 @@ bool hasFolder() const; + /// Whether to generate the `readProperty`/`writeProperty` methods for + /// bytecode emission. + bool useCustomPropertiesEncoding() const; + private: /// Populates the vectors containing operands, attributes, results and traits. void populateOpStructure(); diff --git a/mlir/lib/Bytecode/Reader/BytecodeReader.cpp b/mlir/lib/Bytecode/Reader/BytecodeReader.cpp --- a/mlir/lib/Bytecode/Reader/BytecodeReader.cpp +++ b/mlir/lib/Bytecode/Reader/BytecodeReader.cpp @@ -478,7 +478,7 @@ ArrayRef versionBuffer; /// Lazy loaded dialect version from the handle above. - std::unique_ptr loadedVersion; + std::shared_ptr loadedVersion; }; /// This struct represents an operation name entry within the bytecode. @@ -1038,7 +1038,8 @@ } LogicalResult read(Location fileLoc, DialectReader &dialectReader, - OperationName *opName, OperationState &opState) { + OperationName *opName, OperationState &opState, + const BytecodeDialect &dialect) { uint64_t propertiesIdx; if (failed(dialectReader.readVarInt(propertiesIdx))) return failure(); @@ -1067,8 +1068,10 @@ StringRef(rawProperties.begin(), rawProperties.size()), fileLoc); DialectReader propReader = dialectReader.withEncodingReader(reader); - auto *iface = opName->getInterface(); - if (iface) + // Load the version into the reader. + propReader.loadVersion(dialect.loadedVersion); + + if (auto *iface = opName->getInterface()) return iface->readProperties(propReader, opState); if (opName->isRegistered()) return propReader.emitError( @@ -1342,8 +1345,7 @@ /// Parse an operation name reference using the given reader, and set the /// `wasRegistered` flag that indicates if the bytecode was produced by a /// context where opName was registered. - FailureOr parseOpName(EncodingReader &reader, - std::optional &wasRegistered); + FailureOr parseOpName(EncodingReader &reader); //===--------------------------------------------------------------------===// // Attribute/Type Section @@ -1770,13 +1772,11 @@ return success(); } -FailureOr -BytecodeReader::Impl::parseOpName(EncodingReader &reader, - std::optional &wasRegistered) { +FailureOr +BytecodeReader::Impl::parseOpName(EncodingReader &reader) { BytecodeOperationName *opName = nullptr; if (failed(parseEntry(reader, opNames, opName, "operation name"))) return failure(); - wasRegistered = opName->wasRegistered; // Check to see if this operation name has already been resolved. If we // haven't, load the dialect and build the operation name. if (!opName->opName) { @@ -1800,7 +1800,7 @@ getContext()); } } - return *opName->opName; + return opName; } //===----------------------------------------------------------------------===// @@ -2145,11 +2145,14 @@ RegionReadState &readState, bool &isIsolatedFromAbove) { // Parse the name of the operation. - std::optional wasRegistered; - FailureOr opName = parseOpName(reader, wasRegistered); - if (failed(opName)) + FailureOr bytecodeOpName = parseOpName(reader); + if (failed(bytecodeOpName)) return failure(); + OperationName opName = *(*bytecodeOpName)->opName; + std::optional wasRegistered = (*bytecodeOpName)->wasRegistered; + BytecodeDialect &bytecodeDialect = *(*bytecodeOpName)->dialect; + // Parse the operation mask, which indicates which components of the operation // are present. uint8_t opMask; @@ -2163,7 +2166,7 @@ // With the location and name resolved, we can start building the operation // state. - OperationState opState(opLoc, *opName); + OperationState opState(opLoc, opName); // Parse the attributes of the operation. if (opMask & bytecode::OpEncodingMask::kHasAttrs) { @@ -2176,7 +2179,7 @@ if (opMask & bytecode::OpEncodingMask::kHasProperties) { // kHasProperties wasn't emitted in older bytecode, we should never get // there without also having the `wasRegistered` flag available. - if (!wasRegistered) + if (!wasRegistered.has_value()) return emitError(fileLoc, "Unexpected missing `wasRegistered` opname flag at " "bytecode version ") @@ -2184,11 +2187,11 @@ // When an operation is emitted without being registered, the properties are // stored as an attribute. Otherwise the op must implement the bytecode // interface and control the serialization. - if (wasRegistered) { + if (*wasRegistered) { DialectReader dialectReader(attrTypeReader, stringReader, resourceReader, reader); - if (failed( - propertiesReader.read(fileLoc, dialectReader, &*opName, opState))) + if (failed(propertiesReader.read(fileLoc, dialectReader, &opName, opState, + bytecodeDialect))) return failure(); } else { // If the operation wasn't registered when it was emitted, the properties diff --git a/mlir/lib/TableGen/Operator.cpp b/mlir/lib/TableGen/Operator.cpp --- a/mlir/lib/TableGen/Operator.cpp +++ b/mlir/lib/TableGen/Operator.cpp @@ -854,3 +854,7 @@ } bool Operator::hasFolder() const { return def.getValueAsBit("hasFolder"); } + +bool Operator::useCustomPropertiesEncoding() const { + return def.getValueAsBit("useCustomPropertiesEncoding"); +} diff --git a/mlir/test/Bytecode/versioning/versioned-op-with-native-prop-1.12.mlirbc b/mlir/test/Bytecode/versioning/versioned-op-with-native-prop-1.12.mlirbc new file mode 100644 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@ : () -> () +} + diff --git a/mlir/test/Bytecode/versioning/versioned_bytecode.mlir b/mlir/test/Bytecode/versioning/versioned_bytecode.mlir --- a/mlir/test/Bytecode/versioning/versioned_bytecode.mlir +++ b/mlir/test/Bytecode/versioning/versioned_bytecode.mlir @@ -4,19 +4,19 @@ // Test roundtrip //===--------------------------------------------------------------------===// -// RUN: mlir-opt %S/versioned-op-1.12.mlirbc -emit-bytecode \ +// RUN: mlir-opt %S/versioned-op-with-prop-1.12.mlirbc -emit-bytecode \ // RUN: -emit-bytecode-version=0 | mlir-opt -o %t.1 && \ -// RUN: mlir-opt %S/versioned-op-1.12.mlirbc -o %t.2 && \ +// RUN: mlir-opt %S/versioned-op-with-prop-1.12.mlirbc -o %t.2 && \ // RUN: diff %t.1 %t.2 //===--------------------------------------------------------------------===// // Test invalid versions //===--------------------------------------------------------------------===// -// RUN: not mlir-opt %S/versioned-op-1.12.mlirbc -emit-bytecode \ +// RUN: not mlir-opt %S/versioned-op-with-prop-1.12.mlirbc -emit-bytecode \ // RUN: -emit-bytecode-version=-1 2>&1 | FileCheck %s --check-prefix=ERR_VERSION_NEGATIVE // ERR_VERSION_NEGATIVE: unsupported version requested -1, must be in range [{{[0-9]+}}, {{[0-9]+}}] -// RUN: not mlir-opt %S/versioned-op-1.12.mlirbc -emit-bytecode \ +// RUN: not mlir-opt %S/versioned-op-with-prop-1.12.mlirbc -emit-bytecode \ // RUN: -emit-bytecode-version=999 2>&1 | FileCheck %s --check-prefix=ERR_VERSION_FUTURE // ERR_VERSION_FUTURE: unsupported version requested 999, must be in range [{{[0-9]+}}, {{[0-9]+}}] diff --git a/mlir/test/Bytecode/versioning/versioned_op.mlir b/mlir/test/Bytecode/versioning/versioned_op.mlir --- a/mlir/test/Bytecode/versioning/versioned_op.mlir +++ b/mlir/test/Bytecode/versioning/versioned_op.mlir @@ -1,5 +1,7 @@ // This file contains test cases related to the dialect post-parsing upgrade // mechanism. +// COM: those tests parse bytecode that was generated before test dialect +// adopted `usePropertiesFromAttributes`. //===--------------------------------------------------------------------===// // Test generic @@ -10,7 +12,7 @@ // COM: version: 2.0 // COM: "test.versionedA"() <{dims = 123 : i64, modifier = false}> : () -> () // COM: } -// RUN: mlir-opt %S/versioned-op-2.0.mlirbc 2>&1 | FileCheck %s --check-prefix=CHECK1 +// RUN: mlir-opt %S/versioned-op-with-prop-2.0.mlirbc 2>&1 | FileCheck %s --check-prefix=CHECK1 // CHECK1: "test.versionedA"() <{dims = 123 : i64, modifier = false}> : () -> () //===--------------------------------------------------------------------===// @@ -22,8 +24,8 @@ // COM: version: 1.12 // COM: "test.versionedA"() <{dimensions = 123 : i64}> : () -> () // COM: } -// RUN: mlir-opt %S/versioned-op-1.12.mlirbc 2>&1 | FileCheck %s --check-prefix=CHECK2 -// CHECK2: "test.versionedA"() <{dims = 123 : i64, modifier = false}> : () -> () +// RUN: mlir-opt %S/versioned-op-with-prop-1.12.mlirbc 2>&1 | FileCheck %s --check-prefix=CHECK3 +// CHECK3: "test.versionedA"() <{dims = 123 : i64, modifier = false}> : () -> () //===--------------------------------------------------------------------===// // Test forbidden downgrade diff --git a/mlir/test/Bytecode/versioning/versioned_op_with_native_properties.mlir b/mlir/test/Bytecode/versioning/versioned_op_with_native_properties.mlir new file mode 100644 --- /dev/null +++ b/mlir/test/Bytecode/versioning/versioned_op_with_native_properties.mlir @@ -0,0 +1,28 @@ +// This file contains test cases related to the dialect post-parsing upgrade +// mechanism. +// COM: those tests parse bytecode that was generated before test dialect +// adopted `usePropertiesFromAttributes`. + +//===--------------------------------------------------------------------===// +// Test generic +//===--------------------------------------------------------------------===// + +// COM: bytecode contains +// COM: module { +// COM: version: 2.0 +// COM: test.with_versioned_properties 1 | 2 +// COM: } +// RUN: mlir-opt %S/versioned-op-with-native-prop-2.0.mlirbc 2>&1 | FileCheck %s --check-prefix=CHECK1 +// CHECK1: test.with_versioned_properties 1 | 2 + +//===--------------------------------------------------------------------===// +// Test upgrade +//===--------------------------------------------------------------------===// + +// COM: bytecode contains +// COM: module { +// COM: version: 1.12 + +// COM: } +// RUN: mlir-opt %S/versioned-op-with-native-prop-1.12.mlirbc 2>&1 | FileCheck %s --check-prefix=CHECK3 +// CHECK3: test.with_versioned_properties 1 | 0 diff --git a/mlir/test/lib/Dialect/Test/TestDialect.h b/mlir/test/lib/Dialect/Test/TestDialect.h --- a/mlir/test/lib/Dialect/Test/TestDialect.h +++ b/mlir/test/lib/Dialect/Test/TestDialect.h @@ -84,6 +84,17 @@ return content == rhs.content; } }; +struct VersionedProperties { + // For the sake of testing, assume that this object was associated to + // version 1.2 of the test dialect when having only one int value. In the + // current version 2.0, the property has two values and on upgrade. We also + // assume that the class is upgrade-able if value2 = 0. + int value1; + int value2; + bool operator==(const VersionedProperties &rhs) const { + return value1 == rhs.value1 && value2 == rhs.value2; + } +}; } // namespace test #define GET_OP_CLASSES diff --git a/mlir/test/lib/Dialect/Test/TestDialect.cpp b/mlir/test/lib/Dialect/Test/TestDialect.cpp --- a/mlir/test/lib/Dialect/Test/TestDialect.cpp +++ b/mlir/test/lib/Dialect/Test/TestDialect.cpp @@ -114,6 +114,17 @@ const PropertiesWithCustomPrint &prop); static ParseResult customParseProperties(OpAsmParser &parser, PropertiesWithCustomPrint &prop); +static LogicalResult setPropertiesFromAttribute(VersionedProperties &prop, + Attribute attr, + InFlightDiagnostic *diagnostic); +static DictionaryAttr +getPropertiesAsAttribute(MLIRContext *ctx, + const VersionedProperties &prop); +static llvm::hash_code computeHash(const VersionedProperties &prop); +static void customPrintProperties(OpAsmPrinter &p, + const VersionedProperties &prop); +static ParseResult customParseProperties(OpAsmParser &parser, + VersionedProperties &prop); void test::registerTestDialect(DialectRegistry ®istry) { registry.insert(); @@ -190,12 +201,12 @@ writer.writeVarInt(version.minor); // minor } - std::unique_ptr + std::shared_ptr readVersion(DialectBytecodeReader &reader) const final { uint64_t major, minor; if (failed(reader.readVarInt(major)) || failed(reader.readVarInt(minor))) return nullptr; - auto version = std::make_unique(); + auto version = std::make_shared(); version->major = major; version->minor = minor; return version; @@ -211,14 +222,11 @@ << "current test dialect version is 2.0, can't parse version: " << version.major << "." << version.minor; } - // Prior version 2.0, the old op supported only a single attribute called - // "dimensions". We can perform the upgrade. topLevelOp->walk([](TestVersionedOpA op) { - if (auto dims = op->getAttr("dimensions")) { - op->removeAttr("dimensions"); - op->setAttr("dims", dims); - } - op->setAttr("modifier", BoolAttr::get(op->getContext(), false)); + // Prior version 2.0, `readProperties` did not process the modifier + // attribute. Handle that according to the version here. + auto &prop = op.getProperties(); + prop.modifier = BoolAttr::get(op->getContext(), false); }); return success(); } @@ -1932,6 +1940,55 @@ prop.label = std::make_shared(std::move(label)); return success(); } +static LogicalResult +setPropertiesFromAttribute(VersionedProperties &prop, Attribute attr, + InFlightDiagnostic *diagnostic) { + DictionaryAttr dict = dyn_cast(attr); + if (!dict) { + if (diagnostic) + *diagnostic << "expected DictionaryAttr to set VersionedProperties"; + return failure(); + } + auto value1Attr = dict.getAs("value1"); + if (!value1Attr) { + if (diagnostic) + *diagnostic << "expected IntegerAttr for key `value1`"; + return failure(); + } + auto value2Attr = dict.getAs("value2"); + if (!value2Attr) { + if (diagnostic) + *diagnostic << "expected IntegerAttr for key `value2`"; + return failure(); + } + + prop.value1 = value1Attr.getValue().getSExtValue(); + prop.value2 = value2Attr.getValue().getSExtValue(); + return success(); +} +static DictionaryAttr +getPropertiesAsAttribute(MLIRContext *ctx, + const VersionedProperties &prop) { + SmallVector attrs; + Builder b{ctx}; + attrs.push_back(b.getNamedAttr("value1", b.getI32IntegerAttr(prop.value1))); + attrs.push_back(b.getNamedAttr("value2", b.getI32IntegerAttr(prop.value2))); + return b.getDictionaryAttr(attrs); +} +static llvm::hash_code computeHash(const VersionedProperties &prop) { + return llvm::hash_combine(prop.value1, prop.value2); +} +static void customPrintProperties(OpAsmPrinter &p, + const VersionedProperties &prop) { + p << prop.value1 << " | " << prop.value2; +} +static ParseResult customParseProperties(OpAsmParser &parser, + VersionedProperties &prop) { + if (parser.parseInteger(prop.value1) || parser.parseVerticalBar() || + parser.parseInteger(prop.value2)) + return failure(); + return success(); +} static bool parseUsingPropertyInCustom(OpAsmParser &parser, int64_t value[3]) { return parser.parseLSquare() || parser.parseInteger(value[0]) || @@ -1945,6 +2002,66 @@ printer << '[' << value << ']'; } +LogicalResult +TestVersionedOpA::readProperties(::mlir::DialectBytecodeReader &reader, + ::mlir::OperationState &state) { + auto &prop = state.getOrAddProperties(); + if (::mlir::failed(reader.readAttribute(prop.dims))) + return ::mlir::failure(); + + // Check if we have a version. If not, assume we are parsing the current + // version. + auto maybeVersion = reader.getDialectVersion(); + if (maybeVersion.get()) { + // If version is less than 2.0, there is no additional attribute to parse. + // We can materialize missing properties post parsing before verification. + const auto &version = + static_cast(*maybeVersion); + if ((version.major < 2)) { + return success(); + } + } + + if (::mlir::failed(reader.readAttribute(prop.modifier))) + return ::mlir::failure(); + return ::mlir::success(); +} + +void TestVersionedOpA::writeProperties(::mlir::DialectBytecodeWriter &writer) { + auto &prop = getProperties(); + writer.writeAttribute(prop.dims); + writer.writeAttribute(prop.modifier); +} + +::mlir::LogicalResult TestOpWithVersionedProperties::readFromMlirBytecode( + ::mlir::DialectBytecodeReader &reader, test::VersionedProperties &prop) { + uint64_t value1, value2 = 0; + if (failed(reader.readVarInt(value1))) + return failure(); + + // Check if we have a version. If not, assume we are parsing the current + // version. + auto maybeVersion = reader.getDialectVersion(); + if (maybeVersion.get()) { + // If version is less than 2.0, there is no additional attribute to parse. + // We can materialize missing properties post parsing before verification. + const auto &version = + static_cast(*maybeVersion); + if ((version.major >= 2) && failed(reader.readVarInt(value2))) + return failure(); + } + + prop.value1 = value1; + prop.value2 = value2; + return success(); +} +void TestOpWithVersionedProperties::writeToMlirBytecode( + ::mlir::DialectBytecodeWriter &writer, + const test::VersionedProperties &prop) { + writer.writeVarInt(prop.value1); + writer.writeVarInt(prop.value2); +} + #include "TestOpEnums.cpp.inc" #include "TestOpInterfaces.cpp.inc" #include "TestTypeInterfaces.cpp.inc" diff --git a/mlir/test/lib/Dialect/Test/TestOps.td b/mlir/test/lib/Dialect/Test/TestOps.td --- a/mlir/test/lib/Dialect/Test/TestOps.td +++ b/mlir/test/lib/Dialect/Test/TestOps.td @@ -3295,6 +3295,10 @@ AnyI64Attr:$dims, BoolAttr:$modifier ); + + // Since we use properties to store attributes, we need a custom encoding + // reader/writer to handle versioning. + let useCustomPropertiesEncoding = 1; } def TestVersionedOpB : TEST_Op<"versionedB"> { @@ -3414,6 +3418,51 @@ }]; } +def VersionedProperties : Property<"VersionedProperties"> { + let convertToAttribute = [{ + getPropertiesAsAttribute($_ctxt, $_storage) + }]; + let convertFromAttribute = [{ + return setPropertiesFromAttribute($_storage, $_attr, $_diag); + }]; + let hashProperty = [{ + computeHash($_storage); + }]; +} + +def TestOpWithVersionedProperties : TEST_Op<"with_versioned_properties"> { + let assemblyFormat = "prop-dict attr-dict"; + let arguments = (ins + VersionedProperties:$prop + ); + let extraClassDeclaration = [{ + void printProperties(::mlir::MLIRContext *ctx, ::mlir::OpAsmPrinter &p, + const Properties &prop); + static ::mlir::ParseResult parseProperties(::mlir::OpAsmParser &parser, + ::mlir::OperationState &result); + static ::mlir::LogicalResult readFromMlirBytecode( + ::mlir::DialectBytecodeReader &, + test::VersionedProperties &prop); + static void writeToMlirBytecode( + ::mlir::DialectBytecodeWriter &, + const test::VersionedProperties &prop); + }]; + let extraClassDefinition = [{ + void TestOpWithVersionedProperties::printProperties(::mlir::MLIRContext *ctx, + ::mlir::OpAsmPrinter &p, const Properties &prop) { + customPrintProperties(p, prop.prop); + } + ::mlir::ParseResult TestOpWithVersionedProperties::parseProperties( + ::mlir::OpAsmParser &parser, + ::mlir::OperationState &result) { + Properties &prop = result.getOrAddProperties(); + if (customParseProperties(parser, prop.prop)) + return failure(); + return success(); + } + }]; +} + #endif // TEST_OPS diff --git a/mlir/tools/mlir-tblgen/OpDefinitionsGen.cpp b/mlir/tools/mlir-tblgen/OpDefinitionsGen.cpp --- a/mlir/tools/mlir-tblgen/OpDefinitionsGen.cpp +++ b/mlir/tools/mlir-tblgen/OpDefinitionsGen.cpp @@ -1131,21 +1131,6 @@ "getDiag")) ->body(); - auto &readPropertiesMethod = - opClass - .addStaticMethod( - "::mlir::LogicalResult", "readProperties", - MethodParameter("::mlir::DialectBytecodeReader &", "reader"), - MethodParameter("::mlir::OperationState &", "state")) - ->body(); - - auto &writePropertiesMethod = - opClass - .addMethod( - "void", "writeProperties", - MethodParameter("::mlir::DialectBytecodeWriter &", "writer")) - ->body(); - opClass.declare("Properties", "FoldAdaptor::Properties"); // Convert the property to the attribute form. @@ -1349,6 +1334,34 @@ } verifyInherentAttrsMethod << " return ::mlir::success();"; + if (op.useCustomPropertiesEncoding()) { + opClass + .declareStaticMethod( + "::mlir::LogicalResult", "readProperties", + MethodParameter("::mlir::DialectBytecodeReader &", "reader"), + MethodParameter("::mlir::OperationState &", "state")); + opClass + .declareMethod( + "void", "writeProperties", + MethodParameter("::mlir::DialectBytecodeWriter &", "writer")); + return; + } + + auto &readPropertiesMethod = + opClass + .addStaticMethod( + "::mlir::LogicalResult", "readProperties", + MethodParameter("::mlir::DialectBytecodeReader &", "reader"), + MethodParameter("::mlir::OperationState &", "state")) + ->body(); + + auto &writePropertiesMethod = + opClass + .addMethod( + "void", "writeProperties", + MethodParameter("::mlir::DialectBytecodeWriter &", "writer")) + ->body(); + // Populate bytecode serialization logic. readPropertiesMethod << " auto &prop = state.getOrAddProperties(); (void)prop;";