diff mbox series

[RFC/PATCH] C++ constexpr vs. floating point exceptions.

Message ID 005b01d7aedd$91545eb0$b3fd1c10$@nextmovesoftware.com
State Changes Requested
Headers show
Series [RFC/PATCH] C++ constexpr vs. floating point exceptions. | expand

Commit Message

Roger Sayle Sept. 21, 2021, 11:41 a.m. UTC
I was wondering if I may ask the C++ language experts for their opinion
on whether (potential) floating point exceptions/traps can be ignored
in constant expressions; this is related to PR c++/96862.  I think my
question boils down to whether (or not) the following is valid C++:

constexpr float my_inf = 7.0 / 0.0;

[This is currently an error with "-O2", but OK with "-O2 -ffast-math"!]

There's a long history of g++'s semantics being accidentally tied to
the middle-end's constant folding, such that the current status quo
is that some middle-end bugs can't be fixed without breaking C++,
and vice versa.  I'm hoping that the patch below (following Jakub's
lead with rounding math) might be a next step to improving things,
provided that my understanding of the desired/correct behaviour of
the C++ front-end is correct.

This patch has been tested on x86_64-pc-linux-gnu with a "make bootstrap"
and "make -k check" with no new failures after tweaking two checks in
g++.dg/ubsan/pr63956.C.  With this change the middle-end can become more
strict about respecting flag_trapping_math without affecting C++'s
behavior.  Ideally, what the front-end considers valid should be
independent of whether the user specified -fno-trapping-math (or
-ffast-math) to the middle-end.

Thoughts?  Ok for mainline?


2021-09-21  Roger Sayle  <roger@nextmovesoftware.com>

gcc/cp/ChangeLog
	* constexpr.c (cxx_eval_outermost_const_expr): Temporarily disable
	the middle-end from honoring floating point exceptions/traps while
	folding "manifestly constant" expressions.

gcc/testsuite/ChangeLog
	* g++.dg/ubsan/pr63956.C: Update to (always) allow floating point
	division in constexpr (if both operands are constexpr).

Roger
--

Comments

Xi Ruoyao Sept. 21, 2021, 1:06 p.m. UTC | #1
On Tue, 2021-09-21 at 12:41 +0100, Roger Sayle wrote:
> 
> I was wondering if I may ask the C++ language experts for their
> opinion
> on whether (potential) floating point exceptions/traps can be ignored
> in constant expressions; this is related to PR c++/96862.  I think my
> question boils down to whether (or not) the following is valid C++:
> 
> constexpr float my_inf = 7.0 / 0.0;

It's not.  C++ disallows constexpr from invoking undefined behaviors in
Clauses 4 through 19, while division by zero is an undefined behavior in
Clause 8.

> [This is currently an error with "-O2", but OK with "-O2 -ffast-
> math"!]

> There's a long history of g++'s semantics being accidentally tied to
> the middle-end's constant folding, such that the current status quo
> is that some middle-end bugs can't be fixed without breaking C++,
> and vice versa.  I'm hoping that the patch below (following Jakub's
> lead with rounding math) might be a next step to improving things,
> provided that my understanding of the desired/correct behaviour of
> the C++ front-end is correct.
> 
> This patch has been tested on x86_64-pc-linux-gnu with a "make
> bootstrap"
> and "make -k check" with no new failures after tweaking two checks in
> g++.dg/ubsan/pr63956.C.

> With this change the middle-end can become more
> strict about respecting flag_trapping_math without affecting C++'s
> behavior.  Ideally, what the front-end considers valid should be
> independent of whether the user specified -fno-trapping-math (or
> -ffast-math) to the middle-end.

I think we can allow a constexpr to contain floating div-by-zero with -
fno-trapping-math, if we consider -fno-trapping-math make the behavior
"no longer undefined" but I'm not sure.

However -ffast-math also enables -ffinite-math-only, which makes div-by-
zero absolutely undefined.  So to me the expected behavior is:

g++ t.cc                                       -> Compile error
g++ t.cc -ffast-math                           -> Compile error
g++ t.cc -fno-trapping-math                    -> Ok
g++ t.cc -fno-trapping-math -ffinite-math-only -> Compile Error

> Thoughts?  Ok for mainline?

