Understanding the philosophy behind building operations, regions and blocks

I am a beginner and have been experimenting with various MLIR components. While building the MLIR program for an IR dialect and understanding the various APIs, it seems like there are 2 ways to create them. One way is to define operations through the ODS framework and then use builder.create(…) which would internally call the build methods associated with every operation and thus there is a restriction offered by the dialect and its operations.The other way which offers more flexibility is to use OperationState class through which arbitrary shapes can be created with very little restrictions on how to build these operations and their respective nested regions and operations. Is my understanding correct? I will be glad if someone can share a little more perspective on this and when should either method be preferred?

Thanks.

OperationState is a low-level API, that is actually used by all the ODS generated code.

The point of the ODS generated code is to provide easy to use and “hard to misused” API, with C++ type safety (it also handles all the API for accessing and modifying an op after it has been created by the way). The OperationState API on the other hand is harder to get right.

Since the ODS generated code is nothing more than a wrapper on the OperationState, there is nothing you can do with the ODS generated code than you can’t equally do with the OperationState directly.

Thanks Mehdi. That was very helpful.