Skip to content

[BitCollections] sign extend BitArray conversions to integers wider than a word - #808

Merged
lorentey merged 1 commit into
apple:mainfrom
HuzaifaChaudary:fix/bitarray-wide-signed-conversion
Nov 3, 2026
Merged

lorentey merged 1 commit into
apple:mainfrom
HuzaifaChaudary:fix/bitarray-wide-signed-conversion

Conversation

@HuzaifaChaudary

Copy link
Copy Markdown
Contributor

converting a negative BitArray to an integer type wider than UInt lost the sign above the first word . _convert starts from zero and shifts each word in , and a word is unsigned , so nothing ever sets the bits above it .

let bits: BitArray = "1"
Int128(truncatingIfNeeded: bits) // 18446744073709551615 , should be -1
Int128(exactly: bits)            // nil , should be -1
Int128(bits)                     // traps

on a 32 bit platform UInt is 32 bits , so Int64 would go down the same path . i did not run that one .

the fix starts a negative value out as all ones , so the words shifted in keep the sign . narrower types are not affected , the shift drops those bits like before . the trap message also named BitArray as the type it could not convert to , it names the integer type now .

the new test covers Int128 and UInt128 around the 64 and 128 bit edges , and fails before the change . the existing conversion tests only use Int8 and UInt8 . i also ran a random check of the conversions to Int8 , UInt8 , Int , UInt , Int128 and UInt128 against a plain [Bool] , and only the signed 128 bit ones were off . all 123 BitCollectionsTests pass .

Checklist

  • I've read the Contribution Guidelines
  • My contributions are licensed under the Swift license.
  • I've followed the coding style of the rest of the project.
  • I've added tests covering all new code paths my change adds to the project (if appropriate).
  • I've added benchmarks covering new functionality (if appropriate).
  • I've verified that my change does not break any existing tests or introduce unexplained benchmark regressions.
  • I've updated the documentation if necessary.

…han a word

converting a negative bit array to `Int128` lost the sign above the first
64 bits , so `Int128(truncatingIfNeeded: "1" as BitArray)` gave
18446744073709551615 , not -1 , `Int128(exactly:)` gave nil , and
`Int128(_:)` trapped . a negative value now starts out as all ones , so the
words shifted in keep the sign .

the trap message also named `BitArray` as the type it could not convert
to , it names the integer type now .

@mangeshraut712 mangeshraut712 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I traced _foreachTwosComplementWordDownward (sign-extends the top word via _Word(upTo: end.bit).complement() for negative values) against the new ~.zero prefill: for negative values the prefill survives only in types wider than the array, which is exactly the case being fixed, and it is flushed out by the <<= UInt.bitWidth shifts for equal/narrower widths. I also re-verified _convertExactly's guard (isNegative == (value < 0)) and word-replay loop still agree with the new sign extension for cases like "1" + zeros(127) -> .min and ones(128) -> -1/.max. Nice fix — the \(Self.self) -> (T.self) correction in the fatalError message is a good catch too. One non-blocking suggestion inline.

var value: T = .zero
// Negative values start out with all bits set, so that they remain sign
// extended in types that are wider than the words we shift in.
var value: T = T.isSigned && self.last == true ? ~.zero : .zero

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified: this prefill only affects signed conversions where T is wider than the bit array, since value <<= UInt.bitWidth in the closure flushes all-ones out of any narrower type. Together with the sign-extended top word from _foreachTwosComplementWordDownward, Int128(truncatingIfNeeded: bits("1" + zeros(63))) correctly gives -2^63 rather than +2^63.

Non-blocking suggestion: the doc comments for init(truncatingIfNeeded:) / init?(exactly:) still only show same-width examples (e.g. Int8 cases). Since this PR introduces widening sign-extension semantics, one example pair like Int128(truncatingIfNeeded: "1" as BitArray) == -1 and Int128(exactly: "0" + ones(127)) == .max would lock in the new behavior for future readers.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for checking it . the examples already cover this , Int8(truncatingIfNeeded: "1" as BitArray) is -1 , a 1 bit array sign extended to 8 bits . the pull request does not change what the docs say , it only makes types wider than a word follow it , so i would leave the examples as they are . the new test pins the Int128 and UInt128 cases .

@lorentey lorentey left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, thank you!

@lorentey lorentey added this to the 1.7.6 milestone Nov 3, 2026
@lorentey
lorentey merged commit 9d026ae into apple:main Nov 3, 2026
81 of 83 checks passed
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.

3 participants