DecimalFloatingPoint Protocol Review

In another post, @scanon outlined some basic requirements before we can define fixed-width Decimal numbers:

There is now a newly updated DecimalFloatingPoint protocol incorporating all known comments to be reviewed by anyone who is interested:

/// A radix-10 (decimal) floating-point type.
///
/// The `DecimalFloatingPoint` protocol extends the `FloatingPoint` protocol
/// with operations specific to floating-point decimal types, as defined by the
/// [IEEE 754 specification][spec]. `DecimalFloatingPoint` is implemented in
/// the standard library by `Decimal32`, `Decimal64`, and `Decimal128` where
/// available.
///
/// [spec]: http://ieeexplore.ieee.org/servlet/opac?punumber=4610933
public protocol DecimalFloatingPoint : FloatingPoint {
  
  /// A type that represents the encoded significand of a value.
  associatedtype RawSignificand: UnsignedInteger
  
  /// A type that represents the encoded exponent of a value.
  associatedtype RawExponent : UnsignedInteger
  
  /// Creates a new instance from the specified sign and bit patterns.
  ///
  /// The values passed as `exponentBitPattern` is interpreted in the
  /// decimal interchange format defined by the [IEEE 754 specification][spec].
  ///
  /// [spec]: http://ieeexplore.ieee.org/servlet/opac?punumber=4610933
  ///
  /// The `significandBitPattern` are the big-endian, integer decimal digits
  /// of the number.  For example, the integer number `314` represents a
  /// significand of `314`.
  ///
  /// - Parameters:
  ///   - sign: The sign of the new value.
  ///   - exponentBitPattern: The bit pattern to use for the exponent field of
  ///     the new value.
  ///   - significandBitPattern: Bit pattern to use for the significand field
  ///     of the new value.
  init(sign: Sign, exponentBitPattern: RawExponent,
       significandBitPattern: RawSignificand)
  
  /// Creates a new instance from the given value, rounded to the closest
  /// possible representation.
  ///
  /// If two representable values are equally close, the result is the value
  /// with more trailing zeros in its significand bit pattern.
  ///
  /// - Parameter value: A floating-point value to be converted.
  init<Source:DecimalFloatingPoint>(_ value: Source)
  
  /// Creates a new instance from the given value, if it can be represented
  /// exactly.
  ///
  /// If the given floating-point value cannot be represented exactly, the
  /// result is `nil`. A value that is NaN ("not a number") cannot be
  /// represented exactly if its payload cannot be encoded exactly.
  ///
  /// - Parameter value: A floating-point value to be converted.
  init?<Source:DecimalFloatingPoint>(exactly value: Source)
  
  /// The number of bits used to represent the type's exponent.
  static var exponentBitCount: Int { get }
  
  /// The available number of significand digits.
  ///
  /// For fixed-width decimal floating-point types, this is the actual number
  /// of significand digits.
  ///
  /// For extensible decimal floating-point types, `significandDigitCount`
  /// should be the maximum allowed significand width (both fractional and
  /// integral) digits of the significand. If there is no upper limit, then
  /// `significandDigitCount` should be `Int.max`.
  static var significandDigitCount: Int { get }
  
  /// The raw encoding of the value's exponent field.
  ///
  /// This value is unadjusted by the type's exponent bias.
  var exponentBitPattern: RawExponent { get }
  
  /// The raw binary integer decimal encoding of the value's significand field.
  var significandBitPattern: RawSignificand { get }
  
  /// The floating-point value with the same sign and exponent as this value,
  /// but with a significand of 1.0.
  ///
  /// A *decade* is a set of decimal floating-point values that all have the
  /// same sign and exponent. The `decade` property is a member of the same
  /// decade as this value, but with a unit significand.
  ///
  /// In this example, `x` has a value of `21.5`, which is stored as
  /// `2.15 * 10**1`, where `**` is exponentiation. Therefore, `x.decade` is
  /// equal to `1.0 * 10**1`, or `10.0`.
  ///```
  /// let x = 21.5
  /// // x.significand == 2.15
  /// // x.exponent == 1
  ///
  /// let y = x.decade
  /// // y == 10.0
  /// // y.significand == 1.0
  /// // y.exponent == 1
  ///```
  var decade: Self { get }
  
  /// The number of digits required to represent the value's significand.
  ///
  /// If this value is a finite nonzero number, `significandDigitCount` is the
  /// number of decimal digits required to represent the value of
  /// `significand`; otherwise, `significandDigitCount` is -1. The value of
  /// `significandDigitCount` is always -1 or from one to the
  /// `significandMaxDigitCount`. For example:
  ///
  /// - For any representable power of ten, `significandDigitCount` is one,
  ///   because significand` is `1`.
  /// - If `x` is 10, `x.significand` is `10` in decimal, so
  ///   `x.significandDigitCount` is 2.
  /// - If `x` is Decimal32.pi, `x.significand` is `3.141593` in
  ///   decimal, and `x.significandDigitCount` is 7.
  var significandDigitCount: Int { get }
  
}
3 Likes

