Skip to content

Commit 92baa32

Browse files
committed
Add async overloads of all OutputSpan providing APIs
## Motivation It is currently not possible to use the new `OutputSpan` based APIs in asynchronous contexts. In particular, it is not possible to initialize or append to a `Unique/RigidArray` by reading asynchronously from the network or a file. ## Modifications This PR adds new overloads to all the existing APIs that provide an `OutputSpan` to take an asynchronous closure. The PR also adds tests for all those new methods to show that they are properly working. ## Result We can now interact with these output spans asynchronously.
1 parent 8e5e4a8 commit 92baa32

15 files changed

+626
-0
lines changed

Sources/BasicContainers/RigidArray+Append.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,37 @@ extension RigidArray where Element: ~Copyable {
8484
}
8585
return try body(&span)
8686
}
87+
88+
/// Append a given number of items to the end of this array by populating
89+
/// an output span.
90+
///
91+
/// If the array does not have sufficient capacity to store the new items in
92+
/// the buffer, then this triggers a runtime error.
93+
///
94+
/// - Parameters
95+
/// - count: The number of items to append to the array.
96+
/// - body: A callback that gets called precisely once to directly
97+
/// populate newly reserved storage within the array. The function
98+
/// is allowed to initialize fewer than `count` items. The array is
99+
/// appended however many items the callback adds to the output span
100+
/// before it returns (or before it throws an error).
101+
///
102+
/// - Complexity: O(`count`)
103+
@_alwaysEmitIntoClient
104+
public nonisolated(nonsending) mutating func append<E: Error, Result: ~Copyable>(
105+
count: Int,
106+
initializingWith body: nonisolated(nonsending) (inout OutputSpan<Element>) async throws(E) -> Result
107+
) async throws(E) -> Result {
108+
// FIXME: This is extremely similar to `edit()`, except it provides a narrower span.
109+
precondition(freeCapacity >= count, "RigidArray capacity overflow")
110+
let buffer = _freeSpace._extracting(first: count)
111+
var span = OutputSpan(buffer: buffer, initializedCount: 0)
112+
defer {
113+
_count &+= span.finalize(for: buffer)
114+
span = OutputSpan()
115+
}
116+
return try await body(&span)
117+
}
87118
}
88119

89120
@available(SwiftStdlib 5.0, *)

Sources/BasicContainers/RigidArray+Initializers.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,25 @@ extension RigidArray where Element: ~Copyable {
3636
self.init(capacity: capacity)
3737
try edit(body)
3838
}
39+
40+
/// Creates a new array with the specified capacity, directly initializing
41+
/// its storage using an async closure that populates an output span.
42+
///
43+
/// - Parameters:
44+
/// - capacity: The storage capacity of the new array.
45+
/// - body: A callback that gets called precisely once to directly
46+
/// populate newly reserved storage within the array. The function
47+
/// is allowed to add fewer than `capacity` items. The array is
48+
/// initialized with however many items the callback adds to the
49+
/// output span before it returns (or before it throws an error).
50+
@inlinable
51+
public nonisolated(nonsending) init<E: Error>(
52+
capacity: Int,
53+
initializingWith body: nonisolated(nonsending) (inout OutputSpan<Element>) async throws(E) -> Void
54+
) async throws(E) {
55+
self.init(capacity: capacity)
56+
try await edit(body)
57+
}
3958
}
4059

4160
@available(SwiftStdlib 5.0, *)

Sources/BasicContainers/RigidArray+Insertions.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,46 @@ extension RigidArray where Element: ~Copyable {
9191
}
9292
return body(&span)
9393
}
94+
95+
/// Inserts a given number of new items into this array at the specified
96+
/// position, using an async callback to directly initialize array storage by
97+
/// populating an output span.
98+
///
99+
/// All existing elements at or following the specified position are moved to
100+
/// make room for the new items.
101+
///
102+
/// If the capacity of the array isn't sufficient to accommodate the new
103+
/// elements, then this method triggers a runtime error.
104+
///
105+
/// - Parameters:
106+
/// - count: The number of items to insert into the array.
107+
/// - index: The position at which to insert the new items.
108+
/// `index` must be a valid index in the array.
109+
/// - body: A callback that gets called precisely once to directly
110+
/// populate newly reserved storage within the array. The function
111+
/// is called with an empty output span of capacity matching the
112+
/// supplied count, and it must fully populate it before returning.
113+
///
114+
/// - Complexity: O(`self.count` + `count`)
115+
@inlinable
116+
public nonisolated(nonsending) mutating func insert<Result: ~Copyable>(
117+
count: Int,
118+
at index: Int,
119+
initializingWith body: nonisolated(nonsending) (inout OutputSpan<Element>) async -> Result
120+
) async -> Result {
121+
// FIXME: This does not allow `body` to throw, to prevent having to move the tail twice. Is that okay?
122+
precondition(index >= 0 && index <= self.count, "Index out of bounds")
123+
precondition(count <= freeCapacity, "RigidArray capacity overflow")
124+
let target = unsafe _openGap(at: index, count: count)
125+
var span = OutputSpan(buffer: target, initializedCount: 0)
126+
defer {
127+
let c = span.finalize(for: target)
128+
precondition(c == count, "Inserted fewer items than promised")
129+
_count &+= c
130+
span = OutputSpan()
131+
}
132+
return await body(&span)
133+
}
94134
}
95135

