Skip to content

Commit bfa1ed9

Browse files
committed
[CIR] Properly ensure terminating IfOp and ScopeOp regions
The code changes modify the `cir.if` and `cir.scope` operations to ensure that their code regions are properly terminated. Previously, the if/else and scope regions could be left completely empty which is non-trivially expected in most code inspecting these ops. This led, for example, to a crash when and if clause was left empty in the source code. Now, the child regions must be terminated, either explicitly or implicitly by the default builder and assembly parser. This change improves the clarity and correctness of the code.
1 parent 94289aa commit bfa1ed9

File tree

9 files changed

+101
-40
lines changed

9 files changed

+101
-40
lines changed

clang/include/clang/CIR/Dialect/IR/CIROps.td

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -750,8 +750,10 @@ def IfOp : CIR_Op<"if",
750750
}
751751
```
752752

753-
`cir.if` defines no values and the 'else' can be omitted. `cir.yield` must
754-
explicitly terminate the region if it has more than one block.
753+
`cir.if` defines no values and the 'else' can be omitted. The if/else
754+
regions must be terminated. If the region has only one block, the terminator
755+
can be left out, and `cir.yield` terminator will be inserted implictly.
756+
Otherwise, the region must be explicitly terminated.
755757
}];
756758
let arguments = (ins CIR_BoolType:$condition);
757759
let regions = (region AnyRegion:$thenRegion, AnyRegion:$elseRegion);
@@ -1070,6 +1072,16 @@ def ScopeOp : CIR_Op<"scope", [
10701072
custom<OmittedTerminatorRegion>($scopeRegion) (`:` type($results)^)? attr-dict
10711073
}];
10721074

1075+
let extraClassDeclaration = [{
1076+
/// Determine whether the scope is empty, meaning it contains a single block
1077+
/// terminated by a cir.yield.
1078+
bool isEmpty() {
1079+
auto &entry = getRegion().front();
1080+
return getRegion().hasOneBlock() &&
1081+
llvm::isa<cir::YieldOp>(entry.front());
1082+
}
1083+
}];
1084+
10731085
let builders = [
10741086
// Scopes for yielding values.
10751087
OpBuilder<(ins
@@ -1200,7 +1212,7 @@ def ShiftOp : CIR_Op<"shift", [Pure]> {
12001212
be either integer type or vector of integer type. However, they must be
12011213
either all vector of integer type, or all integer type. If they are vectors,
12021214
each vector element of the shift target is shifted by the corresponding
1203-
shift amount in the shift amount vector.
1215+
shift amount in the shift amount vector.
12041216

12051217
```mlir
12061218
%7 = cir.shift(left, %1 : !u64i, %4 : !s32i) -> !u64i
@@ -1879,17 +1891,17 @@ def SwitchOp : CIR_Op<"switch",
18791891
is an integral condition value.
18801892

18811893
The set of `cir.case` operations and their enclosing `cir.switch`
1882-
represents the semantics of a C/C++ switch statement. Users can use
1894+
represents the semantics of a C/C++ switch statement. Users can use
18831895
`collectCases(llvm::SmallVector<CaseOp> &cases)` to collect the `cir.case`
18841896
operation in the `cir.switch` operation easily.
18851897

18861898
The `cir.case` operations doesn't have to be in the region of `cir.switch`
18871899
directly. However, when all the `cir.case` operations lives in the region
18881900
of `cir.switch` directly and there is no other operations except the ending
1889-
`cir.yield` operation in the region of `cir.switch` directly, we call the
1890-
`cir.switch` operation is in a simple form. Users can use
1901+
`cir.yield` operation in the region of `cir.switch` directly, we call the
1902+
`cir.switch` operation is in a simple form. Users can use
18911903
`bool isSimpleForm(llvm::SmallVector<CaseOp> &cases)` member function to
1892-
detect if the `cir.switch` operation is in a simple form. The simple form
1904+
detect if the `cir.switch` operation is in a simple form. The simple form
18931905
makes analysis easier to handle the `cir.switch` operation
18941906
and makes the boundary to give up pretty clear.
18951907

