At the moment, the “math” dialect does not say anything about undefined behavior. And the operations are all defined in ODS as Pure which implies that it does not exist.
This means that a lowering to arith/llvm must be conservative and guard against some inputs. But even then we’re underspecifying what should happen. For example what is the expected result for math.ipowi(0, -2)? The expansion pow(x, -2) is 1/x * 1/x but that’s UB when x is zero…
So we should decide:
should we introduce UB in the math dialect for this kind of invalid operation?
if not we need to define the behavior carefully and make sure our lowering are defensive enough here.
Would it make sense to have some equivalent of fastmath flags that results in UB when set, but guarantees a specific value when unset? We can then, e.g., refuse to lower to arith/llvm without the flag but lower to library calls with matherror handling MATH_ERRNO, MATH_ERREXCEPT, math_errhandling - cppreference.com.
There is somewhat related topic, but please note that MATH_ERRNO, etc. are all related to floating-point operators/functions. In this particular case, we are talking about integer division by zero, which I cannot define as anything but UB.
A flag would make sense, but I would not treat is as mandatory.
Not all languages/targets have UB for the same operations. For example, in some embedded targets, division by zero is just zero because they can’t trap, and in some languages, wrap-around is part of the semantics (not UB), and we need to be able to represent those things in generic IR.
If your language requires some semantics to be UB, it can add to the IR as a flag. If some target requires an operation to not be UB, it can remove it. So I’d leave for the lowering state to handle the remaining cases if any are left over, and passes to treat flags in the appropriate way but not require them to exist.
x / 0 and such arith ops are producing poison on invalid inputs and pure ops are allowed to produce poison (and ub.poison itself is pure), why math should be any different?
By chance i have stumbled upon a case that seems like a good illustration for this discussion.
GH #132202 shows that lowering tosa.pow through -math-expand-ops produces -nan for pow(-94, -47), where the LLVM intrinsic and libm both give -0. The root cause is that the default math.powf expansion is exp(b * log(a)), and log(a) is NaN for a < 0. Two of the involved ops (math.exp, math.log) are individually well-defined for the sub-expressions they see; but the composition of them silently produces NaN where libm doesn’t.
Being unaware of this thread, I tried to approach it from a “let’s produce mathematically correct expansion” standpoint and opened a PR #193231 that fixes that specific issue by making the expansion of powf branch on the sign of a: non-negative bases use the cheap exp*log form and negative bases get a technically-correct sign-fixing form that handles odd integer / even integer / non-integer exponents distinctly.
@mehdi_amini brought this thread up in review (thanks a lot, by the way, I’m not sure how i missed it).
I don’t have a strong opinion about which of the strategies here wins in the long run. Personally, i like ftynse’s take about fastmath-equivalent flags, but that’s just my opinion. The original ticket i was trying to solve (#132202) is not a massive pain anyway, but it can be a source of some surprises in math results.