CONSTRAINT_ERROR from imported C function

I’m trying to use some imported C functions, some of which could return a NaN or infinity. I get a CONSTRAINT_ERROR, when assigning to an IEEE 754 float or an Interfaces.C.double. Is there a way to have a NaN or infinity across the C boundary so I can handle it explicitly?

Here’s a reduced example:

// C file
#include <math.h>
double return_nan()
{
    return NAN;
}

Ada side:

function return_nan() return Interfaces.IEEE_Float_64
with Import => True, Convention => C;

Value : constant Interfaces.IEEE_Float_64 := return_nan;  --  error here

This is what I get on Linux, I get a similar error on Windows:

raised CONSTRAINT_ERROR : c_import_examples.adb:15 invalid data
[/home/paul/dev/ada/c_import_examples/bin/c_import_examples]
0x40380d C_Import_Examples at c_import_examples.adb:15
0x403c52 Main at b__c_import_examples.adb:248
[/lib/x86_64-linux-gnu/libc.so.6]
0x7f1963aad248
0x7f1963aad303
[/home/paul/dev/ada/c_import_examples/bin/c_import_examples]
0x40354f _start at ???
0xfffffffffffffffe

Interfaces.IEEE_Float_64 is declared as

type IEEE_Float_64 is digits 15;

so it probably has correct semantics, which means no garbage. Furthermore, if you return double, then it is double, not IEEE_Float_64.

The correct declaration would be

   function return_nan return Interfaces.C.double
      with Import => True, Convention => C;

I get the same error even with Interfaces.C.double.

This is incorrect, if you’re referring to non-numeric values as “garbage” — the ARM clearly states in B.2 (10) that “Floating point types corresponding to each floating point format fully supported by the hardware.” — meaning that the IEEE_Float_X-types must conform to the standard (IEEE-754) [provided the type is supported in HW].

The proper way to isolate/exclude non-numerics is with Subtype X is Float_Type range Float_Type'Range; — which constrains the subtype to the numeric values, but absent such constraining [of IEEE-754] the type must [on IEEE-754 HW] present the full type’s value-set, to include the non-numeric representations.

To do NaN, I’d recommend integer-overlay + constants:

  I : Constant Unsigned_32:= 16#7FC00000#;
  F : Constant IEEE_Float_32
    with Import, Address => I'Address;
  F2 :  IEEE_Float_64:= IEEE_Float_64( F );

Note: I used https://www.h-schmidt.net/FloatConverter/IEEE754.html for the integer-valuation. (Which is why I was using 32- instead of 64-bit.)

2 Likes

However, if you’re going to do multiple values, it might be better to do record-type + overlay.

It works perfectly on x86:

   function return_nan return Interfaces.C.double
      with Import => True, Convention => C;
