Skip to content

Commit ff66fb8

Browse files
committed
Implement a zero-allocation collection abstraction for expression IDs
1 parent 29416d7 commit ff66fb8

File tree

1 file changed

+83
-55
lines changed

1 file changed

+83
-55
lines changed

Sources/Testing/SourceAttribution/ExpressionID.swift

Lines changed: 83 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
public struct __ExpressionID: Sendable {
2323
/// The ID of the root node in an expression graph.
2424
static var root: Self {
25-
Self(_elements: .none)
25+
Self(elements: .none)
2626
}
2727

2828
/// An enumeration that attempts to efficiently store the key path elements
@@ -44,74 +44,102 @@ public struct __ExpressionID: Sendable {
4444
}
4545

4646
/// The elements of this identifier.
47-
private var _elements: Elements
47+
fileprivate var elements: Elements
48+
}
4849

49-
/// A representation of this instance suitable for use as a key path in an
50-
/// instance of `Graph` where the key type is `UInt32`.
50+
// MARK: - Equatable, Hashable
51+
52+
extension __ExpressionID: Equatable, Hashable {}
53+
extension __ExpressionID.Elements: Equatable, Hashable {}
54+
55+
// MARK: - Collection
56+
57+
extension __ExpressionID {
58+
/// A type representing the elements in a key path produced from the unique
59+
/// identifier of an expression.
5160
///
52-
/// The values in this collection, being swift-syntax node IDs, are never more
53-
/// than 32 bits wide.
54-
var keyPath: some RandomAccessCollection<UInt32> {
55-
// Helper function to unpack a sequence of words into bit indices for use as
56-
// a Graph's key path.
57-
func makeKeyPath(from words: some RandomAccessCollection<UInt64>) -> [UInt32] {
58-
// Assume approximately 1/4 of the bits are populated. We can always tweak
59-
// this guesstimate after gathering more real-world data.
60-
var result = [UInt32]()
61-
result.reserveCapacity((words.count * UInt64.bitWidth) / 4)
62-
63-
for (bitOffset, word) in words.enumerated() {
64-
var word = word
65-
while word != 0 {
66-
let bit = word.trailingZeroBitCount
67-
result.append(UInt32(bit + bitOffset))
68-
word = word & (word &- 1) // Mask off the bit we just counted.
69-
}
61+
/// Instances of this type can be used to produce keys and key paths for an
62+
/// instance of `Graph` whose key type is `UInt32`.
63+
fileprivate struct KeyPath: Collection {
64+
/// Underlying storage for the collection.
65+
var elements: __ExpressionID.Elements
66+
67+
var underestimatedCount: Int {
68+
count
69+
}
70+
71+
var count: Int {
72+
switch elements {
73+
case .none:
74+
0
75+
case let .packed(word):
76+
word.nonzeroBitCount
77+
case let .keyPath(keyPath):
78+
keyPath.count
7079
}
80+
}
7181

72-
return result
82+
var startIndex: Int {
83+
switch elements {
84+
case .none, .keyPath:
85+
0
86+
case let .packed(word):
87+
word.trailingZeroBitCount
88+
}
7389
}
7490

75-
switch _elements {
76-
case .none:
77-
return []
78-
case let .packed(word):
79-
// Assume approximately 1/4 of the bits are populated. We can always tweak
80-
// this guesstimate after gathering more real-world data.
81-
var result = [UInt32]()
82-
result.reserveCapacity(UInt64.bitWidth / 4)
83-
84-
var word = word
85-
while word != 0 {
86-
let bit = word.trailingZeroBitCount
87-
result.append(UInt32(bit))
88-
word = word & (word &- 1) // Mask off the bit we just counted.
91+
var endIndex: Int {
92+
switch elements {
93+
case .none:
94+
0
95+
case .packed:
96+
UInt64.bitWidth
97+
case let .keyPath(keyPath):
98+
keyPath.count
8999
}
100+
}
90101

91-
return result
92-
case let .keyPath(keyPath):
93-
return keyPath
102+
func index(after i: Int) -> Int {
103+
switch elements {
104+
case .none, .keyPath:
105+
return i + 1
106+
case let .packed(word):
107+
// Mask off the low bits including the one at `i`. The trailing zero
108+
// count of the resulting value equals the next actual bit index.
109+
let nextIndex = i + 1
110+
let maskedWord = word & (~0 << nextIndex)
111+
return maskedWord.trailingZeroBitCount
112+
}
94113
}
95-
}
96-
}
97114

98-
// MARK: - Equatable, Hashable
115+
subscript(position: Int) -> UInt32 {
116+
switch elements {
117+
case .none:
118+
fatalError("Unreachable")
119+
case .packed:
120+
UInt32(position)
121+
case let .keyPath(keyPath):
122+
keyPath[position]
123+
}
124+
}
125+
}
99126

100-
extension __ExpressionID: Equatable, Hashable {}
101-
extension __ExpressionID.Elements: Equatable, Hashable {}
127+
/// A representation of this instance suitable for use as a key path in an
128+
/// instance of `Graph` where the key type is `UInt32`.
129+
///
130+
/// The values in this collection, being swift-syntax node IDs, are never more
131+
/// than 32 bits wide.
132+
var keyPath: some Collection<UInt32> {
133+
KeyPath(elements: elements)
134+
}
135+
}
102136

103137
#if DEBUG
104138
// MARK: - CustomStringConvertible, CustomDebugStringConvertible
105139

106140
extension __ExpressionID: CustomStringConvertible, CustomDebugStringConvertible {
107-
/// The number of bits in a nybble.
108-
private static var _bitsPerNybble: Int { 4 }
109-
110-
/// The number of nybbles in a word.
111-
private static var _nybblesPerWord: Int { UInt64.bitWidth / _bitsPerNybble }
112-
113141
public var description: String {
114-
switch _elements {
142+
switch elements {
115143
case .none:
116144
return "0"
117145
case let .packed(word):
@@ -134,14 +162,14 @@ extension __ExpressionID: CustomStringConvertible, CustomDebugStringConvertible
134162
extension __ExpressionID: ExpressibleByIntegerLiteral {
135163
public init(integerLiteral: UInt64) {
136164
if integerLiteral == 0 {
137-
self.init(_elements: .none)
165+
self.init(elements: .none)
138166
} else {
139-
self.init(_elements: .packed(integerLiteral))
167+
self.init(elements: .packed(integerLiteral))
140168
}
141169
}
142170

143171
public init(_ keyPath: UInt32...) {
144-
self.init(_elements: .keyPath(keyPath))
172+
self.init(elements: .keyPath(keyPath))
145173
}
146174
}
147175

0 commit comments

Comments
 (0)