Overall, appreciate the careful study of the existing protocols. Some points:

We don't expose this for binary floating-point types, and I'm not certain that it's necessary here either; the bias is mostly useful for internal use and, even when it's needed elsewhere for useful generic algorithms (which is our guiding star for what goes into a protocol), can be ascertained without a dedicated API.

If we were to add it, we would want to consider this holistically for all the floating-point protocols. (Which is to say, for a DecimalFloatingPoint protocol built on top of Swift-as-it-is, my feedback would be to leave it out.)

Note that Swift offers no user-facing control over how floating-point rounding mode (i.e., which of the two closest representations of a notionally infinite-precision result is returned from a computation); the FloatingPointRoundingRule is for rounding to integers and isn't meant for this use (there are options for rounding mode which would never make sense for rounding to an integer).

If we were to add an API it would, as above, have to be considered holistically for binary and decimal floating-point.

Two issues here:

First, the bitPattern type shouldn't be RawSignificand, which is documented on BinaryFloatingPoint (and in your proposed design here) to be wide enough only for the significand bits—see, for example, Float80 where this makes a difference. Even if, in practice, it wouldn't be an issue for the few concrete types contemplated here, we don't want to burn a requirement into the design, as the first API does here, that RawSignificand be wide enough to represent all the bits of the entire value.

