-
Notifications
You must be signed in to change notification settings - Fork 167
[Low level] Store SourceLanguage properties outside of the main structure use a new bit-set type for "sets" of languages #1355
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
Open
d-ronnqvist
wants to merge
21
commits into
swiftlang:main
Choose a base branch
from
d-ronnqvist:tiny-source-language
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
10dabb4
Implement SourceLanguage as a tiny identifier with properties stored …
d-ronnqvist 4fdb68c
Deprecate mutating SourceLanguage properties
d-ronnqvist 3cb5dd8
Remove unnecessary custom language sorting
d-ronnqvist d1fa784
Avoid accessing the language ID where it's not necessary
d-ronnqvist f27559e
Move "_TinySmallValueIntSet" to DocCCommon as "_FixedSizeBitSet"
d-ronnqvist 4caf109
Support different sizes of _FixedSizeBitSet
d-ronnqvist 090cad1
Add Collection conformance to _FixedSizeBitSet
d-ronnqvist 3e1396a
Specialize a few common collection methods
d-ronnqvist b9a6030
Use masking shifts and overflow adds in other _FixedSizeBitSet implem…
d-ronnqvist 6ac19ea
Add a SmallSourceLanguageSet type
d-ronnqvist d61d3a0
Rely only in `_id` comparison for known language sorting
d-ronnqvist 5273e94
Use new small language set type in link resolution code
d-ronnqvist d6e63ae
Use new small language set type inside ResolvedTopicReference
d-ronnqvist 3d3d580
Use new small language set type inside DocumentationDataVariantsTrait
d-ronnqvist bf5b3b5
Fix implementation code comments about direction for layout of values…
d-ronnqvist dc384b9
Merge branch 'main' into tiny-source-language
d-ronnqvist 793db44
Add new source files to CMakeLists for Windows CI
d-ronnqvist 2c71020
Merge branch 'main' into tiny-source-language
d-ronnqvist 577068b
User simpler parameter name for ResolvedTopicReference initializer
d-ronnqvist 9c8a9c1
Update additional callers to prefer passing source languages rather t…
d-ronnqvist 9dc59b6
Misc minor code comment fixes and clarifications
d-ronnqvist File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,294 @@ | ||
| /* | ||
| This source file is part of the Swift.org open source project | ||
|
|
||
| Copyright (c) 2024-2025 Apple Inc. and the Swift project authors | ||
| Licensed under Apache License v2.0 with Runtime Library Exception | ||
|
|
||
| See https://swift.org/LICENSE.txt for license information | ||
| See https://swift.org/CONTRIBUTORS.txt for Swift project authors | ||
| */ | ||
|
|
||
| /// A fixed size bit set, used for storing very small amounts of small integer values. | ||
| /// | ||
| /// This type can only store values that are `0 ..< Storage.bitWidth` which makes it _unsuitable_ as a general purpose set-algebra type. | ||
| /// However, in specialized cases where the caller can guarantee that all values are in bounds, this type can offer a memory and performance improvement. | ||
| package struct _FixedSizeBitSet<Storage: FixedWidthInteger & Sendable>: Sendable { | ||
| package typealias Element = Int | ||
|
|
||
| package init() {} | ||
|
|
||
| @usableFromInline | ||
| private(set) var storage: Storage = 0 | ||
|
|
||
| @inlinable | ||
| init(storage: Storage) { | ||
| self.storage = storage | ||
| } | ||
| } | ||
|
|
||
| // MARK: Set Algebra | ||
|
|
||
| extension _FixedSizeBitSet: SetAlgebra { | ||
| private static func mask(_ number: Int) -> Storage { | ||
| precondition(number < Storage.bitWidth, "Number \(number) is out of bounds (0..<\(Storage.bitWidth))") | ||
| return 1 &<< number | ||
| } | ||
|
|
||
| @inlinable | ||
| @discardableResult | ||
| mutating package func insert(_ member: Int) -> (inserted: Bool, memberAfterInsert: Int) { | ||
| let newStorage = storage | _FixedSizeBitSet.mask(member) | ||
| defer { | ||
| storage = newStorage | ||
| } | ||
| return (newStorage != storage, member) | ||
| } | ||
|
|
||
| @inlinable | ||
| @discardableResult | ||
| mutating package func remove(_ member: Int) -> Int? { | ||
| let newStorage = storage & ~_FixedSizeBitSet.mask(member) | ||
| defer { | ||
| storage = newStorage | ||
| } | ||
| return newStorage != storage ? member : nil | ||
| } | ||
|
|
||
| @inlinable | ||
| @discardableResult | ||
| mutating package func update(with member: Int) -> Int? { | ||
| let (inserted, _) = insert(member) | ||
| return inserted ? nil : member | ||
| } | ||
|
|
||
| @inlinable | ||
| package func contains(_ member: Int) -> Bool { | ||
| storage & _FixedSizeBitSet.mask(member) != 0 | ||
| } | ||
|
|
||
| @inlinable | ||
| package func isSuperset(of other: Self) -> Bool { | ||
| (storage & other.storage) == other.storage | ||
| } | ||
|
|
||
| @inlinable | ||
| package func union(_ other: Self) -> Self { | ||
| .init(storage: storage | other.storage) | ||
| } | ||
|
|
||
| @inlinable | ||
| package func intersection(_ other: Self) -> Self { | ||
| .init(storage: storage & other.storage) | ||
| } | ||
|
|
||
| @inlinable | ||
| package func symmetricDifference(_ other: Self) -> Self { | ||
| .init(storage: storage ^ other.storage) | ||
| } | ||
|
|
||
| @inlinable | ||
| mutating package func formUnion(_ other: Self) { | ||
| storage |= other.storage | ||
| } | ||
|
|
||
| @inlinable | ||
| mutating package func formIntersection(_ other: Self) { | ||
| storage &= other.storage | ||
| } | ||
|
|
||
| @inlinable | ||
| mutating package func formSymmetricDifference(_ other: Self) { | ||
| storage ^= other.storage | ||
| } | ||
|
|
||
| @inlinable | ||
| package var isEmpty: Bool { | ||
| storage == 0 | ||
| } | ||
| } | ||
|
|
||
| // MARK: Sequence | ||
|
|
||
| extension _FixedSizeBitSet: Sequence { | ||
| @inlinable | ||
| package func makeIterator() -> some IteratorProtocol<Int> { | ||
| _Iterator(set: self) | ||
| } | ||
|
|
||
| private struct _Iterator: IteratorProtocol { | ||
| typealias Element = Int | ||
|
|
||
| private var storage: Storage | ||
| private var current: Int = -1 | ||
|
|
||
| @inlinable | ||
| init(set: _FixedSizeBitSet) { | ||
| self.storage = set.storage | ||
| } | ||
|
|
||
| @inlinable | ||
| mutating func next() -> Int? { | ||
| guard storage != 0 else { | ||
| return nil | ||
| } | ||
| // If the set is somewhat sparse, we can find the next element faster by shifting to the next value. | ||
| // This saves needing to do `contains()` checks for all the numbers since the previous element. | ||
| let amountToShift = storage.trailingZeroBitCount + 1 | ||
| storage &>>= amountToShift | ||
|
|
||
| current &+= amountToShift | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, but to a much smaller degree than above. Here I'm just avoiding the overflow check that Swift performs on additions by default. |
||
| return current | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // MARK: Collection | ||
|
|
||
| extension _FixedSizeBitSet: Collection { | ||
| // Collection conformance requires an `Index` type, that the collection can advance, and `startIndex` and `endIndex` accessors that follow certain requirements. | ||
| // | ||
| // For this design, as a hidden implementation detail, the `Index` holds the bit offset to the element. | ||
|
|
||
| @inlinable | ||
| package subscript(position: Index) -> Int { | ||
| precondition(position.bit < Storage.bitWidth, "Index \(position.bit) out of bounds") | ||
| // Because the index stores the bit offset, which is also the value, we can simply return the value without accessing the storage. | ||
| return Int(position.bit) | ||
| } | ||
|
|
||
| package struct Index: Comparable { | ||
| // The bit offset into the storage to the value | ||
| fileprivate var bit: UInt8 | ||
|
|
||
| package static func < (lhs: Self, rhs: Self) -> Bool { | ||
| lhs.bit < rhs.bit | ||
| } | ||
| } | ||
|
|
||
| @inlinable | ||
| package var startIndex: Index { | ||
| // This is the index (bit offset) to the smallest value in the bit set. | ||
| Index(bit: UInt8(storage.trailingZeroBitCount)) | ||
| } | ||
|
|
||
| @inlinable | ||
| package var endIndex: Index { | ||
| // For a valid collection, the end index is required to be _exactly_ one past the last in-bounds index, meaning; `index(after: LAST_IN-BOUNDS_INDEX)` | ||
| // If the collection implementation doesn't satisfy this requirement, it will have an infinitely long `indices` collection. | ||
| // This either results in infinite implementations or hits internal preconditions in other Swift types that that collection has more elements than its `count`. | ||
|
|
||
| // See `index(after:)` below for explanation of how the index after is calculated. | ||
| let lastInBoundsBit = UInt8(Storage.bitWidth &- storage.leadingZeroBitCount) | ||
| return Index(bit: lastInBoundsBit &+ UInt8((storage &>> lastInBoundsBit).trailingZeroBitCount)) | ||
| } | ||
|
|
||
| @inlinable | ||
| package func index(after currentIndex: Index) -> Index { | ||
| // To advance the index we have to find the next 1 bit _after_ the current bit. | ||
| // For example, consider the following 16 bits, where values are represented from right to left: | ||
| // 0110 0010 0110 0010 | ||
| // | ||
| // To go from the first index to the second index, we need to count the number of 0 bits between it and the next 1 bit. | ||
| // We get this value by shifting the bits by one past the current index: | ||
| // 0110 0010 0110 0010 | ||
| // ╰╴current index | ||
| // 0001 1000 1001 1000 | ||
| // ~~~ 3 trailing zero bits | ||
| // | ||
| // The second index's absolute value is the one past the first index's value plus the number of trailing zero bits in the shifted value. | ||
| // | ||
| // For the third index we repeat the same process, starting by shifting the bits by one past second index: | ||
| // 0110 0010 0110 0010 | ||
| // ╰╴current index | ||
| // 0000 0001 1000 1001 | ||
| // 0 trailing zero bits | ||
| // | ||
| // This time there are no trailing zero bits in the shifted value, so the third index's absolute value is just one past the second index. | ||
| let shift = currentIndex.bit &+ 1 | ||
| return Index(bit: shift &+ UInt8((storage &>> shift).trailingZeroBitCount)) | ||
| } | ||
|
|
||
| @inlinable | ||
| package func formIndex(after index: inout Index) { | ||
| // See `index(after:)` above for explanation. | ||
| index.bit &+= 1 | ||
| index.bit &+= UInt8((storage &>> index.bit).trailingZeroBitCount) | ||
| } | ||
|
|
||
| @inlinable | ||
| package func distance(from start: Index, to end: Index) -> Int { | ||
| // To compute the distance between two indices we have to find the number 1 bit from the start index to (but excluding) the end index. | ||
d-ronnqvist marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // For example, consider the following 16 bits, where values are represented from right to left: | ||
| // 0110 0010 0110 0010 | ||
| // end╶╯ ╰╴start | ||
| // | ||
| // To find the distance between the second index and the fourth index, we need to count the number of 0 bits between it and the next 1 bit. | ||
| // We limit the calculation to this range in two steps. | ||
| // | ||
| // First, we mask out all the bits above the end index: | ||
| // end╶╮ ╭╴start | ||
| // 0110 0010 0110 0010 | ||
| // 0000 0011 1111 1111 mask | ||
| // | ||
| // Because collections can have end indices that extend out-of-bounds we need to clamp the mask from a larger integer type to avoid it wrapping around to 0. | ||
| let mask = Storage(clamping: (1 &<< UInt(end.bit)) &- 1) | ||
| var distance = storage & mask | ||
|
|
||
| // Then, we shift away all the bits below the start index: | ||
| // end╶╮ ╭╴start | ||
| // 0000 0010 0110 0010 | ||
| // 0000 0000 0000 1001 | ||
| distance &>>= start.bit | ||
|
|
||
| // The distance from start to end is the number of 1 bits in this number. | ||
| return distance.nonzeroBitCount | ||
| } | ||
|
|
||
| @inlinable | ||
| package var first: Element? { | ||
| isEmpty ? nil : storage.trailingZeroBitCount | ||
| } | ||
|
|
||
| @inlinable | ||
| package func min() -> Element? { | ||
| first // The elements are already sorted | ||
| } | ||
|
|
||
| @inlinable | ||
| package func sorted() -> [Element] { | ||
| Array(self) // The elements are already sorted | ||
| } | ||
|
|
||
| @inlinable | ||
| package var count: Int { | ||
| storage.nonzeroBitCount | ||
| } | ||
| } | ||
|
|
||
| // MARK: Hashable | ||
|
|
||
| extension _FixedSizeBitSet: Hashable {} | ||
|
|
||
| // MARK: Combinations | ||
|
|
||
| extension _FixedSizeBitSet { | ||
| /// Returns a list of all possible combinations of the elements in the set, in order of increasing number of elements. | ||
| package func allCombinationsOfValues() -> [Self] { | ||
| // Leverage the fact that bits of an Int represent the possible combinations. | ||
| let smallest = storage.trailingZeroBitCount | ||
|
|
||
| var combinations: [Self] = [] | ||
| combinations.reserveCapacity((1 &<< count /*known to be less than Storage.bitWidth */) - 1) | ||
|
|
||
| for raw in 1 ... storage &>> smallest { | ||
| let combination = Self(storage: Storage(raw &<< smallest)) | ||
|
|
||
| // Filter out any combinations that include columns that are the same for all overloads | ||
| guard self.isSuperset(of: combination) else { continue } | ||
|
|
||
| combinations.append(combination) | ||
| } | ||
| // The bits of larger and larger Int values won't be in order of number of bits set, so we sort them. | ||
| return combinations.sorted(by: { $0.count < $1.count }) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| /* | ||
| This source file is part of the Swift.org open source project | ||
|
|
||
| Copyright (c) 2025 Apple Inc. and the Swift project authors | ||
| Licensed under Apache License v2.0 with Runtime Library Exception | ||
|
|
||
| See https://swift.org/LICENSE.txt for license information | ||
| See https://swift.org/CONTRIBUTORS.txt for Swift project authors | ||
| */ | ||
|
|
||
| #if os(macOS) || os(iOS) | ||
| import Darwin | ||
|
|
||
| // This type is designs to have the same API surface as 'Synchronization.Mutex'. | ||
d-ronnqvist marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // It's different from 'SwiftDocC.Synchronized' which requires that the wrapped values is `Copyable`. | ||
| // | ||
| // When we can require macOS 15.0 we can remove this custom type and use 'Synchronization.Mutex' directly on all platforms. | ||
| struct Mutex<Value: ~Copyable>: ~Copyable, @unchecked Sendable { | ||
| private var value: UnsafeMutablePointer<Value> | ||
| private var lock: UnsafeMutablePointer<os_unfair_lock> | ||
|
|
||
| init(_ initialValue: consuming sending Value) { | ||
| value = UnsafeMutablePointer<Value>.allocate(capacity: 1) | ||
| value.initialize(to: initialValue) | ||
|
|
||
| lock = UnsafeMutablePointer<os_unfair_lock>.allocate(capacity: 1) | ||
| lock.initialize(to: os_unfair_lock()) | ||
| } | ||
|
|
||
| deinit { | ||
| value.deallocate() | ||
| lock.deallocate() | ||
| } | ||
|
|
||
| borrowing func withLock<Result: ~Copyable, E: Error>(_ body: (inout sending Value) throws(E) -> sending Result) throws(E) -> sending Result { | ||
| os_unfair_lock_lock(lock) | ||
| defer { os_unfair_lock_unlock(lock) } | ||
|
|
||
| return try body(&value.pointee) | ||
| } | ||
| } | ||
|
|
||
| //extension Mutex: where Value: ~Copyable {} | ||
d-ronnqvist marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| #else | ||
| import Synchronization | ||
|
|
||
| typealias Mutex = Synchronization.Mutex | ||
| #endif | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also very clever! Is the
&there for performance reasons?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes,
&>>performs a "masking" shift, like what you'd get from>>in C.Swift calls the plain
>>operator for a "smart shift" which has these behaviors:Because of the bounds checks and checks for negative inputs, the compiled assembly requires a handful of comparisons before making the shift or returning
0. The masking shift compiles to a single instruction.