From d73aef069cbb2f986d3f3a0af2adf4c3554d3a8c Mon Sep 17 00:00:00 2001 From: Michael Griebling Date: Tue, 18 Apr 2023 09:42:22 -0400 Subject: [PATCH 1/4] Added improved LosslessStringConvertible init() with order of magnitude faster performance. Removed unnecessary _digits and _digitRadix. Fixed a cut/paste problem in "^=" function. Added test cases for string conversion performance and logical operation tests. --- Sources/BigIntModule/BigInt.swift | 127 +++++++++++++++++++++------- Tests/BigIntTests/BigIntTests.swift | 68 +++++++++++++++ 2 files changed, 166 insertions(+), 29 deletions(-) diff --git a/Sources/BigIntModule/BigInt.swift b/Sources/BigIntModule/BigInt.swift index 2e2721c6..c5b65c44 100644 --- a/Sources/BigIntModule/BigInt.swift +++ b/Sources/BigIntModule/BigInt.swift @@ -30,11 +30,11 @@ public struct BigInt: SignedInteger { words[words.endIndex - 1] > Int.max } - private static let _digits: [BigInt] = (0 ... 36).map { - BigInt(_uncheckedWords: [UInt(bitPattern: $0)]) - } +// private static let _digits: [BigInt] = (0 ... 36).map { +// BigInt(_uncheckedWords: [UInt(bitPattern: $0)]) +// } - private static let _digitRadix = BigInt(_uncheckedWords: [0, 1]) + // private static let _digitRadix = BigInt(_uncheckedWords: [0, 1]) } // MARK: - Basic Behaviors @@ -96,39 +96,108 @@ extension BigInt: LosslessStringConvertible { public init?(_ description: String) { self.init(description, radix: 10) } - + + + //////////////////////////////////////////////////////////////////////////// + /// + /// NEW CODE STARTS + + private static func _pow(_ lhs: UInt, _ rhs: UInt) -> UInt { + // guard rhs>=0 else { return 0 } /* lhs ** (-rhs) = 0 */ + var lexp = rhs + var x = lhs + var y = UInt(1) + while lexp > 0 { + if !lexp.isMultiple(of: 2) { y*=x } + lexp >>= 1 + if lexp > 0 { x=x*x } // could be 2X faster with squared function + } + return y + } + + private func _maxDigits(forRadix radix:Int) -> Int { + switch radix { + case 2: return 62 + case 8: return 20 + case 10: return 18 + case 16: return 14 + default: return 11 // safe but not optimal for other radices + } + } + + /// Speeds the time to initialize a BigInt by about a factor of 16 for + /// the test case of the string for 512! using radix 10. A radix 36 test + /// was 10X faster. BTW, the factorial test code was sped up by almost 2 + /// times (the string to number code accounted for a large part of the + /// total time). public init?(_ description: T, radix: Int = 10) where T: StringProtocol { precondition(2 ... 36 ~= radix, "Radix not in range 2 ... 36") self = 0 - let isNegative = description.hasPrefix("-") let hasPrefix = isNegative || description.hasPrefix("+") - let utf8 = description.utf8.dropFirst(hasPrefix ? 1 : 0) - guard !utf8.isEmpty else { return nil } - - for var byte in utf8 { - switch byte { - case UInt8(ascii: "0") ... UInt8(ascii: "9"): - byte -= UInt8(ascii: "0") - case UInt8(ascii: "A") ... UInt8(ascii: "Z"): - byte -= UInt8(ascii: "A") - byte += 10 - case UInt8(ascii: "a") ... UInt8(ascii: "z"): - byte -= UInt8(ascii: "a") - byte += 10 - default: + var str = description.dropFirst(hasPrefix ? 1 : 0) + guard !str.isEmpty else { return nil } + + /// Main speed-up is due to converting chunks of string via + /// the Int64() initializer instead of a character at a time. + /// We also get free radix digit checks. + let maxDigits = _maxDigits(forRadix: radix) + while !str.isEmpty { + let block = str.prefix(maxDigits) + let size = block.count + str.removeFirst(size) + if let word = UInt(block, radix: radix) { + self *= BigInt(Self._pow(UInt(radix), UInt(size))) + self += BigInt(word) + } else { return nil } - guard byte < radix else { return nil } - self *= BigInt._digits[radix] - self += BigInt._digits[Int(byte)] } if isNegative { self.negate() } } + + /// NEW CODE ENDS + /// + ////////////////////////////////////////////////////////////////////////////////////////////////// + + +//// Original code +// public init?(_ description: T, radix: Int = 10) where T: StringProtocol { +// precondition(2 ... 36 ~= radix, "Radix not in range 2 ... 36") +// +// self = 0 +// +// let isNegative = description.hasPrefix("-") +// let hasPrefix = isNegative || description.hasPrefix("+") +// let utf8 = description.utf8.dropFirst(hasPrefix ? 1 : 0) +// guard !utf8.isEmpty else { return nil } +// +// for var byte in utf8 { +// switch byte { +// case UInt8(ascii: "0") ... UInt8(ascii: "9"): +// byte -= UInt8(ascii: "0") +// case UInt8(ascii: "A") ... UInt8(ascii: "Z"): +// byte -= UInt8(ascii: "A") +// byte += 10 +// case UInt8(ascii: "a") ... UInt8(ascii: "z"): +// byte -= UInt8(ascii: "a") +// byte += 10 +// default: +// return nil +// } +// guard byte < radix else { return nil } +// self *= BigInt._digits[radix] +// self += BigInt._digits[Int(byte)] +// } +// +// if isNegative { +// self.negate() +// } +// } } extension BigInt: Decodable { @@ -158,8 +227,8 @@ extension BigInt: Encodable { extension BigInt: ExpressibleByIntegerLiteral { public init(integerLiteral value: Int) { - if value >= 0, value < BigInt._digits.count { - self = BigInt._digits[value] + if value >= 0, value <= UInt.max { + words = [UInt(value)] // No need for a table lookup here } else { words = [UInt(bitPattern: value)] } @@ -372,8 +441,8 @@ extension BigInt: BinaryInteger { } public init(_ source: T) where T: BinaryInteger { - if source >= 0, source < BigInt._digits.count { - self = BigInt._digits[Int(source)] + if source >= 0, source < UInt.max { + words = [UInt(source)] // no need for _digits } else { words = Words(source.words) if source > 0 && source.words[source.words.endIndex - 1] > Int.max { @@ -464,7 +533,7 @@ extension BigInt: BinaryInteger { BigInt._signExtend(lhsWords: &lhs.words, rhsWords: &rhsWords) for i in 0 ..< rhsWords.count { - lhs.words[i] &= rhsWords[i] + lhs.words[i] ^= rhsWords[i] } BigInt._dropExcessWords(words: &lhs.words) @@ -634,7 +703,7 @@ extension BigInt { /// See _The Art of Computer Programming_ volume 2 by Donald Knuth, Section 4.3.1: The Classical Algorithms @usableFromInline internal static func _div(lhs: BigInt, rhs: BigInt) -> (quotient: BigInt, remainder: BigInt) { - precondition(rhs != _digits[0], "Division by zero error!") + precondition(rhs != 0, "Division by zero error!") if lhs.words.count == 1, rhs.words.count == 1 { let (quot, rem) = Int(bitPattern: lhs.words[0]).quotientAndRemainder(dividingBy: Int(bitPattern: rhs.words[0])) diff --git a/Tests/BigIntTests/BigIntTests.swift b/Tests/BigIntTests/BigIntTests.swift index 444ae38a..81f45395 100644 --- a/Tests/BigIntTests/BigIntTests.swift +++ b/Tests/BigIntTests/BigIntTests.swift @@ -81,6 +81,37 @@ final class BigIntTests: XCTestCase { 7AHD1LS5YKZ66F4UPG0RCBGG000000000000000000000000000000000000000000000000000\ 000000000000000000000000000000000000000000000000000000000000000000000000000 """ + + static let descriptionFactorial512_radix10 = + """ + 3477289793132605363283045917545604711992250655643514570342474831551610412066352543473209850\ + 3395022536443224331102139454529500170207006901326415311326093794135871186404471618686104089\ + 9557497361427588282356254968425012480396855239725120562512065555822121708786443620799246550\ + 9591872320268380814151785881725352800207863134700768597399809657208738499042913738268415847\ + 1279861843038733804232977180172476769109501954575898694273251503355152959500987699927955393\ + 1070378592917099002397061907147143424113252117585950817850896618433994140232823316432187410\ + 3563412623863324969543199731304073425672820273985793825430484568768008623499281404119054312\ + 7619743567460328184253074417752736588572162951225387238661311882154084789749310739838195608\ + 1763695236422795880296204301770808809477147632428639299038833046264585834888158847387737841\ + 8434136648928335862091963669797757488958218269240400578451402875222386750821375703159545267\ + 2743709490491479678264100074077789791913409339353042276095514021138717365004735834735337923\ + 4387609261306673773281412893026941927424000000000000000000000000000000000000000000000000000\ + 000000000000000000000000000000000000000000000000000000000000000000000000000 + """ + static let descriptionFactorial512_radix16 = + """ + 8f9ef398c97defd735dfa6eb05f0aab2afbf84ea79a8e30b10dd6a305f7e1fc1243dc22f19bb1fc48602a8019d5\ + 889719e4de855351eb6fc5db53c44cfc9ad3d56120ebd8e9ac5cfcac1f438a9c62189b0e1987b27344ac0a871b0\ + bafacb4b900597d9408ffe7329be5cf061ccc22723714a2c5576bdb663c32b7e9a9a51f799a6dfd461f7f5805ae\ + 1b9e79950d5552be34cd47ad1b4abd6a731f34825654ad34f676d84533464c50503d7643ffe6a616f055754e580\ + b59be37a89987abde817d5ecc43903a676a7259ca793dc1975dab19b63d0855003af4981fbd726b009309cceb9e\ + 70bd68b548a0f17b78d27da4f1d829ca1adafe45e65a720e2ff815382c9fcbd81342636f6cd97e790ebbaa766f5\ + 122cf6c1585707c09ca491f07603c33c95a4fce736bf54255f16b085aa2ef59cd9883929a14c35be1c7f54547db\ + d2ff9b17bf93175b3950bdf82b97bc3d6ffceb5b3466231ce4c655db08ea6d0ac135113d0253b49f1d15dcf5ffe\ + 372a3edc3ea1a747d78baa21c6163be4580e989c93731057959e3c803a1d292aab93f30419d63f5667be6146889\ + f9532c580769e1a06eb145800000000000000000000000000000000000000000000000000000000000000000000\ + 00000000000000000000000000000000000000000000000000000000000 + """ // MARK: - Basic arithmetic @@ -133,6 +164,16 @@ final class BigIntTests: XCTestCase { """) } } + + func testStringToBigIntImprovements() { + var expectedNumber: BigInt! + + measure { + expectedNumber = BigInt(Self.descriptionFactorial512_radix36, radix: 36) + } + + XCTAssertEqual(expectedNumber, BigInt(Self.descriptionFactorial512_radix10, radix: 10)) + } func testFactorial() { var expectedNumber: BigInt? @@ -279,6 +320,33 @@ final class BigIntTests: XCTestCase { let bar = BigInt(bitPattern: UInt.max) XCTAssertEqual(bar, BigInt(-1)) } + + // MARK: - Testing logical functions + + func testLogical() { + let a = BigInt("7FFF555512340000", radix: 16)! + let b = BigInt("0000ABCD9876FFFF", radix: 16)! + + let aAndb = String(a & b, radix: 16) + let aOrb = String(a | b, radix: 16) + let aXorb = String(a ^ b, radix: 16) + let notb = String(~b, radix: 16) + + let shiftLeft1 = String(a << 16, radix:16) + let shiftLeft2 = String(a << -3, radix:16) + let shiftRight1 = String(a >> 1000, radix:16) + let shiftRight2 = String(a >> -7, radix:16) + + print("a & b = 0x\(aAndb)"); XCTAssertEqual(aAndb, "14510340000") + print("a | b = 0x\(aOrb)"); XCTAssertEqual(aOrb, "7fffffdd9a76ffff") + print("a ^ b = 0x\(aXorb)"); XCTAssertEqual(aXorb, "7ffffe988a42ffff") + print("~b = 0x\(notb)"); XCTAssertEqual(notb, "-abcd98770000") + + print("a << 16 = \(shiftLeft1)"); XCTAssertEqual(shiftLeft1, "7fff5555123400000000") + print("a << -3 = \(shiftLeft2)"); XCTAssertEqual(shiftLeft2, "fffeaaaa2468000") + print("a >> 1000 = \(shiftRight1)"); XCTAssertEqual(shiftRight1, "0") + print("a >> -7 = \(shiftRight2)"); XCTAssertEqual(shiftRight2, "3fffaaaa891a000000") + } // MARK: - Converting to/from textual representations From 400967992d76fa6d75d1954bcab1a2158c1e1297 Mon Sep 17 00:00:00 2001 From: Michael Griebling Date: Wed, 19 Apr 2023 10:44:05 -0400 Subject: [PATCH 2/4] Fixed a negate() corner case issue where a negative integer was turned into a negative. Fixed division/remainder signs after a multi-word division. --- Sources/BigIntModule/BigInt.swift | 48 +++++++++++++++++++++++------ Tests/BigIntTests/BigIntTests.swift | 13 ++++++++ 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/Sources/BigIntModule/BigInt.swift b/Sources/BigIntModule/BigInt.swift index c5b65c44..559923b2 100644 --- a/Sources/BigIntModule/BigInt.swift +++ b/Sources/BigIntModule/BigInt.swift @@ -29,12 +29,6 @@ public struct BigInt: SignedInteger { internal var _isNegative: Bool { words[words.endIndex - 1] > Int.max } - -// private static let _digits: [BigInt] = (0 ... 36).map { -// BigInt(_uncheckedWords: [UInt(bitPattern: $0)]) -// } - - // private static let _digitRadix = BigInt(_uncheckedWords: [0, 1]) } // MARK: - Basic Behaviors @@ -379,6 +373,7 @@ extension BigInt: SignedNumeric { public mutating func negate() { var isOverflow = true + let isNegative = self._isNegative for i in 0 ..< words.count { if isOverflow { (words[i], isOverflow) = (~words[i]).addingReportingOverflow(1) @@ -388,6 +383,12 @@ extension BigInt: SignedNumeric { } BigInt._dropExcessWords(words: &words) + if self != Self.zero && self._isNegative == isNegative { + // Corner case where numbers like `0x8000000000000000 ... 0000` + // remain unchanged after negation so we make sure any negative + // numbers are truly negated into positive numbers + if isNegative { words.append(0) } // make the number positive + } } @inlinable @@ -442,7 +443,7 @@ extension BigInt: BinaryInteger { public init(_ source: T) where T: BinaryInteger { if source >= 0, source < UInt.max { - words = [UInt(source)] // no need for _digits + words = [UInt(source)] } else { words = Words(source.words) if source > 0 && source.words[source.words.endIndex - 1] > Int.max { @@ -732,7 +733,13 @@ extension BigInt { } BigInt._dropExcessWords(words: ") - return (quotient: BigInt(_uncheckedWords: quot), remainder: BigInt(r)) + // signs are based on the Int definitions + switch (lhsIsNeg, rhsIsNeg) { + case (false, true): return (-BigInt(_uncheckedWords: quot), BigInt(r)) + case (false, false): return ( BigInt(_uncheckedWords: quot), BigInt(r)) + case (true, false): return (-BigInt(_uncheckedWords: quot), -BigInt(r)) + case (true, true): return ( BigInt(_uncheckedWords: quot), -BigInt(r)) + } } while rhsWords[rhsWords.endIndex - 1] == 0 { @@ -839,8 +846,14 @@ extension BigInt { BigInt._dropExcessWords(words: ") BigInt._dropExcessWords(words: &rem) - - return (BigInt(_uncheckedWords: quot), BigInt(_uncheckedWords: rem)) + + // signs are based on the Int definitions + switch (lhsIsNeg, rhsIsNeg) { + case (false, true): return (-BigInt(_uncheckedWords: quot), BigInt(_uncheckedWords: rem)) + case (false, false): return ( BigInt(_uncheckedWords: quot), BigInt(_uncheckedWords: rem)) + case (true, false): return (-BigInt(_uncheckedWords: quot), -BigInt(_uncheckedWords: rem)) + case (true, true): return ( BigInt(_uncheckedWords: quot), -BigInt(_uncheckedWords: rem)) + } } private static func _signExtend(lhsWords: inout Words, rhsWords: inout Words) { @@ -877,3 +890,18 @@ extension BigInt { } } } + +//extension BigInt : CustomStringConvertible { +// +// public var description: String { +// /// Kludge fix for negative numbers like `0x8000000000000000 ... 0000` +// /// whose strings otherwise look like this `-(\'((,*+*),-//+)\'+-(*,*+.+\'+-\',+/.-**(0(` +// /// Probably a division/remainder issue in not detecting the right sign +// if self._isNegative && words.count > 1 && self.magnitude == self { +// let num = BigInt(_uncheckedWords: self.words + [0]) // add leading zero word so sign is positive +// return "-" + String(num, radix: 10) +// } +// return String(self, radix: 10) +// } +// +//} diff --git a/Tests/BigIntTests/BigIntTests.swift b/Tests/BigIntTests/BigIntTests.swift index 81f45395..0ca24528 100644 --- a/Tests/BigIntTests/BigIntTests.swift +++ b/Tests/BigIntTests/BigIntTests.swift @@ -116,6 +116,19 @@ final class BigIntTests: XCTestCase { // MARK: - Basic arithmetic func testDivision() { + // Signed division test + let numa = BigInt("-18446744073709551616")! + let dena = BigInt(123) + let expecteda = BigInt(Int64(-149973529054549200)) + XCTAssertEqual(numa / dena, expecteda) + + let numb = BigInt("18446744073709551616")! + let denb = BigInt(-123) + XCTAssertEqual(numb / denb, expecteda) + let expectedb = BigInt(Int64(149973529054549200)) + XCTAssertEqual(numa / denb, expectedb) + + // Previous test cases let num1 = BigInt("18446744073709551616")! let den1 = BigInt(123) let expected1 = BigInt(UInt64(149973529054549200)) From 5e55107fc77914be6b3483ade8d1160f1f922434 Mon Sep 17 00:00:00 2001 From: Michael Griebling Date: Wed, 19 Apr 2023 16:12:57 -0400 Subject: [PATCH 3/4] Fixes issue with FloatingPoint overflow and other integer test overflows. --- Sources/BigIntModule/BigInt.swift | 55 +----------------- Tests/BigIntTests/BigIntTests.swift | 88 +++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 53 deletions(-) diff --git a/Sources/BigIntModule/BigInt.swift b/Sources/BigIntModule/BigInt.swift index 559923b2..a01d3c0e 100644 --- a/Sources/BigIntModule/BigInt.swift +++ b/Sources/BigIntModule/BigInt.swift @@ -157,41 +157,6 @@ extension BigInt: LosslessStringConvertible { /// NEW CODE ENDS /// ////////////////////////////////////////////////////////////////////////////////////////////////// - - -//// Original code -// public init?(_ description: T, radix: Int = 10) where T: StringProtocol { -// precondition(2 ... 36 ~= radix, "Radix not in range 2 ... 36") -// -// self = 0 -// -// let isNegative = description.hasPrefix("-") -// let hasPrefix = isNegative || description.hasPrefix("+") -// let utf8 = description.utf8.dropFirst(hasPrefix ? 1 : 0) -// guard !utf8.isEmpty else { return nil } -// -// for var byte in utf8 { -// switch byte { -// case UInt8(ascii: "0") ... UInt8(ascii: "9"): -// byte -= UInt8(ascii: "0") -// case UInt8(ascii: "A") ... UInt8(ascii: "Z"): -// byte -= UInt8(ascii: "A") -// byte += 10 -// case UInt8(ascii: "a") ... UInt8(ascii: "z"): -// byte -= UInt8(ascii: "a") -// byte += 10 -// default: -// return nil -// } -// guard byte < radix else { return nil } -// self *= BigInt._digits[radix] -// self += BigInt._digits[Int(byte)] -// } -// -// if isNegative { -// self.negate() -// } -// } } extension BigInt: Decodable { @@ -442,7 +407,7 @@ extension BigInt: BinaryInteger { } public init(_ source: T) where T: BinaryInteger { - if source >= 0, source < UInt.max { + if source >= 0 && source < Int.max { words = [UInt(source)] } else { words = Words(source.words) @@ -460,7 +425,7 @@ extension BigInt: BinaryInteger { } public init(truncatingIfNeeded source: T) where T: BinaryInteger { - words = Words(source.words) + self.init(source) //words = Words(source.words) } public var bitWidth: Int { words.count * UInt.bitWidth } @@ -492,7 +457,6 @@ extension BigInt: BinaryInteger { @inlinable public static func % (lhs: BigInt, rhs: BigInt) -> BigInt { let (_, result) = _div(lhs: lhs, rhs: rhs) - return result } @@ -890,18 +854,3 @@ extension BigInt { } } } - -//extension BigInt : CustomStringConvertible { -// -// public var description: String { -// /// Kludge fix for negative numbers like `0x8000000000000000 ... 0000` -// /// whose strings otherwise look like this `-(\'((,*+*),-//+)\'+-(*,*+.+\'+-\',+/.-**(0(` -// /// Probably a division/remainder issue in not detecting the right sign -// if self._isNegative && words.count > 1 && self.magnitude == self { -// let num = BigInt(_uncheckedWords: self.words + [0]) // add leading zero word so sign is positive -// return "-" + String(num, radix: 10) -// } -// return String(self, radix: 10) -// } -// -//} diff --git a/Tests/BigIntTests/BigIntTests.swift b/Tests/BigIntTests/BigIntTests.swift index 0ca24528..d837385d 100644 --- a/Tests/BigIntTests/BigIntTests.swift +++ b/Tests/BigIntTests/BigIntTests.swift @@ -307,6 +307,94 @@ final class BigIntTests: XCTestCase { XCTAssertEqual(-BigInt("-1234567890123456789012345678901234567890")!, +BigInt("+1234567890123456789012345678901234567890")!) } + + func testsFromViolet() { + // -9223372036854775808 = Int64.min, obviously '-Int64.min' overflows + let int: Int64 = -9223372036854775808 + var big = BigInt(int) + XCTAssertEqual(-big, big * -1) + + // 9223372036854775808 = UInt64(1) << Float80.significandBitCount + var int2 : UInt64 = 9223372036854775808 + big = BigInt(int2) + let fromInt = Float80(exactly: int2) // works + let fromBigInt = Float80(exactly: big) // crash (not anymore) + + // 18446744073709551615 = UInt64.max - was crashing + int2 = 18446744073709551615 + big = BigInt(int2) + let revert = UInt64(big) + } + + func test_initFromInt_exactly() { + let int: UInt64 = 18446744073709551614 + let big = BigInt(exactly: int)! + let revert = UInt64(exactly: big) + XCTAssertEqual(int, revert) + } + + func test_initFromInt_clamping() { + let int: UInt64 = 18446744073709551614 + let big = BigInt(clamping: int) + let revert = UInt64(clamping: big) + XCTAssertEqual(int, revert) + } + + func test_initFromInt_truncatingIfNeeded() { + let int: UInt64 = 18446744073709551615 + let big = BigInt(truncatingIfNeeded: int) + let intString = String(int, radix: 10, uppercase: false) + let bigString = String(big, radix: 10, uppercase: false) + XCTAssertEqual(bigString, intString) + } + + func test_node_div_incorrectSign() { + // positive / negative = negative + var lhs = BigInt("18446744073709551615")! + var rhs = BigInt("-1")! + var expected = BigInt("-18446744073709551615")! + XCTAssertEqual(lhs / rhs, expected) + + // negative / positive = negative + lhs = BigInt("-340282366920938463481821351505477763074")! + rhs = BigInt("18446744073709551629")! + expected = BigInt("-18446744073709551604")! + XCTAssertEqual(lhs / rhs, expected) + } + + func test_node_mod_incorrectSign() { + // SMALL % BIG = SMALL + // We need to satisfy: BIG * 0 + result = SMALL -> result = SMALL + // The same, but on the standard Swift.Int to prove the point: + XCTAssertEqual(-1 % 123, -1) + XCTAssertEqual(-1 % -123, -1) + // In general the 'reminder' follows the 'lhs' sign (round toward 0). + // Except for the case where 'lhs' is negative and 'reminder' is 0. + + var lhs = BigInt("-1")! + var rhs = BigInt("18446744073709551615")! + XCTAssertEqual(lhs % rhs, lhs) + + // Also fails if 'rhs' is negative + lhs = BigInt("-7730941133")! + rhs = BigInt("-18446744073709551615")! + XCTAssertEqual(lhs % rhs, lhs) + } + + // From my observations all of the `xor` tests are failing. + func test_node_xor() { + var lhs = BigInt("0")! + var rhs = BigInt("1")! + var expected = BigInt("1")! + XCTAssertEqual(lhs ^ rhs, expected) + XCTAssertEqual(0 ^ 1, 1) // Proof + + lhs = BigInt("0")! + rhs = BigInt("-1")! + expected = BigInt("-1")! + XCTAssertEqual(lhs ^ rhs, expected) + XCTAssertEqual(0 ^ -1, -1) // Proof + } func testHashable() { let foo = BigInt("1234567890123456789012345678901234567890")! From 67b17c2feccebbf3bf9c3ff91856ad51a427b4d7 Mon Sep 17 00:00:00 2001 From: Michael Griebling Date: Wed, 19 Apr 2023 17:25:50 -0400 Subject: [PATCH 4/4] Fix corner-case overflow with Int.min and -1 division. --- Sources/BigIntModule/BigInt.swift | 10 ++- Tests/BigIntTests/BigIntTests.swift | 96 +++++++++++++++++++++++++++-- 2 files changed, 99 insertions(+), 7 deletions(-) diff --git a/Sources/BigIntModule/BigInt.swift b/Sources/BigIntModule/BigInt.swift index a01d3c0e..bf4b39be 100644 --- a/Sources/BigIntModule/BigInt.swift +++ b/Sources/BigIntModule/BigInt.swift @@ -670,9 +670,15 @@ extension BigInt { internal static func _div(lhs: BigInt, rhs: BigInt) -> (quotient: BigInt, remainder: BigInt) { precondition(rhs != 0, "Division by zero error!") + // Speed up single-word divisions if lhs.words.count == 1, rhs.words.count == 1 { - let (quot, rem) = Int(bitPattern: lhs.words[0]).quotientAndRemainder(dividingBy: Int(bitPattern: rhs.words[0])) - return (BigInt(_uncheckedWords: [UInt(bitPattern: quot)]), BigInt(_uncheckedWords: [UInt(bitPattern: rem)])) + // check for corner case that causes overflow: Int.min / -1 + let lhsInt = Int(bitPattern: lhs.words[0]) + let rhsInt = Int(bitPattern: rhs.words[0]) + if !(lhsInt == Int.min && rhsInt == -1) { + let (quot, rem) = lhsInt.quotientAndRemainder(dividingBy: rhsInt) + return (BigInt(_uncheckedWords: [UInt(bitPattern: quot)]), BigInt(_uncheckedWords: [UInt(bitPattern: rem)])) + } } let lhsIsNeg = lhs._isNegative diff --git a/Tests/BigIntTests/BigIntTests.swift b/Tests/BigIntTests/BigIntTests.swift index d837385d..95cbafa7 100644 --- a/Tests/BigIntTests/BigIntTests.swift +++ b/Tests/BigIntTests/BigIntTests.swift @@ -308,7 +308,7 @@ final class BigIntTests: XCTestCase { +BigInt("+1234567890123456789012345678901234567890")!) } - func testsFromViolet() { + func testsWhichCrashed() { // -9223372036854775808 = Int64.min, obviously '-Int64.min' overflows let int: Int64 = -9223372036854775808 var big = BigInt(int) @@ -317,13 +317,20 @@ final class BigIntTests: XCTestCase { // 9223372036854775808 = UInt64(1) << Float80.significandBitCount var int2 : UInt64 = 9223372036854775808 big = BigInt(int2) - let fromInt = Float80(exactly: int2) // works - let fromBigInt = Float80(exactly: big) // crash (not anymore) + let _ = Float80(exactly: int2) // works + let _ = Float80(exactly: big) // crash (not anymore) // 18446744073709551615 = UInt64.max - was crashing int2 = 18446744073709551615 big = BigInt(int2) - let revert = UInt64(big) + let _ = UInt64(big) + + // was generating an overflow + let lhsInt = -9223372036854775808 + let rhsInt = -1 + let lhs = BigInt(lhsInt) + let rhs = BigInt(rhsInt) + _ = lhs / rhs // Overflow } func test_initFromInt_exactly() { @@ -348,7 +355,21 @@ final class BigIntTests: XCTestCase { XCTAssertEqual(bigString, intString) } - func test_node_div_incorrectSign() { + func test_unaryMinus() { + // -9223372036854775808 = Int.min + // 'Int.min' negation overflows - ok now + let int = -9223372036854775808 + let expected = BigInt(int.magnitude) + + let big = -BigInt(int) + XCTAssertEqual(big, expected) + + var negated = BigInt(int) + negated.negate() + XCTAssertEqual(negated, expected, "\(negated) == \(expected)") + } + + func test_div_sign() { // positive / negative = negative var lhs = BigInt("18446744073709551615")! var rhs = BigInt("-1")! @@ -361,7 +382,34 @@ final class BigIntTests: XCTestCase { expected = BigInt("-18446744073709551604")! XCTAssertEqual(lhs / rhs, expected) } + + func test_magnitude() { + let big = BigInt(-9223372036854775808) + let expect = UInt64(9223372036854775808) + XCTAssertEqual(big.magnitude, BigInt(expect)) + } + + // ApplyA_ApplyB_Equals_ApplyAB.swift + func test_a_b_ab() { + // (a/-3) / -5 = a/15 + let lhs = BigInt("-18446744073709551615")! + let a = BigInt(-3) + let b = BigInt(-5) + let ab = BigInt(15) + + let r0 = (lhs / a) / b + let r1 = lhs / ab + XCTAssertEqual(r0, r1) + } + // ApplyA_UndoA.swift + func test_a_undoA() { + let n = BigInt(-1) + let x = BigInt(-9223372036854775808) + XCTAssertEqual(n, (n + x) - x) + XCTAssertEqual(n, (n * x) / x) + } + func test_node_mod_incorrectSign() { // SMALL % BIG = SMALL // We need to satisfy: BIG * 0 + result = SMALL -> result = SMALL @@ -395,6 +443,44 @@ final class BigIntTests: XCTestCase { XCTAssertEqual(lhs ^ rhs, expected) XCTAssertEqual(0 ^ -1, -1) // Proof } + + func test_xor_truthTable() { + let lhsWord : UInt8 = 0b1100 + let rhsWord : UInt8 = 0b1010 + + let lhs = BigInt(lhsWord) + let rhs = BigInt(rhsWord) + + let expected = BigInt(lhsWord ^ rhsWord) + XCTAssertEqual(lhs ^ rhs, expected) + XCTAssertEqual(rhs ^ lhs, expected) + } + + func test_binarySub() { + // https://www.wolframalpha.com/input?i=-922337203685477587+-+%28-9223372036854775808%29 + let lhs = BigInt("-922337203685477587")! + let rhs = BigInt("-9223372036854775808")! + let expected = BigInt("8301034833169298221")! + XCTAssertEqual(lhs - rhs, expected) + // The same on Swift.Int: + XCTAssertEqual(-922337203685477587 - (-9223372036854775808), 8301034833169298221) + } + + func test_binarySub_2() { + typealias Word = UInt + + let intMax = Int.max + let intMaxAsWord = Word(intMax.magnitude) + + // intMax - (-(Word.max - intMaxAsWord)) = + // intMax - (-Word.max + intMaxAsWord) = + // intMax + Word.max - intMaxAsWord = + // Word.max + let max = BigInt(intMax) + let value = -BigInt((Word.max - intMaxAsWord)) + let expected = BigInt(Word.max) + XCTAssertEqual(max - value, expected) + } func testHashable() { let foo = BigInt("1234567890123456789012345678901234567890")!