96136
@available(SwiftStdlib 5.0, *)

Sources/BasicContainers/RigidArray+Replacements.swift

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,50 @@ extension RigidArray where Element: ~Copyable {
6666
}
6767
return body(&span)
6868
}
69+
70+
/// Replace the specified subrange of elements with new items populated by an
71+
/// async callback function directly into the newly allocated storage.
72+
///
73+
/// This method first removes any existing items from the target subrange.
74+
/// If the capacity of the array isn't sufficient to accommodate the new
75+
/// elements, then this method triggers a runtime error.
76+
///
77+
/// If you pass a zero-length range as the `subrange` parameter, then
78+
/// this method is equivalent to calling
79+
/// `insert(count: newCount, initializingWith: body)`.
80+
///
81+
/// Likewise, if you pass a zero for `newCount`, then this method
82+
/// removes the elements in the given subrange without any replacement.
83+
/// Calling `removeSubrange(subrange)` is preferred in this case.
84+
///
85+
/// - Parameters
86+
/// - subrange: The subrange of the array to replace. The bounds of
87+
/// the range must be valid indices in the array.
88+
/// - newCount: the number of items to replace the old subrange.
89+
/// - body: A callback that gets called precisely once to directly
90+
/// populate newly reserved storage within the array. The function
91+
/// is called with an empty output span of capacity `newCount`,
92+
/// and it must fully populate it before returning.
93+
///
94+
/// - Complexity: O(`self.count` + `newCount`)
95+
@inlinable
96+
public nonisolated(nonsending) mutating func replaceSubrange<Result: ~Copyable>(
97+
_ subrange: Range<Int>,
98+
newCount: Int,
99+
initializingWith body: nonisolated(nonsending) (inout OutputSpan<Element>) async -> Result
100+
) async -> Result {
101+
// FIXME: Should we allow throwing (and a partially filled output span)?
102+
// FIXME: Should we have a version of this with two closures, to allow custom-consuming the old items?
103+
// replaceSubrange(5..<10, newCount: 3, consumingWith: {...}, initializingWith: {...})
104+
let target = _gapForReplacement(of: subrange, withNewCount: newCount)
105+
var span = OutputSpan(buffer: target, initializedCount: 0)
106+
defer {
107+
let c = span.finalize(for: target)
108+
precondition(c == newCount, "Inserted fewer items than promised")
109+
span = OutputSpan()
110+
}
111+
return await body(&span)
112+
}
69113
}
70114

71115
@available(SwiftStdlib 5.0, *)

Sources/BasicContainers/RigidArray.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,35 @@ extension RigidArray where Element: ~Copyable {
237237
return try body(&span)
238238
}
239239

240+
/// Arbitrarily edit the storage underlying this array by invoking a
241+
/// user-supplied async closure with a mutable `OutputSpan` view over it.
242+
/// This method calls its function argument precisely once, allowing it to
243+
/// arbitrarily modify the contents of the output span it is given.
244+
/// The argument is free to add, remove or reorder any items; however,
245+
/// it is not allowed to replace the span or change its capacity.
246+
///
247+
/// When the function argument finishes (whether by returning or throwing an
248+
/// error) the rigid array instance is updated to match the final contents of
249+
/// the output span.
250+
///
251+
/// - Parameter body: A function that edits the contents of this array through
252+
/// an `OutputSpan` argument. This method invokes this function
253+
/// precisely once.
254+
/// - Returns: This method returns the result of its function argument.
255+
/// - Complexity: Adds O(1) overhead to the complexity of the function
256+
/// argument.
257+
@inlinable
258+
public nonisolated(nonsending) mutating func edit<E: Error, R: ~Copyable>(
259+
_ body: nonisolated(nonsending) (inout OutputSpan<Element>) async throws(E) -> R
260+
) async throws(E) -> R {
261+
var span = OutputSpan(buffer: _storage, initializedCount: _count)
262+
defer {
263+
_count = span.finalize(for: _storage)
264+
span = OutputSpan()
265+
}
266+
return try await body(&span)
267+
}
268+
240269
// FIXME: Stop using and remove this in favor of `edit`
241270
@unsafe
242271
@inlinable