begin
   Put_Line ("NaN: " & Interfaces.C.double'Image (return_nan));

Outputs:

NaN: NaN*****************

Do you have IBM 360 at home? :sunglasses:

It is semantically garbage, which should never be used in production code.

As for B.2 it just states that the floating point types must be backed by the hardware, e.g. not emulated. It does not mean that the hardware (IEEE 754 non-numerals) must be supported by Ada.

It works OK on macOS aarch64.

$ ./nan
NaN*****************

BTW, in your function return_nan() return Interfaces.IEEE_Float_64, the () shouldn’t be there

You sometimes get unpredictable NaN and infinities from numerical instability. NaN propagation leads to really weird bugs, but a few years ago I saw it slow down my Ada ray tracer by ~30% when I added validity ranges to my float type for debugging (it was simple and not using SIMD).

:stuck_out_tongue: , the dangers of a C++ programmer manually writing Ada code into a forum post.

I am getting this on Windows 10 and on Debian Linux, which is why I asked about it. Apparently it’s caused by something enabled in development mode with Alire, since release mode works just fine, which I should have thought to try before asking. I’ll look into this more tonight.

I said nothing about emulation.
Nor that you should typically use it in production code.

The Interfaces package is, quite obviously, for interfacing — and thus, if the standard [implemented in HW] is to be interfaced, then the type must support the non-numerics.

Or, do you think it sensible, that given a sensor interface using float NaN signaling improper & invalid conditions ought to raise Constraint_Error before being able to be handled?

Subtype Real is Interfaces.IEEE_Float_64 range
   Interfaces.IEEE_Float_64'Range;

Task Sensor is
  Entry Get( Value : Real );
  Entry Shutdown;
End Sensor;

Task Body Sensor is
   Reading : Real'Base
      with Import, Address => To_Address( 16#DEAD_BEEF# );
   Finished : Boolean:= False;
Begin
   loop
      Exit when Finished;
      select
         when Reading in Real =>
         accept Get( Value : Real ) do
            Value:= Reading;
         end Get;
      or
         accept Shutdown;
         Finished:= True;
      end select;
   end loop;
End Sensor;

Or something conceptually similar: I may be misremembering how guards work, so it might require a bit bore complexity to signal the guard-value changing.

Sensors normally use 16-bit integers. IEEE 754 is rare. I had such once connected over ModBus. IEEE 754 does not specify endianness, so you must decode the value anyway. In the Simple Components there are packages for that: Simple components for Ada during decoding you would check validity, range etc.

Yes, I know.

Sure, and that’s true for an external source.
For a source that is internal (hardware the compiler is supporting), then B.2 (10) is operational: “Floating point types corresponding to each floating point format fully supported by the hardware.”

If it’s supported by the hardware, then the compiler interfacing said hardware should know its particularities — as I said, the base type backing IEEE-754 should incorporate all possible values, even the non-numeric ones. And the proper way to use FP-values in your program is typically to strip out the non-numerics with a subtype constraint. (Certainly this is the preferred manner in software-engineering: let the Constraint_Error catch the the error at the earlies possible state within your process… but no earlier.) — the TL;DR here is this: having the Interfaces-type raise Constraint_Eror on non-numeric values itself would force handling these values to a possibly premature state.

Which is why, typically, you want to use Subtype Real is IEEE_Float_32 range IEEE_Float_32'Range; and operate on the numeric-values, letting the Constraint_Error handle the encounters with the non-numeric values.

For my HW, I’ve used records and representation-clauses to interface; I thought I had some generics around somewhere… but I have no idea where they went.

   Type Float_32_Exponent is range -127..128
     with Size => 8;

   Type Float_32_Mantissa is range 0..2**23-1
     with Size => 23;

   Type IEEE_754_32 is record
      Sign     : Boolean;
      Exponent : Float_32_Exponent;
      Mantissa : Float_32_Mantissa;
   end record
   with Size => 32, Pack, Bit_Order => System.high_Order_First;

   For IEEE_754_32 use record
      Sign     at 0 range 0..0;
      Exponent at 0 range 1..8;
      Mantissa at 0 range 9..31;
   end record;

   NAN      : Constant IEEE_754_32:= (False, Float_32_Exponent'Last,  2#10000000000000000000000#);
   Inf_Pos  : Constant IEEE_754_32:= (False, Float_32_Exponent'Last,  2#00000000000000000000000#);
   Inf_Neg  : Constant IEEE_754_32:= (True,  Float_32_Exponent'Last,  2#00000000000000000000000#);
   Zero_Neg : Constant IEEE_754_32:= (True,  Float_32_Exponent'First, 2#00000000000000000000000#);
   Zero_Pos : Constant IEEE_754_32:= (False, Float_32_Exponent'First, 2#00000000000000000000000#);

Could be that you are compiling with -gnatVf(or the wider -gnatVa)?

-gnatVf

‘Validity checks for floating-point values.’

Specifying this switch enables validity checking for floating-point values in the same contexts where validity checking is enabled for other scalar values. In the absence of this switch, validity checking is not performed for floating-point values. This takes precedence over other statements about performing validity checking for scalar objects in various scenarios. One way to look at it is that if this switch is not set, then whenever any of the other rules in this section use the word “scalar” they really mean “scalar and not floating-point”. If -gnatVf is specified, then validity checking also applies for floating-point values, and NaNs and infinities are considered invalid, as well as out-of-range values for constrained types. The exact contexts in which floating-point values are checked depends on the setting of other options. For example, -gnatVif or -gnatVfi (the order does not matter) specifies that floating-point parameters of mode in should be validity checked.

2 Likes

I don’t work in embedded, so it is not rare in my applications.

It is definitely -gnatVa inserted by Alire.


Development.Runtime_Checks = "None"

I get a NaN.


Development.Runtime_Checks = "Default"

I get a CONSTRAINT_ERROR. I didn’t expect this.


Development.Runtime_Checks = "Overflow"

I get a NaN.


Development.Runtime_Checks = "Everything"

I get a CONSTRAINT_ERROR.