Repository navigation
Description
This is a follow-up to #233213. That issue reported four groups of ops that are Pure (and therefore AlwaysSpeculatable) although they can trap, so -loop-invariant-code-motion hoists them out of loops that may run zero times. #235770 fixed arith.floordivsi and llvm.urem/llvm.srem, and the issue was closed. The other two groups are still Pure:
tensor.pad. It isPureno matter what its padding region computes.tensor.generate, which has the same kind of body, is onlyRecursiveMemoryEffects.
llvm-project/mlir/include/mlir/Dialect/Tensor/IR/TensorOps.td
Lines 1251 to 1256 in 018b722
def Tensor_PadOp : Tensor_Op<"pad", [ DeclareOpInterfaceMethods<OpAsmOpInterface, ["getAsmResultNames"]>, DeclareOpInterfaceMethods<ReifyRankedShapedTypeOpInterface, [ "reifyResultShapes"]>, AttrSizedOperandSegments, Pure, affine.delinearize_index. Its doc says "A dynamic basis element being 0 or negative causes undefined behavior". The same holds foraffine.applywith a semi-affine map (floordiv/modby a symbol). Both arePure.
llvm-project/mlir/include/mlir/Dialect/Affine/IR/AffineOps.td
Lines 1109 to 1110 in 018b722
def AffineDelinearizeIndexOp : Affine_Op<"delinearize_index", [Pure, Elementwise,
llvm-project/mlir/include/mlir/Dialect/Affine/IR/AffineOps.td
Lines 1161 to 1163 in 018b722
Due to the constraints of affine maps, all the basis elements must be strictly positive. A dynamic basis element being 0 or negative causes undefined behavior.
In every reproducer below the loop runs zero times exactly when the divisor is 0, so the original program is UB-free. Without LICM, each one prints its untouched initial value. That value is -1; the index versions print it as 18446744073709551615. With --loop-invariant-code-motion, each one dies with SIGFPE.
($LLVM_BUILD is the LLVM/MLIR build directory.)
$ RUN="mlir-runner -e main -entry-point-result=void -shared-libs=$LLVM_BUILD/lib/libmlir_c_runner_utils.so,$LLVM_BUILD/lib/libmlir_runner_utils.so"1. tensor.pad whose padding region divides
pad.mlir:
// tensor.pad is declared Pure (AlwaysSpeculatable + NoMemoryEffect) regardless of
// what its padding region computes (tensor.generate, with the same kind of body,
// is only RecursiveMemoryEffects and not speculatable). Here the padding value
// is %x / %d, which is UB when %d == 0; the loop guarding the pad runs zero
// times in exactly that case, so the original program never divides by zero.
func.func @f(%t: tensor<2xi32>, %x: i32, %d: i32, %n: index, %out: memref<1xi32>) {
%c0 = arith.constant 0 : index
%c1 = arith.constant 1 : index
scf.for %i = %c0 to %n step %c1 {
%p = tensor.pad %t low[1] high[1] {
^bb0(%j: index):
%v = arith.divsi %x, %d : i32
tensor.yield %v : i32
} : tensor<2xi32> to tensor<4xi32>
%e = tensor.extract %p[%c0] : tensor<4xi32>
memref.store %e, %out[%c0] : memref<1xi32>
}
return
}
func.func @main() {
%c0 = arith.constant 0 : index
%t = arith.constant dense<[5, 6]> : tensor<2xi32>
%out = memref.alloca() : memref<1xi32>
%m1 = arith.constant -1 : i32
memref.store %m1, %out[%c0] : memref<1xi32>
%x = arith.constant 7 : i32
%d = arith.constant 0 : i32
%n = arith.constant 0 : index
call @f(%t, %x, %d, %n, %out) : (tensor<2xi32>, i32, i32, index, memref<1xi32>) -> ()
%v = memref.load %out[%c0] : memref<1xi32>
vector.print %v : i32
return
}$ LOWER_T="--one-shot-bufferize=bufferize-function-boundaries --buffer-deallocation-pipeline --convert-linalg-to-loops --expand-strided-metadata --lower-affine --convert-scf-to-cf --convert-to-llvm --reconcile-unrealized-casts"
$ mlir-opt pad.mlir $LOWER_T | $RUN
-1
$ mlir-opt pad.mlir --loop-invariant-code-motion | mlir-opt $LOWER_T | $RUN
Floating point exception (core dumped) # exit code 1362. affine.delinearize_index and semi-affine affine.apply
delinearize.mlir:
// affine.delinearize_index is Pure although its documentation says "A dynamic
// basis element being 0 or negative causes undefined behavior". The loop below
// iterates %j over [0, %c): when %c == 0 it runs zero times, so the original
// program never evaluates the delinearization with a zero basis.
func.func @f(%x: index, %c: index, %out: memref<2xindex>) {
%c0 = arith.constant 0 : index
%c1 = arith.constant 1 : index
scf.for %j = %c0 to %c step %c1 {
%q:2 = affine.delinearize_index %x into (%c) : index, index
memref.store %q#0, %out[%c0] : memref<2xindex>
memref.store %q#1, %out[%c1] : memref<2xindex>
}
return
}
func.func @main() {
%c0 = arith.constant 0 : index
%c1 = arith.constant 1 : index
%m1 = arith.constant -1 : index
%out = memref.alloca() : memref<2xindex>
memref.store %m1, %out[%c0] : memref<2xindex>
memref.store %m1, %out[%c1] : memref<2xindex>
%x = arith.constant 7 : index
%c = arith.constant 0 : index
call @f(%x, %c, %out) : (index, index, memref<2xindex>) -> ()
%a = memref.load %out[%c0] : memref<2xindex>
%b = memref.load %out[%c1] : memref<2xindex>
vector.print %a : index
vector.print %b : index
return
}apply.mlir:
// Variant: affine.apply with a semi-affine map (floordiv/mod by a symbol) is Pure.
// The loop runs zero times exactly when the divisor symbol %c is 0.
func.func @f(%x: index, %c: index, %out: memref<2xindex>) {
%c0 = arith.constant 0 : index
%c1 = arith.constant 1 : index
scf.for %j = %c0 to %c step %c1 {
%q0 = affine.apply affine_map<()[s0, s1] -> (s0 floordiv s1)>()[%x, %c]
%q1 = affine.apply affine_map<()[s0, s1] -> (s0 mod s1)>()[%x, %c]
memref.store %q0, %out[%c0] : memref<2xindex>
memref.store %q1, %out[%c1] : memref<2xindex>
}
return
}
func.func @main() {
%c0 = arith.constant 0 : index
%c1 = arith.constant 1 : index
%m1 = arith.constant -1 : index
%out = memref.alloca() : memref<2xindex>
memref.store %m1, %out[%c0] : memref<2xindex>
memref.store %m1, %out[%c1] : memref<2xindex>
%x = arith.constant 7 : index
%c = arith.constant 0 : index
call @f(%x, %c, %out) : (index, index, memref<2xindex>) -> ()
%a = memref.load %out[%c0] : memref<2xindex>
%b = memref.load %out[%c1] : memref<2xindex>
vector.print %a : index
vector.print %b : index
return
}$ LOWER_A="--affine-expand-index-ops --lower-affine --convert-scf-to-cf --convert-to-llvm --reconcile-unrealized-casts"
$ mlir-opt delinearize.mlir $LOWER_A | $RUN
18446744073709551615
18446744073709551615
$ mlir-opt delinearize.mlir --loop-invariant-code-motion | mlir-opt $LOWER_A | $RUN
Floating point exception (core dumped) # exit code 136
$ mlir-opt apply.mlir $LOWER_A | $RUN
18446744073709551615
18446744073709551615
$ mlir-opt apply.mlir --loop-invariant-code-motion | mlir-opt $LOWER_A | $RUN
Floating point exception (core dumped) # exit code 136Suggested fixes:
- make
tensor.padRecursivelySpeculatable/RecursiveMemoryEffects(liketensor.generate); - make
affine.delinearize_indexand semi-affineaffine.applyspeculatable only when the divisors and basis are known to be positive, following theConditionallySpeculatablepattern used in [mlir][arith][llvm] Mark floordivsi/urem/srem as ConditionallySpeculatable #235770.
Version: llvm-project main @ 4c407e8 (2026-10-20), Release + assertions, x86_64 Linux. Also reproduced on main @ a60677d (2026-11-12).
Disclosure: I wrote this report with the help of Claude Code. I reproduced and reviewed everything in this report myself.
Activity
llvmorg-github-actions commentedon Nov 12, 2026
@llvm/issue-subscribers-mlir
Author: Jinbao Chen (wildoranges)
Details
This is a follow-up to #233213. That issue reported four groups of ops that are `Pure` (and therefore `AlwaysSpeculatable`) although they can trap, so `-loop-invariant-code-motion` hoists them out of loops that may run zero times. #235770 fixed `arith.floordivsi` and `llvm.urem`/`llvm.srem`, and the issue was closed. The other two groups are still `Pure`:tensor.pad. It isPureno matter what its padding region computes.tensor.generate, which has the same kind of body, is onlyRecursiveMemoryEffects.llvm-project/mlir/include/mlir/Dialect/Tensor/IR/TensorOps.td
Lines 1251 to 1256 in 018b722
affine.delinearize_index. Its doc says "A dynamic basis element being 0 or negative causes undefined behavior". The same holds foraffine.applywith a semi-affine map (floordiv/modby a symbol). Both arePure.llvm-project/mlir/include/mlir/Dialect/Affine/IR/AffineOps.td
Lines 1109 to 1110 in 018b722
llvm-project/mlir/include/mlir/Dialect/Affine/IR/AffineOps.td
Lines 1161 to 1163 in 018b722
In every reproducer below the loop runs zero times exactly when the divisor is 0, so the original program is UB-free. Without LICM, each one prints its untouched initial value. That value is -1; the
indexversions print it as 18446744073709551615. With--loop-invariant-code-motion, each one dies with SIGFPE.(
$LLVM_BUILDis the LLVM/MLIR build directory.)$ RUN="mlir-runner -e main -entry-point-result=void -shared-libs=$LLVM_BUILD/lib/libmlir_c_runner_utils.so,$LLVM_BUILD/lib/libmlir_runner_utils.so"1. tensor.pad whose padding region divides
pad.mlir:2. affine.delinearize_index and semi-affine affine.apply
delinearize.mlir:apply.mlir:Suggested fixes:
tensor.padRecursivelySpeculatable/RecursiveMemoryEffects(liketensor.generate);affine.delinearize_indexand semi-affineaffine.applyspeculatable only when the divisors and basis are known to be positive, following theConditionallySpeculatablepattern used in [mlir][arith][llvm] Mark floordivsi/urem/srem as ConditionallySpeculatable #235770.Version: llvm-project main @ 4c407e8 (2026-10-20), Release + assertions, x86_64 Linux. Also reproduced on main @ a60677d (2026-11-12).
Disclosure: I wrote this report with the help of Claude Code. I reproduced and reviewed everything in this report myself.