Skip to content

Commit 6554165

Browse files
authored
Merge pull request #82097 from glessard/rdar152615664-issue82024-62-with-small-string-spans
[6.2, stdlib] fix small-string UTF8Span support
2 parents f26af0e + 0130dea commit 6554165

File tree

10 files changed

+167
-30
lines changed

10 files changed

+167
-30
lines changed

stdlib/private/StdlibUnittest/StdlibUnittest.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -752,11 +752,14 @@ public func expectNil<T>(_ value: T?,
752752
}
753753

754754
@discardableResult
755-
public func expectNotNil<T>(_ value: T?,
755+
@lifetime(copy value)
756+
public func expectNotNil<T: ~Copyable & ~Escapable>(
757+
_ value: consuming T?,
756758
_ message: @autoclosure () -> String = "",
757759
stackTrace: SourceLocStack = SourceLocStack(),
758760
showFrame: Bool = true,
759-
file: String = #file, line: UInt = #line) -> T? {
761+
file: String = #file, line: UInt = #line
762+
) -> T? {
760763
if value == nil {
761764
expectationFailure("expected optional to be non-nil", trace: message(),
762765
stackTrace: stackTrace.pushIf(showFrame, file: file, line: line))

stdlib/public/core/StringGuts.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import SwiftShims
1717
// functionality and guidance for efficiently working with Strings.
1818
//
1919
@frozen
20+
@_addressableForDependencies
2021
public // SPI(corelibs-foundation)
2122
struct _StringGuts: @unchecked Sendable {
2223
@usableFromInline

stdlib/public/core/StringUTF8View.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ extension String {
8989
/// print(String(s1.utf8.prefix(15))!)
9090
/// // Prints "They call me 'B"
9191
@frozen
92-
@_addressableForDependencies
9392
public struct UTF8View: Sendable {
9493
@usableFromInline
9594
internal var _guts: _StringGuts

stdlib/public/core/Substring.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,6 @@ extension Substring: LosslessStringConvertible {
630630

631631
extension Substring {
632632
@frozen
633-
@_addressableForDependencies
634633
public struct UTF8View: Sendable {
635634
@usableFromInline
636635
internal var _slice: Slice<String.UTF8View>

stdlib/public/core/UTF8Span.swift

Lines changed: 94 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -201,36 +201,112 @@ extension String {
201201
}
202202

203203
@available(SwiftStdlib 6.2, *)
204-
public var utf8Span: UTF8Span {
204+
private var _span: Span<UTF8.CodeUnit> {
205205
@lifetime(borrow self)
206206
borrowing get {
207-
let isKnownASCII = _guts.isASCII
208-
let utf8 = self.utf8
209-
let span = utf8.span
210-
let result = unsafe UTF8Span(
211-
unchecked: span,
212-
isKnownASCII: isKnownASCII)
213-
return unsafe _overrideLifetime(result, borrowing: self)
207+
#if _runtime(_ObjC)
208+
// handle non-UTF8 Objective-C bridging cases here
209+
if !_guts.isFastUTF8, _guts._object.hasObjCBridgeableObject {
210+
let storage = _guts._getOrAllocateAssociatedStorage()
211+
let (start, count) = unsafe (storage.start, storage.count)
212+
let span = unsafe Span(_unsafeStart: start, count: count)
213+
return unsafe _overrideLifetime(span, borrowing: self)
214+
}
215+
#endif
216+
let count = _guts.count
217+
if _guts.isSmall {
218+
let a = Builtin.addressOfBorrow(self)
219+
let address = unsafe UnsafePointer<UTF8.CodeUnit>(a)
220+
let span = unsafe Span(_unsafeStart: address, count: count)
221+
return unsafe _overrideLifetime(span, borrowing: self)
222+
}
223+
let isFastUTF8 = _guts.isFastUTF8
224+
_precondition(isFastUTF8, "String must be contiguous UTF8")
225+
let buffer = unsafe _guts._object.fastUTF8
226+
let span = unsafe Span(_unsafeElements: buffer)
227+
return unsafe _overrideLifetime(span, borrowing: self)
214228
}
215229
}
216-
}
217230

218-
extension Substring {
231+
/// A UTF8span over the code units that make up this string.
232+
///
233+
/// - Note: In the case of bridged UTF16 String instances (on Apple
234+
/// platforms,) this property transcodes the code units the first time
235+
/// it is called. The transcoded buffer is cached, and subsequent calls
236+
/// to `span` can reuse the buffer.
237+
///
238+
/// Returns: a `UTF8Span` over the code units of this String.
239+
///
240+
/// Complexity: O(1) for native UTF8 Strings,
241+
/// amortized O(1) for bridged UTF16 Strings.
219242
@available(SwiftStdlib 6.2, *)
220243
public var utf8Span: UTF8Span {
221244
@lifetime(borrow self)
222245
borrowing get {
223-
let isKnownASCII = base._guts.isASCII
224-
let utf8 = self.utf8
225-
let span = utf8.span
226-
let result = unsafe UTF8Span(
227-
unchecked: span,
228-
isKnownASCII: isKnownASCII)
229-
return unsafe _overrideLifetime(result, borrowing: self)
246+
unsafe UTF8Span(unchecked: _span, isKnownASCII: _guts.isASCII)
230247
}
231248
}
232249
}
233250

251+
extension Substring {
234252

253+
@available(SwiftStdlib 6.2, *)
254+
private var _span: Span<UTF8.CodeUnit> {
255+
@lifetime(borrow self)
256+
borrowing get {
257+
#if _runtime(_ObjC)
258+
// handle non-UTF8 Objective-C bridging cases here
259+
if !_wholeGuts.isFastUTF8, _wholeGuts._object.hasObjCBridgeableObject {
260+
let base: String.UTF8View = _slice._base.utf8
261+
let first = base._foreignDistance(from: base.startIndex, to: startIndex)
262+
let count = base._foreignDistance(from: startIndex, to: endIndex)
263+
let span = base.span._extracting(first..<(first &+ count))
264+
return unsafe _overrideLifetime(span, borrowing: self)
265+
}
266+
#endif
267+
let first = _slice._startIndex._encodedOffset
268+
let end = _slice._endIndex._encodedOffset
269+
if _wholeGuts.isSmall {
270+
let a = Builtin.addressOfBorrow(self)
271+
let offset = first &+ (2 &* MemoryLayout<String.Index>.stride)
272+
let start = unsafe UnsafePointer<UTF8.CodeUnit>(a).advanced(by: offset)
273+
let span = unsafe Span(_unsafeStart: start, count: end &- first)
274+
return unsafe _overrideLifetime(span, borrowing: self)
275+
}
276+
let isFastUTF8 = _wholeGuts.isFastUTF8
277+
_precondition(isFastUTF8, "Substring must be contiguous UTF8")
278+
var span = unsafe Span(_unsafeElements: _wholeGuts._object.fastUTF8)
279+
span = span._extracting(first..<end)
280+
return unsafe _overrideLifetime(span, borrowing: self)
281+
}
282+
}
235283

236-
284+
/// A UTF8Span over the code units that make up this substring.
285+
///
286+
/// - Note: In the case of bridged UTF16 String instances (on Apple
287+
/// platforms,) this property needs to transcode the code units every time
288+
/// it is called.
289+
/// For example, if `string` has the bridged UTF16 representation,
290+
/// for word in string.split(separator: " ") {
291+
/// useSpan(word.span)
292+
/// }
293+
/// is accidentally quadratic because of this issue. A workaround is to
294+
/// explicitly convert the string into its native UTF8 representation:
295+
/// var nativeString = consume string
296+
/// nativeString.makeContiguousUTF8()
297+
/// for word in nativeString.split(separator: " ") {
298+
/// useSpan(word.span)
299+
/// }
300+
/// This second option has linear time complexity, as expected.
301+
///
302+
/// Returns: a `UTF8Span` over the code units of this Substring.
303+
///
304+
/// Complexity: O(1) for native UTF8 Strings, O(n) for bridged UTF16 Strings.
305+
@available(SwiftStdlib 6.2, *)
306+
public var utf8Span: UTF8Span {
307+
@lifetime(borrow self)
308+
borrowing get {
309+
unsafe UTF8Span(unchecked: _span, isKnownASCII: base._guts.isASCII)
310+
}
311+
}
312+
}

test/SILOptimizer/lifetime_dependence/semantics.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ public func testTrivialInoutBorrow(p: inout UnsafePointer<Int>) -> Span<Int> {
417417

418418
private let immortalInt = 0
419419

420-
private let immortalString = ""
420+
private let immortalStrings: [String] = []
421421

422422
@lifetime(immortal)
423423
func testImmortalInt() -> Span<Int> {
@@ -427,10 +427,10 @@ func testImmortalInt() -> Span<Int> {
427427
}
428428

429429
@lifetime(immortal)
430-
func testImmortalString() -> Span<String> {
431-
let nilBasedBuffer = UnsafeBufferPointer<String>(start: nil, count: 0)
430+
func testImmortalStrings() -> Span<[String]> {
431+
let nilBasedBuffer = UnsafeBufferPointer<[String]>(start: nil, count: 0)
432432
let span = Span(base: nilBasedBuffer.baseAddress, count: nilBasedBuffer.count)
433-
return _overrideLifetime(span, borrowing: immortalString)
433+
return _overrideLifetime(span, borrowing: immortalStrings)
434434
}
435435

436436
let ptr = UnsafePointer<Int>(bitPattern: 1)!

test/api-digester/Outputs/stability-stdlib-source-base.swift.expected

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,8 +363,6 @@ Func ContiguousArray.withUnsafeMutableBufferPointer(_:) is now without rethrows
363363

364364
// Adoption of @_addressableForDependencies
365365
Struct CollectionOfOne is now with @_addressableForDependencies
366-
Struct String.UTF8View is now with @_addressableForDependencies
367-
Struct Substring.UTF8View is now with @_addressableForDependencies
368366

369367
Protocol CodingKey has added inherited protocol SendableMetatype
370368
Protocol Error has added inherited protocol SendableMetatype

test/api-digester/stability-stdlib-abi-without-asserts.test

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -825,8 +825,7 @@ Func _SliceBuffer.withUnsafeMutableBufferPointer(_:) has mangled name changing f
825825
Struct String.Index has added a conformance to an existing protocol CustomDebugStringConvertible
826826

827827
Struct CollectionOfOne is now with @_addressableForDependencies
828-
Struct String.UTF8View is now with @_addressableForDependencies
829-
Struct Substring.UTF8View is now with @_addressableForDependencies
828+
Struct _StringGuts is now with @_addressableForDependencies
830829

831830
Enum _SwiftifyInfo is a new API without '@available'
832831
Enum _SwiftifyExpr is a new API without '@available'

test/stdlib/OptionalGeneralizations.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,23 @@ suite.test("Initializer references") {
9292
expectTrue(r != nil)
9393
}
9494
}
95+
96+
suite.test("expectNotNil()") {
97+
func opt1<T: ~Copyable>(_ t: consuming T) -> T? { Optional.some(t) }
98+
_ = expectNotNil(opt1(TrivialStruct()))
99+
_ = expectNotNil(opt1(NoncopyableStruct()))
100+
_ = expectNotNil(opt1(RegularClass()))
101+
#if $NonescapableTypes
102+
@lifetime(copy t)
103+
func opt2<T: ~Copyable & ~Escapable>(_ t: consuming T) -> T? { t }
104+
105+
let ne = NonescapableStruct()
106+
_ = expectNotNil(opt2(ne))
107+
108+
let ncne = NoncopyableNonescapableStruct()
109+
_ = expectNotNil(opt2(ncne))
110+
111+
let nent = NonescapableNontrivialStruct()
112+
_ = expectNotNil(opt2(nent))
113+
#endif
114+
}

test/stdlib/Span/StringUTF8SpanProperty.swift

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,45 @@ suite.test("Span from Large Native String's Substring")
8585
expectEqual(span[i], u[i])
8686
}
8787
}
88+
89+
suite.test("Span from String.utf8Span")
90+
.require(.stdlib_6_2).code {
91+
guard #available(SwiftStdlib 6.2, *) else { return }
92+
93+
let s = String(200)
94+
let utf8span = s.utf8Span
95+
let span1 = utf8span.span
96+
let utf8view = s.utf8
97+
let span2 = utf8view.span
98+
expectEqual(span1.count, span2.count)
99+
for (i,j) in zip(span1.indices, span2.indices) {
100+
expectEqual(span1[i], span2[j])
101+
}
102+
}
103+
104+
suite.test("UTF8Span from Span")
105+
.require(.stdlib_6_2).code {
106+
guard #available(SwiftStdlib 6.2, *) else { return }
107+
108+
let s = String(200).utf8
109+
let span1 = s.span
110+
guard let utf8 = expectNotNil(try? UTF8Span(validating: span1)) else { return }
111+
112+
let span2 = utf8.span
113+
expectTrue(span1.isIdentical(to: span2))
114+
}
115+
116+
suite.test("Span from Substring.utf8Span")
117+
.require(.stdlib_6_2).code {
118+
guard #available(SwiftStdlib 6.2, *) else { return }
119+
120+
let s = String(22000).dropFirst().dropLast()
121+
let utf8span = s.utf8Span
122+
let span1 = utf8span.span
123+
let utf8view = s.utf8
124+
let span2 = utf8view.span
125+
expectEqual(span1.count, span2.count)
126+
for (i,j) in zip(span1.indices, span2.indices) {
127+
expectEqual(span1[i], span2[j])
128+
}
129+
}

0 commit comments

Comments
 (0)