clang 23.0.0git
FlattenCFG.cpp
Go to the documentation of this file.
1//===----------------------------------------------------------------------===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8//
9// This file implements pass that inlines CIR operations regions into the parent
10// function region.
11//
12//===----------------------------------------------------------------------===//
13
14#include "PassDetail.h"
15#include "mlir/Dialect/Func/IR/FuncOps.h"
16#include "mlir/IR/Block.h"
17#include "mlir/IR/Builders.h"
18#include "mlir/IR/PatternMatch.h"
19#include "mlir/Interfaces/SideEffectInterfaces.h"
20#include "mlir/Support/LogicalResult.h"
21#include "mlir/Transforms/DialectConversion.h"
22#include "mlir/Transforms/GreedyPatternRewriteDriver.h"
28#include "llvm/ADT/TypeSwitch.h"
29
30using namespace mlir;
31using namespace cir;
32
33namespace mlir {
34#define GEN_PASS_DEF_CIRFLATTENCFG
35#include "clang/CIR/Dialect/Passes.h.inc"
36} // namespace mlir
37
38namespace {
39
40/// Lowers operations with the terminator trait that have a single successor.
41void lowerTerminator(mlir::Operation *op, mlir::Block *dest,
42 mlir::PatternRewriter &rewriter) {
43 assert(op->hasTrait<mlir::OpTrait::IsTerminator>() && "not a terminator");
44 mlir::OpBuilder::InsertionGuard guard(rewriter);
45 rewriter.setInsertionPoint(op);
46 rewriter.replaceOpWithNewOp<cir::BrOp>(op, dest);
47}
48
49/// Walks a region while skipping operations of type `Ops`. This ensures the
50/// callback is not applied to said operations and its children.
51template <typename... Ops>
52void walkRegionSkipping(
53 mlir::Region &region,
54 mlir::function_ref<mlir::WalkResult(mlir::Operation *)> callback) {
55 region.walk<mlir::WalkOrder::PreOrder>([&](mlir::Operation *op) {
56 if (isa<Ops...>(op))
57 return mlir::WalkResult::skip();
58 return callback(op);
59 });
60}
61
62/// Check whether a region contains any nested op with regions (i.e. structured
63/// CIR ops that must be flattened before their parent). The greedy pattern
64/// rewriter doesn't guarantee inside-out processing order — when a pattern
65/// fires and modifies IR, newly created ops go onto the worklist and can be
66/// visited in any order. So each flattening pattern must explicitly defer
67/// until its nested structured ops are flat.
68///
69/// CaseOps are excluded because they are structural children of SwitchOp and
70/// are handled by the SwitchOp flattening pattern.
71static bool hasNestedOpsToFlatten(mlir::Region &region) {
72 return region
73 .walk([](mlir::Operation *op) {
74 if (op->getNumRegions() > 0 && !isa<cir::CaseOp>(op))
75 return mlir::WalkResult::interrupt();
76 return mlir::WalkResult::advance();
77 })
78 .wasInterrupted();
79}
80
81/// True if `op` is a non-returning terminator — currently `cir.unreachable`
82/// or `cir.trap`. Such terminators don't fall through and don't yield a
83/// value, so when flattening a region they can be left in place rather than
84/// being replaced with a branch to the continuation block. Add new ops here
85/// (e.g. a hypothetical `cir.abort`) so every flattening pattern picks them
86/// up at once.
87static bool isNonReturningTerminator(mlir::Operation *op) {
88 return mlir::isa_and_nonnull<cir::UnreachableOp, cir::TrapOp>(op);
89}
90
91/// Rewrite the terminator of `region`'s exit block so that, after
92/// flattening, control falls through to `continueBlock`. The exit
93/// terminator is expected to be either:
94/// - `cir.yield`: replaced with `cir.br` to `continueBlock` (yielded
95/// args become the destination block's arguments).
96/// - non-returning (`cir.unreachable`, `cir.trap`): left in place — no
97/// branch is needed.
98///
99/// On success returns `success()`. If the terminator is anything else, an
100/// error is emitted and `failure()` is returned. NOTE: callers in this
101/// file have typically already mutated IR (splitBlock / createBlock) by
102/// the time this is invoked, so the MLIR pattern rewriter contract
103/// requires them to still return `success()` from the surrounding
104/// pattern; the `failure()` here just signals "stop trying to wire up
105/// this region".
106static mlir::LogicalResult
107rewriteRegionExitToContinue(mlir::PatternRewriter &rewriter,
108 mlir::Region &region, mlir::Block *continueBlock,
109 llvm::StringRef regionDescription) {
110 mlir::Operation *terminator = region.back().getTerminator();
111 rewriter.setInsertionPointToEnd(&region.back());
112 if (auto yieldOp = mlir::dyn_cast<cir::YieldOp>(terminator)) {
113 rewriter.replaceOpWithNewOp<cir::BrOp>(yieldOp, yieldOp.getArgs(),
114 continueBlock);
115 return mlir::success();
116 }
117 if (isNonReturningTerminator(terminator))
118 return mlir::success();
119 terminator->emitError("unexpected terminator in ")
120 << regionDescription
121 << " region, expected yield, unreachable, or trap, got: "
122 << terminator->getName();
123 return mlir::failure();
124}
125
126struct CIRFlattenCFGPass : public impl::CIRFlattenCFGBase<CIRFlattenCFGPass> {
127
128 CIRFlattenCFGPass() = default;
129 void runOnOperation() override;
130};
131
132struct CIRIfFlattening : public mlir::OpRewritePattern<cir::IfOp> {
133 using OpRewritePattern<IfOp>::OpRewritePattern;
134
135 mlir::LogicalResult
136 matchAndRewrite(cir::IfOp ifOp,
137 mlir::PatternRewriter &rewriter) const override {
138 mlir::OpBuilder::InsertionGuard guard(rewriter);
139 mlir::Location loc = ifOp.getLoc();
140 bool emptyElse = ifOp.getElseRegion().empty();
141 mlir::Block *currentBlock = rewriter.getInsertionBlock();
142 mlir::Block *remainingOpsBlock =
143 rewriter.splitBlock(currentBlock, rewriter.getInsertionPoint());
144 mlir::Block *continueBlock;
145 if (ifOp->getResults().empty())
146 continueBlock = remainingOpsBlock;
147 else
148 llvm_unreachable("NYI");
149
150 // Inline the region
151 mlir::Block *thenBeforeBody = &ifOp.getThenRegion().front();
152 mlir::Block *thenAfterBody = &ifOp.getThenRegion().back();
153 rewriter.inlineRegionBefore(ifOp.getThenRegion(), continueBlock);
154
155 rewriter.setInsertionPointToEnd(thenAfterBody);
156 if (auto thenYieldOp =
157 dyn_cast<cir::YieldOp>(thenAfterBody->getTerminator())) {
158 rewriter.replaceOpWithNewOp<cir::BrOp>(thenYieldOp, thenYieldOp.getArgs(),
159 continueBlock);
160 }
161
162 rewriter.setInsertionPointToEnd(continueBlock);
163
164 // Has else region: inline it.
165 mlir::Block *elseBeforeBody = nullptr;
166 mlir::Block *elseAfterBody = nullptr;
167 if (!emptyElse) {
168 elseBeforeBody = &ifOp.getElseRegion().front();
169 elseAfterBody = &ifOp.getElseRegion().back();
170 rewriter.inlineRegionBefore(ifOp.getElseRegion(), continueBlock);
171 } else {
172 elseBeforeBody = elseAfterBody = continueBlock;
173 }
174
175 rewriter.setInsertionPointToEnd(currentBlock);
176 cir::BrCondOp::create(rewriter, loc, ifOp.getCondition(), thenBeforeBody,
177 elseBeforeBody);
178
179 if (!emptyElse) {
180 rewriter.setInsertionPointToEnd(elseAfterBody);
181 if (auto elseYieldOP =
182 dyn_cast<cir::YieldOp>(elseAfterBody->getTerminator())) {
183 rewriter.replaceOpWithNewOp<cir::BrOp>(
184 elseYieldOP, elseYieldOP.getArgs(), continueBlock);
185 }
186 }
187
188 rewriter.replaceOp(ifOp, continueBlock->getArguments());
189 return mlir::success();
190 }
191};
192
193class CIRScopeOpFlattening : public mlir::OpRewritePattern<cir::ScopeOp> {
194public:
195 using OpRewritePattern<cir::ScopeOp>::OpRewritePattern;
196
197 mlir::LogicalResult
198 matchAndRewrite(cir::ScopeOp scopeOp,
199 mlir::PatternRewriter &rewriter) const override {
200 mlir::OpBuilder::InsertionGuard guard(rewriter);
201 mlir::Location loc = scopeOp.getLoc();
202
203 // Empty scope: just remove it.
204 // TODO: Remove this logic once CIR uses MLIR infrastructure to remove
205 // trivially dead operations. MLIR canonicalizer is too aggressive and we
206 // need to either (a) make sure all our ops model all side-effects and/or
207 // (b) have more options in the canonicalizer in MLIR to temper
208 // aggressiveness level.
209 if (scopeOp.isEmpty()) {
210 rewriter.eraseOp(scopeOp);
211 return mlir::success();
212 }
213
214 // Split the current block before the ScopeOp to create the inlining
215 // point.
216 mlir::Block *currentBlock = rewriter.getInsertionBlock();
217 mlir::Block *continueBlock =
218 rewriter.splitBlock(currentBlock, rewriter.getInsertionPoint());
219 if (scopeOp.getNumResults() > 0)
220 continueBlock->addArguments(scopeOp.getResultTypes(), loc);
221
222 // Inline body region.
223 mlir::Block *beforeBody = &scopeOp.getScopeRegion().front();
224 mlir::Block *afterBody = &scopeOp.getScopeRegion().back();
225 rewriter.inlineRegionBefore(scopeOp.getScopeRegion(), continueBlock);
226
227 // Save stack and then branch into the body of the region.
228 rewriter.setInsertionPointToEnd(currentBlock);
230 cir::BrOp::create(rewriter, loc, mlir::ValueRange(), beforeBody);
231
232 // Replace the scopeop return with a branch that jumps out of the body.
233 // Stack restore before leaving the body region.
234 rewriter.setInsertionPointToEnd(afterBody);
235 if (auto yieldOp = dyn_cast<cir::YieldOp>(afterBody->getTerminator())) {
236 rewriter.replaceOpWithNewOp<cir::BrOp>(yieldOp, yieldOp.getArgs(),
237 continueBlock);
238 }
239
240 // Replace the op with values return from the body region.
241 rewriter.replaceOp(scopeOp, continueBlock->getArguments());
242
243 return mlir::success();
244 }
245};
246
247class CIRSwitchOpFlattening : public mlir::OpRewritePattern<cir::SwitchOp> {
248public:
249 using OpRewritePattern<cir::SwitchOp>::OpRewritePattern;
250
251 inline void rewriteYieldOp(mlir::PatternRewriter &rewriter,
252 cir::YieldOp yieldOp,
253 mlir::Block *destination) const {
254 rewriter.setInsertionPoint(yieldOp);
255 rewriter.replaceOpWithNewOp<cir::BrOp>(yieldOp, yieldOp.getOperands(),
256 destination);
257 }
258
259 // Return the new defaultDestination block.
260 Block *condBrToRangeDestination(cir::SwitchOp op,
261 mlir::PatternRewriter &rewriter,
262 mlir::Block *rangeDestination,
263 mlir::Block *defaultDestination,
264 const APInt &lowerBound,
265 const APInt &upperBound) const {
266 auto condType = mlir::cast<cir::IntType>(op.getCondition().getType());
267 bool isSigned = condType.isSigned();
268 assert(
269 (isSigned ? lowerBound.sle(upperBound) : lowerBound.ule(upperBound)) &&
270 "Invalid range");
271 mlir::Block *resBlock = rewriter.createBlock(defaultDestination);
272
273 // Build the range check at the switch operand's width. The classic
274 // `sub x, lo; ule (x - lo), (hi - lo)` idiom is a cyclic-distance test that
275 // is correct for both signed and unsigned switches, so the comparison is
276 // always unsigned.
277 cir::IntType uIntType =
278 cir::IntType::get(op.getContext(), condType.getWidth(),
279 /*isSigned=*/false);
280
281 cir::ConstantOp lowerBoundValue = cir::ConstantOp::create(
282 rewriter, op.getLoc(), cir::IntAttr::get(condType, lowerBound));
283 mlir::Value diffValue = cir::SubOp::create(
284 rewriter, op.getLoc(), op.getCondition(), lowerBoundValue);
285
286 // Use an unsigned comparison to check whether the condition is in range.
287 // cir.cmp takes its signedness from the operand type, so a signed
288 // difference needs a same-width integral cast to the unsigned type (a
289 // signedness reinterpretation) to make the `le` unsigned; an unsigned
290 // difference already has the right type.
291 if (isSigned)
292 diffValue = cir::CastOp::create(rewriter, op.getLoc(), uIntType,
293 CastKind::integral, diffValue);
294
295 cir::ConstantOp rangeLength = cir::ConstantOp::create(
296 rewriter, op.getLoc(),
297 cir::IntAttr::get(uIntType, upperBound - lowerBound));
298
299 cir::CmpOp cmpResult = cir::CmpOp::create(
300 rewriter, op.getLoc(), cir::CmpOpKind::le, diffValue, rangeLength);
301 cir::BrCondOp::create(rewriter, op.getLoc(), cmpResult, rangeDestination,
302 defaultDestination);
303 return resBlock;
304 }
305
306 mlir::LogicalResult
307 matchAndRewrite(cir::SwitchOp op,
308 mlir::PatternRewriter &rewriter) const override {
309 // All nested structured CIR ops must be flattened before the switch.
310 // Break statements inside nested structured ops would create branches to
311 // blocks outside those ops' regions, which is invalid. Fail the match so
312 // the pattern rewriter will process them first.
313 for (mlir::Region &region : op->getRegions())
314 if (hasNestedOpsToFlatten(region))
315 return mlir::failure();
316
317 // Empty switch statement: just erase it.
318 if (op.getBody().hasOneBlock() &&
319 op.getBody().front().without_terminator().empty()) {
320 rewriter.eraseOp(op);
321 return mlir::success();
322 }
323
324 llvm::SmallVector<CaseOp> cases;
325 op.collectCases(cases);
326
327 // Create exit block from the next node of cir.switch op.
328 mlir::Block *exitBlock = rewriter.splitBlock(
329 rewriter.getBlock(), op->getNextNode()->getIterator());
330
331 // We lower cir.switch op in the following process:
332 // 1. Inline the region from the switch op after switch op.
333 // 2. Traverse each cir.case op:
334 // a. Record the entry block, block arguments and condition for every
335 // case. b. Inline the case region after the case op.
336 // 3. Replace the empty cir.switch.op with the new cir.switchflat op by the
337 // recorded block and conditions.
338
339 // First we have to handle the rewrite of all of the 'break' ops to make
340 // sure they now go to the right place, including the ones in the pre-case
341 // blcoks.
342 walkRegionSkipping<cir::LoopOpInterface, cir::SwitchOp>(
343 op.getBody(), [&](mlir::Operation *op) {
344 if (!isa<cir::BreakOp>(op))
345 return mlir::WalkResult::advance();
346
347 lowerTerminator(op, exitBlock, rewriter);
348 return mlir::WalkResult::skip();
349 });
350
351 // inline everything from switch body between the switch op and the exit
352 // block.
353 {
354 cir::YieldOp switchYield = nullptr;
355 // Clear switch operation.
356 for (mlir::Block &block :
357 llvm::make_early_inc_range(op.getBody().getBlocks()))
358 if (auto yieldOp = dyn_cast<cir::YieldOp>(block.getTerminator()))
359 switchYield = yieldOp;
360
361 assert(!op.getBody().empty());
362 mlir::Block *originalBlock = op->getBlock();
363 mlir::Block *swopBlock =
364 rewriter.splitBlock(originalBlock, op->getIterator());
365 rewriter.inlineRegionBefore(op.getBody(), exitBlock);
366
367 if (switchYield)
368 rewriteYieldOp(rewriter, switchYield, exitBlock);
369
370 rewriter.setInsertionPointToEnd(originalBlock);
371 cir::BrOp::create(rewriter, op.getLoc(), swopBlock);
372 }
373
374 // Allocate required data structures (disconsider default case in
375 // vectors).
376 llvm::SmallVector<mlir::APInt, 8> caseValues;
377 llvm::SmallVector<mlir::Block *, 8> caseDestinations;
378 llvm::SmallVector<mlir::ValueRange, 8> caseOperands;
379
380 llvm::SmallVector<std::pair<APInt, APInt>> rangeValues;
381 llvm::SmallVector<mlir::Block *> rangeDestinations;
382 llvm::SmallVector<mlir::ValueRange> rangeOperands;
383
384 // Initialize default case as optional.
385 mlir::Block *defaultDestination = exitBlock;
386 mlir::ValueRange defaultOperands = exitBlock->getArguments();
387
388 // Digest the case statements values and bodies.
389 for (cir::CaseOp caseOp : cases) {
390 mlir::Region &region = caseOp.getCaseRegion();
391
392 // Found default case: save destination and operands.
393 switch (caseOp.getKind()) {
394 case cir::CaseOpKind::Default:
395 defaultDestination = &region.front();
396 defaultOperands = defaultDestination->getArguments();
397 break;
398 case cir::CaseOpKind::Range:
399 assert(caseOp.getValue().size() == 2 &&
400 "Case range should have 2 case value");
401 rangeValues.push_back(
402 {cast<cir::IntAttr>(caseOp.getValue()[0]).getValue(),
403 cast<cir::IntAttr>(caseOp.getValue()[1]).getValue()});
404 rangeDestinations.push_back(&region.front());
405 rangeOperands.push_back(rangeDestinations.back()->getArguments());
406 break;
407 case cir::CaseOpKind::Anyof:
408 case cir::CaseOpKind::Equal:
409 // AnyOf cases kind can have multiple values, hence the loop below.
410 for (const mlir::Attribute &value : caseOp.getValue()) {
411 caseValues.push_back(cast<cir::IntAttr>(value).getValue());
412 caseDestinations.push_back(&region.front());
413 caseOperands.push_back(caseDestinations.back()->getArguments());
414 }
415 break;
416 }
417
418 // Track fallthrough in cases.
419 for (mlir::Block &blk : region.getBlocks()) {
420 if (blk.getNumSuccessors())
421 continue;
422
423 if (auto yieldOp = dyn_cast<cir::YieldOp>(blk.getTerminator())) {
424 mlir::Operation *nextOp = caseOp->getNextNode();
425 assert(nextOp && "caseOp is not expected to be the last op");
426 mlir::Block *oldBlock = nextOp->getBlock();
427 mlir::Block *newBlock =
428 rewriter.splitBlock(oldBlock, nextOp->getIterator());
429 rewriter.setInsertionPointToEnd(oldBlock);
430 cir::BrOp::create(rewriter, nextOp->getLoc(), mlir::ValueRange(),
431 newBlock);
432 rewriteYieldOp(rewriter, yieldOp, newBlock);
433 }
434 }
435
436 mlir::Block *oldBlock = caseOp->getBlock();
437 mlir::Block *newBlock =
438 rewriter.splitBlock(oldBlock, caseOp->getIterator());
439
440 mlir::Block &entryBlock = caseOp.getCaseRegion().front();
441 rewriter.inlineRegionBefore(caseOp.getCaseRegion(), newBlock);
442
443 // Create a branch to the entry of the inlined region.
444 rewriter.setInsertionPointToEnd(oldBlock);
445 cir::BrOp::create(rewriter, caseOp.getLoc(), &entryBlock);
446 }
447
448 // Remove all cases since we've inlined the regions.
449 for (cir::CaseOp caseOp : cases) {
450 mlir::Block *caseBlock = caseOp->getBlock();
451 // Erase the block with no predecessors here to make the generated code
452 // simpler a little bit.
453 if (caseBlock->hasNoPredecessors())
454 rewriter.eraseBlock(caseBlock);
455 else
456 rewriter.eraseOp(caseOp);
457 }
458
459 bool isSigned =
460 mlir::cast<cir::IntType>(op.getCondition().getType()).isSigned();
461 for (auto [rangeVal, operand, destination] :
462 llvm::zip(rangeValues, rangeOperands, rangeDestinations)) {
463 APInt lowerBound = rangeVal.first;
464 APInt upperBound = rangeVal.second;
465
466 // An empty range (lo > hi in the switch's signedness) is unreachable.
467 if (isSigned ? lowerBound.sgt(upperBound) : lowerBound.ugt(upperBound))
468 continue;
469
470 // If range is small, add multiple switch instruction cases.
471 // This magical number is from the original CGStmt code.
472 constexpr uint64_t kSmallRangeThreshold = 64;
473 APInt rangeSize = upperBound - lowerBound;
474 if (rangeSize.ult(kSmallRangeThreshold)) {
475 // Expand into individual cases. rangeSize < kSmallRangeThreshold, so
476 // the inclusive case count (rangeSize + 1) fits in a uint64_t. Drive
477 // termination by the count rather than by comparing caseValue to
478 // upperBound: when upperBound is the type's maximum the final
479 // caseValue++ wraps past the top, which is harmless because the
480 // wrapped value is never used.
481 APInt caseValue = lowerBound;
482 for (uint64_t n = rangeSize.getZExtValue() + 1; n != 0; --n) {
483 caseValues.push_back(caseValue++);
484 caseOperands.push_back(operand);
485 caseDestinations.push_back(destination);
486 }
487 continue;
488 }
489
490 defaultDestination =
491 condBrToRangeDestination(op, rewriter, destination,
492 defaultDestination, lowerBound, upperBound);
493 defaultOperands = operand;
494 }
495
496 // Set switch op to branch to the newly created blocks.
497 rewriter.setInsertionPoint(op);
498 rewriter.replaceOpWithNewOp<cir::SwitchFlatOp>(
499 op, op.getCondition(), defaultDestination, defaultOperands, caseValues,
500 caseDestinations, caseOperands);
501
502 return mlir::success();
503 }
504};
505
506class CIRLoopOpInterfaceFlattening
507 : public mlir::OpInterfaceRewritePattern<cir::LoopOpInterface> {
508public:
509 using mlir::OpInterfaceRewritePattern<
510 cir::LoopOpInterface>::OpInterfaceRewritePattern;
511
512 inline void lowerConditionOp(cir::ConditionOp op, mlir::Block *body,
513 mlir::Block *exit,
514 mlir::PatternRewriter &rewriter) const {
515 mlir::OpBuilder::InsertionGuard guard(rewriter);
516 rewriter.setInsertionPoint(op);
517 rewriter.replaceOpWithNewOp<cir::BrCondOp>(op, op.getCondition(), body,
518 exit);
519 }
520
521 mlir::LogicalResult
522 matchAndRewrite(cir::LoopOpInterface op,
523 mlir::PatternRewriter &rewriter) const final {
524 // All nested structured CIR ops must be flattened before the loop.
525 // Break/continue statements inside nested structured ops would create
526 // branches to blocks outside those ops' regions, which is invalid. Fail
527 // the match so the pattern rewriter will process them first.
528 for (mlir::Region &region : op->getRegions())
529 if (hasNestedOpsToFlatten(region))
530 return mlir::failure();
531
532 // Setup CFG blocks.
533 mlir::Block *entry = rewriter.getInsertionBlock();
534 mlir::Block *exit =
535 rewriter.splitBlock(entry, rewriter.getInsertionPoint());
536 mlir::Block *cond = &op.getCond().front();
537 mlir::Block *body = &op.getBody().front();
538 mlir::Block *step =
539 (op.maybeGetStep() ? &op.maybeGetStep()->front() : nullptr);
540
541 // Setup loop entry branch.
542 rewriter.setInsertionPointToEnd(entry);
543 cir::BrOp::create(rewriter, op.getLoc(), &op.getEntry().front());
544
545 // Branch from condition region to body or exit. The ConditionOp may not
546 // be in the first block of the condition region if a cleanup scope was
547 // already flattened within it, introducing multiple blocks. The
548 // ConditionOp is always the terminator of the last block.
549 auto conditionOp =
550 cast<cir::ConditionOp>(op.getCond().back().getTerminator());
551 lowerConditionOp(conditionOp, body, exit, rewriter);
552
553 // TODO(cir): Remove the walks below. It visits operations unnecessarily.
554 // However, to solve this we would likely need a custom DialectConversion
555 // driver to customize the order that operations are visited.
556
557 // Lower continue statements.
558 mlir::Block *dest = (step ? step : cond);
559 op.walkBodySkippingNestedLoops([&](mlir::Operation *op) {
560 if (!isa<cir::ContinueOp>(op))
561 return mlir::WalkResult::advance();
562
563 lowerTerminator(op, dest, rewriter);
564 return mlir::WalkResult::skip();
565 });
566
567 // Lower break statements.
568 walkRegionSkipping<cir::LoopOpInterface, cir::SwitchOp>(
569 op.getBody(), [&](mlir::Operation *op) {
570 if (!isa<cir::BreakOp>(op))
571 return mlir::WalkResult::advance();
572
573 lowerTerminator(op, exit, rewriter);
574 return mlir::WalkResult::skip();
575 });
576
577 // Lower optional body region yield.
578 for (mlir::Block &blk : op.getBody().getBlocks()) {
579 auto bodyYield = dyn_cast<cir::YieldOp>(blk.getTerminator());
580 if (bodyYield)
581 lowerTerminator(bodyYield, (step ? step : cond), rewriter);
582 }
583
584 // Lower mandatory step region yield. Like the condition region, the
585 // YieldOp may be in the last block rather than the first if a cleanup
586 // scope was already flattened within the step region.
587 if (step)
588 lowerTerminator(
589 cast<cir::YieldOp>(op.maybeGetStep()->back().getTerminator()), cond,
590 rewriter);
591
592 // Move region contents out of the loop op.
593 rewriter.inlineRegionBefore(op.getCond(), exit);
594 rewriter.inlineRegionBefore(op.getBody(), exit);
595 if (step)
596 rewriter.inlineRegionBefore(*op.maybeGetStep(), exit);
597
598 rewriter.eraseOp(op);
599 return mlir::success();
600 }
601};
602
603class CIRTernaryOpFlattening : public mlir::OpRewritePattern<cir::TernaryOp> {
604public:
605 using OpRewritePattern<cir::TernaryOp>::OpRewritePattern;
606
607 mlir::LogicalResult
608 matchAndRewrite(cir::TernaryOp op,
609 mlir::PatternRewriter &rewriter) const override {
610 Location loc = op->getLoc();
611 Block *condBlock = rewriter.getInsertionBlock();
612 Block::iterator opPosition = rewriter.getInsertionPoint();
613 Block *remainingOpsBlock = rewriter.splitBlock(condBlock, opPosition);
614 llvm::SmallVector<mlir::Location, 2> locs;
615 // Ternary result is optional, make sure to populate the location only
616 // when relevant.
617 if (op->getResultTypes().size())
618 locs.push_back(loc);
619 Block *continueBlock =
620 rewriter.createBlock(remainingOpsBlock, op->getResultTypes(), locs);
621 cir::BrOp::create(rewriter, loc, remainingOpsBlock);
622
623 Region &trueRegion = op.getTrueRegion();
624 Block *trueBlock = &trueRegion.front();
625 // Wire up the true region's exit (cir.yield -> br, cir.unreachable /
626 // cir.trap kept as-is). IR has already been modified by splitBlock /
627 // createBlock above, so per the MLIR pattern rewriter contract we must
628 // still return success() if the terminator turns out to be unexpected.
629 if (failed(rewriteRegionExitToContinue(rewriter, trueRegion, continueBlock,
630 "ternary true")))
631 return mlir::success();
632 rewriter.inlineRegionBefore(trueRegion, continueBlock);
633
634 Block *falseBlock = continueBlock;
635 Region &falseRegion = op.getFalseRegion();
636
637 falseBlock = &falseRegion.front();
638 if (failed(rewriteRegionExitToContinue(rewriter, falseRegion, continueBlock,
639 "ternary false")))
640 return mlir::success();
641 rewriter.inlineRegionBefore(falseRegion, continueBlock);
642
643 rewriter.setInsertionPointToEnd(condBlock);
644 cir::BrCondOp::create(rewriter, loc, op.getCond(), trueBlock, falseBlock);
645
646 rewriter.replaceOp(op, continueBlock->getArguments());
647
648 // Ok, we're done!
649 return mlir::success();
650 }
651};
652
653// Get or create the cleanup destination slot for a function. This slot is
654// shared across all cleanup scopes in the function to track which exit path
655// to take after running cleanup code when there are multiple exits.
656static cir::AllocaOp getOrCreateCleanupDestSlot(cir::FuncOp funcOp,
657 mlir::PatternRewriter &rewriter,
658 mlir::Location loc) {
659 mlir::Block &entryBlock = funcOp.getBody().front();
660
661 // Look for an existing cleanup dest slot in the entry block.
662 auto it = llvm::find_if(entryBlock, [](auto &op) {
663 return mlir::isa<AllocaOp>(&op) &&
664 mlir::cast<AllocaOp>(&op).getCleanupDestSlot();
665 });
666 if (it != entryBlock.end())
667 return mlir::cast<cir::AllocaOp>(*it);
668
669 // Create a new cleanup dest slot at the start of the entry block.
670 mlir::OpBuilder::InsertionGuard guard(rewriter);
671 rewriter.setInsertionPointToStart(&entryBlock);
672 cir::IntType s32Type =
673 cir::IntType::get(rewriter.getContext(), 32, /*isSigned=*/true);
674 cir::PointerType ptrToS32Type = cir::PointerType::get(s32Type);
675 cir::CIRDataLayout dataLayout(funcOp->getParentOfType<mlir::ModuleOp>());
676 uint64_t alignment = dataLayout.getAlignment(s32Type, true).value();
677 auto allocaOp = cir::AllocaOp::create(
678 rewriter, loc, ptrToS32Type, "__cleanup_dest_slot",
679 /*alignment=*/rewriter.getI64IntegerAttr(alignment));
680 allocaOp.setCleanupDestSlot(true);
681 return allocaOp;
682}
683
684/// Shared EH flattening utilities used by both CIRCleanupScopeOpFlattening
685/// and CIRTryOpFlattening.
686
687// Collect all function calls in a region that may throw exceptions and need
688// to be replaced with try_call operations. Skips calls marked nothrow.
689// Nested cleanup scopes and try ops are always flattened before their
690// enclosing parents, so there are no nested regions to skip here.
691static void
692collectThrowingCalls(mlir::Region &region,
693 llvm::SmallVectorImpl<cir::CallOp> &callsToRewrite) {
694 region.walk([&](cir::CallOp callOp) {
695 if (!callOp.getNothrow())
696 callsToRewrite.push_back(callOp);
697 });
698}
699
700// Collect all cir.throw operations in a region that need to be replaced
701// with cir.try_throw operations so they can unwind through an enclosing
702// cleanup or catch handler. Nested cleanup scopes and try ops are always
703// flattened before their enclosing parents, so there are no nested
704// regions to skip here.
705static void
706collectThrows(mlir::Region &region,
707 llvm::SmallVectorImpl<cir::ThrowOp> &throwsToRewrite) {
708 region.walk(
709 [&](cir::ThrowOp throwOp) { throwsToRewrite.push_back(throwOp); });
710}
711
712// Collect all cir.resume operations in a region that come from
713// already-flattened try or cleanup scope operations. These resume ops need
714// to be chained through this scope's EH handler instead of unwinding
715// directly to the caller. Nested cleanup scopes and try ops are always
716// flattened before their enclosing parents, so there are no nested regions
717// to skip here.
718static void collectResumeOps(mlir::Region &region,
720 region.walk([&](cir::ResumeOp resumeOp) { resumeOps.push_back(resumeOp); });
721}
722
723// Create a shared unwind destination block. The block contains a
724// cir.eh.initiate operation (optionally with the cleanup attribute) and a
725// branch to the given destination block, passing the eh_token.
726static mlir::Block *buildUnwindBlock(mlir::Block *dest, bool isCleanupOnly,
727 mlir::Location loc,
728 mlir::Block *insertBefore,
729 mlir::PatternRewriter &rewriter) {
730 mlir::Block *unwindBlock = rewriter.createBlock(insertBefore);
731 rewriter.setInsertionPointToEnd(unwindBlock);
732 auto ehInitiate =
733 cir::EhInitiateOp::create(rewriter, loc, /*cleanup=*/isCleanupOnly);
734 cir::BrOp::create(rewriter, loc, mlir::ValueRange{ehInitiate.getEhToken()},
735 dest);
736 return unwindBlock;
737}
738
739// Create a shared terminate unwind block for throwing calls in EH cleanup
740// regions. When an exception is thrown during cleanup (unwinding), the C++
741// standard requires that std::terminate() be called.
742static mlir::Block *buildTerminateUnwindBlock(mlir::Location loc,
743 mlir::Block *insertBefore,
744 mlir::PatternRewriter &rewriter) {
745 mlir::Block *terminateBlock = rewriter.createBlock(insertBefore);
746 rewriter.setInsertionPointToEnd(terminateBlock);
747 auto ehInitiate = cir::EhInitiateOp::create(rewriter, loc, /*cleanup=*/false);
748 cir::EhTerminateOp::create(rewriter, loc, ehInitiate.getEhToken());
749 return terminateBlock;
750}
751
752class CIRCleanupScopeOpFlattening
753 : public mlir::OpRewritePattern<cir::CleanupScopeOp> {
754public:
755 using OpRewritePattern<cir::CleanupScopeOp>::OpRewritePattern;
756
757 struct CleanupExit {
758 // An operation that exits the cleanup scope (yield, break, continue,
759 // return, etc.)
760 mlir::Operation *exitOp;
761
762 // A unique identifier for this exit's destination (used for switch dispatch
763 // when there are multiple exits).
764 int destinationId;
765
766 CleanupExit(mlir::Operation *op, int id) : exitOp(op), destinationId(id) {}
767 };
768
769 // Determine whether a goto operation transfers control to a label that
770 // exists somewhere inside the given region (or any of its nested regions).
771 // Label names are unique within a function, so finding a matching cir.label
772 // inside the region implies that the goto definitely targets that label and
773 // therefore stays within the region. If no match is found, the goto either
774 // exits the region or its target is unknown; in either case the caller must
775 // treat it as exiting the region.
776 static bool gotoTargetsLabelInRegion(cir::GotoOp gotoOp,
777 mlir::Region &region) {
778 llvm::StringRef targetLabel = gotoOp.getLabel();
779 return region
780 .walk([&](cir::LabelOp labelOp) {
781 if (labelOp.getLabel() == targetLabel)
782 return mlir::WalkResult::interrupt();
783 return mlir::WalkResult::advance();
784 })
785 .wasInterrupted();
786 }
787
788 // Collect all operations that exit a cleanup scope body. Return, goto, break,
789 // and continue can all require branches through the cleanup region. When a
790 // loop is encountered, only return and goto are collected because break and
791 // continue are handled by the loop and stay within the cleanup scope. When a
792 // switch is encountered, return, goto and continue are collected because they
793 // may all branch through the cleanup, but break is local to the switch. When
794 // a nested cleanup scope is encountered, we recursively collect exits since
795 // any return, goto, break, or continue from the nested cleanup will also
796 // branch through the outer cleanup.
797 //
798 // A goto is only treated as an exit if its target label is not somewhere
799 // inside the cleanup body region. Gotos whose target label is within the
800 // cleanup body stay inside the cleanup scope and need no special handling
801 // during flattening; they are simply inlined along with the rest of the
802 // body region.
803 //
804 // This function assigns unique destination IDs to each exit, which are
805 // used when multi-exit cleanup scopes are flattened.
806 void collectExits(mlir::Region &cleanupBodyRegion,
807 llvm::SmallVectorImpl<CleanupExit> &exits,
808 int &nextId) const {
809 // Collect yield terminators from the body region. We do this separately
810 // because yields in nested operations, including those in nested cleanup
811 // scopes, won't branch through the outer cleanup region.
812 for (mlir::Block &block : cleanupBodyRegion) {
813 auto *terminator = block.getTerminator();
814 if (isa<cir::YieldOp>(terminator))
815 exits.emplace_back(terminator, nextId++);
816 }
817
818 // Helper to decide whether an op is a goto that needs to be treated as an
819 // exit from the cleanup scope being flattened. If op is a goto and targets
820 // a label inside the cleanup body region, control stays within the cleanup
821 // and we leave the goto in place.
822 auto isGotoThatExitsCleanup = [&](mlir::Operation *op) {
823 auto gotoOp = dyn_cast<cir::GotoOp>(op);
824 return gotoOp && !gotoTargetsLabelInRegion(gotoOp, cleanupBodyRegion);
825 };
826
827 // Lambda to walk a loop and collect only returns and gotos.
828 // Break and continue inside loops are handled by the loop itself.
829 // Loops don't require special handling for nested switch or cleanup scopes
830 // because break and continue never branch out of the loop.
831 auto collectExitsInLoop = [&](mlir::Operation *loopOp) {
832 loopOp->walk<mlir::WalkOrder::PreOrder>([&](mlir::Operation *nestedOp) {
833 if (isa<cir::ReturnOp>(nestedOp)) {
834 exits.emplace_back(nestedOp, nextId++);
835 } else if (isGotoThatExitsCleanup(nestedOp)) {
836 exits.emplace_back(nestedOp, nextId++);
837 }
838 return mlir::WalkResult::advance();
839 });
840 };
841
842 // Forward declaration for mutual recursion.
843 std::function<void(mlir::Region &, bool)> collectExitsInCleanup;
844 std::function<void(mlir::Operation *)> collectExitsInSwitch;
845
846 // Lambda to collect exits from a switch. Collects return/goto/continue but
847 // not break (handled by switch). For nested loops/cleanups, recurses.
848 collectExitsInSwitch = [&](mlir::Operation *switchOp) {
849 switchOp->walk<mlir::WalkOrder::PreOrder>([&](mlir::Operation *nestedOp) {
850 if (isa<cir::CleanupScopeOp>(nestedOp)) {
851 // Walk the nested cleanup, but ignore break statements because they
852 // will be handled by the switch we are currently walking.
853 collectExitsInCleanup(
854 cast<cir::CleanupScopeOp>(nestedOp).getBodyRegion(),
855 /*ignoreBreak=*/true);
856 return mlir::WalkResult::skip();
857 } else if (isa<cir::LoopOpInterface>(nestedOp)) {
858 collectExitsInLoop(nestedOp);
859 return mlir::WalkResult::skip();
860 } else if (isa<cir::ReturnOp, cir::ContinueOp>(nestedOp)) {
861 exits.emplace_back(nestedOp, nextId++);
862 } else if (isGotoThatExitsCleanup(nestedOp)) {
863 exits.emplace_back(nestedOp, nextId++);
864 }
865 return mlir::WalkResult::advance();
866 });
867 };
868
869 // Lambda to collect exits from a cleanup scope body region. This collects
870 // break (optionally), continue, return, and goto, handling nested loops,
871 // switches, and cleanups appropriately.
872 collectExitsInCleanup = [&](mlir::Region &region, bool ignoreBreak) {
873 region.walk<mlir::WalkOrder::PreOrder>([&](mlir::Operation *op) {
874 // We need special handling for break statements because if this cleanup
875 // scope was nested within a switch op, break will be handled by the
876 // switch operation and therefore won't exit the cleanup scope enclosing
877 // the switch. We're only collecting exits from the cleanup that started
878 // this walk. Exits from nested cleanups will be handled when we flatten
879 // the nested cleanup.
880 if (!ignoreBreak && isa<cir::BreakOp>(op)) {
881 exits.emplace_back(op, nextId++);
882 } else if (isa<cir::ContinueOp, cir::ReturnOp>(op)) {
883 exits.emplace_back(op, nextId++);
884 } else if (isGotoThatExitsCleanup(op)) {
885 exits.emplace_back(op, nextId++);
886 } else if (isa<cir::CleanupScopeOp>(op)) {
887 // Recurse into nested cleanup's body region.
888 collectExitsInCleanup(cast<cir::CleanupScopeOp>(op).getBodyRegion(),
889 /*ignoreBreak=*/ignoreBreak);
890 return mlir::WalkResult::skip();
891 } else if (isa<cir::LoopOpInterface>(op)) {
892 // This kicks off a separate walk rather than continuing to dig deeper
893 // in the current walk because we need to handle break and continue
894 // differently inside loops.
895 collectExitsInLoop(op);
896 return mlir::WalkResult::skip();
897 } else if (isa<cir::SwitchOp>(op)) {
898 // This kicks off a separate walk rather than continuing to dig deeper
899 // in the current walk because we need to handle break differently
900 // inside switches.
901 collectExitsInSwitch(op);
902 return mlir::WalkResult::skip();
903 }
904 return mlir::WalkResult::advance();
905 });
906 };
907
908 // Collect exits from the body region.
909 collectExitsInCleanup(cleanupBodyRegion, /*ignoreBreak=*/false);
910 }
911
912 // Check if an operand's defining op should be moved to the destination block.
913 // We only sink constants and simple loads. Anything else should be saved
914 // to a temporary alloca and reloaded at the destination block.
915 static bool shouldSinkReturnOperand(mlir::Value operand,
916 cir::ReturnOp returnOp) {
917 // Block arguments can't be moved
918 mlir::Operation *defOp = operand.getDefiningOp();
919 if (!defOp)
920 return false;
921
922 // Only move constants and loads to the dispatch block. For anything else,
923 // we'll store to a temporary and reload in the dispatch block.
924 if (!mlir::isa<cir::ConstantOp, cir::LoadOp>(defOp))
925 return false;
926
927 // Check if the return is the only user
928 if (!operand.hasOneUse())
929 return false;
930
931 // Only move ops that are in the same block as the return.
932 if (defOp->getBlock() != returnOp->getBlock())
933 return false;
934
935 if (auto loadOp = mlir::dyn_cast<cir::LoadOp>(defOp)) {
936 // Only attempt to move loads of allocas in the entry block.
937 mlir::Value ptr = loadOp.getAddr();
938 auto funcOp = returnOp->getParentOfType<cir::FuncOp>();
939 assert(funcOp && "Return op has no function parent?");
940 mlir::Block &funcEntryBlock = funcOp.getBody().front();
941
942 // Check if it's an alloca in the function entry block
943 if (auto allocaOp =
944 mlir::dyn_cast_if_present<cir::AllocaOp>(ptr.getDefiningOp()))
945 return allocaOp->getBlock() == &funcEntryBlock;
946
947 return false;
948 }
949
950 // Make sure we only fall through to here with constants.
951 assert(mlir::isa<cir::ConstantOp>(defOp) && "Expected constant op");
952 return true;
953 }
954
955 // For returns with operands in cleanup dispatch blocks, the operands may not
956 // dominate the dispatch block. This function handles that by either sinking
957 // the operand's defining op to the dispatch block (for constants and simple
958 // loads) or by storing to a temporary alloca and reloading it.
959 void
960 getReturnOpOperands(cir::ReturnOp returnOp, mlir::Operation *exitOp,
961 mlir::Location loc, mlir::PatternRewriter &rewriter,
962 llvm::SmallVectorImpl<mlir::Value> &returnValues) const {
963 mlir::Block *destBlock = rewriter.getInsertionBlock();
964 auto funcOp = exitOp->getParentOfType<cir::FuncOp>();
965 assert(funcOp && "Return op has no function parent?");
966 mlir::Block &funcEntryBlock = funcOp.getBody().front();
967
968 for (mlir::Value operand : returnOp.getOperands()) {
969 if (shouldSinkReturnOperand(operand, returnOp)) {
970 // Sink the defining op to the dispatch block.
971 mlir::Operation *defOp = operand.getDefiningOp();
972 rewriter.moveOpBefore(defOp, destBlock, destBlock->end());
973 returnValues.push_back(operand);
974 } else {
975 // Create an alloca in the function entry block.
976 cir::AllocaOp alloca;
977 {
978 mlir::OpBuilder::InsertionGuard guard(rewriter);
979 rewriter.setInsertionPointToStart(&funcEntryBlock);
980 cir::CIRDataLayout dataLayout(
981 funcOp->getParentOfType<mlir::ModuleOp>());
982 uint64_t alignment =
983 dataLayout.getAlignment(operand.getType(), true).value();
984 cir::PointerType ptrType = cir::PointerType::get(operand.getType());
985 alloca =
986 cir::AllocaOp::create(rewriter, loc, ptrType, "__ret_operand_tmp",
987 rewriter.getI64IntegerAttr(alignment));
988 }
989
990 // Store the operand value at the original return location.
991 {
992 mlir::OpBuilder::InsertionGuard guard(rewriter);
993 rewriter.setInsertionPoint(exitOp);
994 cir::StoreOp::create(rewriter, loc, operand, alloca,
995 /*isVolatile=*/false,
996 /*isNontemporal=*/false,
997 /*alignment=*/mlir::IntegerAttr(),
998 cir::SyncScopeKindAttr(), cir::MemOrderAttr());
999 }
1000
1001 // Reload the value from the temporary alloca in the destination block.
1002 rewriter.setInsertionPointToEnd(destBlock);
1003 auto loaded =
1004 cir::LoadOp::create(rewriter, loc, alloca, /*isDeref=*/false,
1005 /*isVolatile=*/false, /*isNontemporal=*/false,
1006 /*alignment=*/mlir::IntegerAttr(),
1007 cir::SyncScopeKindAttr(), cir::MemOrderAttr(),
1008 /*invariant=*/false);
1009 returnValues.push_back(loaded);
1010 }
1011 }
1012 }
1013
1014 // Create the appropriate terminator for an exit operation in the dispatch
1015 // block. For return ops with operands, this handles the dominance issue by
1016 // either moving the operand's defining op to the dispatch block (if it's a
1017 // trivial use) or by storing to a temporary alloca and loading it.
1018 mlir::LogicalResult
1019 createExitTerminator(mlir::Operation *exitOp, mlir::Location loc,
1020 mlir::Block *continueBlock,
1021 mlir::PatternRewriter &rewriter) const {
1022 return llvm::TypeSwitch<mlir::Operation *, mlir::LogicalResult>(exitOp)
1023 .Case<cir::YieldOp>([&](auto) {
1024 // Yield becomes a branch to continue block.
1025 cir::BrOp::create(rewriter, loc, continueBlock);
1026 return mlir::success();
1027 })
1028 .Case<cir::BreakOp>([&](auto) {
1029 // Break is preserved for later lowering by enclosing switch/loop.
1030 cir::BreakOp::create(rewriter, loc);
1031 return mlir::success();
1032 })
1033 .Case<cir::ContinueOp>([&](auto) {
1034 // Continue is preserved for later lowering by enclosing loop.
1035 cir::ContinueOp::create(rewriter, loc);
1036 return mlir::success();
1037 })
1038 .Case<cir::ReturnOp>([&](auto returnOp) {
1039 // Return from the cleanup exit. Note, if this is a return inside a
1040 // nested cleanup scope, the flattening of the outer scope will handle
1041 // branching through the outer cleanup.
1042 if (returnOp.hasOperand()) {
1043 llvm::SmallVector<mlir::Value, 2> returnValues;
1044 getReturnOpOperands(returnOp, exitOp, loc, rewriter, returnValues);
1045 cir::ReturnOp::create(rewriter, loc, returnValues);
1046 } else {
1047 cir::ReturnOp::create(rewriter, loc);
1048 }
1049 return mlir::success();
1050 })
1051 .Case<cir::GotoOp>([&](auto gotoOp) {
1052 // Gotos that target a label within the cleanup body region are
1053 // filtered out by collectExits and never reach this code, so any
1054 // goto that does reach here transfers control out of the cleanup
1055 // scope. The goto is just moved to the exit block.
1056 cir::GotoOp::create(rewriter, loc, gotoOp.getLabel());
1057 return mlir::success();
1058 })
1059 .Default([&](mlir::Operation *op) {
1060 cir::UnreachableOp::create(rewriter, loc);
1061 return op->emitError(
1062 "unexpected exit operation in cleanup scope body");
1063 });
1064 }
1065
1066#ifndef NDEBUG
1067 // Check that no block other than the last one in a region exits the region.
1068 static bool regionExitsOnlyFromLastBlock(mlir::Region &region) {
1069 for (mlir::Block &block : region) {
1070 if (&block == &region.back())
1071 continue;
1072 bool expectedTerminator =
1073 llvm::TypeSwitch<mlir::Operation *, bool>(block.getTerminator())
1074 // It is theoretically possible to have a cleanup block with
1075 // any of the following exits in non-final blocks, but we won't
1076 // currently generate any CIR that does that, and being able to
1077 // assume that it doesn't happen simplifies the implementation.
1078 // If we ever need to handle this case, the code will need to
1079 // be updated to handle it.
1080 .Case<cir::YieldOp, cir::ReturnOp, cir::ResumeFlatOp,
1081 cir::ContinueOp, cir::BreakOp, cir::GotoOp>(
1082 [](auto) { return false; })
1083 // We expect that call operations have not yet been rewritten
1084 // as try_call operations. A call can unwind out of the cleanup
1085 // scope, but we will be handling that during flattening. The
1086 // only case where a try_call could be present inside an
1087 // unflattened cleanup region is if the cleanup contained a
1088 // nested try-catch region, and that isn't expected as of the
1089 // time of this implementation. If it does, this could be
1090 // updated to tolerate it.
1091 .Case<cir::TryCallOp>([](auto) { return false; })
1092 // Likewise, we don't expect to find an EH dispatch operation
1093 // because we weren't expecting try-catch regions nested in the
1094 // cleanup region.
1095 .Case<cir::EhDispatchOp>([](auto) { return false; })
1096 // In theory, it would be possible to have a flattened switch
1097 // operation that does not exit the cleanup region. For now,
1098 // that's not happening.
1099 .Case<cir::SwitchFlatOp>([](auto) { return false; })
1100 // These aren't expected either, but if they occur, they don't
1101 // exit the region, so that's OK.
1102 .Case<cir::UnreachableOp, cir::TrapOp>([](auto) { return true; })
1103 // Indirect branches are not expected.
1104 .Case<cir::IndirectBrOp>([](auto) { return false; })
1105 // We do expect branches, but we don't expect them to leave
1106 // the region.
1107 .Case<cir::BrOp>([&](cir::BrOp brOp) {
1108 assert(brOp.getDest()->getParent() == &region &&
1109 "branch destination is not in the region");
1110 return true;
1111 })
1112 .Case<cir::BrCondOp>([&](cir::BrCondOp brCondOp) {
1113 assert(brCondOp.getDestTrue()->getParent() == &region &&
1114 "branch destination is not in the region");
1115 assert(brCondOp.getDestFalse()->getParent() == &region &&
1116 "branch destination is not in the region");
1117 return true;
1118 })
1119 // What else could there be?
1120 .Default([](mlir::Operation *) -> bool {
1121 llvm_unreachable("unexpected terminator in cleanup region");
1122 });
1123 if (!expectedTerminator)
1124 return false;
1125 }
1126 return true;
1127 }
1128#endif
1129
1130 // Build the EH cleanup block structure by cloning the cleanup region. The
1131 // cloned entry block gets an !cir.eh_token argument and a cir.begin_cleanup
1132 // inserted at the top. All cir.yield terminators that might exit the cleanup
1133 // region are replaced with cir.end_cleanup + cir.resume.
1134 //
1135 // For a single-block cleanup region, this produces:
1136 //
1137 // ^eh_cleanup(%eh_token : !cir.eh_token):
1138 // %ct = cir.begin_cleanup %eh_token : !cir.eh_token -> !cir.cleanup_token
1139 // <cloned cleanup operations>
1140 // cir.end_cleanup %ct : !cir.cleanup_token
1141 // cir.resume %eh_token : !cir.eh_token
1142 //
1143 // For a multi-block cleanup region (e.g. containing a flattened cir.if),
1144 // the same wrapping is applied around the cloned block structure: the entry
1145 // block gets begin_cleanup and all exit blocks (those terminated by yield)
1146 // get end_cleanup + resume.
1147 //
1148 // If this cleanup scope is nested within a TryOp, the resume will be updated
1149 // to branch to the catch dispatch block of the enclosing try operation when
1150 // the TryOp is flattened.
1151 mlir::Block *buildEHCleanupBlocks(cir::CleanupScopeOp cleanupOp,
1152 mlir::Location loc,
1153 mlir::Block *insertBefore,
1154 mlir::PatternRewriter &rewriter) const {
1155 assert(regionExitsOnlyFromLastBlock(cleanupOp.getCleanupRegion()) &&
1156 "cleanup region has exits in non-final blocks");
1157
1158 // Track the block before the insertion point so we can find the cloned
1159 // blocks after cloning.
1160 mlir::Block *blockBeforeClone = insertBefore->getPrevNode();
1161
1162 // Clone the entire cleanup region before insertBefore.
1163 rewriter.cloneRegionBefore(cleanupOp.getCleanupRegion(), insertBefore);
1164
1165 // Find the first cloned block.
1166 mlir::Block *clonedEntry = blockBeforeClone
1167 ? blockBeforeClone->getNextNode()
1168 : &insertBefore->getParent()->front();
1169
1170 // Add the eh_token argument to the cloned entry block and insert
1171 // begin_cleanup at the top.
1172 auto ehTokenType = cir::EhTokenType::get(rewriter.getContext());
1173 mlir::Value ehToken = clonedEntry->addArgument(ehTokenType, loc);
1174
1175 rewriter.setInsertionPointToStart(clonedEntry);
1176 auto beginCleanup = cir::BeginCleanupOp::create(rewriter, loc, ehToken);
1177
1178 // Replace the yield terminator in the last cloned block with
1179 // end_cleanup + resume.
1180 mlir::Block *lastClonedBlock = insertBefore->getPrevNode();
1181 auto yieldOp =
1182 mlir::dyn_cast<cir::YieldOp>(lastClonedBlock->getTerminator());
1183 if (yieldOp) {
1184 rewriter.setInsertionPoint(yieldOp);
1185 cir::EndCleanupOp::create(rewriter, loc, beginCleanup.getCleanupToken());
1186 rewriter.replaceOpWithNewOp<cir::ResumeOp>(yieldOp, ehToken);
1187 } else {
1188 cleanupOp->emitError("Not yet implemented: cleanup region terminated "
1189 "with non-yield operation");
1190 }
1191
1192 return clonedEntry;
1193 }
1194
1195 // Flatten a cleanup scope. The body region's exits branch to the cleanup
1196 // block, and the cleanup block branches to destination blocks whose contents
1197 // depend on the type of operation that exited the body region. Yield becomes
1198 // a branch to the block after the cleanup scope, break and continue are
1199 // preserved for later lowering by enclosing switch or loop, and return
1200 // is preserved as is.
1201 //
1202 // If there are multiple exits from the cleanup body, a destination slot and
1203 // switch dispatch are used to continue to the correct destination after the
1204 // cleanup is complete. A destination slot alloca is created at the function
1205 // entry block. Each exit operation is replaced by a store of its unique ID to
1206 // the destination slot and a branch to cleanup. An operation is appended to
1207 // the to branch to a dispatch block that loads the destination slot and uses
1208 // switch.flat to branch to the correct destination.
1209 //
1210 // If the cleanup scope requires EH cleanup, any call operations in the body
1211 // that may throw are replaced with cir.try_call operations that unwind to an
1212 // EH cleanup block. The cleanup block(s) will be terminated with a cir.resume
1213 // operation. If this cleanup scope is enclosed by a try operation, the
1214 // flattening of the try operation flattening will replace the cir.resume with
1215 // a branch to a catch dispatch block. Otherwise, the cir.resume operation
1216 // remains in place and will unwind to the caller.
1217 mlir::LogicalResult
1218 flattenCleanup(cir::CleanupScopeOp cleanupOp,
1219 llvm::SmallVectorImpl<CleanupExit> &exits,
1220 llvm::SmallVectorImpl<cir::CallOp> &callsToRewrite,
1221 llvm::SmallVectorImpl<cir::ThrowOp> &throwsToRewrite,
1222 llvm::SmallVectorImpl<cir::ResumeOp> &resumeOpsToChain,
1223 mlir::PatternRewriter &rewriter) const {
1224 mlir::Location loc = cleanupOp.getLoc();
1225 cir::CleanupKind cleanupKind = cleanupOp.getCleanupKind();
1226 bool hasNormalCleanup = cleanupKind == cir::CleanupKind::Normal ||
1227 cleanupKind == cir::CleanupKind::All;
1228 bool hasEHCleanup = cleanupKind == cir::CleanupKind::EH ||
1229 cleanupKind == cir::CleanupKind::All;
1230 bool isMultiExit = exits.size() > 1;
1231
1232 // Get references to region blocks before inlining.
1233 mlir::Block *bodyEntry = &cleanupOp.getBodyRegion().front();
1234 mlir::Block *cleanupEntry = &cleanupOp.getCleanupRegion().front();
1235 mlir::Block *cleanupExit = &cleanupOp.getCleanupRegion().back();
1236 assert(regionExitsOnlyFromLastBlock(cleanupOp.getCleanupRegion()) &&
1237 "cleanup region has exits in non-final blocks");
1238 auto cleanupYield = dyn_cast<cir::YieldOp>(cleanupExit->getTerminator());
1239 if (!cleanupYield) {
1240 return rewriter.notifyMatchFailure(cleanupOp,
1241 "Not yet implemented: cleanup region "
1242 "terminated with non-yield operation");
1243 }
1244
1245 // For multiple exits from the body region, get or create a destination slot
1246 // at function entry. The slot is shared across all cleanup scopes in the
1247 // function. This is only needed if the cleanup scope requires normal
1248 // cleanup.
1249 cir::AllocaOp destSlot;
1250 if (isMultiExit && hasNormalCleanup) {
1251 auto funcOp = cleanupOp->getParentOfType<cir::FuncOp>();
1252 if (!funcOp)
1253 return cleanupOp->emitError("cleanup scope not inside a function");
1254 destSlot = getOrCreateCleanupDestSlot(funcOp, rewriter, loc);
1255 }
1256
1257 // Split the current block to create the insertion point.
1258 mlir::Block *currentBlock = rewriter.getInsertionBlock();
1259 mlir::Block *continueBlock =
1260 rewriter.splitBlock(currentBlock, rewriter.getInsertionPoint());
1261
1262 // Build EH cleanup blocks if needed. This must be done before inlining
1263 // the cleanup region since buildEHCleanupBlocks clones from it. The unwind
1264 // block is inserted before the EH cleanup entry so that the final layout
1265 // is: body -> normal cleanup -> exit -> unwind -> EH cleanup -> continue.
1266 // EH cleanup blocks are needed when there are throwing calls or throws
1267 // that need to be rewritten, or when there are resume ops from
1268 // already-flattened inner cleanup scopes that need to chain through this
1269 // cleanup's EH handler.
1270 mlir::Block *unwindBlock = nullptr;
1271 mlir::Block *ehCleanupEntry = nullptr;
1272 if (hasEHCleanup && (!callsToRewrite.empty() || !throwsToRewrite.empty() ||
1273 !resumeOpsToChain.empty())) {
1274 ehCleanupEntry =
1275 buildEHCleanupBlocks(cleanupOp, loc, continueBlock, rewriter);
1276 // The unwind block is only needed when there are throwing calls or
1277 // throws that need a shared unwind destination. Resume ops from inner
1278 // cleanups branch directly to the EH cleanup entry.
1279 if (!callsToRewrite.empty() || !throwsToRewrite.empty())
1280 unwindBlock = buildUnwindBlock(ehCleanupEntry, /*isCleanupOnly=*/true,
1281 loc, ehCleanupEntry, rewriter);
1282 }
1283
1284 // All normal flow blocks are inserted before this point — either before
1285 // the unwind block (if it exists), or before the EH cleanup entry (if EH
1286 // cleanup exists but no unwind block is needed), or before the continue
1287 // block.
1288 mlir::Block *normalInsertPt =
1289 unwindBlock ? unwindBlock
1290 : (ehCleanupEntry ? ehCleanupEntry : continueBlock);
1291
1292 // Inline the body region.
1293 rewriter.inlineRegionBefore(cleanupOp.getBodyRegion(), normalInsertPt);
1294
1295 // Inline the cleanup region for the normal cleanup path.
1296 if (hasNormalCleanup)
1297 rewriter.inlineRegionBefore(cleanupOp.getCleanupRegion(), normalInsertPt);
1298
1299 // Branch from current block to body entry.
1300 rewriter.setInsertionPointToEnd(currentBlock);
1301 cir::BrOp::create(rewriter, loc, bodyEntry);
1302
1303 // Handle normal exits.
1304 mlir::LogicalResult result = mlir::success();
1305 if (hasNormalCleanup) {
1306 // Create the exit/dispatch block (after cleanup, before continue).
1307 mlir::Block *exitBlock = rewriter.createBlock(normalInsertPt);
1308
1309 // Rewrite the cleanup region's yield to branch to exit block.
1310 rewriter.setInsertionPoint(cleanupYield);
1311 rewriter.replaceOpWithNewOp<cir::BrOp>(cleanupYield, exitBlock);
1312
1313 if (isMultiExit) {
1314 // Build the dispatch switch in the exit block.
1315 rewriter.setInsertionPointToEnd(exitBlock);
1316
1317 // Load the destination slot value.
1318 auto slotValue =
1319 cir::LoadOp::create(rewriter, loc, destSlot, /*isDeref=*/false,
1320 /*isVolatile=*/false, /*isNontemporal=*/false,
1321 /*alignment=*/mlir::IntegerAttr(),
1322 cir::SyncScopeKindAttr(), cir::MemOrderAttr(),
1323 /*invariant=*/false);
1324
1325 // Create destination blocks for each exit and collect switch case info.
1326 llvm::SmallVector<mlir::APInt, 8> caseValues;
1327 llvm::SmallVector<mlir::Block *, 8> caseDestinations;
1328 llvm::SmallVector<mlir::ValueRange, 8> caseOperands;
1329 cir::IntType s32Type =
1330 cir::IntType::get(rewriter.getContext(), 32, /*isSigned=*/true);
1331
1332 for (const CleanupExit &exit : exits) {
1333 // Create a block for this destination.
1334 mlir::Block *destBlock = rewriter.createBlock(normalInsertPt);
1335 rewriter.setInsertionPointToEnd(destBlock);
1336 result =
1337 createExitTerminator(exit.exitOp, loc, continueBlock, rewriter);
1338
1339 // Add to switch cases.
1340 caseValues.push_back(
1341 llvm::APInt(32, static_cast<uint64_t>(exit.destinationId), true));
1342 caseDestinations.push_back(destBlock);
1343 caseOperands.push_back(mlir::ValueRange());
1344
1345 // Replace the original exit op with: store dest ID, branch to
1346 // cleanup.
1347 rewriter.setInsertionPoint(exit.exitOp);
1348 auto destIdConst = cir::ConstantOp::create(
1349 rewriter, loc, cir::IntAttr::get(s32Type, exit.destinationId));
1350 cir::StoreOp::create(rewriter, loc, destIdConst, destSlot,
1351 /*isVolatile=*/false,
1352 /*isNontemporal=*/false,
1353 /*alignment=*/mlir::IntegerAttr(),
1354 cir::SyncScopeKindAttr(), cir::MemOrderAttr());
1355 rewriter.replaceOpWithNewOp<cir::BrOp>(exit.exitOp, cleanupEntry);
1356
1357 // If the exit terminator creation failed, we're going to end up with
1358 // partially flattened code, but we'll also have reported an error so
1359 // that's OK. We need to finish out this function to keep the IR in a
1360 // valid state to help diagnose the error. This is a temporary
1361 // possibility during development. It shouldn't ever happen after the
1362 // implementation is complete.
1363 if (result.failed())
1364 break;
1365 }
1366
1367 // Create the default destination (unreachable).
1368 mlir::Block *defaultBlock = rewriter.createBlock(normalInsertPt);
1369 rewriter.setInsertionPointToEnd(defaultBlock);
1370 cir::UnreachableOp::create(rewriter, loc);
1371
1372 // Build the switch.flat operation in the exit block.
1373 rewriter.setInsertionPointToEnd(exitBlock);
1374 cir::SwitchFlatOp::create(rewriter, loc, slotValue, defaultBlock,
1375 mlir::ValueRange(), caseValues,
1376 caseDestinations, caseOperands);
1377 } else {
1378 // Single exit: put the appropriate terminator directly in the exit
1379 // block.
1380 rewriter.setInsertionPointToEnd(exitBlock);
1381 mlir::Operation *exitOp = exits[0].exitOp;
1382 result = createExitTerminator(exitOp, loc, continueBlock, rewriter);
1383
1384 // Replace body exit with branch to cleanup entry.
1385 rewriter.setInsertionPoint(exitOp);
1386 rewriter.replaceOpWithNewOp<cir::BrOp>(exitOp, cleanupEntry);
1387 }
1388 } else {
1389 // EH-only cleanup: normal exits skip the cleanup entirely.
1390 // Replace yield exits with branches to the continue block.
1391 for (CleanupExit &exit : exits) {
1392 if (isa<cir::YieldOp>(exit.exitOp)) {
1393 rewriter.setInsertionPoint(exit.exitOp);
1394 rewriter.replaceOpWithNewOp<cir::BrOp>(exit.exitOp, continueBlock);
1395 }
1396 // Non-yield exits (break, continue, return) stay as-is since no normal
1397 // cleanup is needed.
1398 }
1399 }
1400
1401 // Replace non-nothrow calls and throws with try_call/try_throw
1402 // operations. All calls and throws within this cleanup scope share the
1403 // same unwind destination.
1404 if (hasEHCleanup) {
1405 for (cir::CallOp callOp : callsToRewrite)
1406 replaceCallWithTryCall(callOp, unwindBlock, loc, rewriter);
1407 for (cir::ThrowOp throwOp : throwsToRewrite)
1408 replaceThrowWithTryThrow(throwOp, unwindBlock, loc, rewriter);
1409 }
1410
1411 // Handle throwing calls and throws in EH cleanup blocks. When an
1412 // exception is thrown during cleanup code that runs on the exception
1413 // unwind path, the C++ standard requires that std::terminate() be
1414 // called. Replace such calls and throws with try_call/try_throw
1415 // operations that unwind to a terminate block containing
1416 // cir.eh.initiate + cir.eh.terminate.
1417 if (ehCleanupEntry) {
1418 llvm::SmallVector<cir::CallOp> ehCleanupThrowingCalls;
1419 llvm::SmallVector<cir::ThrowOp> ehCleanupThrows;
1420 for (mlir::Block *block = ehCleanupEntry; block != continueBlock;
1421 block = block->getNextNode()) {
1422 block->walk([&](mlir::Operation *op) {
1423 if (auto callOp = mlir::dyn_cast<cir::CallOp>(op)) {
1424 if (!callOp.getNothrow())
1425 ehCleanupThrowingCalls.push_back(callOp);
1426 } else if (auto throwOp = mlir::dyn_cast<cir::ThrowOp>(op)) {
1427 ehCleanupThrows.push_back(throwOp);
1428 }
1429 });
1430 }
1431 if (!ehCleanupThrowingCalls.empty() || !ehCleanupThrows.empty()) {
1432 mlir::Block *terminateBlock =
1433 buildTerminateUnwindBlock(loc, continueBlock, rewriter);
1434 for (cir::CallOp callOp : ehCleanupThrowingCalls)
1435 replaceCallWithTryCall(callOp, terminateBlock, loc, rewriter);
1436 for (cir::ThrowOp throwOp : ehCleanupThrows)
1437 replaceThrowWithTryThrow(throwOp, terminateBlock, loc, rewriter);
1438 }
1439 }
1440
1441 // Chain inner EH cleanup resume ops to this cleanup's EH handler.
1442 // Each cir.resume from an already-flattened inner cleanup is replaced
1443 // with a branch to the outer EH cleanup entry, passing the eh_token
1444 // from the inner's begin_cleanup so that the same in-flight exception
1445 // flows through the outer cleanup before unwinding to the caller.
1446 if (ehCleanupEntry) {
1447 for (cir::ResumeOp resumeOp : resumeOpsToChain) {
1448 mlir::Value ehToken = resumeOp.getEhToken();
1449 rewriter.setInsertionPoint(resumeOp);
1450 rewriter.replaceOpWithNewOp<cir::BrOp>(
1451 resumeOp, mlir::ValueRange{ehToken}, ehCleanupEntry);
1452 }
1453 }
1454
1455 // Erase the original cleanup scope op.
1456 rewriter.eraseOp(cleanupOp);
1457
1458 // Always return success because the IR has been modified (blocks split,
1459 // regions inlined, ops erased, etc.). The MLIR pattern rewriter contract
1460 // requires that if a pattern modifies IR, it must return success().
1461 return mlir::success();
1462 }
1463
1464 mlir::LogicalResult
1465 matchAndRewrite(cir::CleanupScopeOp cleanupOp,
1466 mlir::PatternRewriter &rewriter) const override {
1467 mlir::OpBuilder::InsertionGuard guard(rewriter);
1468
1469 // All nested structured CIR ops must be flattened before the cleanup scope.
1470 // Operations like loops, switches, scopes, and ifs may contain exits
1471 // (return, break, continue) that the cleanup scope will replace with
1472 // branches to the cleanup entry. If those exits are inside a structured
1473 // op's region, the branch would reference a block outside that region,
1474 // which is invalid. Fail the match so they are processed first.
1475 //
1476 // Before checking, erase any trivially dead nested cleanup scopes. These
1477 // arise from deactivated cleanups (e.g. partial-construction guards for
1478 // lambda captures). The greedy rewriter may have already DCE'd them, but
1479 // when a trivially dead nested op is erased first, the parent isn't always
1480 // re-added to the worklist, so we handle it here.
1481 llvm::SmallVector<cir::CleanupScopeOp> deadNestedOps;
1482 cleanupOp.getBodyRegion().walk([&](cir::CleanupScopeOp nested) {
1483 if (mlir::isOpTriviallyDead(nested))
1484 deadNestedOps.push_back(nested);
1485 });
1486 for (auto op : deadNestedOps)
1487 rewriter.eraseOp(op);
1488
1489 if (hasNestedOpsToFlatten(cleanupOp.getBodyRegion()))
1490 return mlir::failure();
1491
1492 cir::CleanupKind cleanupKind = cleanupOp.getCleanupKind();
1493
1494 // Collect all exits from the body region.
1495 llvm::SmallVector<CleanupExit> exits;
1496 int nextId = 0;
1497 collectExits(cleanupOp.getBodyRegion(), exits, nextId);
1498
1499 assert(!exits.empty() && "cleanup scope body has no exit");
1500
1501 // Collect non-nothrow calls and throws that need to be converted to
1502 // try_call/try_throw. This is only needed for EH and All cleanup kinds,
1503 // but the vectors will simply be empty for Normal cleanup.
1504 llvm::SmallVector<cir::CallOp> callsToRewrite;
1505 llvm::SmallVector<cir::ThrowOp> throwsToRewrite;
1506 if (cleanupKind != cir::CleanupKind::Normal) {
1507 collectThrowingCalls(cleanupOp.getBodyRegion(), callsToRewrite);
1508 collectThrows(cleanupOp.getBodyRegion(), throwsToRewrite);
1509 }
1510
1511 // Collect resume ops from already-flattened inner cleanup scopes that
1512 // need to chain through this cleanup's EH handler.
1513 llvm::SmallVector<cir::ResumeOp> resumeOpsToChain;
1514 if (cleanupKind != cir::CleanupKind::Normal)
1515 collectResumeOps(cleanupOp.getBodyRegion(), resumeOpsToChain);
1516
1517 return flattenCleanup(cleanupOp, exits, callsToRewrite, throwsToRewrite,
1518 resumeOpsToChain, rewriter);
1519 }
1520};
1521
1522// Trace an !cir.eh_token value back through block arguments to find the
1523// cir.eh.initiate operation that defines it. Returns {} if the defining op
1524// cannot be found (e.g. multiple predecessors).
1525static cir::EhInitiateOp traceToEhInitiate(mlir::Value ehToken) {
1526 while (ehToken) {
1527 if (auto initiate = ehToken.getDefiningOp<cir::EhInitiateOp>())
1528 return initiate;
1529 auto blockArg = mlir::dyn_cast<mlir::BlockArgument>(ehToken);
1530 if (!blockArg)
1531 return {};
1532 mlir::Block *pred = blockArg.getOwner()->getSinglePredecessor();
1533 if (!pred)
1534 return {};
1535 auto brOp = mlir::dyn_cast<cir::BrOp>(pred->getTerminator());
1536 if (!brOp)
1537 return {};
1538 ehToken = brOp.getDestOperands()[blockArg.getArgNumber()];
1539 }
1540 return {};
1541}
1542
1543class CIRTryOpFlattening : public mlir::OpRewritePattern<cir::TryOp> {
1544public:
1545 using OpRewritePattern<cir::TryOp>::OpRewritePattern;
1546
1547 // Build the catch dispatch block with a cir.eh.dispatch operation.
1548 // The dispatch block receives an !cir.eh_token argument and dispatches
1549 // to the appropriate catch handler blocks based on exception types.
1550 mlir::Block *buildCatchDispatchBlock(
1551 cir::TryOp tryOp, mlir::ArrayAttr handlerTypes,
1552 llvm::SmallVectorImpl<mlir::Block *> &catchHandlerBlocks,
1553 mlir::Location loc, mlir::Block *insertBefore,
1554 mlir::PatternRewriter &rewriter) const {
1555 mlir::Block *dispatchBlock = rewriter.createBlock(insertBefore);
1556 auto ehTokenType = cir::EhTokenType::get(rewriter.getContext());
1557 mlir::Value ehToken = dispatchBlock->addArgument(ehTokenType, loc);
1558
1559 rewriter.setInsertionPointToEnd(dispatchBlock);
1560
1561 // Build the catch types and destinations for the dispatch.
1562 llvm::SmallVector<mlir::Attribute> catchTypeAttrs;
1563 llvm::SmallVector<mlir::Block *> catchDests;
1564 mlir::Block *defaultDest = nullptr;
1565 bool defaultIsCatchAll = false;
1566
1567 for (auto [typeAttr, handlerBlock] :
1568 llvm::zip(handlerTypes, catchHandlerBlocks)) {
1569 if (mlir::isa<cir::CatchAllAttr>(typeAttr)) {
1570 assert(!defaultDest && "multiple catch_all or unwind handlers");
1571 defaultDest = handlerBlock;
1572 defaultIsCatchAll = true;
1573 } else if (mlir::isa<cir::UnwindAttr>(typeAttr)) {
1574 assert(!defaultDest && "multiple catch_all or unwind handlers");
1575 defaultDest = handlerBlock;
1576 defaultIsCatchAll = false;
1577 } else {
1578 // This is a typed catch handler (GlobalViewAttr with type info).
1579 catchTypeAttrs.push_back(typeAttr);
1580 catchDests.push_back(handlerBlock);
1581 }
1582 }
1583
1584 assert(defaultDest && "dispatch must have a catch_all or unwind handler");
1585
1586 mlir::ArrayAttr catchTypesArrayAttr;
1587 if (!catchTypeAttrs.empty())
1588 catchTypesArrayAttr = rewriter.getArrayAttr(catchTypeAttrs);
1589
1590 cir::EhDispatchOp::create(rewriter, loc, ehToken, catchTypesArrayAttr,
1591 defaultIsCatchAll, defaultDest, catchDests);
1592
1593 return dispatchBlock;
1594 }
1595
1596 // Flatten a single catch handler region. Each handler region has an
1597 // !cir.eh_token argument and starts with cir.begin_catch, followed by
1598 // a cir.cleanup.scope containing the handler body (with cir.end_catch in
1599 // its cleanup region), and ending with cir.yield.
1600 //
1601 // After flattening, the handler region becomes a block that receives the
1602 // eh_token, calls begin_catch, runs the handler body inline, calls
1603 // end_catch, and branches to the continue block.
1604 //
1605 // The cleanup scope inside the catch handler is expected to have been
1606 // flattened before we get here, so what we see in the handler region is
1607 // already flat code with begin_catch at the top and end_catch in any place
1608 // that we would exit the catch handler. We just need to inline the region
1609 // and fix up terminators.
1610 mlir::Block *flattenCatchHandler(mlir::Region &handlerRegion,
1611 mlir::Block *continueBlock,
1612 mlir::Location loc,
1613 mlir::Block *insertBefore,
1614 mlir::PatternRewriter &rewriter) const {
1615 // The handler region entry block has the !cir.eh_token argument.
1616 mlir::Block *handlerEntry = &handlerRegion.front();
1617
1618 // Inline the handler region before insertBefore.
1619 rewriter.inlineRegionBefore(handlerRegion, insertBefore);
1620
1621 // Replace yield terminators in the handler with branches to continue.
1622 for (mlir::Block &block : llvm::make_range(handlerEntry->getIterator(),
1623 insertBefore->getIterator())) {
1624 if (auto yieldOp = dyn_cast<cir::YieldOp>(block.getTerminator())) {
1625 // Verify that end_catch is the last non-branch operation before
1626 // this yield. After cleanup scope flattening, end_catch may be
1627 // in a predecessor block rather than immediately before the yield.
1628 // Walk back through predecessors (including multi-predecessor
1629 // blocks), verifying that each intermediate block contains only a
1630 // branch terminator, until we find end_catch as the last
1631 // non-terminator in some block.
1632 // Verify that end_catch is reachable on some predecessor path
1633 // before this yield. After cleanup scope flattening, end_catch
1634 // may be separated from yield by conditional branches (e.g.,
1635 // from flattened cir.if inside the catch body).
1636 assert(([&]() {
1637 if (mlir::Operation *prev = yieldOp->getPrevNode())
1638 return isa<cir::EndCatchOp>(prev);
1639 llvm::SmallPtrSet<mlir::Block *, 8> visited;
1640 llvm::SmallVector<mlir::Block *, 4> worklist;
1641 for (mlir::Block *pred : block.getPredecessors())
1642 worklist.push_back(pred);
1643 while (!worklist.empty()) {
1644 mlir::Block *b = worklist.pop_back_val();
1645 if (!visited.insert(b).second)
1646 continue;
1647 mlir::Operation *term = b->getTerminator();
1648 if (mlir::Operation *prev = term->getPrevNode()) {
1649 if (isa<cir::EndCatchOp>(prev))
1650 return true;
1651 }
1652 for (mlir::Block *pred : b->getPredecessors())
1653 worklist.push_back(pred);
1654 }
1655 return false;
1656 }()) &&
1657 "expected end_catch reachable before yield "
1658 "in catch handler");
1659 rewriter.setInsertionPoint(yieldOp);
1660 rewriter.replaceOpWithNewOp<cir::BrOp>(yieldOp, continueBlock);
1661 }
1662 }
1663
1664 return handlerEntry;
1665 }
1666
1667 // Flatten an unwind handler region. The unwind region just contains a
1668 // cir.resume that continues unwinding. We inline it and leave the resume
1669 // in place. If this try op is nested inside an EH cleanup or another try op,
1670 // the enclosing op will rewrite the resume as a branch to its cleanup or
1671 // dispatch block when it is flattened. Otherwise, the resume will unwind to
1672 // the caller.
1673 mlir::Block *flattenUnwindHandler(mlir::Region &unwindRegion,
1674 mlir::Location loc,
1675 mlir::Block *insertBefore,
1676 mlir::PatternRewriter &rewriter) const {
1677 mlir::Block *unwindEntry = &unwindRegion.front();
1678 rewriter.inlineRegionBefore(unwindRegion, insertBefore);
1679 return unwindEntry;
1680 }
1681
1682 mlir::LogicalResult
1683 matchAndRewrite(cir::TryOp tryOp,
1684 mlir::PatternRewriter &rewriter) const override {
1685 // All nested structured CIR ops must be flattened before the try op.
1686 // Cleanup scopes and nested try ops need to be flat so EH cleanup is
1687 // properly handled. Other structured ops (scopes, ifs, loops, switches,
1688 // ternaries) must be flat because replaceCallWithTryCall creates try_call
1689 // ops whose unwind destination is outside the structured op's region,
1690 // which would be an invalid cross-region reference.
1691 for (mlir::Region &region : tryOp->getRegions())
1692 if (hasNestedOpsToFlatten(region))
1693 return mlir::failure();
1694
1695 mlir::OpBuilder::InsertionGuard guard(rewriter);
1696 mlir::Location loc = tryOp.getLoc();
1697
1698 mlir::ArrayAttr handlerTypes = tryOp.getHandlerTypesAttr();
1699 mlir::MutableArrayRef<mlir::Region> handlerRegions =
1700 tryOp.getHandlerRegions();
1701
1702 // Collect throwing calls and throws in the try body.
1703 llvm::SmallVector<cir::CallOp> callsToRewrite;
1704 collectThrowingCalls(tryOp.getTryRegion(), callsToRewrite);
1705 llvm::SmallVector<cir::ThrowOp> throwsToRewrite;
1706 collectThrows(tryOp.getTryRegion(), throwsToRewrite);
1707
1708 // Collect resume ops from already-flattened cleanup scopes in the try body.
1709 llvm::SmallVector<cir::ResumeOp> resumeOpsToChain;
1710 collectResumeOps(tryOp.getTryRegion(), resumeOpsToChain);
1711
1712 // Split the current block and inline the try body.
1713 mlir::Block *currentBlock = rewriter.getInsertionBlock();
1714 mlir::Block *continueBlock =
1715 rewriter.splitBlock(currentBlock, rewriter.getInsertionPoint());
1716
1717 // Get references to try body blocks before inlining.
1718 mlir::Block *bodyEntry = &tryOp.getTryRegion().front();
1719 mlir::Block *bodyExit = &tryOp.getTryRegion().back();
1720
1721 // Inline the try body region before the continue block.
1722 rewriter.inlineRegionBefore(tryOp.getTryRegion(), continueBlock);
1723
1724 // Branch from the current block to the body entry.
1725 rewriter.setInsertionPointToEnd(currentBlock);
1726 cir::BrOp::create(rewriter, loc, bodyEntry);
1727
1728 // Replace the try body's yield terminator with a branch to continue.
1729 if (auto bodyYield = dyn_cast<cir::YieldOp>(bodyExit->getTerminator())) {
1730 rewriter.setInsertionPoint(bodyYield);
1731 rewriter.replaceOpWithNewOp<cir::BrOp>(bodyYield, continueBlock);
1732 }
1733
1734 // If there are no handlers, we're done.
1735 if (!handlerTypes || handlerTypes.empty()) {
1736 rewriter.eraseOp(tryOp);
1737 return mlir::success();
1738 }
1739
1740 // If there are no throwing calls, no throws, and no resume ops from
1741 // inner cleanup scopes, exceptions cannot reach the catch handlers.
1742 // Drop all uses from the (unreachable) handler regions before erasing
1743 // the try op, since handler ops may reference values that were inlined
1744 // from the try body into the parent block.
1745 if (callsToRewrite.empty() && throwsToRewrite.empty() &&
1746 resumeOpsToChain.empty()) {
1747 for (mlir::Region &handlerRegion : handlerRegions)
1748 for (mlir::Block &block : handlerRegion)
1749 block.dropAllDefinedValueUses();
1750 rewriter.eraseOp(tryOp);
1751 return mlir::success();
1752 }
1753
1754 // Build the catch handler blocks.
1755
1756 // First, flatten all handler regions and collect the entry blocks.
1757 llvm::SmallVector<mlir::Block *> catchHandlerBlocks;
1758
1759 for (const auto &[idx, typeAttr] : llvm::enumerate(handlerTypes)) {
1760 mlir::Region &handlerRegion = handlerRegions[idx];
1761
1762 if (mlir::isa<cir::UnwindAttr>(typeAttr)) {
1763 mlir::Block *unwindEntry =
1764 flattenUnwindHandler(handlerRegion, loc, continueBlock, rewriter);
1765 catchHandlerBlocks.push_back(unwindEntry);
1766 } else {
1767 mlir::Block *handlerEntry = flattenCatchHandler(
1768 handlerRegion, continueBlock, loc, continueBlock, rewriter);
1769 catchHandlerBlocks.push_back(handlerEntry);
1770 }
1771 }
1772
1773 // Build the catch dispatch block.
1774 mlir::Block *dispatchBlock =
1775 buildCatchDispatchBlock(tryOp, handlerTypes, catchHandlerBlocks, loc,
1776 catchHandlerBlocks.front(), rewriter);
1777
1778 // Check whether the try has a catch-all handler. When catch-all is
1779 // present, the personality function will always stop unwinding at this
1780 // frame (because catch-all matches every exception type). The LLVM
1781 // landingpad therefore needs "catch ptr null" rather than "cleanup".
1782 // The downstream pipeline (EHABILowering + LowerToLLVM) emits
1783 // "catch ptr null" when the EhInitiateOp has neither cleanup nor typed
1784 // catch types, so we clear the cleanup flag on every EhInitiateOp that
1785 // feeds into a dispatch with a catch-all handler.
1786 bool hasCatchAll =
1787 handlerTypes && llvm::any_of(handlerTypes, [](mlir::Attribute attr) {
1788 return mlir::isa<cir::CatchAllAttr>(attr);
1789 });
1790
1791 // Build a block to be the unwind desination for throwing calls/throws
1792 // and replace the calls/throws with try_call/try_throw ops. Note that
1793 // the unwind block created here is something different than the unwind
1794 // handler that we may have created above. The unwind handler continues
1795 // unwinding after uncaught exceptions. This is the block that will
1796 // eventually become the landing pad for invoke instructions.
1797 bool isCleanupOnly = tryOp.getCleanup() && !hasCatchAll;
1798 if (!callsToRewrite.empty() || !throwsToRewrite.empty()) {
1799 // Create a shared unwind block for all throwing calls/throws.
1800 mlir::Block *unwindBlock = buildUnwindBlock(dispatchBlock, isCleanupOnly,
1801 loc, dispatchBlock, rewriter);
1802
1803 for (cir::CallOp callOp : callsToRewrite)
1804 replaceCallWithTryCall(callOp, unwindBlock, loc, rewriter);
1805 for (cir::ThrowOp throwOp : throwsToRewrite)
1806 replaceThrowWithTryThrow(throwOp, unwindBlock, loc, rewriter);
1807 }
1808
1809 // Chain resume ops from inner cleanup scopes.
1810 // Resume ops from already-flattened cleanup scopes within the try body
1811 // should branch to the catch dispatch block instead of unwinding directly.
1812 for (cir::ResumeOp resumeOp : resumeOpsToChain) {
1813 // When there is a catch-all handler, clear the cleanup flag on the
1814 // cir.eh.initiate that produced this token. With catch-all, the LLVM
1815 // landingpad needs "catch ptr null" instead of "cleanup".
1816 if (hasCatchAll) {
1817 if (auto ehInitiate = traceToEhInitiate(resumeOp.getEhToken())) {
1818 rewriter.modifyOpInPlace(ehInitiate,
1819 [&] { ehInitiate.removeCleanupAttr(); });
1820 }
1821 }
1822
1823 mlir::Value ehToken = resumeOp.getEhToken();
1824 rewriter.setInsertionPoint(resumeOp);
1825 rewriter.replaceOpWithNewOp<cir::BrOp>(
1826 resumeOp, mlir::ValueRange{ehToken}, dispatchBlock);
1827 }
1828
1829 // Finally, erase the original try op ----
1830 rewriter.eraseOp(tryOp);
1831
1832 return mlir::success();
1833 }
1834};
1835
1836void populateFlattenCFGPatterns(RewritePatternSet &patterns) {
1837 patterns
1838 .add<CIRIfFlattening, CIRLoopOpInterfaceFlattening, CIRScopeOpFlattening,
1839 CIRSwitchOpFlattening, CIRTernaryOpFlattening,
1840 CIRCleanupScopeOpFlattening, CIRTryOpFlattening>(
1841 patterns.getContext());
1842}
1843
1844void CIRFlattenCFGPass::runOnOperation() {
1845 RewritePatternSet patterns(&getContext());
1846 populateFlattenCFGPatterns(patterns);
1847
1848 // Collect operations to apply patterns.
1849 llvm::SmallVector<Operation *, 16> ops;
1850 getOperation()->walk<mlir::WalkOrder::PostOrder>([&](Operation *op) {
1851 if (isa<IfOp, ScopeOp, SwitchOp, LoopOpInterface, TernaryOp, CleanupScopeOp,
1852 TryOp>(op))
1853 ops.push_back(op);
1854 });
1855
1856 // Apply patterns.
1857 if (applyOpPatternsGreedily(ops, std::move(patterns)).failed())
1858 signalPassFailure();
1859}
1860
1861} // namespace
1862
1863namespace mlir {
1864
1865std::unique_ptr<Pass> createCIRFlattenCFGPass() {
1866 return std::make_unique<CIRFlattenCFGPass>();
1867}
1868
1869} // namespace mlir
mlir::Block * replaceThrowWithTryThrow(cir::ThrowOp throwOp, mlir::Block *unwindDest, mlir::Location loc, mlir::RewriterBase &rewriter)
Replace a cir::ThrowOp with a cir::TryThrowOp whose unwind destination is unwindDest.
mlir::Block * replaceCallWithTryCall(cir::CallOp callOp, mlir::Block *unwindDest, mlir::Location loc, mlir::RewriterBase &rewriter)
Replace a cir::CallOp with a cir::TryCallOp whose unwind destination is unwindDest.
llvm::APInt APInt
Definition FixedPoint.h:19
ASTEdit insertBefore(RangeSelector S, TextGenerator Replacement)
Inserts Replacement before S, leaving the source selected by \S unchanged.
@ Default
Set to the current date and time.
unsigned long uint64_t
std::unique_ptr< Pass > createCIRFlattenCFGPass()
int const char * function
Definition c++config.h:31
float __ovld __cnfn step(float, float)
Returns 0.0 if x < edge, otherwise it returns 1.0.
static bool stackSaveOp()