Skip to content

[SR-15132][SR-15134] Fix Decimal implementation of significand APIs #3068

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 31, 2021

Conversation

xwu
Copy link
Contributor

@xwu xwu commented Aug 29, 2021

This PR fixes two three newly discovered issues with Decimal implementations of APIs that deliberately mimic those of FloatingPoint.

First, as reported in SR-15132, significand shouldn't be negative, but Decimal returns a negative "significand" for a negative value:

import Foundation

let x = -42 as Double
x.significand.sign // plus
let y = -42 as Decimal
y.significand.sign // minus (!)

Second and not reported in a separate bug, for types conforming to FloatingPoint, the initializer init(sign:exponent:significand:) returns a value where the sign is determined by the following notional calculation:

(sign == .minus ? -1 : 1) * significand * radix ** exponent

In other words, the sign of the result is not determined by sign alone, but rather the result is negative if and only if sign and significand.sign are of opposite signs. This is not respected by Decimal:

import Foundation

let x = -42 as Double
Double(sign: .plus, exponent: 0, significand: x) // -42
let y = -42 as Decimal
Decimal(sign: .plus, exponent: 0, significand: y) // 42 (!)

Third, now reported in SR-15134, init(sign:exponent:significand:) currently barfs when exponent is incremented out of range:

import Foundation
let a = Decimal.leastNonzeroMagnitude
print(Decimal(sign: .plus, exponent: -10, significand: a))
// 100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
let b = Decimal.greatestFiniteMagnitude
print(Decimal(sign: .plus, exponent: 10, significand: b))
// 0.00000000000000000000000000000000000000000000000000000000000000000000000000000000340282366920938463463374607431768211455

Instead, the value should properly underflow or overflow (to NaN since Decimal has no representation of infinity), respectively, in line with the behavior of binary floating-point types:

let x = Double.leastNonzeroMagnitude
print(Double(sign: .plus, exponent: -10, significand: x)) // 0.0
let y = Double.greatestFiniteMagnitude
print(Double(sign: .plus, exponent: 10, significand: y)) // inf

To fix this, delegate the work of increasing or decreasing the exponent to NSDecimalMultiplyByPowerOf10.

These issues are addressed for both the overlay and the swift-corelibs implementation; tests are added.

@xwu xwu requested review from stephentyrone and spevans August 29, 2021 03:22
@xwu
Copy link
Contributor Author

xwu commented Aug 29, 2021

@swift-ci test

@xwu
Copy link
Contributor Author

xwu commented Aug 29, 2021

@swift-ci test

@spevans
Copy link
Contributor

spevans commented Aug 29, 2021

I agree your fix is correct in that it makes it compatible with the operation of the types conforming to FloatingPoint as documented: https://developer.apple.com/documentation/swift/float/1847179-init however the parameters are listed as being:

sign: The sign to use for the new value.
exponent: The new value’s exponent.
significand: The new value’s significand.

Do you think the documentation is assuming the significand is always positive? From my reading of it, the sign parameter sets the sign of the resulting value.

I wonder if a better fix would be to trap on a negative significand - although that could be considered API breaking.
I think the documentation may need to be made a bit clearer as it currently seems to contradict itself.

I'd be interested to hear @stephentyrone thoughts on this.

Also, at some point the documentation for https://developer.apple.com/documentation/foundation/decimal/2143030-init needs to be expanded since its single line summary could be interpreted as the sign parameter does indeed set the value's sign.

@xwu
Copy link
Contributor Author

xwu commented Aug 29, 2021

@spevans The Float documentation's description of the arguments assumes (nay, explicitly requires) that significand is a significand (that is, has positive sign and zero exponent). When that's not the case, then neither is sign the sign to use for the new value nor is exponent the new value's exponent. The behavior of the operation when significand isn't a significand is described later on, which explicitly lays out the notional mathematical transformation and notes that the initializer is meant to be our spelling of the IEEE operation scaleB.

It's certainly a possible point of confusion and the documentation of the arguments could use at least a caveat (maybe something like, "The sign to use for the new value when significand has positive sign and zero exponent; for other values of significand, see Description").

However, the single line summary is an accurate description in that, when a user passes the sign, exponent, and significand of existing values to this initializer, it behaves exactly as the summary describes. In fact, the first two bugs in the Decimal implementation "cancel" each other out in this respect such that that's the case even today.

@xwu xwu changed the title [SR-15132] Fix Decimal implementation of significand APIs [SR-15132][SR-15134] Fix Decimal implementation of significand APIs Aug 29, 2021
@xwu
Copy link
Contributor Author

xwu commented Aug 29, 2021

Note that I'm rolling in a fix for yet another bug in init(sign:exponent:significand:); the PR description has been updated accordingly.

@xwu
Copy link
Contributor Author

xwu commented Aug 29, 2021

@swift-ci test

@xwu xwu force-pushed the decimal-significand branch from fc1f6fe to 9fe6948 Compare August 29, 2021 20:07
@xwu
Copy link
Contributor Author

xwu commented Aug 29, 2021

@swift-ci test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants