Skip to content

SR-13837: Swift Decimal type crashes on a specific double value #2926

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 1 commit into from
Dec 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions Sources/Foundation/Decimal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public struct Decimal {
return Int32(__exponent)
}
set {
__exponent = Int8(truncatingIfNeeded: newValue)
__exponent = Int8(newValue)
}
}

Expand Down Expand Up @@ -83,8 +83,9 @@ public struct Decimal {
}

public init(_exponent: Int32, _length: UInt32, _isNegative: UInt32, _isCompact: UInt32, _reserved: UInt32, _mantissa: (UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16)) {
precondition(_length <= 15)
self._mantissa = _mantissa
self.__exponent = Int8(truncatingIfNeeded: _exponent)
self.__exponent = Int8(_exponent)
self.__lengthAndFlags = UInt8(_length & 0b1111)
self.__reserved = 0
self._isNegative = _isNegative
Expand Down Expand Up @@ -625,16 +626,36 @@ extension Decimal {
self = Decimal()
let negative = value < 0
var val = negative ? -1 * value : value
var exponent = 0
var exponent: Int8 = 0

// Try to get val as close to UInt64.max whilst adjusting the exponent
// to reduce the number of digits after the decimal point.
while val < Double(UInt64.max - 1) {
guard exponent > Int8.min else {
setNaN()
return
}
val *= 10.0
exponent -= 1
}
while Double(UInt64.max - 1) < val {
while Double(UInt64.max) <= val {
guard exponent < Int8.max else {
setNaN()
return
}
val /= 10.0
exponent += 1
}
var mantissa = UInt64(val)

var mantissa: UInt64
let maxMantissa = Double(UInt64.max).nextDown
if val > maxMantissa {
// UInt64(Double(UInt64.max)) gives an overflow error, this is the largest
// mantissa that can be set.
mantissa = UInt64(maxMantissa)
} else {
mantissa = UInt64(val)
}

var i: Int32 = 0
// This is a bit ugly but it is the closest approximation of the C
Expand Down Expand Up @@ -1922,6 +1943,7 @@ extension Decimal {
fileprivate static let maxSize: UInt32 = UInt32(NSDecimalMaxSize)

fileprivate init(length: UInt32, mantissa: (UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16)) {
precondition(length <= 15)
self._mantissa = mantissa
self.__exponent = 0
self.__lengthAndFlags = 0
Expand Down
57 changes: 55 additions & 2 deletions Tests/Foundation/Tests/TestDecimal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ class TestDecimal: XCTestCase {
XCTAssertEqual(d1._exponent, 0)
XCTAssertEqual(d1._length, 4)
}

func test_Constants() {
XCTAssertEqual(8, NSDecimalMaxSize)
XCTAssertEqual(32767, NSDecimalNoScale)
Expand Down Expand Up @@ -217,8 +218,8 @@ class TestDecimal: XCTestCase {
let reserved: UInt32 = (1<<18 as UInt32) + (1<<17 as UInt32) + 1
let mantissa: (UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16) = (6, 7, 8, 9, 10, 11, 12, 13)
var explicit = Decimal(
_exponent: 0x17f,
_length: 0xff,
_exponent: 0x7f,
_length: 0x0f,
_isNegative: 3,
_isCompact: 4,
_reserved: reserved,
Expand Down Expand Up @@ -501,6 +502,11 @@ class TestDecimal: XCTestCase {
XCTAssertTrue(NSDecimalIsNotANumber(&result), "NaN e5")

XCTAssertFalse(Double(truncating: NSDecimalNumber(decimal: Decimal(0))).isNaN)
XCTAssertTrue(Decimal(Double.leastNonzeroMagnitude).isNaN)
XCTAssertTrue(Decimal(Double.leastNormalMagnitude).isNaN)
XCTAssertTrue(Decimal(Double.greatestFiniteMagnitude).isNaN)
XCTAssertTrue(Decimal(Double("1e-129")!).isNaN)
XCTAssertTrue(Decimal(Double("0.1e-128")!).isNaN)
}

func test_NegativeAndZeroMultiplication() {
Expand Down Expand Up @@ -827,6 +833,52 @@ class TestDecimal: XCTestCase {
XCTAssertEqual(1, negativeSix.raising(toPower: 0))
}

func test_parseDouble() throws {
XCTAssertEqual(Decimal(Double(0.0)), Decimal(Int.zero))
XCTAssertEqual(Decimal(Double(-0.0)), Decimal(Int.zero))

// These values can only be represented as Decimal.nan
XCTAssertEqual(Decimal(Double.nan), Decimal.nan)
XCTAssertEqual(Decimal(Double.signalingNaN), Decimal.nan)

// These values are out out range for Decimal
XCTAssertEqual(Decimal(-Double.leastNonzeroMagnitude), Decimal.nan)
XCTAssertEqual(Decimal(Double.leastNonzeroMagnitude), Decimal.nan)
XCTAssertEqual(Decimal(-Double.leastNormalMagnitude), Decimal.nan)
XCTAssertEqual(Decimal(Double.leastNormalMagnitude), Decimal.nan)
XCTAssertEqual(Decimal(-Double.greatestFiniteMagnitude), Decimal.nan)
XCTAssertEqual(Decimal(Double.greatestFiniteMagnitude), Decimal.nan)

// SR-13837
let testDoubles: [(Double, String)] = [
(1.8446744073709550E18, "1844674407370954752"),
(1.8446744073709551E18, "1844674407370954752"),
(1.8446744073709552E18, "1844674407370955264"),
(1.8446744073709553E18, "1844674407370955264"),
(1.8446744073709554E18, "1844674407370955520"),
(1.8446744073709555E18, "1844674407370955520"),

(1.8446744073709550E19, "18446744073709547520"),
(1.8446744073709551E19, "18446744073709552640"),
(1.8446744073709552E19, "18446744073709552640"),
(1.8446744073709553E19, "18446744073709552640"),
(1.8446744073709554E19, "18446744073709555200"),
(1.8446744073709555E19, "18446744073709555200"),

(1.8446744073709550E20, "184467440737095526400"),
(1.8446744073709551E20, "184467440737095526400"),
(1.8446744073709552E20, "184467440737095526400"),
(1.8446744073709553E20, "184467440737095526400"),
(1.8446744073709554E20, "184467440737095552000"),
(1.8446744073709555E20, "184467440737095552000"),
]

for (d, s) in testDoubles {
XCTAssertEqual(Decimal(d), Decimal(string: s))
XCTAssertEqual(Decimal(d).description, try XCTUnwrap(Decimal(string: s)).description)
}
}

func test_doubleValue() {
XCTAssertEqual(NSDecimalNumber(decimal:Decimal(0)).doubleValue, 0)
XCTAssertEqual(NSDecimalNumber(decimal:Decimal(1)).doubleValue, 1)
Expand Down Expand Up @@ -1302,6 +1354,7 @@ class TestDecimal: XCTestCase {
("test_SimpleMultiplication", test_SimpleMultiplication),
("test_SmallerNumbers", test_SmallerNumbers),
("test_ZeroPower", test_ZeroPower),
("test_parseDouble", test_parseDouble),
("test_doubleValue", test_doubleValue),
("test_NSDecimalNumberValues", test_NSDecimalNumberValues),
("test_bridging", test_bridging),
Expand Down
2 changes: 1 addition & 1 deletion Tests/Foundation/Tests/TestJSONEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class TestJSONEncoder : XCTestCase {
_testFragment(value: true, fragment: "true")
_testFragment(value: Float(1), fragment: "1")
_testFragment(value: Double(2), fragment: "2")
_testFragment(value: Decimal(Double.leastNormalMagnitude), fragment: "0.0000000000000000000000000000000000000000000000000002225073858507201792")
_testFragment(value: Decimal(Double(Float.leastNormalMagnitude)), fragment: "0.000000000000000000000000000000000000011754943508222875648")
_testFragment(value: "test", fragment: "\"test\"")
let v: Int? = nil
_testFragment(value: v, fragment: "null")
Expand Down
33 changes: 17 additions & 16 deletions Tests/Foundation/Tests/TestJSONSerialization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -968,8 +968,9 @@ extension TestJSONSerialization {
XCTAssertTrue(JSONSerialization.isValidJSONObject([NSNumber(value: true), NSNumber(value: Float.greatestFiniteMagnitude), NSNumber(value: Double.greatestFiniteMagnitude)]))
XCTAssertTrue(JSONSerialization.isValidJSONObject([NSNumber(value: Int.max), NSNumber(value: Int8.max), NSNumber(value: Int16.max), NSNumber(value: Int32.max), NSNumber(value: Int64.max)]))
XCTAssertTrue(JSONSerialization.isValidJSONObject([NSNumber(value: UInt.max), NSNumber(value: UInt8.max), NSNumber(value: UInt16.max), NSNumber(value: UInt32.max), NSNumber(value: UInt64.max)]))
XCTAssertTrue(JSONSerialization.isValidJSONObject([NSDecimalNumber(booleanLiteral: true), NSDecimalNumber(decimal: Decimal.greatestFiniteMagnitude), NSDecimalNumber(floatLiteral: Double.greatestFiniteMagnitude), NSDecimalNumber(integerLiteral: Int.min)]))
XCTAssertTrue(JSONSerialization.isValidJSONObject([Decimal(123), Decimal(Double.leastNonzeroMagnitude)]))
XCTAssertTrue(JSONSerialization.isValidJSONObject([NSDecimalNumber(booleanLiteral: true), NSDecimalNumber(decimal: Decimal.greatestFiniteMagnitude)]))
XCTAssertTrue(JSONSerialization.isValidJSONObject([NSDecimalNumber(floatLiteral: Double(Float.greatestFiniteMagnitude)), NSDecimalNumber(integerLiteral: Int.min)]))
XCTAssertTrue(JSONSerialization.isValidJSONObject([Decimal(123), Decimal(Double(Float.leastNonzeroMagnitude))]))

XCTAssertFalse(JSONSerialization.isValidJSONObject(Float.nan))
XCTAssertFalse(JSONSerialization.isValidJSONObject(Float.infinity))
Expand Down Expand Up @@ -1320,19 +1321,19 @@ extension TestJSONSerialization {
}

func test_serialize_NSDecimalNumber() {
let dn0: [Any] = [NSDecimalNumber(floatLiteral: -Double.leastNonzeroMagnitude)]
let dn1: [Any] = [NSDecimalNumber(floatLiteral: Double.leastNonzeroMagnitude)]
let dn2: [Any] = [NSDecimalNumber(floatLiteral: -Double.leastNormalMagnitude)]
let dn3: [Any] = [NSDecimalNumber(floatLiteral: Double.leastNormalMagnitude)]
let dn4: [Any] = [NSDecimalNumber(floatLiteral: -Double.greatestFiniteMagnitude)]
let dn5: [Any] = [NSDecimalNumber(floatLiteral: Double.greatestFiniteMagnitude)]

XCTAssertEqual(try trySerialize(dn0), "[-0.00000000000000000000000000000000000000000000000000000000000000000004940656458412464128]")
XCTAssertEqual(try trySerialize(dn1), "[0.00000000000000000000000000000000000000000000000000000000000000000004940656458412464128]")
XCTAssertEqual(try trySerialize(dn2), "[-0.0000000000000000000000000000000000000000000000000002225073858507201792]")
XCTAssertEqual(try trySerialize(dn3), "[0.0000000000000000000000000000000000000000000000000002225073858507201792]")
XCTAssertEqual(try trySerialize(dn4), "[-17976931348623167488000000000000000000000000000000000]")
XCTAssertEqual(try trySerialize(dn5), "[17976931348623167488000000000000000000000000000000000]")
let dn0: [Any] = [NSDecimalNumber(floatLiteral: Double(-Float.leastNonzeroMagnitude))]
let dn1: [Any] = [NSDecimalNumber(floatLiteral: Double(Float.leastNonzeroMagnitude))]
let dn2: [Any] = [NSDecimalNumber(floatLiteral: Double(-Float.leastNormalMagnitude))]
let dn3: [Any] = [NSDecimalNumber(floatLiteral: Double(Float.leastNormalMagnitude))]
let dn4: [Any] = [NSDecimalNumber(floatLiteral: Double(-Float.greatestFiniteMagnitude))]
let dn5: [Any] = [NSDecimalNumber(floatLiteral: Double(Float.greatestFiniteMagnitude))]

XCTAssertEqual(try trySerialize(dn0), "[-0.0000000000000000000000000000000000000000000014012984643248173056]")
XCTAssertEqual(try trySerialize(dn1), "[0.0000000000000000000000000000000000000000000014012984643248173056]")
XCTAssertEqual(try trySerialize(dn2), "[-0.000000000000000000000000000000000000011754943508222875648]")
XCTAssertEqual(try trySerialize(dn3), "[0.000000000000000000000000000000000000011754943508222875648]")
XCTAssertEqual(try trySerialize(dn4), "[-340282346638528921600000000000000000000]")
XCTAssertEqual(try trySerialize(dn5), "[340282346638528921600000000000000000000]")
XCTAssertEqual(try trySerialize([NSDecimalNumber(string: "0.0001"), NSDecimalNumber(string: "0.00"), NSDecimalNumber(string: "-0.0")]), "[0.0001,0,0]")
XCTAssertEqual(try trySerialize([NSDecimalNumber(integerLiteral: Int(Int16.min)), NSDecimalNumber(integerLiteral: 0), NSDecimalNumber(integerLiteral: Int(Int16.max))]), "[-32768,0,32767]")
XCTAssertEqual(try trySerialize([NSDecimalNumber(booleanLiteral: true), NSDecimalNumber(booleanLiteral: false)]), "[1,0]")
Expand Down Expand Up @@ -1379,7 +1380,7 @@ extension TestJSONSerialization {
XCTAssertEqual(try trySerialize(true, options: .fragmentsAllowed), "true")
XCTAssertEqual(try trySerialize(Float(1), options: .fragmentsAllowed), "1")
XCTAssertEqual(try trySerialize(Double(2), options: .fragmentsAllowed), "2")
XCTAssertEqual(try trySerialize(Decimal(Double.leastNormalMagnitude), options: .fragmentsAllowed), "0.0000000000000000000000000000000000000000000000000002225073858507201792")
XCTAssertEqual(try trySerialize(Decimal(Double(Float.leastNormalMagnitude)), options: .fragmentsAllowed), "0.000000000000000000000000000000000000011754943508222875648")
XCTAssertEqual(try trySerialize("test", options: .fragmentsAllowed), "\"test\"")
}

Expand Down