Based on the reasoning above I think it's not OK.  But anyway I'm not a
maintainer.

> 2021-09-21  Roger Sayle  <roger@nextmovesoftware.com>
> 
> gcc/cp/ChangeLog
>         * constexpr.c (cxx_eval_outermost_const_expr): Temporarily
> disable
>         the middle-end from honoring floating point exceptions/traps
> while
>         folding "manifestly constant" expressions.
> 
> gcc/testsuite/ChangeLog
>         * g++.dg/ubsan/pr63956.C: Update to (always) allow floating
> point
>         division in constexpr (if both operands are constexpr).
> 
> Roger
> --
>
Roger Sayle Sept. 21, 2021, 1:15 p.m. UTC | #2
Can you double check?  Integer division by zero is undefined, but isn't floating point
division by zero defined by the appropriate IEEE standards?

Roger
--

-----Original Message-----
From: Xi Ruoyao <xry111@mengyan1223.wang> 
Sent: 21 September 2021 14:07
To: Roger Sayle <roger@nextmovesoftware.com>; 'GCC Patches' <gcc-patches@gcc.gnu.org>
Subject: Re: [RFC/PATCH] C++ constexpr vs. floating point exceptions.

On Tue, 2021-09-21 at 12:41 +0100, Roger Sayle wrote:
> 
> I was wondering if I may ask the C++ language experts for their 
> opinion on whether (potential) floating point exceptions/traps can be 
> ignored in constant expressions; this is related to PR c++/96862.  I 
> think my question boils down to whether (or not) the following is 
> valid C++:
> 
> constexpr float my_inf = 7.0 / 0.0;

It's not.  C++ disallows constexpr from invoking undefined behaviors in Clauses 4 through 19, while division by zero is an undefined behavior in Clause 8.

> [This is currently an error with "-O2", but OK with "-O2 -ffast- 
> math"!]

> There's a long history of g++'s semantics being accidentally tied to 
> the middle-end's constant folding, such that the current status quo is 
> that some middle-end bugs can't be fixed without breaking C++, and 
> vice versa.  I'm hoping that the patch below (following Jakub's lead 
> with rounding math) might be a next step to improving things, provided 
> that my understanding of the desired/correct behaviour of the C++ 
> front-end is correct.
> 
> This patch has been tested on x86_64-pc-linux-gnu with a "make 
> bootstrap"
> and "make -k check" with no new failures after tweaking two checks in
> g++.dg/ubsan/pr63956.C.

> With this change the middle-end can become more strict about 
> respecting flag_trapping_math without affecting C++'s behavior.  
> Ideally, what the front-end considers valid should be independent of 
> whether the user specified -fno-trapping-math (or
> -ffast-math) to the middle-end.

I think we can allow a constexpr to contain floating div-by-zero with - fno-trapping-math, if we consider -fno-trapping-math make the behavior "no longer undefined" but I'm not sure.

However -ffast-math also enables -ffinite-math-only, which makes div-by- zero absolutely undefined.  So to me the expected behavior is:

g++ t.cc                                       -> Compile error
g++ t.cc -ffast-math                           -> Compile error
g++ t.cc -fno-trapping-math                    -> Ok
g++ t.cc -fno-trapping-math -ffinite-math-only -> Compile Error

> Thoughts?  Ok for mainline?

Based on the reasoning above I think it's not OK.  But anyway I'm not a maintainer.

> 2021-09-21  Roger Sayle  <roger@nextmovesoftware.com>
> 
> gcc/cp/ChangeLog
>         * constexpr.c (cxx_eval_outermost_const_expr): Temporarily 
> disable
>         the middle-end from honoring floating point exceptions/traps 
> while
>         folding "manifestly constant" expressions.
> 
> gcc/testsuite/ChangeLog
>         * g++.dg/ubsan/pr63956.C: Update to (always) allow floating 
> point
>         division in constexpr (if both operands are constexpr).
> 
> Roger
> --
> 

--
Xi Ruoyao <xry111@mengyan1223.wang>
School of Aerospace Science and Technology, Xidian University
Jakub Jelinek Sept. 21, 2021, 1:22 p.m. UTC | #3
On Tue, Sep 21, 2021 at 02:15:59PM +0100, Roger Sayle wrote:
> Can you double check?  Integer division by zero is undefined, but isn't floating point
> division by zero defined by the appropriate IEEE standards?

