Skip to content

Commit 6144666

Browse files
authored
Merge pull request #595 from rintaro/syntaxtext-equals
SyntaxText: improve SyntaxText.== function
2 parents 23d38a1 + dadd43f commit 6144666

File tree

2 files changed

+49
-3
lines changed

2 files changed

+49
-3
lines changed

Sources/SwiftSyntax/SyntaxText.swift

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ public struct SyntaxText {
5959
}
6060

6161
/// Base address of the memory range this string refers to.
62+
///
63+
/// If the `baseAddress` is `nil`, the text is empty. However, text can be
64+
/// `isEmpty` even with a non-`nil` base address.
6265
public var baseAddress: UnsafePointer<UInt8>? {
6366
buffer.baseAddress
6467
}
@@ -148,10 +151,19 @@ extension SyntaxText: Hashable {
148151
if lhs.buffer.count != rhs.buffer.count {
149152
return false
150153
}
151-
if lhs.isEmpty || lhs.buffer.baseAddress == rhs.buffer.baseAddress {
154+
guard let lBase = lhs.baseAddress, let rBase = rhs.baseAddress else {
155+
// If either `baseAddress` is `nil`, both are empty so returns `true`.
152156
return true
153157
}
154-
return compareMemory(lhs.baseAddress!, rhs.baseAddress!, lhs.count)
158+
// We don't do `lhs.baseAddress == rhs.baseAddress` shortcut, because in
159+
// SwiftSyntax use cases, comparing the same SyntaxText instances is
160+
// extremely rare, and checking it causes extra branch.
161+
// The most common usage is comparing parsed text with a static text e.g.
162+
// `token.text == "func"`. In such cases `compareMemory`(`memcmp`) is
163+
// optimzed to a `cmp` or similar opcode if either operand is a short static
164+
// text. So the same-baseAddress shortcut doesn't give us a huge performance
165+
// boost even if they actually refer the same memory.
166+
return compareMemory(lBase, rBase, lhs.count)
155167
}
156168

157169
public func hash(into hasher: inout Hasher) {
@@ -209,7 +221,7 @@ extension String {
209221
private func compareMemory(
210222
_ s1: UnsafePointer<UInt8>, _ s2: UnsafePointer<UInt8>, _ count: Int
211223
) -> Bool {
212-
assert(count > 0)
224+
assert(count >= 0)
213225
#if canImport(Darwin)
214226
return Darwin.memcmp(s1, s2, count) == 0
215227
#elseif canImport(Glibc)

Tests/SwiftSyntaxTest/SyntaxTextTests.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,40 @@ final class SyntaxTextTests: XCTestCase {
4545
XCTAssertEqual(SyntaxText(rebasing: text[2..<2]), SyntaxText(rebasing: text[3..<3]))
4646
}
4747

48+
func testEmptyCompare() throws {
49+
let text: SyntaxText = "0123456789"
50+
51+
let emptyDefault = SyntaxText()
52+
let emptyStatic: SyntaxText = ""
53+
let emptySlice1 = SyntaxText(rebasing: text[1..<1])
54+
let emptySlice2 = SyntaxText(rebasing: text[6..<6])
55+
56+
XCTAssertTrue(emptyDefault.baseAddress == nil && emptyDefault.isEmpty)
57+
XCTAssertTrue(emptyStatic.baseAddress != nil && emptyStatic.isEmpty)
58+
XCTAssertTrue(emptySlice1.baseAddress != nil && emptySlice1.isEmpty)
59+
XCTAssertTrue(emptySlice2.baseAddress != nil && emptySlice2.isEmpty)
60+
61+
XCTAssertTrue(emptyDefault == emptyDefault)
62+
XCTAssertTrue(emptyDefault == emptyStatic)
63+
XCTAssertTrue(emptyDefault == emptySlice1)
64+
XCTAssertTrue(emptyDefault == emptySlice2)
65+
66+
XCTAssertTrue(emptyStatic == emptyDefault)
67+
XCTAssertTrue(emptyStatic == emptyStatic)
68+
XCTAssertTrue(emptyStatic == emptySlice1)
69+
XCTAssertTrue(emptyStatic == emptySlice2)
70+
71+
XCTAssertTrue(emptySlice1 == emptyDefault)
72+
XCTAssertTrue(emptySlice1 == emptyStatic)
73+
XCTAssertTrue(emptySlice1 == emptySlice1)
74+
XCTAssertTrue(emptySlice1 == emptySlice2)
75+
76+
XCTAssertTrue(emptySlice2 == emptyDefault)
77+
XCTAssertTrue(emptySlice2 == emptyStatic)
78+
XCTAssertTrue(emptySlice2 == emptySlice1)
79+
XCTAssertTrue(emptySlice2 == emptySlice2)
80+
}
81+
4882
func testFirstRange() throws {
4983
let text: SyntaxText = "0123456789012345"
5084

0 commit comments

Comments
 (0)