Skip to content

Commit 1043e9e

Browse files
committed
Improvements and bugfixes for DoubleWidth prototype
These are used only in TestSupport, but it's still nice to get them right. Signed types had a long-standing bug in how overflow was computed for multiplication, and masking shifts would do the wrong thing when the bitwdith was not a power of two and the shift count was negative. I also added implementations of &*, &+, and &-.
1 parent e7af7df commit 1043e9e

File tree

2 files changed

+52
-22
lines changed

2 files changed

+52
-22
lines changed

Sources/_TestSupport/DoubleWidth.swift

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift Numerics open source project
44
//
5-
// Copyright (c) 2017-2021 Apple Inc. and the Swift Numerics project authors
5+
// Copyright (c) 2017-2024 Apple Inc. and the Swift Numerics project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -281,17 +281,11 @@ extension DoubleWidth : FixedWidthInteger {
281281
by rhs: DoubleWidth
282282
) -> (partialValue: DoubleWidth, overflow: Bool) {
283283
let (carry, product) = multipliedFullWidth(by: rhs)
284-
let result = DoubleWidth(truncatingIfNeeded: product)
285-
286-
let isNegative = DoubleWidth.isSigned &&
287-
(self < (0 as DoubleWidth)) != (rhs < (0 as DoubleWidth))
288-
let didCarry = isNegative
289-
? carry != ~(0 as DoubleWidth)
290-
: carry != (0 as DoubleWidth)
291-
let hadPositiveOverflow =
292-
DoubleWidth.isSigned && !isNegative && product.leadingZeroBitCount == 0
293-
294-
return (result, didCarry || hadPositiveOverflow)
284+
let partialValue = DoubleWidth(truncatingIfNeeded: product)
285+
// Overflow has occured if carry is not just the sign-extension of
286+
// partialValue (which is zero when Base is unsigned).
287+
let overflow = carry != (partialValue >> DoubleWidth.bitWidth)
288+
return (partialValue, overflow)
295289
}
296290

297291
public func quotientAndRemainder(
@@ -331,7 +325,11 @@ extension DoubleWidth : FixedWidthInteger {
331325
if DoubleWidth.isSigned && other == -1 && self == .min { return (0, true) }
332326
return (quotientAndRemainder(dividingBy: other).remainder, false)
333327
}
334-
328+
329+
// When using a pre-Swift 6.0 runtime, `&*` is not a protocol requirement of
330+
// FixedWidthInteger, which results in the default implementation of this
331+
// operation ending up recursively calling itself forever. In order to avoid
332+
// this, we keep the concrete implementation around.
335333
public func multipliedFullWidth(
336334
by other: DoubleWidth
337335
) -> (high: DoubleWidth, low: DoubleWidth.Magnitude) {
@@ -453,18 +451,21 @@ extension DoubleWidth : FixedWidthInteger {
453451

454452
/// Returns this value "masked" by its bit width.
455453
///
456-
/// "Masking" notionally involves repeatedly incrementing or decrementing this
457-
/// value by `self.bitWidth` until the result is contained in the range
458-
/// `0..<self.bitWidth`.
454+
/// "Masking" notionally involves repeatedly incrementing or decrementing
455+
/// this value by `self.bitWidth` until the result is contained in the
456+
/// range `0..<self.bitWidth`.
459457
internal func _masked() -> DoubleWidth {
460-
// FIXME(integers): test types with bit widths that aren't powers of 2
458+
let bits = DoubleWidth(DoubleWidth.bitWidth)
461459
if DoubleWidth.bitWidth.nonzeroBitCount == 1 {
462-
return self & DoubleWidth(DoubleWidth.bitWidth &- 1)
460+
return self & (bits &- 1)
463461
}
464-
if DoubleWidth.isSigned && self._storage.high < (0 as High) {
465-
return self % DoubleWidth(DoubleWidth.bitWidth) + self
466-
}
467-
return self % DoubleWidth(DoubleWidth.bitWidth)
462+
let reduced = self % bits
463+
// bitWidth is always positive, but the value being reduced might have
464+
// been negative, in which case reduced will also be negative. We need
465+
// the representative in [0, bitWidth), so conditionally add the count
466+
// to get the positive residue.
467+
if Base.isSigned && reduced < 0 { return reduced &+ bits }
468+
return reduced
468469
}
469470

470471
public static func &<<=(lhs: inout DoubleWidth, rhs: DoubleWidth) {
@@ -590,6 +591,31 @@ extension DoubleWidth : FixedWidthInteger {
590591
precondition(!overflow, "Overflow in %=")
591592
lhs = result
592593
}
594+
595+
public static func &+(
596+
lhs: DoubleWidth, rhs: DoubleWidth
597+
) -> DoubleWidth {
598+
let (low, carry) = lhs.low.addingReportingOverflow(rhs.low)
599+
let high = lhs.high &+ rhs.high &+ (carry ? 1 : 0)
600+
return DoubleWidth(high, low)
601+
}
602+
603+
public static func &-(
604+
lhs: DoubleWidth, rhs: DoubleWidth
605+
) -> DoubleWidth {
606+
let (low, borrow) = lhs.low.subtractingReportingOverflow(rhs.low)
607+
let high = lhs.high &- rhs.high &- (borrow ? 1 : 0)
608+
return DoubleWidth(high, low)
609+
}
610+
611+
public static func &*(
612+
lhs: DoubleWidth, rhs: DoubleWidth
613+
) -> DoubleWidth {
614+
let p00 = lhs.low.multipliedFullWidth(by: rhs.low)
615+
let p10 = lhs.high &* Base(truncatingIfNeeded: rhs.low)
616+
let p01 = Base(truncatingIfNeeded: lhs.low) &* rhs.high
617+
return DoubleWidth(p10 &+ p01 &+ Base(truncatingIfNeeded: p00.high), p00.low)
618+
}
593619

594620
public init(_truncatingBits bits: UInt) {
595621
_storage.low = Low(_truncatingBits: bits)

Tests/IntegerUtilitiesTests/DoubleWidthTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,4 +522,8 @@ final class DoubleWidthTests: XCTestCase {
522522
checkUnsignedIntegerConformance(0 as _UInt128)
523523
checkUnsignedIntegerConformance(0 as _UInt1024)
524524
}
525+
526+
func testMultiplyOverflow() {
527+
XCTAssertFalse(_Int128(-1).multipliedReportingOverflow(by: 0).overflow)
528+
}
525529
}

0 commit comments

Comments
 (0)