https://eel.is/c++draft/expr.mul#4 doesn't make the division by zero
behavior conditional on integral types.
C has similar wording.

	Jakub
Richard Biener Sept. 21, 2021, 1:34 p.m. UTC | #4
On Tue, Sep 21, 2021 at 3:07 PM Xi Ruoyao via Gcc-patches
<gcc-patches@gcc.gnu.org> wrote:
>
> On Tue, 2021-09-21 at 12:41 +0100, Roger Sayle wrote:
> >
> > I was wondering if I may ask the C++ language experts for their
> > opinion
> > on whether (potential) floating point exceptions/traps can be ignored
> > in constant expressions; this is related to PR c++/96862.  I think my
> > question boils down to whether (or not) the following is valid C++:
> >
> > constexpr float my_inf = 7.0 / 0.0;
>
> It's not.  C++ disallows constexpr from invoking undefined behaviors in
> Clauses 4 through 19, while division by zero is an undefined behavior in
> Clause 8.
>
> > [This is currently an error with "-O2", but OK with "-O2 -ffast-
> > math"!]
>
> > There's a long history of g++'s semantics being accidentally tied to
> > the middle-end's constant folding, such that the current status quo
> > is that some middle-end bugs can't be fixed without breaking C++,
> > and vice versa.  I'm hoping that the patch below (following Jakub's
> > lead with rounding math) might be a next step to improving things,
> > provided that my understanding of the desired/correct behaviour of
> > the C++ front-end is correct.
> >
> > This patch has been tested on x86_64-pc-linux-gnu with a "make
> > bootstrap"
> > and "make -k check" with no new failures after tweaking two checks in
> > g++.dg/ubsan/pr63956.C.
>
> > With this change the middle-end can become more
> > strict about respecting flag_trapping_math without affecting C++'s
> > behavior.  Ideally, what the front-end considers valid should be
> > independent of whether the user specified -fno-trapping-math (or
> > -ffast-math) to the middle-end.
>
> I think we can allow a constexpr to contain floating div-by-zero with -
> fno-trapping-math, if we consider -fno-trapping-math make the behavior
> "no longer undefined" but I'm not sure.

Btw, -ftrapping-math also makes inexact operations possibly trapping
so I'm not sure if any combination of -ftrapping-math and -ffinite-math-only
can capture what C++ requires here.

> However -ffast-math also enables -ffinite-math-only, which makes div-by-
> zero absolutely undefined.  So to me the expected behavior is:
>
> g++ t.cc                                       -> Compile error
> g++ t.cc -ffast-math                           -> Compile error
> g++ t.cc -fno-trapping-math                    -> Ok
> g++ t.cc -fno-trapping-math -ffinite-math-only -> Compile Error
>
> > Thoughts?  Ok for mainline?
>
> Based on the reasoning above I think it's not OK.  But anyway I'm not a
> maintainer.
>
> > 2021-09-21  Roger Sayle  <roger@nextmovesoftware.com>
> >
> > gcc/cp/ChangeLog
> >         * constexpr.c (cxx_eval_outermost_const_expr): Temporarily
> > disable
> >         the middle-end from honoring floating point exceptions/traps
> > while
> >         folding "manifestly constant" expressions.
> >
> > gcc/testsuite/ChangeLog
> >         * g++.dg/ubsan/pr63956.C: Update to (always) allow floating
> > point
> >         division in constexpr (if both operands are constexpr).
> >
> > Roger
> > --
> >
>
> --
> Xi Ruoyao <xry111@mengyan1223.wang>
> School of Aerospace Science and Technology, Xidian University
Xi Ruoyao Sept. 21, 2021, 1:38 p.m. UTC | #5
On Tue, 2021-09-21 at 14:15 +0100, Roger Sayle wrote:
> 
> Can you double check?  Integer division by zero is undefined, but isn't
> floating point
> division by zero defined by the appropriate IEEE standards?

It's an undefined behavior in C++ standard.  Even if we assume C++ *was*
IEEE-754 conforming (C++ standard is not saying "C++ is IEEE-754
conforming"!), it would be still not "something can be evaluated in
compile time".  IEEE 754 says the default behavior (it can be altered
with some way, for example using feenableexcept() on systems with Glibc)
of floating div-by-zero with both operands finite is "The divideByZero
exception shall be signaled", and:

"The operations of this standard, when exceptional, can as a side effect
raise some of the following status flags: inexact, underflow, overflow,
divideByZero, and invalid operation."

And this is a side effect in real-life: those status flags are normally
implemented as some bits in the floating point status register of the
CPU and can be queried, for example ISO C defined fegetexceptflag() to
do this.

So it's a side effect anyway.  You can't evaluate a side-effect during
compilation.

BTW the "correct" way to get a NaN in C++ seems:

#include <limits>
constexpr double my_nan = std::numeric_limits<double>::quiet_NaN();

If the C++ impl supports ISO/IEC 60559 (an alias of IEEE 754), it will
produce a quiet NaN which can be used in constexpr (you can use
signal_NaN() instead of quiet_NaN() if you want a signaling NaN). 
Otherwise the compilation will fail.
Xi Ruoyao Sept. 21, 2021, 1:46 p.m. UTC | #6
On Tue, 2021-09-21 at 21:38 +0800, Xi Ruoyao via Gcc-patches wrote:

> BTW the "correct" way to get a NaN in C++ seems:
> 
> #include <limits>
> constexpr double my_nan = std::numeric_limits<double>::quiet_NaN();

Sorry, we were discussing inf, not NaN...  Then

constexpr double my_inf = std::numeric_limits<double>::infinity();

If the C++ impl supports ISO/IEC 60559 (an alias of IEEE 754), it will
produce a positive infinity. Otherwise the compilation will fail.
Roger Sayle Sept. 21, 2021, 1:57 p.m. UTC | #7
Thanks.  It's a minefield.  I'm regretting using a division by zero example
(as it unintentionally drags in the whole undefined behaviour thing).

How about my recently added g++.dg/pr88173-1.C, is the following valid,
i.e. const, if ordered comparisons against NaNs should raise an exception
with (the default) -ftrapping-math?  Or only with -fno-trapping-math?

constexpr bool my_false1 = __builtin_nan("") < 1.0;
constexpr bool my_false2 = __builtin_nans("") < 1.0;

I think Richard Beiner was right; some trapping math is acceptable and
other trapping math is unacceptable, with (currently) no distinction
(in the middle-end) between the two.

Perhaps the g++ front-end relies on/is abusing flag_trapping_math
as a proxy to ensure that division by zero is undefined (is unfolded).

Cheers,
Roger
--

-----Original Message-----
From: Jakub Jelinek <jakub@redhat.com> 
Sent: 21 September 2021 14:22
To: Roger Sayle <roger@nextmovesoftware.com>; Jason Merrill
<jason@redhat.com>; Jonathan Wakely <jwakely@redhat.com>
Cc: 'Xi Ruoyao' <xry111@mengyan1223.wang>; 'GCC Patches'
<gcc-patches@gcc.gnu.org>
Subject: Re: [RFC/PATCH] C++ constexpr vs. floating point exceptions.

On Tue, Sep 21, 2021 at 02:15:59PM +0100, Roger Sayle wrote:
> Can you double check?  Integer division by zero is undefined, but 
> isn't floating point division by zero defined by the appropriate IEEE
standards?

https://eel.is/c++draft/expr.mul#4 doesn't make the division by zero
behavior conditional on integral types.
C has similar wording.

	Jakub
Joseph Myers Sept. 21, 2021, 8:29 p.m. UTC | #8
On Tue, 21 Sep 2021, Jakub Jelinek via Gcc-patches wrote:

> On Tue, Sep 21, 2021 at 02:15:59PM +0100, Roger Sayle wrote:
> > Can you double check?  Integer division by zero is undefined, but isn't floating point
> > division by zero defined by the appropriate IEEE standards?
> 
> https://eel.is/c++draft/expr.mul#4 doesn't make the division by zero
> behavior conditional on integral types.
> C has similar wording.

The position for C is that Annex F semantics take precedence over all the 
ways in which floating-point arithmetic has undefined behavior in the main 
body of the standard.  So floating-point overflow and division by zero are 
fully defined in the presence of Annex F support, while out-of-range 
conversions from floating point to integer types produce an unspecified 
value (not necessarily the same unspecified value for different executions 
of the conversion in the abstract machine - as discussed in bug 93806, GCC 
can get that wrong and act as if a single execution of such a conversion 
in the abstract machine produces more than one result).