Sources/BasicContainers/UniqueArray+Append.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,31 @@ extension UniqueArray where Element: ~Copyable {
6060
_ensureFreeCapacity(count)
6161
return try _storage.append(count: count, initializingWith: body)
6262
}
63+
64+
/// Append a given number of items to the end of this array by populating
65+
/// an output span with an async closure.
66+
///
67+
/// If the array does not have sufficient capacity to hold the requested
68+
/// number of new elements, then this reallocates the array's storage to
69+
/// grow its capacity, using a geometric growth rate.
70+
///
71+
/// - Parameters
72+
/// - count: The number of items to append to the array.
73+
/// - body: A callback that gets called precisely once to directly
74+
/// populate newly reserved storage within the array. The function
75+
/// is allowed to initialize fewer than `count` items. The array is
76+
/// appended however many items the callback adds to the output span
77+
/// before it returns (or before it throws an error).
78+
///
79+
/// - Complexity: O(`count`)
80+
@_alwaysEmitIntoClient
81+
public nonisolated(nonsending) mutating func append<E: Error, Result: ~Copyable>(
82+
count: Int,
83+
initializingWith body: nonisolated(nonsending) (inout OutputSpan<Element>) async throws(E) -> Result
84+
) async throws(E) -> Result {
85+
_ensureFreeCapacity(count)
86+
return try await _storage.append(count: count, initializingWith: body)
87+
}
6388
}
6489

6590
@available(SwiftStdlib 5.0, *)

Sources/BasicContainers/UniqueArray+Initializers.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,25 @@ extension UniqueArray where Element: ~Copyable {
3636
self.init(capacity: capacity)
3737
try edit(body)
3838
}
39+
40+
/// Creates a new array with the specified capacity, directly initializing
41+
/// its storage using an async closure that populates an output span.
42+
///
43+
/// - Parameters:
44+
/// - capacity: The storage capacity of the new array.
45+
/// - body: A callback that gets called precisely once to directly
46+
/// populate newly reserved storage within the array. The function
47+
/// is allowed to add fewer than `capacity` items. The array is
48+
/// initialized with however many items the callback adds to the
49+
/// output span before it returns (or before it throws an error).
50+
@inlinable
51+
public nonisolated(nonsending) init<E: Error>(
52+
capacity: Int,
53+
initializingWith body: nonisolated(nonsending) (inout OutputSpan<Element>) async throws(E) -> Void
54+
) async throws(E) {
55+
self.init(capacity: capacity)
56+
try await edit(body)
57+
}
3958
}
4059

4160
@available(SwiftStdlib 5.0, *)

Sources/BasicContainers/UniqueArray+Insertions.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,38 @@ extension UniqueArray where Element: ~Copyable {
7878
_ensureFreeCapacity(count)
7979
return _storage.insert(count: count, at: index, initializingWith: body)
8080
}
81+
82+
/// Inserts a given number of new items into this array at the specified
83+
/// position, using an async callback to directly initialize array storage by
84+
/// populating an output span.
85+
///
86+
/// All existing elements at or following the specified position are moved to
87+
/// make room for the new items.
88+
///
89+
/// If the array does not have sufficient capacity to hold the new elements,
90+
/// then this reallocates storage to extend its capacity, using a geometric
91+
/// growth rate.
92+
///
93+
/// - Parameters:
94+
/// - count: The number of items to insert into the array.
95+
/// - index: The position at which to insert the new items.
96+
/// `index` must be a valid index in the array.
97+
/// - body: A callback that gets called precisely once to directly
98+
/// populate newly reserved storage within the array. The function
99+
/// is called with an empty output span of capacity matching the
100+
/// supplied count, and it must fully populate it before returning.
101+
///
102+
/// - Complexity: O(`self.count` + `count`)
103+
@inlinable
104+
public nonisolated(nonsending) mutating func insert<Result: ~Copyable>(
105+
count: Int,
106+
at index: Int,
107+
initializingWith body: nonisolated(nonsending) (inout OutputSpan<Element>) async -> Result
108+
) async -> Result {
109+
// FIXME: This does not allow `body` to throw, to prevent having to move the tail twice. Is that okay?
110+
_ensureFreeCapacity(count)
111+
return await _storage.insert(count: count, at: index, initializingWith: body)
112+
}
81113
}
82114

