[RFC] Making terminator optional (for single block / graph regions)

This was discussed in a recent ODM, the requirement for every block to have a terminator is an artifact of the evolution of MLIR from a SSA-CFG centric representation to a more generic IR.

This lead to operations like ModuleOp to introduce a companion terminator operation which is never displayed, but has to be explicitly handled. For example the codebase has code like this in many places:
target->addLegalOp<ModuleOp, ModuleTerminatorOp>();
Or even entire patterns:

class ModuleEndConversionPattern
    : public SPIRVToLLVMConversion<spirv::ModuleEndOp> {
  using SPIRVToLLVMConversion<spirv::ModuleEndOp>::SPIRVToLLVMConversion;
  matchAndRewrite(spirv::ModuleEndOp moduleEndOp, ArrayRef<Value> operands,
                  ConversionPatternRewriter &rewriter) const override {
    return success();

I have patch that introduce the possibility to make terminator optional: ⚙ D98468 Define a `NoTerminator` traits that allows operations with a single block region to not provide a terminator

This supposed that the region has a single block and it is intended to be used for GraphRegion. The patch is fairly large but only because to validate the feature I actually applied it to MLIR ModuleOp and deleted ModuleTerminatorOp from the codebase, i.e. ModuleOp does not define this trait SingleBlockImplicitTerminator<"ModuleTerminatorOp"> anymore and instead append the newly available NoTerminator.traits (see the diff in BuiltinOps.td.

If/when we land this, we can then update all the other flavors of Module (SPIRV and GPU upstream) to remove their terminator, but also other region operations like the FunctionLibraryOp in the shape dialect, which has its own ShapeFunctionLibraryTerminatorOp described as “A pseudo op that marks the end of a shape function library”.


Assuming that we can make the codebase reasonably resilient to non-terminator blocks and empty blocks, which when I looked very recently seems to be the case, I’m +1 on doing this. It would be nice to better support this class of operations (using the same line of logic as when graph regions were originally added).

– River

This is fantastic, thank you for pushing on this Mehdi!


One other +1 for this is that it will reduce memory usage for cases that use this. That is perhaps less important for module :wink: but can matter for structure control flow things like sv.if etc.

Haven’t heard any concerns so far, so I’ll likely land this on Monday! I’ll avoid “breaking the world” on a Friday afternoon :slight_smile:

looking forward to it!

Landed! Let me know if you have difficulties to upgrade downstream. It was fairly mechanical and not very involved internally for us.

Awesome! I filed an issue for us to adopt it on the circt side of things. Thank you!


how do I use this new feature? have a few ops that could benefit. (link to patch?)

The patch is linked in the first message, here it is again: ⚙ D98468 Define a `NoTerminator` traits that allows operations with a single block region to not provide a terminator

For a GraphRegion it is fairly simple, you can look at the ModuleOp: llvm-project/BuiltinOps.td at main · llvm/llvm-project · GitHub

Otherwise you can just add the NoTerminator trait to your operation, it applies to all region but requires the SingleBlock trait.

There is nothing more! Let me know if you need anything :slight_smile:


I didn’t make the connection, but this eliminates the need for the ensureTerminator dance for these ops. Beautiful!