We would like to start a discussion on the semantics of the Arithmetic dialect. Currently, there are many ops that we expect to be undefined for some inputs, yet this is not covered by the dialect’s documentation. This makes it difficult to lower to
arith without having to rely on the subsequent lowering to the next dialect (e.g., LLVM, SPIR-V), or reason about the correctness of transforms within the
arith dialect. We propose to gradually start filling in the gaps in the semantics.
Let’s consider two examples that illustrate the difficulties we run into:
arith.muliop does not specify the overflow semantics. It is unclear if one should expect wrapping semantics (result modulo 2^N), or an undefined result. When emitting
arith.muli, one has to either insert some runtime checks to avoid triggering a potential UB, or check the lowering paths to LLVM/SPIR-V/etc. and confirm that
arith.muliwill be converted to an op with defined overflow behavior.
arith.shruiop does not specify what happens with shift amount >= the type bitwidth. LLVM defines the result of such shifts as
poison. When handling shifts in the wide integer emulation pass, the LLVM spec allows us to emit selects to stop
poisonpropagation. If we had a (hypothetical) lowering from
arith.shruito C’s shift operator
>>, these selects wouldn’t have been sufficient as this might trigger an immediate UB.
In the current state of things, all of the following: lowering to
arith, lowering from
arith, transforming within
arith, require confirming the exact semantics with *all* of the lowering targets: LLVM, SPIR-V, and potentially more. We argue it would be better for
arith to define the exact semantics, so that there’s only a single place to check in these scenarios.
We propose to make
arith more specific wrt to potential undefined behavior of each op. We could execute that in a number of ways.
Source IR → arith → Destination IR
One high level consideration is how much ‘defined’ we want the ops to be:
- If we make an op undefined, it becomes difficult to emit it. If the source IR is more defined, it may require runtime checks at the source.
- If we make an op overdefined, it becomes difficult to lower it. If the target IR is less defined, it may require runtime checks at the destination.
Because the two main existing destination IRs, LLVM and SPIR-V, have similar semantics, it seems reasonable to use the LLVM semantics as the starting point.
Another thing to consider is how to implement the changes to the semantics:
arithsemantics to the LLVM Language Reference Manual. This probably requires the least amount of changes, but does not allow for independent evolution of both IRs. For example, it seems unreasonable to require LLVM contributors take the impact on
arithand the rest of MLIR into consideration every LLVM IR change they propose. Also,
arithis not simply a subset of LLVM: it supports more types, e.g., n-D vectors, tensors, which may have more semantic options.
‘Copy’ the current LLVM semantics in one sweep, but allow for future divergence if it makes sense for particular ops.
Consider the semantics on the op-by-op basis, default to the LLVM semantics unless there is a reason not to. This follows the spirit of incremental development and is expected to make code reviews easier, and we can also audit existing lowerings to make sure they are proper wrt the defined semantics step by step.
We propose to move forward with the implementation strategy 3. This allows us to not disrupt the status quo by following what seems to be the existing undocumented expectation, and fill in the gaps through a series of incremental changes developed and reviewed independently.