From e6db03f1786085c786feb4f7decad6848288db58 Mon Sep 17 00:00:00 2001 From: Rick van Voorden Date: Thu, 5 Jun 2025 21:34:46 -0700 Subject: [PATCH] [stdlib] string identical --- benchmark/single-source/StringTests.swift | 18 ++ benchmark/single-source/SubstringTest.swift | 58 +++-- stdlib/public/core/String.swift | 21 +- stdlib/public/core/Substring.swift | 42 ++++ test/stdlib/StringAPI.swift | 58 +++++ test/stdlib/subString.swift | 250 ++++++++++++++++++++ 6 files changed, 427 insertions(+), 20 deletions(-) diff --git a/benchmark/single-source/StringTests.swift b/benchmark/single-source/StringTests.swift index bcc43a2633777..3a66e66b86b4a 100644 --- a/benchmark/single-source/StringTests.swift +++ b/benchmark/single-source/StringTests.swift @@ -49,6 +49,13 @@ public var benchmarks: [BenchmarkInfo] { runFunction: run_iterateWords, tags: [.validation, .String])) } + if #available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) { + result.append( + BenchmarkInfo( + name: "StringIdentical", + runFunction: run_StringIdentical, + tags: [.validation, .String])) + } return result } @@ -1676,3 +1683,14 @@ public func run_iterateWords(_ n: Int) { blackHole(swiftOrgHTML._words) } } + +@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) +public func run_StringIdentical(_ n: Int) { + 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. " + let str2 = str1 + for _ in 0 ..< n { + for _ in 0 ..< 100_000 { + check(str1.isIdentical(to: str2)) + } + } +} diff --git a/benchmark/single-source/SubstringTest.swift b/benchmark/single-source/SubstringTest.swift index 71ef0c774f26f..f813aa98e8a3e 100644 --- a/benchmark/single-source/SubstringTest.swift +++ b/benchmark/single-source/SubstringTest.swift @@ -12,25 +12,36 @@ import TestsUtils -public let benchmarks = [ - BenchmarkInfo(name: "EqualStringSubstring", runFunction: run_EqualStringSubstring, tags: [.validation, .api, .String]), - BenchmarkInfo(name: "EqualSubstringString", runFunction: run_EqualSubstringString, tags: [.validation, .api, .String]), - BenchmarkInfo(name: "EqualSubstringSubstring", runFunction: run_EqualSubstringSubstring, tags: [.validation, .api, .String]), - BenchmarkInfo(name: "EqualSubstringSubstringGenericEquatable", runFunction: run_EqualSubstringSubstringGenericEquatable, tags: [.validation, .api, .String]), - BenchmarkInfo(name: "SubstringRemoveFirst1", runFunction: run_SubstringRemoveFirst1, tags: [.validation, .api, .String]), - BenchmarkInfo(name: "SubstringRemoveLast1", runFunction: run_SubstringRemoveLast1, tags: [.validation, .api, .String]), - BenchmarkInfo(name: "LessSubstringSubstring", runFunction: run_LessSubstringSubstring, tags: [.validation, .api, .String]), - BenchmarkInfo(name: "LessSubstringSubstringGenericComparable", runFunction: run_LessSubstringSubstringGenericComparable, tags: [.validation, .api, .String]), - BenchmarkInfo(name: "StringFromLongWholeSubstring", runFunction: run_StringFromLongWholeSubstring, tags: [.validation, .api, .String]), - BenchmarkInfo(name: "StringFromLongWholeSubstringGeneric", runFunction: run_StringFromLongWholeSubstringGeneric, tags: [.validation, .api, .String]), - BenchmarkInfo(name: "SubstringComparable", runFunction: run_SubstringComparable, tags: [.validation, .api, .String], - setUpFunction: { blackHole(_comparison) }), - BenchmarkInfo(name: "SubstringEqualString", runFunction: run_SubstringEqualString, tags: [.validation, .api, .String]), - BenchmarkInfo(name: "SubstringEquatable", runFunction: run_SubstringEquatable, tags: [.validation, .api, .String]), - BenchmarkInfo(name: "SubstringFromLongString2", runFunction: run_SubstringFromLongString, tags: [.validation, .api, .String]), - BenchmarkInfo(name: "SubstringFromLongStringGeneric2", runFunction: run_SubstringFromLongStringGeneric, tags: [.validation, .api, .String]), - BenchmarkInfo(name: "SubstringTrimmingASCIIWhitespace", runFunction: run_SubstringTrimmingASCIIWhitespace, tags: [.validation, .api, .String]), -] +public var benchmarks: [BenchmarkInfo] { + var result = [ + BenchmarkInfo(name: "EqualStringSubstring", runFunction: run_EqualStringSubstring, tags: [.validation, .api, .String]), + BenchmarkInfo(name: "EqualSubstringString", runFunction: run_EqualSubstringString, tags: [.validation, .api, .String]), + BenchmarkInfo(name: "EqualSubstringSubstring", runFunction: run_EqualSubstringSubstring, tags: [.validation, .api, .String]), + BenchmarkInfo(name: "EqualSubstringSubstringGenericEquatable", runFunction: run_EqualSubstringSubstringGenericEquatable, tags: [.validation, .api, .String]), + BenchmarkInfo(name: "SubstringRemoveFirst1", runFunction: run_SubstringRemoveFirst1, tags: [.validation, .api, .String]), + BenchmarkInfo(name: "SubstringRemoveLast1", runFunction: run_SubstringRemoveLast1, tags: [.validation, .api, .String]), + BenchmarkInfo(name: "LessSubstringSubstring", runFunction: run_LessSubstringSubstring, tags: [.validation, .api, .String]), + BenchmarkInfo(name: "LessSubstringSubstringGenericComparable", runFunction: run_LessSubstringSubstringGenericComparable, tags: [.validation, .api, .String]), + BenchmarkInfo(name: "StringFromLongWholeSubstring", runFunction: run_StringFromLongWholeSubstring, tags: [.validation, .api, .String]), + BenchmarkInfo(name: "StringFromLongWholeSubstringGeneric", runFunction: run_StringFromLongWholeSubstringGeneric, tags: [.validation, .api, .String]), + BenchmarkInfo(name: "SubstringComparable", runFunction: run_SubstringComparable, tags: [.validation, .api, .String], + setUpFunction: { blackHole(_comparison) }), + BenchmarkInfo(name: "SubstringEqualString", runFunction: run_SubstringEqualString, tags: [.validation, .api, .String]), + BenchmarkInfo(name: "SubstringEquatable", runFunction: run_SubstringEquatable, tags: [.validation, .api, .String]), + BenchmarkInfo(name: "SubstringFromLongString2", runFunction: run_SubstringFromLongString, tags: [.validation, .api, .String]), + BenchmarkInfo(name: "SubstringFromLongStringGeneric2", runFunction: run_SubstringFromLongStringGeneric, tags: [.validation, .api, .String]), + BenchmarkInfo(name: "SubstringTrimmingASCIIWhitespace", runFunction: run_SubstringTrimmingASCIIWhitespace, tags: [.validation, .api, .String]), + ] + + if #available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) { + result.append( + BenchmarkInfo( + name: "SubstringIdentical", + runFunction: run_SubstringIdentical, + tags: [.validation, .String])) + } + return result +} // A string that doesn't fit in small string storage and doesn't fit in Latin-1 let longWide = "fὢasὢodὢijὢadὢolὢsjὢalὢsdὢjlὢasὢdfὢijὢliὢsdὢjøὢslὢdiὢalὢiὢ" @@ -332,3 +343,12 @@ public func run _LessSubstringSubstringGenericStringProtocol(_ n: Int) { } } */ + +@inline(never) +@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) +public func run_SubstringIdentical(_ n: Int) { + let (a, b) = (ss1, ss1) + for _ in 1...n*500 { + blackHole(a.isIdentical(to: b)) + } +} diff --git a/stdlib/public/core/String.swift b/stdlib/public/core/String.swift index e6715e91cc6ed..dfad931f56f65 100644 --- a/stdlib/public/core/String.swift +++ b/stdlib/public/core/String.swift @@ -1112,4 +1112,23 @@ extension String { } } - +@available(SwiftStdlib 6.3, *) +extension String { + /// Returns a boolean value indicating whether this string is identical to + /// `other`. + /// + /// Two string values are identical if there is no way to distinguish between + /// them. + /// + /// Comparing strings this way includes comparing (normally) hidden + /// implementation details such as the memory location of any underlying + /// string storage object. Therefore, identical strings are guaranteed to + /// compare equal with `==`, but not all equal strings are considered + /// identical. + /// + /// - Performance: O(1) + @available(SwiftStdlib 6.3, *) + public func isIdentical(to other: Self) -> Bool { + self._isIdentical(to: other) + } +} diff --git a/stdlib/public/core/Substring.swift b/stdlib/public/core/Substring.swift index be818e9698dba..2c2e8d9aa7df9 100644 --- a/stdlib/public/core/Substring.swift +++ b/stdlib/public/core/Substring.swift @@ -1307,3 +1307,45 @@ extension Substring { return Substring(_unchecked: Slice(base: base, bounds: r)) } } + +extension Substring { + /// Returns a boolean value indicating whether this substring is identical to + /// `other`. + /// + /// Two substring values are identical if there is no way to distinguish + /// between them. + /// + /// Comparing substrings this way includes comparing (normally) hidden + /// implementation details such as the memory location of any underlying + /// substring storage object. Therefore, identical substrings are guaranteed + /// to compare equal with `==`, but not all equal substrings are considered + /// identical. + /// + /// - Performance: O(1) + @_alwaysEmitIntoClient + public func _isIdentical(to other: Self) -> Bool { + self._wholeGuts.rawBits == other._wholeGuts.rawBits && + self._offsetRange == other._offsetRange + } +} + +@available(SwiftStdlib 6.3, *) +extension Substring { + /// Returns a boolean value indicating whether this substring is identical to + /// `other`. + /// + /// Two substring values are identical if there is no way to distinguish + /// between them. + /// + /// Comparing substrings this way includes comparing (normally) hidden + /// implementation details such as the memory location of any underlying + /// substring storage object. Therefore, identical substrings are guaranteed + /// to compare equal with `==`, but not all equal substrings are considered + /// identical. + /// + /// - Performance: O(1) + @available(SwiftStdlib 6.3, *) + public func isIdentical(to other: Self) -> Bool { + self._isIdentical(to: other) + } +} diff --git a/test/stdlib/StringAPI.swift b/test/stdlib/StringAPI.swift index fb33db9b53ee7..259db4c170cad 100644 --- a/test/stdlib/StringAPI.swift +++ b/test/stdlib/StringAPI.swift @@ -533,4 +533,62 @@ StringTests.test("hasPrefix/hasSuffix vs Character boundaries") { expectFalse(s2.hasSuffix("\n")) } +StringTests.test("isIdentical(to:) small ascii") +.skip(.custom( + { if #available(SwiftStdlib 6.3, *) { false } else { true } }, + reason: "Requires Swift 6.3's standard library" +)) +.code { + guard #available(SwiftStdlib 6.3, *) else { return } + + let a = "Hello" + let b = "Hello" + + precondition(a == b) + + expectTrue(a.isIdentical(to: a)) + expectTrue(b.isIdentical(to: b)) + expectTrue(a.isIdentical(to: b)) // Both small ASCII strings + expectTrue(b.isIdentical(to: a)) +} + +StringTests.test("isIdentical(to:) small unicode") +.skip(.custom( + { if #available(SwiftStdlib 6.3, *) { false } else { true } }, + reason: "Requires Swift 6.3's standard library" +)) +.code { + guard #available(SwiftStdlib 6.3, *) else { return } + + let a = "Cafe\u{301}" + let b = "Cafe\u{301}" + let c = "Café" + + precondition(a == b) + precondition(b == c) + + expectTrue(a.isIdentical(to: b)) + expectTrue(b.isIdentical(to: a)) + expectFalse(a.isIdentical(to: c)) + expectFalse(b.isIdentical(to: c)) +} + +StringTests.test("isIdentical(to:) large ascii") +.skip(.custom( + { if #available(SwiftStdlib 6.3, *) { false } else { true } }, + reason: "Requires Swift 6.3's standard library" +)) +.code { + guard #available(SwiftStdlib 6.3, *) else { return } + + let a = String(repeating: "foo", count: 1000) + let b = String(repeating: "foo", count: 1000) + + precondition(a == b) + + expectFalse(a.isIdentical(to: b)) // Two large, distinct native strings + expectTrue(a.isIdentical(to: a)) + expectTrue(b.isIdentical(to: b)) +} + runAllTests() diff --git a/test/stdlib/subString.swift b/test/stdlib/subString.swift index 2a7b9e58db2d8..6e0f9cb1f61e2 100644 --- a/test/stdlib/subString.swift +++ b/test/stdlib/subString.swift @@ -31,6 +31,41 @@ func checkHasContiguousStorageSubstring(_ x: Substring.UTF8View) { expectTrue(hasStorage) } +fileprivate func slices( + _ s: String, + from: Int, + to: Int +) -> ( + Substring, + Substring, + Substring +) { + let s1 = s[s.index(s.startIndex, offsetBy: from) ..< + s.index(s.startIndex, offsetBy: to)] + let s2 = s1[s1.startIndex.. Bool { + s.allSatisfy { $0.isEmpty == false } +} + +fileprivate func allEqual( + _ s: Substring... +) -> Bool { + for i in 0..