83115
@available(SwiftStdlib 5.0, *)

Sources/BasicContainers/UniqueArray+Replacements.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,47 @@ extension UniqueArray where Element: ~Copyable {
6363
return _storage.replaceSubrange(
6464
subrange, newCount: newCount, initializingWith: body)
6565
}
66+
67+
/// Replace the specified subrange of elements with new items populated by an
68+
/// async callback function directly into the newly allocated storage.
69+
///
70+
/// This method first removes any existing items from the target subrange.
71+
/// If the array does not have sufficient capacity to accommodate the new
72+
/// elements after the removal, then this reallocates the array's storage to
73+
/// grow its capacity, using a geometric growth rate.
74+
///
75+
/// If you pass a zero-length range as the `subrange` parameter, then
76+
/// this method is equivalent to calling
77+
/// `insert(count: newCount, initializingWith: body)`.
78+
///
79+
/// Likewise, if you pass a zero for `newCount`, then this method
80+
/// removes the elements in the given subrange without any replacement.
81+
/// Calling `removeSubrange(subrange)` is preferred in this case.
82+
///
83+
/// - Parameters
84+
/// - subrange: The subrange of the array to replace. The bounds of
85+
/// the range must be valid indices in the array.
86+
/// - newCount: the number of items to replace the old subrange.
87+
/// - body: A callback that gets called precisely once to directly
88+
/// populate newly reserved storage within the array. The function
89+
/// is called with an empty output span of capacity `newCount`,
90+
/// and it must fully populate it before returning.
91+
///
92+
/// - Complexity: O(`self.count` + `newCount`)
93+
@inlinable
94+
public nonisolated(nonsending) mutating func replaceSubrange<Result: ~Copyable>(
95+
_ subrange: Range<Int>,
96+
newCount: Int,
97+
initializingWith body: nonisolated(nonsending) (inout OutputSpan<Element>) async -> Result
98+
) async -> Result {
99+
// FIXME: Should we allow throwing (and a partially filled output span)?
100+
// FIXME: Should we have a version of this with two closures, to allow custom-consuming the old items?
101+
// replaceSubrange(5..<10, newCount: 3, consumingWith: {...}, initializingWith: {...})
102+
// FIXME: Avoid moving the subsequent elements twice.
103+
_ensureFreeCapacity(newCount - subrange.count)
104+
return await _storage.replaceSubrange(
105+
subrange, newCount: newCount, initializingWith: body)
106+
}
66107
}
67108

68109
@available(SwiftStdlib 5.0, *)

Sources/BasicContainers/UniqueArray.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,30 @@ extension UniqueArray where Element: ~Copyable {
154154
) throws(E) -> R {
155155
try _storage.edit(body)
156156
}
157+
158+
/// Arbitrarily edit the storage underlying this array by invoking a
159+
/// user-supplied async closure with a mutable `OutputSpan` view over it.
160+
/// This method calls its function argument precisely once, allowing it to
161+
/// arbitrarily modify the contents of the output span it is given.
162+
/// The argument is free to add, remove or reorder any items; however,
163+
/// it is not allowed to replace the span or change its capacity.
164+
///
165+
/// When the function argument finishes (whether by returning or throwing an
166+
/// error) the rigid array instance is updated to match the final contents of
167+
/// the output span.
168+
///
169+
/// - Parameter body: A function that edits the contents of this array through
170+
/// an `OutputSpan` argument. This method invokes this function
171+
/// precisely once.
172+
/// - Returns: This method returns the result of its function argument.
173+
/// - Complexity: Adds O(1) overhead to the complexity of the function
174+
/// argument.
175+
@inlinable @inline(__always)
176+
public nonisolated(nonsending) mutating func edit<E: Error, R: ~Copyable>(
177+
_ body: nonisolated(nonsending) (inout OutputSpan<Element>) async throws(E) -> R
178+
) async throws(E) -> R {
179+
try await _storage.edit(body)
180+
}
157181
}
158182

159183
//MARK: - Container primitives

0 commit comments

Comments
 (0)