In C, as specified in Annex F, initializers for floating-point objects 
with static or thread storage duration are evaluated with exceptions 
discarded and the default rounding mode in effect; 7.0 / 0.0 is a fully 
valid initializer for such an object to initialize it to positive 
infinity.  As I understand it, the question for this thread is whether C++ 
constexpr should have a similar rule to C static initializers (which ought 
to apply to 1.0 / 3.0, raising inexact, just as much as to 7.0 / 0.0).
Jason Merrill Sept. 21, 2021, 9:19 p.m. UTC | #9
On Tue, Sep 21, 2021 at 4:30 PM Joseph Myers <joseph@codesourcery.com>
wrote:

> On Tue, 21 Sep 2021, Jakub Jelinek via Gcc-patches wrote:
>
> > On Tue, Sep 21, 2021 at 02:15:59PM +0100, Roger Sayle wrote:
> > > Can you double check?  Integer division by zero is undefined, but
> isn't floating point
> > > division by zero defined by the appropriate IEEE standards?
> >
> > https://eel.is/c++draft/expr.mul#4 doesn't make the division by zero
> > behavior conditional on integral types.
> > C has similar wording.
>
> The position for C is that Annex F semantics take precedence over all the
> ways in which floating-point arithmetic has undefined behavior in the main
> body of the standard.  So floating-point overflow and division by zero are
> fully defined in the presence of Annex F support, while out-of-range
> conversions from floating point to integer types produce an unspecified
> value (not necessarily the same unspecified value for different executions
> of the conversion in the abstract machine - as discussed in bug 93806, GCC
> can get that wrong and act as if a single execution of such a conversion
> in the abstract machine produces more than one result).
>
> In C, as specified in Annex F, initializers for floating-point objects
> with static or thread storage duration are evaluated with exceptions
> discarded and the default rounding mode in effect; 7.0 / 0.0 is a fully
> valid initializer for such an object to initialize it to positive
> infinity.  As I understand it, the question for this thread is whether C++
> constexpr should have a similar rule to C static initializers (which ought
> to apply to 1.0 / 3.0, raising inexact, just as much as to 7.0 / 0.0).
>

The C rules seem to be

F.8.2 Translation
During translation the IEC 60559 default modes are in effect:
 — The rounding direction mode is rounding to nearest.
 — The rounding precision mode (if supported) is set so that results are
not shortened.
 — Trapping or stopping (if supported) is disabled on all floating-point
exceptions.
Recommended practice:
The implementation should produce a diagnostic message for each
translation-time floating-point exception, other than “inexact”; the
implementation should then proceed with the translation of the program.

I think following the same rules for C++ would be appropriate in a
diagnosing context: warn and continue.  In a template argument deduction
(SFINAE) context, where errors become silent substitution failures, it's
probably better to treat them as non-constant.

Jason
Marc Glisse Sept. 21, 2021, 10:14 p.m. UTC | #10
On Tue, 21 Sep 2021, Jason Merrill via Gcc-patches wrote:

