Skip to content

Commit 56da8c0

Browse files
committed
[stdlib] string identical
1 parent 644f364 commit 56da8c0

File tree

6 files changed

+232
-1
lines changed

6 files changed

+232
-1
lines changed

benchmark/single-source/StringTests.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ public var benchmarks: [BenchmarkInfo] {
4040
runFunction: run_StringHasSuffixUnicode,
4141
tags: [.validation, .api, .String],
4242
legacyFactor: 1000),
43+
BenchmarkInfo(
44+
name: "StringIdentical",
45+
runFunction: run_StringIdentical,
46+
tags: [.validation, .api, .String]),
4347
]
4448

4549
if #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) {
@@ -1676,3 +1680,13 @@ public func run_iterateWords(_ n: Int) {
16761680
blackHole(swiftOrgHTML._words)
16771681
}
16781682
}
1683+
1684+
public func run_StringIdentical(_ n: Int) {
1685+
let str1 = "The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. "
1686+
let str2 = str1
1687+
for _ in 0 ..< n {
1688+
for _ in 0 ..< 100_000 {
1689+
check(str1.isIdentical(to: str2))
1690+
}
1691+
}
1692+
}

benchmark/single-source/SubstringTest.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public let benchmarks = [
3030
BenchmarkInfo(name: "SubstringFromLongString2", runFunction: run_SubstringFromLongString, tags: [.validation, .api, .String]),
3131
BenchmarkInfo(name: "SubstringFromLongStringGeneric2", runFunction: run_SubstringFromLongStringGeneric, tags: [.validation, .api, .String]),
3232
BenchmarkInfo(name: "SubstringTrimmingASCIIWhitespace", runFunction: run_SubstringTrimmingASCIIWhitespace, tags: [.validation, .api, .String]),
33+
BenchmarkInfo(name: "SubstringIdentical", runFunction: run_SubstringIdentical, tags: [.validation, .String]),
3334
]
3435

3536
// A string that doesn't fit in small string storage and doesn't fit in Latin-1
@@ -332,3 +333,11 @@ public func run _LessSubstringSubstringGenericStringProtocol(_ n: Int) {
332333
}
333334
}
334335
*/
336+
337+
@inline(never)
338+
public func run_SubstringIdentical(_ n: Int) {
339+
let (a, b) = (ss1, ss1)
340+
for _ in 1...n*500 {
341+
blackHole(a.isIdentical(to: b))
342+
}
343+
}

stdlib/public/core/String.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1112,4 +1112,22 @@ extension String {
11121112
}
11131113
}
11141114

1115-
1115+
extension String {
1116+
/// Returns a boolean value indicating whether this string is identical to
1117+
/// `other`.
1118+
///
1119+
/// Two string values are identical if there is no way to distinguish between
1120+
/// them.
1121+
///
1122+
/// Comparing strings this way includes comparing (normally) hidden
1123+
/// implementation details such as the memory location of any underlying
1124+
/// string storage object. Therefore, identical strings are guaranteed to
1125+
/// compare equal with `==`, but not all equal strings are considered
1126+
/// identical.
1127+
///
1128+
/// - Performance: O(1)
1129+
@backDeployed(before: SwiftStdlib 6.3)
1130+
public func isIdentical(to other: Self) -> Bool {
1131+
self._guts.rawBits == other._guts.rawBits
1132+
}
1133+
}

stdlib/public/core/Substring.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,3 +1307,24 @@ extension Substring {
13071307
return Substring(_unchecked: Slice(base: base, bounds: r))
13081308
}
13091309
}
1310+
1311+
extension Substring {
1312+
/// Returns a boolean value indicating whether this substring is identical to
1313+
/// `other`.
1314+
///
1315+
/// Two substring values are identical if there is no way to distinguish
1316+
/// between them.
1317+
///
1318+
/// Comparing substrings this way includes comparing (normally) hidden
1319+
/// implementation details such as the memory location of any underlying
1320+
/// substring storage object. Therefore, identical substrings are guaranteed
1321+
/// to compare equal with `==`, but not all equal substrings are considered
1322+
/// identical.
1323+
///
1324+
/// - Performance: O(1)
1325+
@backDeployed(before: SwiftStdlib 6.3)
1326+
public func isIdentical(to other: Self) -> Bool {
1327+
self._wholeGuts.rawBits == other._wholeGuts.rawBits &&
1328+
self._offsetRange == other._offsetRange
1329+
}
1330+
}

