| Floating-point operations are stateful in any CPU made during the last 40 years. They depend on a set of configuration flags, which are stored in some internal register, e.g. the MXCSR register of Intel/AMD CPUs. The same program can provide wildly different results if you run it twice, with different settings in the MXCSR register. Any programmer who writes floating-point computations should be very aware that any FP operation depends on the state of a global register. Hopefully, all functions that are invoked are well behaved and if they ever change the global settings they restore them to the value that the register had upon entry, otherwise the effects would be unpredictable. What you say about the floating-point operations being "impure" is incorrect. A pure function may have not only explicit input parameters, but also implicit input parameters, a.k.a. global variables that are read-only. For example any such function can be marked with __attribute__((pure)) in compilers like gcc and clang. The functions that have only explicit input parameters are a subset of the pure functions. For example, such functions are marked with __attribute__((const)) in the gcc and clang compilers. In a rigorous manner, FP operations would not belong in an __attribute__((const)) function, but the compilers do not care about the global variables that are CPU registers, because they assume that those are preserved by all functions (and upon context changes they are saved and restored by the operating system). Enabling some or all kinds of FP exceptions has absolutely no influence on auto-vectorization. It also has no influence on speed, in well-designed CPUs. The only difference is that the 1st error will crash the program, while with all errors masked the program will terminate successfully regardless how many errors happened. However in this case the user should analyze the results, to determine whether they are correct. If with the exceptions masked, the programmer fails to test for NaNs wherever necessary, then the errors will be lost and bad results will be wrongly considered as correct. Many real life things can be modeled correctly only as partially-ordered sets. When this is ignored and they are handled as totally-ordered sets, it is very easy to generate erroneous results. This can be avoided by various tests before comparing any values, but this is exactly what would be done automatically by the compiler in a programming language where data types that are partially ordered can be defined. Having certain exceptions enabled only for an operation or for a function, instead of for an entire executable program, would be a very bad idea. The reason is that this would offer no guarantees that can simplify the program. If you know that the invalid exception is enabled, then there cannot exist NaNs and you can omit testing for them everywhere. Similarly, if the overflow exception is enabled, you know that there cannot be infinities, so you can omit any tests for infinities. If exceptions were enabled only for a part of the program, then the rest of the program can still generate special values and you need to test for them everywhere. So you get the worst of both choices, a complex program that can be crashed by an error. |
https://gcc.gnu.org/onlinedocs/gcc/Common-Attributes.html states:
> However, functions declared with the pure attribute can safely [...] in a way that does not affect [...] or the observable state of the program
So an exception-aware FP op cannot be __attribute__((pure)) in a program that may read the FP status later, or has trapping on FP exceptions enabled.
This is obvious from the fact that gcc & clang will entirely delete a call to a pure function if the result isn't used, thereby making potential FP traps not happen, or accrued exceptions not mutated.
(also traps on exceptions would make FP ops clearly not pure by the definition in functional programming too)
> If exceptions were enabled only for a part of the program, then the rest of the program can still generate special values and you need to test for them everywhere. So you get the worst of both choices, a complex program that can be crashed by an error.
Would be perfectly fine if combined with separate NaNful and NaNless types; design the language s.t. exception handling state must always be (or is automatically set to) the necessary one, and you never get NaNs in the NaNless types.