47 ub Dialect
47.1 Beginner Summary
The ub dialect represents undefined behavior in MLIR. It is a small dialect, but it teaches an important compiler idea: sometimes the compiler needs an IR value that means “using this value can trigger undefined behavior” or a terminator that means “execution cannot legally continue here”.
The dialect has two operations:
| Operation | Meaning |
|---|---|
ub.poison |
Produces a poisoned value. This is deferred undefined behavior. |
ub.unreachable |
Terminates control flow with immediate undefined behavior if reached. |
Most programmers do not write this dialect by hand. It appears in compiler pipelines when an operation needs a placeholder value with undefined semantics, or when lowering must preserve the fact that a control-flow path is impossible.
47.2 Why This Dialect Exists
MLIR needs a target-independent way to represent undefined behavior before IR is lowered to a concrete target such as LLVM IR or SPIR-V.
Without ub, passes would have to choose target-specific operations too early, such as llvm.mlir.poison, llvm.unreachable, spirv.Undef, or spirv.Unreachable. The ub dialect gives MLIR a small shared vocabulary for these concepts:
ub.poisonsays “there is a value here, but the value is poison”.ub.unreachablesays “if execution reaches this point, the program has immediate undefined behavior”.
This keeps undefined-behavior modeling separate from a particular backend.
47.3 When It Matters
ub matters when a pass needs to represent undefined semantics explicitly.
Common situations:
- A rewrite needs to create a value for an impossible or don’t-care case.
- A conversion needs a placeholder value that will become a backend poison or undef value later.
- A pass proves a control-flow path cannot be reached.
- A frontend or optimizer wants to preserve language undefined behavior without lowering directly to LLVM or SPIR-V.
- A conversion target requires every branch to end in a terminator, and the only meaningful terminator is “this path is impossible”.
It also matters when reading lowered IR. Seeing ub.poison tells you that the IR has intentionally stopped promising a concrete value at that point.
47.4 When To Use It
Use ub when you need a portable undefined-behavior marker.
Good uses:
- Materializing a poison value in target-independent MLIR.
- Representing a proven-impossible path before converting to a backend.
- Writing tests for conversion to LLVM or SPIR-V.
- Implementing a pass that needs to match poison constants with
ub::m_Poison().
Avoid direct use when:
- You mean “some arbitrary valid value”. Poison is stronger than “unknown”.
- You need a normal default value. Use an explicit constant instead.
- You are already writing final LLVM IR and specifically need
llvm.mlir.poisonorllvm.unreachable. - You are already writing final SPIR-V and specifically need
spirv.Undeforspirv.Unreachable.
47.5 Core Concepts
47.5.1 Deferred Undefined Behavior
ub.poison creates a poison value. The operation itself does not immediately stop execution. The undefined behavior is deferred until the poison value is used in a way that makes the behavior observable according to the target’s semantics.
This is why ub.poison is a value-producing operation:
%x = ub.poison : i32
return %x : i32
The IR still has a value %x, but the value carries poison semantics.
47.5.2 Immediate Undefined Behavior
ub.unreachable is a terminator. If control flow reaches it, the program has immediate undefined behavior:
func.func @bad_path() {
ub.unreachable
}
This is useful for impossible paths, failed assumptions, or regions where the compiler has proven there is no legal continuation.
47.5.3 Poison Attributes
ub.poison carries a poison attribute. The default is #ub.poison, printed implicitly:
%a = ub.poison : i32
%b = ub.poison <#ub.poison> : i32
Both forms mean the same thing for the built-in poison attribute. The dialect also defines PoisonAttrInterface, which allows attributes to model more specific poison semantics, such as a future partially poisoned vector attribute.
47.5.4 Constant-Like Folding
ub.poison is ConstantLike and Pure. Its folder returns its poison attribute, so canonicalization can merge identical poison constants:
%a = ub.poison : i32
%b = ub.poison : i32
After canonicalization, both uses can share one poison operation.
47.5.5 Inlining
The dialect defines an inliner interface, and UB operations can be inlined. A function returning ub.poison can be inlined into its caller without changing the meaning of the poison value.
47.6 Operations
47.6.1 ub.poison
ub.poison materializes a compile-time poisoned constant value.
Syntax:
%0 = ub.poison : i32
%1 = ub.poison <#ub.poison> : vector<4xi64>
Properties:
| Property | Meaning |
|---|---|
| Result | Any MLIR type. Tests cover integers, complex values, vectors, and tensors. |
| Attribute | A PoisonAttrInterface value, defaulting to #ub.poison. |
| Traits | ConstantLike, Pure. |
| Folding | Folds to its poison attribute. |
Use it when the IR needs a value but the value is intentionally undefined if observed.
47.6.2 ub.unreachable
ub.unreachable is a terminator that represents immediate undefined behavior.
Syntax:
ub.unreachable
Properties:
| Property | Meaning |
|---|---|
| Result | No results. |
| Trait | Terminator. |
| Control flow | Ends the current block. |
Use it for paths that cannot be legally executed.
47.7 Attributes And Interfaces
The dialect defines one built-in attribute and one attribute interface:
| Name | Meaning |
|---|---|
PoisonAttr / #ub.poison |
The default poison value attribute. |
PoisonAttrInterface |
Interface implemented by poison attributes. |
The helper matcher ub::m_Poison() can match poison attributes, poison constants, and operations that fold to poison attributes. This is useful in C++ rewrite patterns:
matchPattern(value, ub::m_Poison());47.8 Transformations
The dialect has a small amount of local behavior:
| Transformation | Effect |
|---|---|
| Canonicalization / folding | Identical ub.poison constants can be merged because the op folds to its poison attribute. |
| Inlining | UB operations are legal to inline. |
Generic convert-to-llvm interface |
The dialect registers conversion patterns for the generic LLVM conversion flow. |
There are no large optimization pipelines specific to ub. Its main role is to be consumed by backend conversions.
47.9 Conversions And Lowering Paths
The important lowering paths are:
| Pass | Input | Output |
|---|---|---|
convert-ub-to-llvm |
ub.poison, ub.unreachable |
llvm.mlir.poison, llvm.unreachable |
convert-ub-to-spirv |
ub.poison, ub.unreachable |
spirv.Undef, spirv.Unreachable |
convert-to-llvm with UB enabled |
ub ops |
Uses the UB dialect’s LLVM conversion interface. |
convert-ub-to-llvm has an index-bitwidth option. If it is 0, the pass derives the index bitwidth from the data layout. Otherwise, it uses the specified bitwidth for index type conversion.
LLVM example:
%0 = ub.poison : index
ub.unreachable
can lower to:
%0 = llvm.mlir.poison : i64
llvm.unreachable
SPIR-V example:
%0 = ub.poison : vector<4xf32>
can lower to:
%0 = spirv.Undef : vector<4xf32>
47.10 Example IR
This example uses ub.poison to return a value whose behavior is intentionally undefined if observed:
func.func @poison_value(%cond: i1) -> i32 {
%bad = ub.poison : i32
return %bad : i32
}
This example uses ub.unreachable for an impossible branch:
func.func @only_true(%cond: i1) {
cf.cond_br %cond, ^ok, ^bad
^ok:
return
^bad:
ub.unreachable
}
After LLVM lowering, the second example’s bad block ends in llvm.unreachable.
47.11 Mental Model
Think of ub as the dialect for “the compiler is no longer promising a normal program value or a normal continuation.”
ub.poison is still a value. It can flow through SSA and be converted later. ub.unreachable is not a value; it is a hard end to a control-flow path.
The distinction matters. Poison says “this value is invalid if used in the wrong way.” Unreachable says “execution cannot legally get past this point.”
47.12 Gotchas
- Poison is not the same as zero, null, or a harmless placeholder.
- Poison is not the same as a runtime error. It gives the optimizer freedom because the program has undefined behavior if the poison becomes relevant.
ub.unreachableis a terminator. You cannot put ordinary operations after it in the same block.ub.poisoncan have any result type, but backend conversion still has to be able to convert that type.convert-ub-to-llvmonly converts the built-in#ub.poisonattribute. A customPoisonAttrInterfaceattribute may need custom lowering.spirv.Undefis the SPIR-V lowering forub.poison; do not read that as a normal deterministic value.
47.13 Source Map
Use these files in the LLVM repo when you need exact behavior:
| Topic | Files |
|---|---|
| Dialect, ops, and attribute definitions | mlir/include/mlir/Dialect/UB/IR/UBOps.td |
| Poison attribute interface | mlir/include/mlir/Dialect/UB/IR/UBOpsInterfaces.td |
| Matchers | mlir/include/mlir/Dialect/UB/IR/UBMatchers.h |
| Dialect implementation | mlir/lib/Dialect/UB/IR/UBOps.cpp |
| LLVM conversion | mlir/include/mlir/Conversion/UBToLLVM/UBToLLVM.h, mlir/lib/Conversion/UBToLLVM/UBToLLVM.cpp |
| SPIR-V conversion | mlir/include/mlir/Conversion/UBToSPIRV/UBToSPIRV.h, mlir/lib/Conversion/UBToSPIRV/UBToSPIRV.cpp |
| Pass declarations | mlir/include/mlir/Conversion/Passes.td |
| Parser/printer tests | mlir/test/Dialect/UB/ops.mlir |
| Canonicalization and inlining tests | mlir/test/Dialect/UB/canonicalize.mlir, mlir/test/Dialect/UB/inlining.mlir |
| Conversion tests | mlir/test/Conversion/UBToLLVM/ub-to-llvm.mlir, mlir/test/Conversion/UBToSPIRV/ub-to-spirv.mlir, mlir/test/Conversion/ConvertToSPIRV/ub.mlir |