|  | 
| 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