New content: add section on ordinary fixed-point types to Numerics#1371
Conversation
Add a comprehensive "Ordinary fixed-point types" section to the "Numerics" chapter of the "Advanced Ada" course.
swbaird
left a comment
There was a problem hiding this comment.
Not approving because of check suppression misuse.
| *small* always equals its *delta*, which must be a power of ten. | ||
| In contrast, for an ordinary fixed-point type, the scaling of the type's | ||
| *small* is a power of two by default. Therefore, ordinary fixed-point | ||
| types are sometimes called binary fixed-point types. |
There was a problem hiding this comment.
This makes it sound like
type T is delta 1.0 / 3.0 with Small => 1.0 / 3.0;
would be called a binary fixed-point type. That seems wrong.
How about "An ordinary fixed -point type with a small that is a power of two is sometimes called ...".
Also, talking about the scaling of the type's Small seems wrong; the Small value is not being scaled.
| default, is a power of two. Therefore, ordinary fixed-point types are sometimes | ||
| called binary fixed-point types. | ||
| .. note:: | ||
| Ordinary fixed-point types can be thought of being closer to the actual |
There was a problem hiding this comment.
Again, this comment doesn't seem right if the small is not a power of two. Having defined the term "binary fixed point type" above, perhaps use it here?
| By default the compiler will choose a scale factor, or :ada:`small`, that is a | ||
| power of 2 no greater than <delta-value>. | ||
| By default the compiler will choose a scale factor, or :ada:`small`, that is a | ||
| power of 2 no greater than <delta-value>. |
There was a problem hiding this comment.
Do we want to talk about what language allows (any power of two that is leq the delta) vs. what GNAT implements. Perhaps give a reference to https://docs.adacore.com/live/wave/gnat_rm/html/gnat_rm/gnat_rm/compatibility_and_porting_guide.html#writing-portable-fixed-point-declarations
That section also discusses the shaving rule, which seems worth mentioning if you haven't already talked about it elsewhere. RM 3.5.9(13) says "An ordinary_fixed_point_definition defines an ordinary fixed point type whose base range includes at least all multiples of small that are between the bounds specified in the real_range_specification. The base range of the type does not necessarily include the specified bounds themselves." That's where the shaving rule comes from.
|
|
||
| We may also rewrite this code with an exact type definition: | ||
| The Q format consists of two numbers: the number of bits for the integer part |
There was a problem hiding this comment.
My first reaction is that perhaps we don't want to mention Q format at all. Or mention it very briefly. For example, after defining an example type
Del : constant := 2.0 ** -8;
Lo : constant := -(2.0 ** 7);
type Q7_8 is delta Del range Lo .. -(Lo + Del) with Small => Del;
, we could add a comment
For readers familiar with Q numeric format (see ), this would be a Q7.8 format type.
The identifier chosen for the type (i.e., "Q7_8") reflects its Q format; the same is true for many
types declared in subsequent examples in this document.
Q format can't describe Ada fixed point types where the small is not a power of two, but it also doesn't work for cases where the delta is a power of two that is larger than one:
type Coarse is delta 2.0 ... with Small => 2.0;
But I don't feel strongly. If you think it is useful to discuss Q format, I don't object. It does describe the cases that come up most commonly in practice.
| of a decimal type is always equal to the delta that we specified. However, for | ||
| ordinary fixed-point types, this doesn't have to be the case |mdash| and if we | ||
| select a delta that is not a power of two (i.e. the typical machine | ||
| representation), the compiler will choose a *small* that is the largest power |
There was a problem hiding this comment.
This GNAT behavior is common for other implementations, but is not required. See previous reference to GNAT documentation regarding portability issues for fixed point types.
| Let's focus on the :ada:`Show (0.1)` call. Note that the value :ada:`0.1` isn't | ||
| a multiple of the *small* of :ada:`TQ15`. In fact, the value 0.1 is rounded to | ||
| the nearest representable value when we assign it. For this reason, | ||
| :ada:`TQ15'Image` shows ``0.09998`` rather than ``0.10000``. |
There was a problem hiding this comment.
And similarly for the Float'Image call too. The loss of the exact value 0.1 takes place when Show is called, so there is nothing to be done inside of Show to recover it. Perhaps this isn't worth mentioning, but if you think there is a chance that readers might think that converting to Float inside Show will somehow magically restore the exact 0.1 value then that would be worth clarifying. Your call.
| to (0.25 - *small*). | ||
|
|
||
|
|
||
| Range of fixed-point subtypes |
There was a problem hiding this comment.
I agree with the decision to say nothing at all about reduced accuracy subtypes (RM J.3)
|
|
||
| C : constant Sample := A + B; | ||
| begin | ||
| -- Two same-signed operands whose sum |
There was a problem hiding this comment.
No, no, no! RM 11.5(26) says: "If a given check has been suppressed, and the corresponding error situation occurs, the execution of the program is erroneous." If adding a Suppress pragma has any impact at all on the behavior of a program, then that means program execution is erroneous. If the suppressed check would never fail, then behavior is unaffected. If it would have failed, then execution is erroneous. Relying on check suppression to do something particular that the user had in mind (e.g., wrapping) is a common error.
|
|
||
| The trick relies on :ada:`Sample` being a Q15 type that fills its 16-bit | ||
| machine word exactly. Suppressing the overflow check lets an out-of-range | ||
| operation *wrap around* at the type bounds, just as the underlying machine |
There was a problem hiding this comment.
For other things that the suppressing an overflow check lets an out-of-range operation do, see the reference to "nasal demons" in the wikipedia article on "undefined behavior". I like the movie "The Wizard of Oz" at least as much as most folks, but let's avoid winged monkeys.
| headroom it needs. In fact, when designing DSP algorithms |mdash| especially | ||
| when targeting fixed-point types |mdash| we have to make sure that the result | ||
| is **always** in the expected type range. Using headroom is common practice to | ||
| guarantee this is always true. |
There was a problem hiding this comment.
In talking about how much headroom is available, would it be worth mentioning System.Fine_Delta? Maybe not. If you are given either bounds or a power-of-two small, then typically find_delta can be used to statically get a maximal value for the other element (i.e. if given bounds, then compute small via F_D; if given small, then similarly compute bounds). this isn't strictly guaranteed to work (and it won't work for enormous values), but it will work in practice (not just for GNAT); and if it doesn't work, it will fail at compile-time. But perhaps this is going too far afield.
No description provided.