This is an archive of the discontinued LLVM Phabricator instance.

Treat constant contexts as being in the default rounding mode.
ClosedPublic

Authored by rsmith on Oct 13 2020, 7:19 PM.

Details

Summary

This addresses a regression where pretty much all C++ compilations using
-frounding-math now fail, due to rounding being performed in constexpr
function definitions in the standard library.

This follows the "manifestly constant evaluated" approach described in
https://reviews.llvm.org/D87528#2270676 -- evaluations that are required
to succeed at compile time are permitted even in regions with dynamic
rounding modes, as are (unfortunately) the evaluation of the
initializers of local variables of const integral types.

Diff Detail

Event Timeline

rsmith created this revision.Oct 13 2020, 7:19 PM
Herald added a project: Restricted Project. · View Herald TranscriptOct 13 2020, 7:19 PM
rsmith requested review of this revision.Oct 13 2020, 7:19 PM

Okay. I can accept the fairly unfortunate thing with const int initializers. When is IsConstantContext set in C, just static initializers?

I would propose to consider solution in D88498. It tries to fix the real reason of the malfunction - using dynamic rounding mode for evaluation of global variable initializers. With the workaround for constexpr functions it allows to compile the test attached and still correctly detect runtime nature of local variable initializers.

clang/lib/AST/ExprConstant.cpp
2533–2534

It turns off the check made by this function. In the case of global variable initializer it fixes the error (using dynamic rounding mode instead of default) but for local variable initializer it creates a new error. Constant evaluator cannot detect that the initializer in the code:

void g() {
  const int k = 3 * (1.0 / 3.0);
  ...
}

is not a constant expression.

clang/test/SemaCXX/rounding-math.cpp
10

This code requires additional solution. The function body is built using dynamic rounding mode, which breaks its constexprness. We can avoid this kind of errors if we assume that body of a constexpr function is parsed using constant rounding mode (in this example it is the default mode). It makes parsing constexpr function different from non-constexpr ones, but enables convenient use:

constexpr int add(float x, float y) { return x + y; }

#pragma STDC FENV_ROUND FE_UPWARD
int a2 = add(2.0F, 0x1.000002p0F);

#pragma STDC FENV_ROUND FE_DOWNWARD
int a3 = add(2.0F, 0x1.000002p0F);

If calls of constexpr functions are processes with FP options acting in the call site, a call to constexpr function becomes equivalent to execution of statements of its body.

I would propose to consider solution in D88498. It tries to fix the real reason of the malfunction - using dynamic rounding mode for evaluation of global variable initializers.

That is not the real reason for the malfunction. If you narrowly look at C, you can convince yourself otherwise, but that's only because global variable initializers are the only place where we need to evaluate floating-point constant expressions in C. In C++, this problem affects all contexts where constant evaluation might happen (array bounds, bit-field widths, and a whole host of others).

D88498's approach also can't correctly handle constexpr functions, for which we want different behavior depending on whether the *caller* is a manifestly constant evaluated context.

clang/test/SemaCXX/rounding-math.cpp
10

I don't understand what you mean by "breaks its constexprness". Using a dynamic rounding mode for the body of the function is exactly what we want here. When the function is called in a manifestly constant evaluated context, we should use the default rounding mode, and when it's called at runtime, we use the appropriate dynamic rounding mode; if we try to constant evaluate it outside of a manifestly constant evaluated constant, we deem it non-constant.

When constexpr function is called at runtime, it's a completely normal function with normal semantics. It would be wrong to use the default rounding mode when parsing its body.

rsmith added inline comments.Oct 14 2020, 4:39 PM
clang/test/SemaCXX/rounding-math.cpp
10

To be clear: in your example with add, both a2 and a3 should be initialized to the same value, which should be rounded with the default rounding mode. Per ISO18661, FENV_ROUND only affects operations in its lexical scope, not functions called within those operations, and per the regular C++ rules, constexpr functions behave like regular functions, not like macros.

I would propose to consider solution in D88498. It tries to fix the real reason of the malfunction - using dynamic rounding mode for evaluation of global variable initializers.

That is not the real reason for the malfunction. If you narrowly look at C, you can convince yourself otherwise, but that's only because global variable initializers are the only place where we need to evaluate floating-point constant expressions in C. In C++, this problem affects all contexts where constant evaluation might happen (array bounds, bit-field widths, and a whole host of others).

You are right, the solution in D88498 was more a workaround, as it was focused on initializers only. The updated version applies dynamic rounding to function bodies only, I hope it could solve the problem of other contexts.

clang/test/SemaCXX/rounding-math.cpp
10

I incorrectly identified the reason why some call was rejected in constant evaluated context. Please ignore this statement. I am sorry.

As C++ case is much more complicated and we need a ruleset based on essential properties rather than implementation viewpoint, I am convinced that the solution based on manifestly constant-evaluated property is the best.
Thank you for the discussion!

Probably there is an issue with code generation. The source:

constexpr float func_01(float x, float y) {
  return x + y;
}

float V1 = func_01(1.0F, 0x0.000001p0F);

compiled with '-frounding-math' must produce dynamic initializer. It however is evaluated at compile time.

clang/test/SemaCXX/rounding-math.cpp
6

It is not used.

rsmith updated this revision to Diff 298724.Oct 16 2020, 12:37 PM
  • Add more testcases to demonstrate behavior in C.

Okay. I can accept the fairly unfortunate thing with const int initializers. When is IsConstantContext set in C, just static initializers?

This affects static initializers, enumerators, bit-widths, array bounds, case labels, and indexes in array designators. For code not relying on extensions, only static initializers are affected, but as an extension we do permit floating-point math in other contexts. I've added some tests for the C side of things to demonstrate.

The C tests reveal one issue: block-scope array bounds get constant-folded in GNU mode even if they contain floating-point operations. That's PR44406; D89523 and D89520 have a fix for that which should hopefully land soon. I don't think we need to block on it.

Probably there is an issue with code generation. The source:

constexpr float func_01(float x, float y) {
  return x + y;
}

float V1 = func_01(1.0F, 0x0.000001p0F);

compiled with '-frounding-math' must produce dynamic initializer. It however is evaluated at compile time.

Evaluating it at compile time is in line with the plan from https://reviews.llvm.org/D87528#2270676. The C++ rule is that initializers for static storage duration variables are first evaluated during translation (therefore, per our plan, in the default rounding mode), and only evaluated at runtime (and therefore in the runtime rounding mode) if the compile-time evaluation fails. This is in line with the C rules; C11 F.8.5 says: "All computation for automatic initialization is done (as if) at execution time; thus, it is affected by any operative modes and raises floating-point exceptions as required by IEC 60559 (provided the state for the FENV_ACCESS pragma is ‘‘on’’). All computation for initialization of objects that have static or thread storage duration is done (as if) at translation time." C++ generalizes this by adding another phase of initialization (at runtime) if the translation-time initialization fails, but the translation-time evaluation of the initializer of V1 succeeds so it should be treated as a constant initializer.

And just a side note: we're currently blocked by this. C++ builds using -frounding-math have not worked at all since D87822 landed. If we can't come to an agreement on a fix very soon, I think we need to revert D87822 while we figure it out.

rjmccall accepted this revision.Oct 16 2020, 12:43 PM

Okay, thanks. This LGTM.

This revision is now accepted and ready to land.Oct 16 2020, 12:43 PM
This revision was landed with ongoing or failed builds.Oct 16 2020, 1:27 PM
This revision was automatically updated to reflect the committed changes.