|
11 | 11 | /// A type providing unique identifiers for expressions captured during
|
12 | 12 | /// expansion of the `#expect()` and `#require()` macros.
|
13 | 13 | ///
|
14 |
| -/// In the future, this type may use [`StaticBigInt`](https://developer.apple.com/documentation/swift/staticbigint) |
15 |
| -/// as its source representation rather than a string literal. |
| 14 | +/// This type tries to optimize for expressions in shallow syntax trees whose |
| 15 | +/// unique identifiers require 64 bits or fewer. Wider unique identifiers are |
| 16 | +/// stored as arrays of 64-bit words. In the future, this type may use |
| 17 | +/// [`StaticBigInt`](https://developer.apple.com/documentation/swift/staticbigint) |
| 18 | +/// to represent expression identifiers instead. |
16 | 19 | ///
|
17 | 20 | /// - Warning: This type is used to implement the `#expect()` and `#require()`
|
18 | 21 | /// macros. Do not use it directly.
|
19 | 22 | public struct __ExpressionID: Sendable {
|
20 | 23 | /// The ID of the root node in an expression graph.
|
21 | 24 | static var root: Self {
|
22 |
| - "" |
| 25 | + Self(_elements: .none) |
23 | 26 | }
|
24 | 27 |
|
25 |
| - /// The string produced at compile time that encodes the unique identifier of |
26 |
| - /// the represented expression. |
27 |
| - var stringValue: String |
| 28 | + /// An enumeration that attempts to efficiently store the key path elements |
| 29 | + /// corresponding to an expression ID. |
| 30 | + fileprivate enum Elements: Sendable { |
| 31 | + /// This ID does not use any words. |
| 32 | + /// |
| 33 | + /// This case represents the root node in a syntax tree. An instance of |
| 34 | + /// `__ExpressionID` storing this case is implicitly equal to `.root`. |
| 35 | + case none |
28 | 36 |
|
29 |
| - /// The number of bits in a nybble. |
30 |
| - private static var _bitsPerNybble: Int { 4 } |
| 37 | + /// This ID packs its corresponding key path value into a single word whose |
| 38 | + /// value is not `0`. |
| 39 | + case packed(_ word: UInt64) |
| 40 | + |
| 41 | + /// This ID contains key path elements that do not fit in a 64-bit integer, |
| 42 | + /// so they are not packed and map directly to the represented key path. |
| 43 | + indirect case keyPath(_ keyPath: [UInt32]) |
| 44 | + } |
| 45 | + |
| 46 | + /// The elements of this identifier. |
| 47 | + private var _elements: Elements |
31 | 48 |
|
32 | 49 | /// A representation of this instance suitable for use as a key path in an
|
33 | 50 | /// instance of `Graph` where the key type is `UInt32`.
|
34 | 51 | ///
|
35 | 52 | /// The values in this collection, being swift-syntax node IDs, are never more
|
36 | 53 | /// than 32 bits wide.
|
37 | 54 | var keyPath: some RandomAccessCollection<UInt32> {
|
38 |
| - let nybbles = stringValue |
39 |
| - .reversed().lazy |
40 |
| - .compactMap { UInt8(String($0), radix: 16) } |
41 |
| - |
42 |
| - return nybbles |
43 |
| - .enumerated() |
44 |
| - .flatMap { i, nybble in |
45 |
| - let nybbleOffset = i * Self._bitsPerNybble |
46 |
| - return (0 ..< Self._bitsPerNybble).lazy |
47 |
| - .filter { (nybble & (1 << $0)) != 0 } |
48 |
| - .map { UInt32(nybbleOffset + $0) } |
| 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 | + } |
49 | 70 | }
|
| 71 | + |
| 72 | + return result |
| 73 | + } |
| 74 | + |
| 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. |
| 89 | + } |
| 90 | + |
| 91 | + return result |
| 92 | + case let .keyPath(keyPath): |
| 93 | + return keyPath |
| 94 | + } |
50 | 95 | }
|
51 | 96 | }
|
52 | 97 |
|
53 | 98 | // MARK: - Equatable, Hashable
|
54 | 99 |
|
55 | 100 | extension __ExpressionID: Equatable, Hashable {}
|
| 101 | +extension __ExpressionID.Elements: Equatable, Hashable {} |
56 | 102 |
|
57 | 103 | #if DEBUG
|
58 | 104 | // MARK: - CustomStringConvertible, CustomDebugStringConvertible
|
59 | 105 |
|
60 | 106 | 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 | + |
61 | 113 | public var description: String {
|
62 |
| - stringValue |
| 114 | + switch _elements { |
| 115 | + case .none: |
| 116 | + return "0" |
| 117 | + case let .packed(word): |
| 118 | + return String(word, radix: 16) |
| 119 | + case let .keyPath(keyPath): |
| 120 | + return keyPath.lazy |
| 121 | + .map { String($0, radix: 16) } |
| 122 | + .joined(separator: ",") |
| 123 | + } |
63 | 124 | }
|
64 | 125 |
|
65 | 126 | public var debugDescription: String {
|
66 |
| - #""\#(stringValue)" → \#(Array(keyPath))"# |
| 127 | + #""\#(description)" → \#(Array(keyPath))"# |
67 | 128 | }
|
68 | 129 | }
|
69 | 130 | #endif
|
70 | 131 |
|
71 |
| -// MARK: - ExpressibleByStringLiteral |
| 132 | +// MARK: - ExpressibleByIntegerLiteral |
| 133 | + |
| 134 | +extension __ExpressionID: ExpressibleByIntegerLiteral { |
| 135 | + public init(integerLiteral: UInt64) { |
| 136 | + if integerLiteral == 0 { |
| 137 | + self.init(_elements: .none) |
| 138 | + } else { |
| 139 | + self.init(_elements: .packed(integerLiteral)) |
| 140 | + } |
| 141 | + } |
72 | 142 |
|
73 |
| -extension __ExpressionID: ExpressibleByStringLiteral { |
74 |
| - public init(stringLiteral: String) { |
75 |
| - stringValue = stringLiteral |
| 143 | + public init(_ keyPath: UInt32...) { |
| 144 | + self.init(_elements: .keyPath(keyPath)) |
76 | 145 | }
|
77 | 146 | }
|
78 | 147 |
|
0 commit comments