diff --git a/mlir/include/mlir/Interfaces/ControlFlowInterfaces.h b/mlir/include/mlir/Interfaces/ControlFlowInterfaces.h --- a/mlir/include/mlir/Interfaces/ControlFlowInterfaces.h +++ b/mlir/include/mlir/Interfaces/ControlFlowInterfaces.h @@ -41,7 +41,7 @@ // RegionBranchOpInterface //===----------------------------------------------------------------------===// -// A constant value to represent unknown number of region invocations. +/// A constant value to represent unknown number of region invocations. extern const int64_t kUnknownNumRegionInvocations; namespace detail { @@ -87,6 +87,36 @@ ValueRange inputs; }; +/// This class represents upper and lower bounds on the number of times a region +/// of a `RegionBranchOpInterface` can be invoked. The lower bound is at least +/// zero, but the upper bound may not be defined. +class InvocationBounds { +public: + /// A constant to represent an unknown upper bound. + static constexpr int64_t kUnknownUpperBound = -1; + + /// Create invocation bounds. The lower bound must be at least 0 and only the + /// upper bound can be unknown. + InvocationBounds(int64_t lb, int64_t ub) : lower(lb), upper(ub) { + assert(lb >= 0 && "lower bound must be at least 0"); + } + + /// Returns true if the number of invocations has an upper bound. + bool hasUpperBound() const { return getUpperBound() != kUnknownUpperBound; } + + /// Return the lower bound. + int64_t getLowerBound() const { return lower; } + + /// Return the upper bound. + int64_t getUpperBound() const { return upper; } + +private: + /// The minimum number of times the successor region will be invoked. + int64_t lower; + /// The maximum number of times the successor region will be invoked. + int64_t upper; +}; + /// Return `true` if `a` and `b` are in mutually exclusive regions as per /// RegionBranchOpInterface. bool insideMutuallyExclusiveRegions(Operation *a, Operation *b); diff --git a/mlir/include/mlir/Interfaces/ControlFlowInterfaces.td b/mlir/include/mlir/Interfaces/ControlFlowInterfaces.td --- a/mlir/include/mlir/Interfaces/ControlFlowInterfaces.td +++ b/mlir/include/mlir/Interfaces/ControlFlowInterfaces.td @@ -102,9 +102,10 @@ let methods = [ InterfaceMethod<[{ Returns the operands of this operation used as the entry arguments when - entering the region at `index`, which was specified as a successor of this - operation by `getSuccessorRegions`. These operands should correspond 1-1 - with the successor inputs specified in `getSuccessorRegions`. + entering the region at `index`, which was specified as a successor of + this operation by `getSuccessorRegions`. These operands should + correspond 1-1 with the successor inputs specified in + `getSuccessorRegions`. }], "::mlir::OperandRange", "getSuccessorEntryOperands", (ins "unsigned":$index), [{}], /*defaultImplementation=*/[{ @@ -127,7 +128,8 @@ successor region must be non-empty. }], "void", "getSuccessorRegions", - (ins "::mlir::Optional":$index, "::mlir::ArrayRef<::mlir::Attribute>":$operands, + (ins "::mlir::Optional":$index, + "::mlir::ArrayRef<::mlir::Attribute>":$operands, "::mlir::SmallVectorImpl<::mlir::RegionSuccessor> &":$regions) >, InterfaceMethod<[{ @@ -149,7 +151,38 @@ assert(countPerRegion.empty()); countPerRegion.resize(numRegions, kUnknownNumRegionInvocations); }] - > + >, + InterfaceMethod<[{ + Populates `invocationBounds` with the minimum and maximum number of + times this operation will invoke the attached regions (assuming the + regions yield normally, i.e. do not abort or invoke an infinite loop). + The minimum number of invocations is at least 0. If the maximum number + of invocations cannot be statically determined, then it will be set to + `InvocationBounds::kUnknownUpperBound`. + + `operands` is a set of optional attributes that either correspond to a + constant values for each operand of this operation, or null if that + operand is not a constant. + }], + "void", "getRegionInvocationBounds", + (ins "::mlir::ArrayRef<::mlir::Attribute>":$operands, + "::llvm::SmallVectorImpl<::mlir::InvocationBounds> &" + :$invocationBounds), + [{}], + /*defaultImplementation=*/[{ + ::llvm::SmallVector countPerRegion; + ::llvm::cast(this->getOperation()).getNumRegionInvocations( + operands, countPerRegion); + for (auto count : countPerRegion) { + if (count == ::mlir::kUnknownNumRegionInvocations) { + invocationBounds.emplace_back( + 0, ::mlir::InvocationBounds::kUnknownUpperBound); + } else { + invocationBounds.emplace_back(count, count); + } + } + }] + >, ]; let verify = [{ diff --git a/mlir/include/mlir/Transforms/ControlFlowSink.h b/mlir/include/mlir/Transforms/ControlFlowSink.h new file mode 100644 --- /dev/null +++ b/mlir/include/mlir/Transforms/ControlFlowSink.h @@ -0,0 +1,22 @@ +#ifndef MLIR_TRANSFORMS_CONTROLFLOWSINK_H_ +#define MLIR_TRANSFORMS_CONTROLFLOWSINK_H_ + +#include "mlir/Interfaces/ControlFlowInterfaces.h" + +namespace mlir { +class DominanceInfo; + +/// Return a list of regions belonging to a `RegionBranchOpInterface` that are +/// candidates for control-flow sink. For basic control-flow sink, the region +/// must be known to be invoked at most once. +LogicalResult getCandidateRegions(RegionBranchOpInterface branch, + SmallVectorImpl ®ionsToSink); + +/// Given a list of regions that belong to a `RegionBranchOpInterface`, try to +/// sink in operations from outside the region whose only uses are in that +/// region. +LogicalResult sinkConditionalCode(ArrayRef regionsToSink, + DominanceInfo &domInfo); +} // end namespace mlir + +#endif // MLIR_TRANSFORMS_CONTROLFLOWSINK_H_ diff --git a/mlir/include/mlir/Transforms/Passes.h b/mlir/include/mlir/Transforms/Passes.h --- a/mlir/include/mlir/Transforms/Passes.h +++ b/mlir/include/mlir/Transforms/Passes.h @@ -65,6 +65,9 @@ std::unique_ptr createCanonicalizerPass(const GreedyRewriteConfig &config); +/// Creates a pass to perform control-flow sinking. +std::unique_ptr createControlFlowSinkPass(); + /// Creates a pass to perform common sub expression elimination. std::unique_ptr createCSEPass(); diff --git a/mlir/include/mlir/Transforms/Passes.td b/mlir/include/mlir/Transforms/Passes.td --- a/mlir/include/mlir/Transforms/Passes.td +++ b/mlir/include/mlir/Transforms/Passes.td @@ -307,6 +307,24 @@ ] # RewritePassUtils.options; } +def ControlFlowSink : Pass<"control-flow-sink"> { + let summary = "Sink operations into conditional blocks"; + let description = [{ + This pass implements a simple control-flow sink on operations that implement + `RegionBranchOpInterface` by moving dominating operations whose only uses + are in a single conditionally-executed region into that region so that + executions paths where their results are not needed do not perform + unnecessary computations. + + This is similar to loop-invariant code motion, which hoists operations out + of regions executed more than once. + }]; + let constructor = "::mlir::createControlFlowSinkPass()"; + let statistics = [ + Statistic<"numSunk", "num-sunk", "Number of operations sunk">, + ]; +} + def CSE : Pass<"cse"> { let summary = "Eliminate common sub-expressions"; let description = [{ diff --git a/mlir/lib/Interfaces/ControlFlowInterfaces.cpp b/mlir/lib/Interfaces/ControlFlowInterfaces.cpp --- a/mlir/lib/Interfaces/ControlFlowInterfaces.cpp +++ b/mlir/lib/Interfaces/ControlFlowInterfaces.cpp @@ -76,6 +76,9 @@ // A constant value to represent unknown number of region invocations. const int64_t mlir::kUnknownNumRegionInvocations = -1; +// A constant to represent an unknown upper bound on the number of invocations. +constexpr int64_t mlir::InvocationBounds::kUnknownUpperBound; + /// Verify that types match along all region control flow edges originating from /// `sourceNo` (region # if source is a region, llvm::None if source is parent /// op). `getInputsTypesForRegion` is a function that returns the types of the diff --git a/mlir/lib/Transforms/CMakeLists.txt b/mlir/lib/Transforms/CMakeLists.txt --- a/mlir/lib/Transforms/CMakeLists.txt +++ b/mlir/lib/Transforms/CMakeLists.txt @@ -5,6 +5,7 @@ BufferResultsToOutParams.cpp BufferUtils.cpp Canonicalizer.cpp + ControlFlowSink.cpp CSE.cpp Inliner.cpp LocationSnapshot.cpp diff --git a/mlir/lib/Transforms/ControlFlowSink.cpp b/mlir/lib/Transforms/ControlFlowSink.cpp new file mode 100644 --- /dev/null +++ b/mlir/lib/Transforms/ControlFlowSink.cpp @@ -0,0 +1,230 @@ +//===- ControlFlowSink.cpp - Code to perform control-flow sinking ---------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// This file implements a basic control-flow sink pass. Control-flow sinking +// moves operations whose only uses are in conditionally-executed blocks so that +// they aren't executed on paths where its results are not needed. +// +//===----------------------------------------------------------------------===// + +#include "mlir/Transforms/ControlFlowSink.h" +#include "PassDetail.h" +#include "mlir/IR/Dominance.h" +#include "mlir/IR/Matchers.h" +#include "mlir/Interfaces/ControlFlowInterfaces.h" +#include "llvm/ADT/SmallPtrSet.h" +#include + +#define DEBUG_TYPE "cf-sink" + +using namespace mlir; + +namespace { +/// Control-Flow Sink pass. +struct ControlFlowSink : public ControlFlowSinkBase { + void runOnOperation() override; +}; + +/// A helper struct for the control-flow sink pass. +class Sinker { +public: + /// Create an operation sinker with given dominance info. + Sinker(DominanceInfo &domInfo) : domInfo(domInfo) {} + + /// Given a list of regions, find operations to sink and sink them. Return the + /// number of operations sunk. + FailureOr sinkRegions(ArrayRef regions) &&; + +private: + /// Given a region and an op which dominates the region, returns true if all + /// users of the given op are dominated by the entry block of the region, and + /// thus the operation can be sunk into the region. + bool allUsersDominatedBy(Operation &op, Region ®ion); + + /// Given a region and a top-level op (an op whose parent region is the given + /// region), determine whether the defining ops of the op's operands can be + /// sunk into the region. + /// + /// Add moved ops to the work queue. + void tryToSinkPredecessors(Operation &op, Region ®ion, + std::deque &stack); + + /// Iterate over all the ops in a region and try to sink their predecessors. + /// Recurse on subgraphs using a work queue. + void sinkRegion(Region ®ion); + + /// An operation that has been marked to be moved into a certain region. + struct OperationMove { + /// The region whose entry block into which the op will be moved. + Region ®ion; + /// The op to move. + Operation &op; + }; + + /// Dominance info to determine op user dominance with respect to regions. + DominanceInfo &domInfo; + /// A list of operations to move and where to move them to. + SmallVector opsToMove; + /// A set of operations that have already been sunk into a region. + SmallPtrSet alreadyMoved; +}; +} // end anonymous namespace + +/// Returns true if an op can be sunk into a region. An op can be sunk if it has +/// no side effects and neither do any nested ops. +static bool canBeSunk(Operation &op) { + // Query for a memory effect interface. + if (auto memInterface = dyn_cast(&op)) { + // If the op has side effects, it cannot be moved. + // TODO: Perhaps there can be a limited way, involving a cost model, to move + // memory-effecting ops. + if (!memInterface.hasNoEffect()) + return false; + // If the op does not have recursive side effects, then it can be moved. + if (!op.hasTrait()) + return true; + } else if (!op.hasTrait()) { + // Otherwise, if the op does not implement the memory effect interface and + // it does not have recursive side effects, then it cannot be known that the + // op is moveable. + return false; + } + + // Recurse into the regions and ensure that all nested ops can also be moved. + for (auto ®ion : op.getRegions()) { + for (auto &block : region) { + for (auto &innerOp : block.without_terminator()) + if (!canBeSunk(innerOp)) + return false; + } + } + return true; +} + +bool Sinker::allUsersDominatedBy(Operation &op, Region ®ion) { + assert(region.findAncestorOpInRegion(op) == nullptr && + "expected op to be defined outside the region"); + return llvm::all_of(op.getUsers(), [&](Operation *user) { + // The user is dominated by the region if the user's defining block is + // dominated by the region's entry block. Or, if the user has been marked to + // be moved into the region, thus recursively detecting subgraphs that can + // be moved. + // + // The behaviour when one of the op's users is in an "island" (a block + // unreachable from the entry block) is to return false so that moving the + // op into the entry block won't break dominance. + return alreadyMoved.contains(user) || + domInfo.dominates(®ion.front(), user->getBlock()); + }); +} + +void Sinker::tryToSinkPredecessors(Operation &op, Region ®ion, + std::deque &stack) { + LLVM_DEBUG(op.print(llvm::dbgs() << "\nContained op:\n")); + for (auto value : op.getOperands()) { + auto *parentOp = value.getDefiningOp(); + // Ignore block arguments and ops that are already inside the region or have + // been marked to be. + if (!parentOp || parentOp->getParentRegion() == ®ion || + alreadyMoved.contains(parentOp)) + continue; + LLVM_DEBUG(parentOp->print(llvm::dbgs() << "\nTry to sink:\n")); + // If the op's users are all in the region and it can be moved, mark it so. + if (allUsersDominatedBy(*parentOp, region) && canBeSunk(*parentOp)) { + alreadyMoved.insert(parentOp); + opsToMove.push_back({region, *parentOp}); + // Add the op to the work queue. + stack.push_back(parentOp); + } + } +} + +void Sinker::sinkRegion(Region ®ion) { + // Initialize the work queue with all the ops in the region. + std::deque stack; + for (auto &block : region) + for (auto &op : block) + stack.push_back(&op); + + // Process all the ops. + while (!stack.empty()) { + auto *op = stack.back(); // depth-first + stack.pop_back(); + tryToSinkPredecessors(*op, region, stack); + } +} + +FailureOr Sinker::sinkRegions(ArrayRef regions) && { + for (auto *region : regions) { + // Ignore empty regions. + if (region->empty()) + continue; + sinkRegion(*region); + } + + // Move the marked ops into the region. If moving one op resulted in another + // able to be moved, then the other will appear later in the list and will be + // inserted before the dependee op. + for (auto &opToMove : opsToMove) { + auto &entry = opToMove.region.front(); + opToMove.op.moveBefore(&entry, entry.begin()); + } + + return opsToMove.size(); +} + +LogicalResult +mlir::getCandidateRegions(RegionBranchOpInterface branch, + SmallVectorImpl ®ionsToSink) { + // Collect constant operands. + SmallVector operands(branch->getNumOperands(), Attribute()); + for (auto &it : llvm::enumerate(branch->getOperands())) + matchPattern(it.value(), m_Constant(&operands[it.index()])); + // Get the invocation bounds. + SmallVector bounds; + branch.getRegionInvocationBounds(operands, bounds); + + // For a simple control-flow sink, only consider regions that are executed at + // most once. + // TODO: allow custom cost models. E.g. + // num_invocations * invocation_chance <= 1 + for (auto ®ion : branch->getRegions()) { + auto &bound = bounds[region.getRegionNumber()]; + LLVM_DEBUG(llvm::dbgs() << "Region #" << region.getRegionNumber() + << " bounds [" << bound.getLowerBound() << ", " + << bound.getUpperBound() << "]\n"); + if (bound.hasUpperBound() && bound.getUpperBound() <= 1) + regionsToSink.push_back(®ion); + } + + return success(); +} + +LogicalResult mlir::sinkConditionalCode(ArrayRef regionsToSink, + DominanceInfo &domInfo) { + return Sinker(domInfo).sinkRegions(regionsToSink); +} + +void ControlFlowSink::runOnOperation() { + auto &domInfo = getAnalysis(); + getOperation()->walk([&](RegionBranchOpInterface branch) { + LLVM_DEBUG(branch->print(llvm::dbgs() << "\nBranch op before:\n")); + SmallVector regionsToSink; + FailureOr result; + if (failed(mlir::getCandidateRegions(branch, regionsToSink)) || + failed(result = Sinker(domInfo).sinkRegions(regionsToSink))) + signalPassFailure(); + // Record the number of sunk operations. + if (succeeded(result)) + numSunk = result.getValue(); + }); +} + +std::unique_ptr mlir::createControlFlowSinkPass() { + return std::make_unique(); +} diff --git a/mlir/test/Transforms/control-flow-sink.mlir b/mlir/test/Transforms/control-flow-sink.mlir new file mode 100644 --- /dev/null +++ b/mlir/test/Transforms/control-flow-sink.mlir @@ -0,0 +1,130 @@ +// RUN: mlir-opt -control-flow-sink %s | FileCheck %s + +// CHECK-LABEL: @test_simple_sink +// CHECK: %0 = arith.subi %arg2, %arg1 : i32 +// CHECK: %1 = test.region_if %arg0: i1 -> i32 then { +// CHECK: %2 = arith.subi %arg1, %arg2 : i32 +// CHECK: test.region_if_yield %2 : i32 +// CHECK: } else { +// CHECK: %2 = arith.addi %arg1, %arg1 : i32 +// CHECK: %3 = arith.addi %0, %2 : i32 +// CHECK: test.region_if_yield %3 : i32 +// CHECK: } join { +// CHECK: %2 = arith.addi %arg2, %arg2 : i32 +// CHECK: %3 = arith.addi %2, %0 : i32 +// CHECK: test.region_if_yield %3 : i32 +// CHECK: } +// CHECK: return %1 : i32 +func @test_simple_sink(%cond : i1, %arg0 : i32, %arg1 : i32) -> i32 { + %0 = arith.subi %arg0, %arg1 : i32 + %1 = arith.subi %arg1, %arg0 : i32 + %2 = arith.addi %arg0, %arg0 : i32 + %3 = arith.addi %arg1, %arg1 : i32 + %result = test.region_if %cond : i1 -> i32 then { + test.region_if_yield %0 : i32 + } else { + %v = arith.addi %1, %2 : i32 + test.region_if_yield %v : i32 + } join { + %v = arith.addi %3, %1 : i32 + test.region_if_yield %v : i32 + } + return %result : i32 +} + +// CHECK-LABEL: @test_region_sink +// CHECK: %0 = test.region_if %arg0: i1 -> i32 then { +// CHECK: %1 = test.region_if %arg0: i1 -> i32 then { +// CHECK: test.region_if_yield %arg1 : i32 +// CHECK: } else { +// CHECK: %2 = arith.subi %arg1, %arg2 : i32 +// CHECK: test.region_if_yield %2 : i32 +// CHECK: } join { +// CHECK: test.region_if_yield %arg2 : i32 +// CHECK: } +// CHECK: test.region_if_yield %1 : i32 +// CHECK: } else { +// CHECK: test.region_if_yield %arg1 : i32 +// CHECK: } join { +// CHECK: test.region_if_yield %arg2 : i32 +// CHECK: } +// CHECK: return %0 : i32 +func @test_region_sink(%cond : i1, %arg0 : i32, %arg1 : i32) -> i32 { + %0 = arith.subi %arg0, %arg1 : i32 + %result0 = test.region_if %cond : i1 -> i32 then { + test.region_if_yield %arg0 : i32 + } else { + test.region_if_yield %0 : i32 + } join { + test.region_if_yield %arg1 : i32 + } + %result1 = test.region_if %cond : i1 -> i32 then { + test.region_if_yield %result0 : i32 + } else { + test.region_if_yield %arg0 : i32 + } join { + test.region_if_yield %arg1 : i32 + } + return %result1 : i32 +} + +// CHECK-LABEL: @test_subgraph_sink +// CHECK: %0 = test.region_if %arg0: i1 -> i32 then { +// CHECK: %1 = arith.subi %arg1, %arg2 : i32 +// CHECK: %2 = arith.addi %arg1, %arg2 : i32 +// CHECK: %3 = arith.subi %arg2, %arg1 : i32 +// CHECK: %4 = arith.muli %3, %3 : i32 +// CHECK: %5 = arith.muli %2, %1 : i32 +// CHECK: %6 = arith.addi %5, %4 : i32 +// CHECK: test.region_if_yield %6 : i32 +// CHECK: } else { +// CHECK: test.region_if_yield %arg1 : i32 +// CHECK: } join { +// CHECK: test.region_if_yield %arg2 : i32 +// CHECK: } +// CHECK: return %0 : i32 +func @test_subgraph_sink(%cond : i1, %arg0 : i32, %arg1 : i32) -> i32 { + %0 = arith.addi %arg0, %arg1 : i32 + %1 = arith.subi %arg0, %arg1 : i32 + %2 = arith.subi %arg1, %arg0 : i32 + %3 = arith.muli %0, %1 : i32 + %4 = arith.muli %2, %2 : i32 + %5 = arith.addi %3, %4 : i32 + %result = test.region_if %cond : i1 -> i32 then { + test.region_if_yield %5 : i32 + } else { + test.region_if_yield %arg0 : i32 + } join { + test.region_if_yield %arg1 : i32 + } + return %result : i32 +} + +// CHECK-LABEL: @test_multiblock_region_sink +// CHECK: %0 = arith.addi %arg1, %arg2 : i32 +// CHECK: %1 = "test.any_cond"() ( { +// CHECK: %3 = arith.addi %0, %arg2 : i32 +// CHECK: %4 = arith.addi %3, %arg1 : i32 +// CHECK: br ^bb1(%4 : i32) +// CHECK: ^bb1(%5: i32): // pred: ^bb0 +// CHECK: %6 = arith.addi %5, %4 : i32 +// CHECK: "test.yield"(%6) : (i32) -> () +// CHECK: }) : () -> i32 +// CHECK: %2 = arith.addi %0, %1 : i32 +// CHECK: return %2 : i32 +func @test_multiblock_region_sink(%cond : i1, %arg0 : i32, %arg1 : i32) -> i32 { + // Value %0 is used in a nested region but also at this level + %0 = arith.addi %arg0, %arg1 : i32 + %1 = arith.addi %0, %arg1 : i32 + %2 = arith.addi %1, %arg0 : i32 + %3 = "test.any_cond"() ({ + ^bb0: + // Value %2 is used in both blocks. + br ^bb1(%2 : i32) + ^bb1(%arg2 : i32): + %4 = arith.addi %arg2, %2 : i32 + "test.yield"(%4) : (i32) -> () + }) : () -> i32 + %result = arith.addi %0, %3 : i32 + return %result : i32 +} 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 @@ -1176,6 +1176,33 @@ regions.push_back(RegionSuccessor(&getElseRegion(), getElseArgs())); } +void RegionIfOp::getRegionInvocationBounds( + ArrayRef operands, + SmallVectorImpl &invocationBounds) { + // Each region is invoked at most once. + invocationBounds.assign(/*NumElts=*/3, /*Elt=*/{0, 1}); +} + +//===----------------------------------------------------------------------===// +// AnyCondOp +//===----------------------------------------------------------------------===// + +void AnyCondOp::getSuccessorRegions(Optional index, + ArrayRef operands, + SmallVectorImpl ®ions) { + // The parent op branches into the only region, and the region branches back + // to the parent op. + if (index) + regions.emplace_back(&getRegion()); + else + regions.emplace_back(getResults()); +} + +void AnyCondOp::getNumRegionInvocations( + ArrayRef operands, SmallVectorImpl &countPerRegion) { + countPerRegion.assign(/*NumElts=*/1, /*Elt=*/1); +} + //===----------------------------------------------------------------------===// // SingleNoTerminatorCustomAsmOp //===----------------------------------------------------------------------===// 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 @@ -2210,14 +2210,15 @@ } def RegionIfOp : TEST_Op<"region_if", - [DeclareOpInterfaceMethods, + [DeclareOpInterfaceMethods, SingleBlockImplicitTerminator<"RegionIfYieldOp">, RecursiveSideEffects]> { let description =[{ Represents an abstract if-then-else-join pattern. In this context, the then and else regions jump to the join region, which finally returns to its parent op. - }]; + }]; let printer = [{ return ::print(p, *this); }]; let parser = [{ return ::parseRegionIfOp(parser, result); }]; @@ -2240,6 +2241,14 @@ }]; } +def AnyCondOp : TEST_Op<"any_cond", + [DeclareOpInterfaceMethods, + RecursiveSideEffects]> { + let results = (outs Variadic:$results); + let regions = (region AnyRegion:$region); +} + //===----------------------------------------------------------------------===// // Test TableGen generated build() methods //===----------------------------------------------------------------------===//