> On Tue, Sep 21, 2021 at 4:30 PM Joseph Myers <joseph@codesourcery.com>
> wrote:
>
>> On Tue, 21 Sep 2021, Jakub Jelinek via Gcc-patches wrote:
>>
>>> On Tue, Sep 21, 2021 at 02:15:59PM +0100, Roger Sayle wrote:
>>>> Can you double check?  Integer division by zero is undefined, but
>> isn't floating point
>>>> division by zero defined by the appropriate IEEE standards?
>>>
>>> https://eel.is/c++draft/expr.mul#4 doesn't make the division by zero
>>> behavior conditional on integral types.
>>> C has similar wording.
>>
>> The position for C is that Annex F semantics take precedence over all the
>> ways in which floating-point arithmetic has undefined behavior in the main
>> body of the standard.  So floating-point overflow and division by zero are
>> fully defined in the presence of Annex F support, while out-of-range
>> conversions from floating point to integer types produce an unspecified
>> value (not necessarily the same unspecified value for different executions
>> of the conversion in the abstract machine - as discussed in bug 93806, GCC
>> can get that wrong and act as if a single execution of such a conversion
>> in the abstract machine produces more than one result).
>>
>> In C, as specified in Annex F, initializers for floating-point objects
>> with static or thread storage duration are evaluated with exceptions
>> discarded and the default rounding mode in effect; 7.0 / 0.0 is a fully
>> valid initializer for such an object to initialize it to positive
>> infinity.  As I understand it, the question for this thread is whether C++
>> constexpr should have a similar rule to C static initializers (which ought
>> to apply to 1.0 / 3.0, raising inexact, just as much as to 7.0 / 0.0).
>>
>
> The C rules seem to be
>
> F.8.2 Translation
> During translation the IEC 60559 default modes are in effect:
> — The rounding direction mode is rounding to nearest.
> — The rounding precision mode (if supported) is set so that results are
> not shortened.
> — Trapping or stopping (if supported) is disabled on all floating-point
> exceptions.
> Recommended practice:
> The implementation should produce a diagnostic message for each
> translation-time floating-point exception, other than “inexact”; the
> implementation should then proceed with the translation of the program.
>
> I think following the same rules for C++ would be appropriate in a

I agree that looking at the C standard is more interesting, C++ is very 
bad at specifying anything float related.

> diagnosing context: warn and continue.  In a template argument deduction
> (SFINAE) context, where errors become silent substitution failures, it's
> probably better to treat them as non-constant.

I am trying to imagine a sfinae example affected by whether 1./0. is 
constant. Does that mean A<0.,__builtin_inf()> would fail to use the 
specialization in

template<double a,double d>struct A{};
template<double a>struct A<a,1/a>{};

? I don't like that, I believe it should use the specialization. With 
ieee754, 1./0. is perfectly well defined as +inf, the only question is 
whether it should also set a flag at runtime, which is not relevant in a 
manifestly consteval context (fetestexcept, etc are not constexpr, that 
should be enough to catch mistakes). If some user wants to forbid 
FE_DIVBYZERO, FE_INEXACT, FE_INVALID, FE_OVERFLOW or FE_UNDERFLOW in 
compile-time operations, that looks like it could be part of a separate 
compiler flag or pragma, like C's FENV_ROUND can affect the rounding mode 
in static initializers (of course C++ templates make pragmas less 
convenient).
diff mbox series

Patch

diff --git a/gcc/cp/constexpr.c b/gcc/cp/constexpr.c
index 8a5dd06..ddea132 100644
--- a/gcc/cp/constexpr.c
+++ b/gcc/cp/constexpr.c
@@ -7276,6 +7276,13 @@  cxx_eval_outermost_constant_expr (tree t, bool allow_non_constant,
 
   /* Turn off -frounding-math for manifestly constant evaluation.  */
   warning_sentinel rm (flag_rounding_math, ctx.manifestly_const_eval);
+  /* For manifestly constant evaluation, trapping (floating point)
+     exceptions don't prevent evaluation at compile-time, so temporarily
+     turn off -fsignaling-nans, -ftrapping-math and -ftrapv. */
+  warning_sentinel sn (flag_signaling_nans, ctx.manifestly_const_eval);
+  warning_sentinel tm (flag_trapping_math, ctx.manifestly_const_eval);
+  warning_sentinel tv (flag_trapv, ctx.manifestly_const_eval);
+
   tree type = initialized_type (t);
   tree r = t;
   bool is_consteval = false;
diff --git a/gcc/testsuite/g++.dg/ubsan/pr63956.C b/gcc/testsuite/g++.dg/ubsan/pr63956.C
index 3a1596e..126ed1d 100644
--- a/gcc/testsuite/g++.dg/ubsan/pr63956.C
+++ b/gcc/testsuite/g++.dg/ubsan/pr63956.C
@@ -69,12 +69,12 @@  constexpr float
 fn4 (float a, float b)
 {
   if (b != 2.0)
-    a = a / b; // { dg-error "is not a constant expression" }
+    a = a / b;
   return a;
 }
 
 constexpr float l1 = fn4 (5.0, 3.0);
-constexpr float l2 = fn4 (7.0, 0.0); // { dg-message "in .constexpr. expansion" }
+constexpr float l2 = fn4 (7.0, 0.0);
 
 constexpr int
 fn5 (const int *a, int b)