I’m currently working on converting CIR to LLVM dialect, however, there are a few unrealized_conversion_cast
s being emitted and I’m not entirely understanding why they are not removed after the conversion is over.
I’m aware that the -reconcile-unrealized-casts
pass can remove these, but I wanted to confirm if there is any method to prevent them from appearing in the first place.
Issue
Take the CIR code below:
#true = #cir.bool<true> : !cir.bool
module {
cir.func @l0() {
cir.loop for(cond : {
%0 = cir.const(#true) : !cir.bool
cir.yield %0 : !cir.bool
}, step : {
cir.yield
}) {
cir.yield
}
cir.return
}
}
The CIR and Builting dialects are marked as illegal, while our target dialect, LLVM, is marked as legal. After converted to LLVM, the code above looks like this:
^bb1: // 3 preds: ^bb0, ^bb2, ^bb3
%0 = llvm.mlir.constant(true) : i8
%1 = builtin.unrealized_conversion_cast %0 : i8 to !cir.bool
%2 = llvm.trunc %0 : i8 to i1
llvm.cond_br %2, ^bb3, ^bb4
^bb2: // no predecessors
llvm.br ^bb1
^bb3: // pred: ^bb1
llvm.br ^bb1
There is an unrealized cast that is seemingly wrong and unnecessary: %1
has no uses and converts a legal type i8
to an illegal one !cir.bool
.
Debugging the conversion, I’ve noticed that, after the operations are converted, we are left with the following code:
^bb1: // 4 preds: ^bb1, ^bb1, ^bb4, ^bb4
%0 = "llvm.mlir.constant"() <{value = true}> : () -> i8
%1 = "cir.const"() {value = #cir.bool<true> : !cir.bool} : () -> !cir.bool
%2 = "builtin.unrealized_conversion_cast"(%1) : (!cir.bool) -> i8
"cir.yield"(%1) : (!cir.bool) -> ()
%3 = "llvm.trunc"(%2) : (i8) -> i1
"llvm.cond_br"(%3)[^bb3, ^bb5] <{operand_segment_sizes = array<i32: 1, 0, 0>}> : (i1) -> ()
"cir.brcond"(%1)[^bb3, ^bb5] : (!cir.bool) -> ()
The unrealized cast above is not the one that persists after the conversion, but a second unrealized cast is added when finalizing the rewrite:
^bb2: // 4 preds: ^bb1, ^bb1, ^bb4, ^bb4
%0 = "llvm.mlir.constant"() <{value = true}> : () -> i8
%1 = "builtin.unrealized_conversion_cast"(%0) : (i8) -> !cir.bool
%2 = "cir.const"() {value = #cir.bool<true> : !cir.bool} : () -> !cir.bool
%3 = "builtin.unrealized_conversion_cast"(%2) : (!cir.bool) -> i8
"cir.yield"(%2) : (!cir.bool) -> ()
%4 = "llvm.trunc"(%0) : (i8) -> i1
"llvm.cond_br"(%4)[^bb3, ^bb5] <{operand_segment_sizes = array<i32: 1, 0, 0>}> : (i1) -> ()
"cir.brcond"(%2)[^bb3, ^bb5] : (!cir.bool) -> ()
Finally, the conversion is considered valid and MLIR proceeds to apply the rewrites. When doing so, it tries to remove unresolved materialization ops, but in the example above It removes only the first unrealized cast, not the second. Resulting in the LLVM Dialect code shown in the second snippet.
Questions
Q1: If use applyFullConversion
, but the final code has a value with an illegal type, shouldn’t the conversion fail?
Q2: Why is the second unrealized cast required at all if its resulting value has no users?
Q3: Why is the second cast, which is created when finalizing the conversion, not automatically removed by MLIR?
Q4: How can I avoid these types of “leftover” unrealized casts when converting between dialects?