test/stdlib/StringAPI.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,4 +533,41 @@ StringTests.test("hasPrefix/hasSuffix vs Character boundaries") {
533533
expectFalse(s2.hasSuffix("\n"))
534534
}
535535

536+
StringTests.test("isIdentical(to:) small ascii") {
537+
let a = "Hello"
538+
let b = "Hello"
539+
540+
precondition(a == b)
541+
542+
expectTrue(a.isIdentical(to: a))
543+
expectTrue(b.isIdentical(to: b))
544+
expectTrue(a.isIdentical(to: b)) // Both small ASCII strings
545+
expectTrue(b.isIdentical(to: a))
546+
}
547+
548+
StringTests.test("isIdentical(to:) small unicode") {
549+
let a = "Cafe\u{301}"
550+
let b = "Cafe\u{301}"
551+
let c = "Café"
552+
553+
precondition(a == b)
554+
precondition(b == c)
555+
556+
expectTrue(a.isIdentical(to: b))
557+
expectTrue(b.isIdentical(to: a))
558+
expectFalse(a.isIdentical(to: c))
559+
expectFalse(b.isIdentical(to: c))
560+
}
561+
562+
StringTests.test("isIdentical(to:) large ascii") {
563+
let a = String(repeating: "foo", count: 1000)
564+
let b = String(repeating: "foo", count: 1000)
565+
566+
precondition(a == b)
567+
568+
expectFalse(a.isIdentical(to: b)) // Two large, distinct native strings
569+
expectTrue(a.isIdentical(to: a))
570+
expectTrue(b.isIdentical(to: b))
571+
}
572+
536573
runAllTests()

test/stdlib/subString.swift

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,41 @@ func checkHasContiguousStorageSubstring(_ x: Substring.UTF8View) {
3131
expectTrue(hasStorage)
3232
}
3333