@@ -1976,7 +1988,7 @@ def SwitchOp : CIR_Op<"switch",
19761988
switch(int cond) {
19771989
l:
19781990
b++;
1979-
1991+
19801992
case 4:
19811993
a++;
19821994
break;
@@ -4136,13 +4148,13 @@ def ReturnAddrOp : CIR_Op<"return_address"> {
41364148
let results = (outs Res<VoidPtr, "">:$result);
41374149

41384150
let description = [{
4139-
Represents call to builtin function ` __builtin_return_address` in CIR.
4140-
This builtin function returns the return address of the current function,
4141-
or of one of its callers.
4151+
Represents call to builtin function ` __builtin_return_address` in CIR.
4152+
This builtin function returns the return address of the current function,
4153+
or of one of its callers.
41424154
The `level` argument is number of frames to scan up the call stack.
4143-
For instance, value of 0 yields the return address of the current function,
4144-
value of 1 yields the return address of the caller of the current function,
4145-
and so forth.
4155+
For instance, value of 0 yields the return address of the current function,
4156+
value of 1 yields the return address of the caller of the current function,
4157+
and so forth.
41464158

41474159
Examples:
41484160

@@ -4282,8 +4294,8 @@ def AbsOp : CIR_Op<"abs", [Pure, SameOperandsAndResultType]> {
42824294
let summary = [{
42834295
libc builtin equivalent abs, labs, llabs
42844296

4285-
The `poison` argument indicate whether the result value
4286-
is a poison value if the first argument is statically or
4297+
The `poison` argument indicate whether the result value
4298+
is a poison value if the first argument is statically or
42874299
dynamically an INT_MIN value.
42884300

42894301
Example:

clang/lib/CIR/Dialect/IR/CIRDialect.cpp

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -208,20 +208,24 @@ LogicalResult ensureRegionTerm(OpAsmParser &parser, Region &region,
208208
Location eLoc = parser.getEncodedSourceLoc(parser.getCurrentLocation());
209209
OpBuilder builder(parser.getBuilder().getContext());
210210

211-
// Region is empty or properly terminated: nothing to do.
212-
if (region.empty() ||
213-
(region.back().mightHaveTerminator() && region.back().getTerminator()))
211+
// Insert empty block in case the region is empty to ensure the terminator
212+
// will be inserted
213+
if (region.empty())
214+
builder.createBlock(&region);
215+
216+
Block &block = region.back();
217+
// Region is properly terminated: nothing to do.
218+
if (!block.empty() && block.back().hasTrait<OpTrait::IsTerminator>())
214219
return success();
215220

216221
// Check for invalid terminator omissions.
217222
if (!region.hasOneBlock())
218223
return parser.emitError(errLoc,
219224
"multi-block region must not omit terminator");
220-
if (region.back().empty())
221-
return parser.emitError(errLoc, "empty region must not omit terminator");
222225

223-
// Terminator was omited correctly: recreate it.
224-
region.back().push_back(builder.create<cir::YieldOp>(eLoc));
226+
// Terminator was omitted correctly: recreate it.
227+
builder.setInsertionPointToEnd(&block);
228+
builder.create<cir::YieldOp>(eLoc);
225229
return success();
226230
}
227231

@@ -1126,8 +1130,11 @@ void cir::IfOp::print(OpAsmPrinter &p) {
11261130
p.printOptionalAttrDict(getOperation()->getAttrs());
11271131
}
11281132

1129-
/// Default callback for IfOp builders. Inserts nothing for now.
1130-
void cir::buildTerminatedBody(OpBuilder &builder, Location loc) {}
1133+
/// Default callback for IfOp builders.
1134+
void cir::buildTerminatedBody(OpBuilder &builder, Location loc) {
1135+
// add cir.yield to the end of the block
1136+
builder.create<cir::YieldOp>(loc);
1137+
}
11311138

11321139
/// Given the region at `index`, or the parent operation if `index` is None,
11331140
/// return the successor regions. These are the regions that may be selected
@@ -1236,7 +1243,17 @@ void cir::ScopeOp::build(
12361243
scopeBuilder(builder, result.location);
12371244
}
12381245

1239-
LogicalResult cir::ScopeOp::verify() { return success(); }
1246+
LogicalResult cir::ScopeOp::verify() {
1247+
if (getRegion().empty()) {
1248+
return emitOpError() << "cir.scope must not be empty since it should "
1249+
"include at least an implicit cir.yield ";
1250+
}
1251+
1252+
if (getRegion().back().empty() ||
1253+
!getRegion().back().getTerminator()->hasTrait<OpTrait::IsTerminator>())
1254+
return emitOpError() << "last block of cir.scope must be terminated";
1255+
return success();
1256+
}
12401257

12411258
//===----------------------------------------------------------------------===//
12421259
// TryOp

clang/lib/CIR/Dialect/Transforms/CIRCanonicalize.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ struct RemoveEmptyScope : public OpRewritePattern<ScopeOp> {
6060
using OpRewritePattern<ScopeOp>::OpRewritePattern;
6161

6262
LogicalResult match(ScopeOp op) const final {
63-
return success(op.getRegion().empty() ||
64-
(op.getRegion().getBlocks().size() == 1 &&
65-
op.getRegion().front().empty()));
63+
// TODO: Remove this logic once CIR uses MLIR infrastructure to remove
64+
// trivially dead operations
65+
return success(op.isEmpty());
6666
}
6767

6868
void rewrite(ScopeOp op, PatternRewriter &rewriter) const final {

clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ struct CIRIfFlattening : public OpRewritePattern<IfOp> {
6666
auto *remainingOpsBlock =
6767
rewriter.splitBlock(currentBlock, rewriter.getInsertionPoint());
6868
mlir::Block *continueBlock;
69-
if (ifOp->getResults().size() == 0)
69+
if (ifOp->getResults().empty())
7070
continueBlock = remainingOpsBlock;
7171
else
7272
llvm_unreachable("NYI");
@@ -125,7 +125,9 @@ class CIRScopeOpFlattening : public mlir::OpRewritePattern<cir::ScopeOp> {
125125
auto loc = scopeOp.getLoc();
126126

127127
// Empty scope: just remove it.
128-
if (scopeOp.getRegion().empty()) {
128+
// TODO: Remove this logic once CIR uses MLIR infrastructure to remove
129+
// trivially dead operations
130+
if (scopeOp.isEmpty()) {
129131
rewriter.eraseOp(scopeOp);
130132
return mlir::success();
131133
}

clang/lib/CIR/Lowering/ThroughMLIR/LowerCIRToMLIR.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -787,7 +787,9 @@ class CIRScopeOpLowering : public mlir::OpConversionPattern<cir::ScopeOp> {
787787
matchAndRewrite(cir::ScopeOp scopeOp, OpAdaptor adaptor,
788788
mlir::ConversionPatternRewriter &rewriter) const override {
789789
// Empty scope: just remove it.
790-
if (scopeOp.getRegion().empty()) {
790+
// TODO: Remove this logic once CIR uses MLIR infrastructure to remove
791+
// trivially dead operations
792+
if (scopeOp.isEmpty()) {
791793
rewriter.eraseOp(scopeOp);
792794
return mlir::success();
793795
}

clang/test/CIR/CodeGen/OpenMP/parallel.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
// CHECK: cir.func
55
void omp_parallel_1() {
66
// CHECK: omp.parallel {
7-
// CHECK-NEXT: cir.scope {
8-
// CHECK-NEXT: }
97
// CHECK-NEXT: omp.terminator
108
// CHECK-NEXT: }
119
#pragma omp parallel

clang/test/CIR/CodeGen/if-constexpr.cpp

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,6 @@ void if0() {
7575
// CHECK-NEXT: cir.store %3, %2 : !s32i, !cir.ptr<!s32i> loc({{.*}})
7676
// CHECK-NEXT: } loc({{.*}})
7777
// CHECK-NEXT: cir.scope {
78-
// Note that Clang does not even emit a block in this case
79-
// CHECK-NEXT: } loc({{.*}})
80-
// CHECK-NEXT: cir.scope {
8178
// CHECK-NEXT: %2 = cir.alloca !s32i, !cir.ptr<!s32i>, ["x", init] {{.*}}
8279
// CHECK-NEXT: %3 = cir.alloca !s32i, !cir.ptr<!s32i>, ["y", init] {{.*}}
8380
// CHECK-NEXT: %4 = cir.const #cir.int<70> : !s32i loc({{.*}})

clang/test/CIR/CodeGen/stmt-expr.c

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44
// Yields void.
55
void test1() { ({ }); }
66
// CHECK: @test1
7-
// CHECK: cir.scope {
8-
// CHECK-NOT: cir.yield
9-
// CHECK: }
7+
// CHECK-NEXT: cir.return
8+
109

1110
// Yields an out-of-scope scalar.
1211
void test2() { ({int x = 3; x; }); }

clang/test/CIR/Lowering/if.cir

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,38 @@ module {
6262
// MLIR-NEXT: ^bb2: // pred: ^bb0
6363
// MLIR-NEXT: llvm.return %arg0 : i32
6464
// MLIR-NEXT: }
65+
66+
// Verify empty if clause is properly lowered to empty block
67+
cir.func @emptyIfClause(%arg0: !s32i) -> !s32i {
68+
// MLIR-LABEL: llvm.func @emptyIfClause
69+
%4 = cir.cast(int_to_bool, %arg0 : !s32i), !cir.bool
70+
// MLIR: llvm.cond_br {{%.*}}, ^[[T:.*]], ^[[PHI:.*]]
71+
cir.if %4 {
72+
// MLIR-NEXT: ^[[T]]:
73+
// MLIR-NEXT: llvm.br ^[[PHI]]
74+
}
75+
// MLIR-NEXT: ^[[PHI]]:
76+
// MLIR-NEXT: llvm.return
77+
cir.return %arg0 : !s32i
78+
}
79+
80+
// Verify empty if-else clauses are properly lowered to empty blocks
81+
// TODO: Fix reversed order of blocks in the test once Issue clangir/#1094 is
82+
// addressed
83+
cir.func @emptyIfElseClause(%arg0: !s32i) -> !s32i {
84+
// MLIR-LABEL: llvm.func @emptyIfElseClause
85+
%4 = cir.cast(int_to_bool, %arg0 : !s32i), !cir.bool
86+
// MLIR: llvm.cond_br {{%.*}}, ^[[T:.*]], ^[[F:.*]]
87+
cir.if %4 {
88+
} else {
89+
}
90+
// MLIR-NEXT: ^[[F]]:
91+
// MLIR-NEXT: llvm.br ^[[PHI:.*]]
92+
// MLIR-NEXT: ^[[T]]:
93+
// MLIR-NEXT: llvm.br ^[[PHI]]
94+
// MLIR-NEXT: ^[[PHI]]:
95+
// MLIR-NEXT: llvm.return
96+
cir.return %arg0 : !s32i
97+
}
98+
6599
}

0 commit comments

Comments
 (0)