We really have been trying to keep in mind that LLVM needs to support multiple front ends, which may be implementing different language standards. As much as possible, I’ve been trying to let the IEEE 754 spec drive my thinking about this, though I’ll admit that on a few points I’ve use the C99 spec as a sort of reference interpretation of IEEE 754.
LLVM’s IRBuilder has been recently updated to provide an abstraction layer between front ends and the optimizer. So, if you’re using IRBuilder, you set need to call setIsFPConstrained() then, optionally, IRBuilder::setDefaultConstraiedExcept() and/or setDefaultConstrainedRounding(). After that, calls to something like IRBuilder::CreateFAdd() will automatically create the constrained intrinsic with the appropriate constraints, regardless of how we end up representing them. If your front end isn’t using IRBuilder, I will admit it gets a bit more complicated.
I wouldn’t be opposed to a solution that involved a custom printer for these arguments, but I don’t think it really adds anything that we wouldn’t get from using tokens as I have proposed. Likewise with the named constant idea. On the other hand, if I’m misusing tokens then maybe what constants would add is a way to avoid that misuse.
Regarding the question of what is exposed to users and how, that’s mostly up to the front end. I would like to clarify how we intend for this to work, in general. Simon touched on this briefly, but I’d like to be a bit more verbose to make sure we’re all on the same page.
There are effectively two distinct modes of source code translation to IR with respect to floating point operations – one where the user is allowed to modify the floating point environment and one where they are not. This may not have been clear to everyone, but by default LLVM IR carries with it the assumption that the runtime rounding mode is “to nearest” and that floating point operations do not have side effects. This was only documented recently, but this is the way the optimizer has always behaved. In this default mode, the IR shouldn’t change the floating point environment. I would encourage front ends to document this more specifically saying that the user is not permitted to change the FP environment.
This leads to the necessity of a second state in which the optimizer does not assume the default rounding mode and does not assume that floating point operations have no side effects. Proscribing these assumptions limits optimization, so we want to continue allowing the assumptions by default. The state where the assumptions are not made is accomplished through the use of constrained intrinsics. However, we do not wish to completely eliminate optimizations in all cases, so we want a way to communicate to the optimizer what it can assume. That is the purpose of the fpround and fpexcept arguments. These are not intended to control the rounding mode or exception reporting. They only tell the compiler what it can assume.
Understanding this, front ends can control these in any way they see fit. For instance, the front end might have a global setting the changes the rounding mode to “toward zero.” In that case, it would create constrained intrinsics for all FP operations and set the rounding mode argument (however we end up representing in) to rmTowardZero (a constant currently defined by LLVM corresponding to the “fpround.towardzero” metadata argument). Then the optimizer can use this information to perform optimizations like constant folding.
Runtime changes to the rounding mode are a separate matter. As I said above, I think front ends should define clear circumstances under which such changes are permitted, but the mechanism for making such changes is independent of the constrained FP intrinsics. For example, consider the following C function.
double foo(double A, double B, double C) {
int OrigRM = fegetround();
fesetround(FE_TOWARDZERO);
double tmp = A + B;
fesetround(OrigRM);
return tmp + C;
}
Assuming the compiler was in a state where it knew fenv access was enabled, I would expect that to get translated to something like this (after SROA cleanup):
define double @foo(double %A, double %B, double %C) {
%orig.rm = call i32 @fegetround()
%ignored = call i32 @fesetround(i32 3072)
%tmp = call double @llvm.experimental.constrained.fadd(double %A, double %B) [ “fpround”(token rmDynamic), “fpexcept”(token rmStrict) ]
%ignored = call i32 @fesetround(i32 %orig.rm)
%result = call double @llvm.experimental.constrained.fadd(double %tmp, double %C) [ “fpround”(token rmDynamic), “fpexcept”(token rmStrict) ]
}
Notice here the literal constant that C defines is still used for the call to fesetround(FE_TOWARDZERO), and the variable is used for the call that restores the rounding mode. Also notice that in both fadd operations, the rounding mode is declared as rmDynamic. I have an idea that we ought to have a pass that recognizes the fesetround library call an uses the information it finds there to change the rounding mode operand in the first fadd to rmTowardZero, but the front end won’t be expected to do that. We’ll probably want an intrinsic to change the rounding mode so that we don’t need to recognize all manner of language-specific libcalls, but that’s a problem for later.
I hope this has been more helpful than tedious. Also, I feel like I should reiterate that I am still seeking all opinions about the use of tokens and operand bundles or any other means of representing the fp constraints. I just want to make sure that we all have the same understanding of what the information I’m trying to represent in IR means.
-Andy