We also don't have an API on BinaryFloatingPoint to initialize the whole value from a bit pattern; if it's broadly useful (it may be—I'd be open to considering it), we should evaluate this holistically and separately for all floating-point types, or at least for both BinaryFloatingPoint and DecimalFloatingPoint in parallel.

Second, the Boolean here—particularly as a property co-equal to, say, the floating-point sign—is surfacing essentially an implementation detail at the level of the protocol. To top it off, it's literally labeling one of two IEEE-approved significand encodings as the true encoding, with the other being called the false encoding (a bit harsh, no?).

Where it matters at the level of working with decimal floating-point values generically is specifically where the significand bit pattern is being supplied as an argument or accessed as a property—namely, the following APIs:

  init(sign: FloatingPointSign, exponentBitPattern: RawExponent, significandBitPattern: RawSignificand)
  var significandBitPattern: RawSignificand { get }

If—if—it's agreed that users being able to access and supply significand bit patterns encoded in either of these ways is important for all conforming types, then the APIs here could be most appropriately something like:

  init(sign: FloatingPointSign, exponentBitPattern: RawExponent, significandBinaryIntegerBitPattern: RawSignificand)
  var significandBinaryIntegerBitPattern: RawSignificand { get }

  init(sign: FloatingPointSign, exponentBitPattern: RawExponent, significandDenselyPackedBitPattern: RawSignificand)
  var significandDenselyPackedBitPattern: RawSignificand { get }

In this manner, the underlying storage would (as it should) be entirely an implementation detail of the type, and both encodings would be equally available to the user as needed.


There are, from memory, a few IEEE-required operations useful for all decimal floating-point types, specified in the standard, which ought to be in this protocol; mostly to do with the fact that there are multiple equivalent representations of the same value. I do not have the standard open in front of me at present; it is not extremely onerous as a list but it does belong here.

3 Likes

Should ExpressibleByFloatLiteral be excluded? The compiler uses Builtin.FPIEEE64 or Builtin.FPIEEE80, which isn't suitable for decimal types.

1 Like

Good point. I guess we don't really have an "ExpressibleByDecimalLiteral". I know there have been a lot of discussions about generic numeric literals, but nothing concrete so far.

Ok, I'll remove this. It is used in a few places but can be extracted from zero.

Good points, @xwu, I'll remove the extra initializer and use ones similar to what you suggest. I wasn't really happy with this approach anyway.

1 Like

I'd like to dispute this point. Although, BinaryFloatingPoint does not have such an initializer, all the floating point numbers do have such initializers.

They also have the inverse operation of interpreting a floating point number in an integer representation.

These methods and accessors have, however, been removed from the DecimalFloatingPoint protocol and just placed in the separate Decimal number implementations as is being done with their binary counterparts until the Binary and Decimal protocols can be synced to both allow these.

There are several reasons for allowing these methods:

  1. Transferring Decimal numbers from one system to another in a standard binary format.
  2. Facilitating testing where known bit patterns for these numbers are used as part of a test suite.

I have modified my original approach, however, in light of your comments, so that two separate initializers and access methods would be used for the BID vs DPD encodings.

Finally, very few people will ever use a DPD significand encoding, so it is recommended we stick with the standard floating point access method returning an integer BID encoded mantissa only. Similarly, the initializer would use the same integer BID encoding for the significand.

Please have a look at the initial post which has been updated with everyone's suggestions.

2 Likes

They sure do, but afaict there isn't much one would want to do generically with the bit pattern that wouldn't start with breaking it down into the sign, (raw) exponent, and (raw) significand.

1 Like

we should have a FixedWidthFloatingPoint, like FixedWidthInteger that has this requirement.

1 Like

That isn’t the reason we don’t have this requirement currently—as I touch on above, there’s much more that one can do with generically with a binary integer’s bit pattern than with a floating-point value’s bit pattern. The significand and its bit pattern and the exponent and its bit pattern are all available for generic operations, as are initializers that take these as arguments. Those are the counterparts to the API you reference required by FixedWidthInteger.

being able to initialize from a bit pattern is still useful even if the bits themselves are completely opaque.

consider:

protocol BSONDecodable
{
    init(bson:BSON.AnyValue) throws
}

extension BSONDecodable
    where Self:FixedWidthDecimal BitPattern:BSONDecodable
{
    init(bson:BSON.AnyValue) throws
    {
        self.init(bitPattern: try .init(bson: bson))
    }
}
1 Like

This can be achieved with sign, raw exponent, and raw significand separately.

can you sketch out how this might work, if we have, say, the following primitives?

extension Int32:BSONDecodable, BSONEncodable
{
    init(bson:BSON.AnyValue) throws
}
extension Int64:BSONDecodable, BSONEncodable
{
    init(bson:BSON.AnyValue) throws
}

I may be misunderstanding your use case, as I'm not familiar with the details of the format or library you mention; however, based on what you've written here, if the sign, raw exponent, and raw significand are each encodable and decodable (as they would be since they'd be of primitive types as per your link), then so too would be any type that's an aggregate of those three properties. In the simplest implementation, then, you would encode and decode a floating-point value in that manner.

If you need a bit pattern specifically, in the absence of an API that just hands the bit pattern to you, that can also be obtained via a generic implementation from these three properties by relying on documented IEEE semantics. This would also have the desirable (or, even essential) effect of allowing you to store bit patterns in any IEEE interchange format independent of what types are supported by the platform. Put concretely, a correct generic implementation would allow you to obtain (and therefore serialize) the 16-bit binary floating-point bit pattern for a given value whether or not Float16 is available on the platform, and you'd be able to obtain the 128-bit, 160-bit, 192-bit, 224-bit, or 256-bit binary floating-point bit pattern (these are all defined formats in the IEEE standard) for a given value in the absence of any support today or ever for Float128, Float160, etc.

If I'm reading correctly that 128-bit decimal values are supposed to be primitives in the data format you mention, and if you need (or just want) the raw bit pattern of such values to be what's encoded in order to be considered properly encoding a primitive type, then it seems you'd ideally want to implement this logic without relying on the presence of any property, initializer, or availability-gated type anyway.

the use case is to efficiently encode a fixed-width decimal using an existing BSON primitive such as Int64. serializing the sign, exponent, and raw significand separately is not an efficient encoding, you have effectively tripled the storage footprint of the decimal value (not counting additional keying overhead).

i’m having a hard time understanding how this is different from the API @mgriebling has proposed. since we are talking about an “N-bit” decimal, it follows that such a type would have an associated bit pattern type that is some FixedWidthInteger.

(for what it’s worth, BSON actually has a 128-bit IEEE decimal primitive. ironically, i’m currently modeling it as an opaque UInt128 value.)

1 Like

It does not, because FixedWidthInteger has all sorts of protocol requirements beyond being a bag of bits that can be used to build a decimal. There's no need to be able to divide bit patterns of decimal numbers, or count their leading zeros.

Are those things easier to implement than decimal arithmetic? Yes. Does requiring that they be implemented impose some additional burden on the author of a decimal type? Also yes.

A revised DecimalFloatingPoint protocol is in the original post with all comments incorporated. The bidBitPattern and dpdBitPattern have been moved to each of the Decimal instances along with the initializers based on these bit patterns. I'll post the new Decimal32 package in a week of so in the other thread and we can discuss these fields and initializers there.

Unfortunately I don't have access to the IEEE standard so could you please elaborate on which operations are missing?

These API are quite niche and purely additive, so they do not have any real effect on the rest of the design and can be added whenever, but they are:

§5.3.2:
quantize(a, b)
Rounds a to the closest numerical value that can be expressed with the same exponent as b. If the exponent is being decreased and the type isn't wide enough to express a with the exponent of b, the result is NaN.

quantum(a)
The decimal analog of binade. It's the decimal number with significand = 1, exponent = exponent(a), and sign = .plus.

§5.7.3:
sameQuantum(a, b)
true if its arguments have the same exponent.

2 Likes

quantum has been implemented under the name decade.
The rest don't seem super urgent, as you say and could be added.