34+
fileprivate func slices(
35+
_ s: String,
36+
from: Int,
37+
to: Int
38+
) -> (
39+
Substring,
40+
Substring,
41+
Substring
42+
) {
43+
let s1 = s[s.index(s.startIndex, offsetBy: from) ..<
44+
s.index(s.startIndex, offsetBy: to)]
45+
let s2 = s1[s1.startIndex..<s1.endIndex]
46+
let s3 = s2[s1.startIndex..<s1.endIndex]
47+
return (s1, s2, s3)
48+
}
49+
50+
fileprivate func allNotEmpty(
51+
_ s: Substring...
52+
) -> Bool {
53+
s.allSatisfy { $0.isEmpty == false }
54+
}
55+
56+
fileprivate func allEqual(
57+
_ s: Substring...
58+
) -> Bool {
59+
for i in 0..<s.count {
60+
for j in (i + 1)..<s.count {
61+
if s[i] != s[j] {
62+
return false
63+
}
64+
}
65+
}
66+
return true
67+
}
68+
3469
SubstringTests.test("Equality") {
3570
let s = "abcdefg"
3671
let s1 = s[s.index(s.startIndex, offsetBy: 2) ..<
@@ -282,4 +317,101 @@ SubstringTests.test("Substring.base") {
282317
}
283318
}
284319

320+
SubstringTests.test("isIdentical(to:) small ascii") {
321+
let (a1, a2, a3) = slices("Hello", from: 2, to: 4)
322+
let (b1, b2, b3) = slices("Hello", from: 2, to: 4)
323+
324+
precondition(allNotEmpty(a1, a2, a3, b1, b2, b3))
325+
precondition(allEqual(a1, a2, a3, b1, b2, b3))
326+
327+
expectTrue(a1.isIdentical(to: a1))
328+
expectTrue(a1.isIdentical(to: a2))
329+
expectTrue(a1.isIdentical(to: a3))
330+
expectTrue(a1.isIdentical(to: b1))
331+
expectTrue(a1.isIdentical(to: b2))
332+
expectTrue(a1.isIdentical(to: b3))
333+
334+
expectTrue(a2.isIdentical(to: a1))
335+
expectTrue(a2.isIdentical(to: a2))
336+
expectTrue(a2.isIdentical(to: a3))
337+
expectTrue(a2.isIdentical(to: b1))
338+
expectTrue(a2.isIdentical(to: b2))
339+
expectTrue(a2.isIdentical(to: b3))
340+
341+
expectTrue(a3.isIdentical(to: a1))
342+
expectTrue(a3.isIdentical(to: a2))
343+
expectTrue(a3.isIdentical(to: a3))
344+
expectTrue(a3.isIdentical(to: b1))
345+
expectTrue(a3.isIdentical(to: b2))
346+
expectTrue(a3.isIdentical(to: b3))
347+
}
348+
349+
SubstringTests.test("isIdentical(to:) small unicode") {
350+
let (a1, a2, a3) = slices("Hello Cafe\u{301}", from: 2, to: 4)
351+
let (b1, b2, b3) = slices("Hello Cafe\u{301}", from: 2, to: 4)
352+
let (c1, c2, c3) = slices("Hello Café", from: 2, to: 4)
353+
354+
precondition(allNotEmpty(a1, a2, a3, b1, b2, b3, c1, c2, c3))
355+
precondition(allEqual(a1, a2, a3, b1, b2, b3, c1, c2, c3))
356+
357+
expectTrue(a1.isIdentical(to: a1))
358+
expectTrue(a1.isIdentical(to: a2))
359+
expectTrue(a1.isIdentical(to: a3))
360+
expectTrue(a1.isIdentical(to: b1))
361+
expectTrue(a1.isIdentical(to: b2))
362+
expectTrue(a1.isIdentical(to: b3))
363+
expectFalse(a1.isIdentical(to: c1))
364+
expectFalse(a1.isIdentical(to: c2))
365+
expectFalse(a1.isIdentical(to: c3))
366+
367+
expectTrue(a2.isIdentical(to: a1))
368+
expectTrue(a2.isIdentical(to: a2))
369+
expectTrue(a2.isIdentical(to: a3))
370+
expectTrue(a2.isIdentical(to: b1))
371+
expectTrue(a2.isIdentical(to: b2))
372+
expectTrue(a2.isIdentical(to: b3))
373+
expectFalse(a2.isIdentical(to: c1))
374+
expectFalse(a2.isIdentical(to: c2))
375+
expectFalse(a2.isIdentical(to: c3))
376+
377+
expectTrue(a3.isIdentical(to: a1))
378+
expectTrue(a3.isIdentical(to: a2))
379+
expectTrue(a3.isIdentical(to: a3))
380+
expectTrue(a3.isIdentical(to: b1))
381+
expectTrue(a3.isIdentical(to: b2))
382+
expectTrue(a3.isIdentical(to: b3))
383+
expectFalse(a3.isIdentical(to: c1))
384+
expectFalse(a3.isIdentical(to: c2))
385+
expectFalse(a3.isIdentical(to: c3))
386+
}
387+
388+
SubstringTests.test("isIdentical(to:) large ascii") {
389+
let (a1, a2, a3) = slices(String(repeating: "Hello", count: 1000), from: 2, to: 4)
390+
let (b1, b2, b3) = slices(String(repeating: "Hello", count: 1000), from: 2, to: 4)
391+
392+
precondition(allNotEmpty(a1, a2, a3, b1, b2, b3))
393+
precondition(allEqual(a1, a2, a3, b1, b2, b3))
394+
395+
expectTrue(a1.isIdentical(to: a1))
396+
expectTrue(a1.isIdentical(to: a2))
397+
expectTrue(a1.isIdentical(to: a3))
398+
expectFalse(a1.isIdentical(to: b1))
399+
expectFalse(a1.isIdentical(to: b2))
400+
expectFalse(a1.isIdentical(to: b3))
401+
402+
expectTrue(a2.isIdentical(to: a1))
403+
expectTrue(a2.isIdentical(to: a2))
404+
expectTrue(a2.isIdentical(to: a3))
405+
expectFalse(a2.isIdentical(to: b1))
406+
expectFalse(a2.isIdentical(to: b2))
407+
expectFalse(a2.isIdentical(to: b3))
408+
409+
expectTrue(a3.isIdentical(to: a1))
410+
expectTrue(a3.isIdentical(to: a2))
411+
expectTrue(a3.isIdentical(to: a3))
412+
expectFalse(a3.isIdentical(to: b1))
413+
expectFalse(a3.isIdentical(to: b2))
414+
expectFalse(a3.isIdentical(to: b3))
415+
}
416+
285417
runAllTests()

0 commit comments

Comments
 (0)