Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions Sources/BasicContainers/RigidArray+Append.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,37 @@ extension RigidArray where Element: ~Copyable {
}
return try body(&span)
}

/// Append a given number of items to the end of this array by populating
/// an output span.
///
/// If the array does not have sufficient capacity to store the new items in
/// the buffer, then this triggers a runtime error.
///
/// - Parameters
/// - count: The number of items to append to the array.
/// - body: A callback that gets called precisely once to directly
/// populate newly reserved storage within the array. The function
/// is allowed to initialize fewer than `count` items. The array is
/// appended however many items the callback adds to the output span
/// before it returns (or before it throws an error).
///
/// - Complexity: O(`count`)
@_alwaysEmitIntoClient
public nonisolated(nonsending) mutating func append<E: Error, Result: ~Copyable>(
count: Int,
initializingWith body: nonisolated(nonsending) (inout OutputSpan<Element>) async throws(E) -> Result
) async throws(E) -> Result {
// FIXME: This is extremely similar to `edit()`, except it provides a narrower span.
precondition(freeCapacity >= count, "RigidArray capacity overflow")
let buffer = _freeSpace._extracting(first: count)
var span = OutputSpan(buffer: buffer, initializedCount: 0)
defer {
_count &+= span.finalize(for: buffer)
span = OutputSpan()
}
return try await body(&span)
}
}

@available(SwiftStdlib 5.0, *)
Expand Down
19 changes: 19 additions & 0 deletions Sources/BasicContainers/RigidArray+Initializers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,25 @@ extension RigidArray where Element: ~Copyable {
self.init(capacity: capacity)
try edit(body)
}

/// Creates a new array with the specified capacity, directly initializing
/// its storage using an async closure that populates an output span.
///
/// - Parameters:
/// - capacity: The storage capacity of the new array.
/// - body: A callback that gets called precisely once to directly
/// populate newly reserved storage within the array. The function
/// is allowed to add fewer than `capacity` items. The array is
/// initialized with however many items the callback adds to the
/// output span before it returns (or before it throws an error).
@inlinable
public nonisolated(nonsending) init<E: Error>(
capacity: Int,
initializingWith body: nonisolated(nonsending) (inout OutputSpan<Element>) async throws(E) -> Void
) async throws(E) {
self.init(capacity: capacity)
try await edit(body)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be acceptable to add those overloads to swift-collections but I think this doesn't really scale well. Any API that needs to wrap this needs to do the same dance.
Is there a better way to describe this pattern, maybe with some kind of builder pattern that can be used as an intermediate step that needs to finalized?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this isn't scalable. This PR is mostly for demonstrating that we need these OutputSpans to be accessible in async contexts. The pitch contains an alternative considered section for this but it argues that the closure based approach taken here is better than wrapping each OutputSpan into a new type or making OutputSpan have a closure-clean-up.

}

@available(SwiftStdlib 5.0, *)
Expand Down
40 changes: 40 additions & 0 deletions Sources/BasicContainers/RigidArray+Insertions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,46 @@ extension RigidArray where Element: ~Copyable {
}
return body(&span)
}

/// Inserts a given number of new items into this array at the specified
/// position, using an async callback to directly initialize array storage by
/// populating an output span.
///
/// All existing elements at or following the specified position are moved to
/// make room for the new items.
///
/// If the capacity of the array isn't sufficient to accommodate the new
/// elements, then this method triggers a runtime error.
///
/// - Parameters:
/// - count: The number of items to insert into the array.
/// - index: The position at which to insert the new items.
/// `index` must be a valid index in the array.
/// - body: A callback that gets called precisely once to directly
/// populate newly reserved storage within the array. The function
/// is called with an empty output span of capacity matching the
/// supplied count, and it must fully populate it before returning.
///
/// - Complexity: O(`self.count` + `count`)
@inlinable
public nonisolated(nonsending) mutating func insert<Result: ~Copyable>(
count: Int,
at index: Int,
initializingWith body: nonisolated(nonsending) (inout OutputSpan<Element>) async -> Result
) async -> Result {
// FIXME: This does not allow `body` to throw, to prevent having to move the tail twice. Is that okay?
precondition(index >= 0 && index <= self.count, "Index out of bounds")
precondition(count <= freeCapacity, "RigidArray capacity overflow")
let target = unsafe _openGap(at: index, count: count)
var span = OutputSpan(buffer: target, initializedCount: 0)
defer {
let c = span.finalize(for: target)
precondition(c == count, "Inserted fewer items than promised")
_count &+= c
span = OutputSpan()
}
return await body(&span)
}
}

@available(SwiftStdlib 5.0, *)
Expand Down
44 changes: 44 additions & 0 deletions Sources/BasicContainers/RigidArray+Replacements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,50 @@ extension RigidArray where Element: ~Copyable {
}
return body(&span)
}

/// Replace the specified subrange of elements with new items populated by an
/// async callback function directly into the newly allocated storage.
///
/// This method first removes any existing items from the target subrange.
/// If the capacity of the array isn't sufficient to accommodate the new
/// elements, then this method triggers a runtime error.
///
/// If you pass a zero-length range as the `subrange` parameter, then
/// this method is equivalent to calling
/// `insert(count: newCount, initializingWith: body)`.
///
/// Likewise, if you pass a zero for `newCount`, then this method
/// removes the elements in the given subrange without any replacement.
/// Calling `removeSubrange(subrange)` is preferred in this case.
///
/// - Parameters
/// - subrange: The subrange of the array to replace. The bounds of
/// the range must be valid indices in the array.
/// - newCount: the number of items to replace the old subrange.
/// - body: A callback that gets called precisely once to directly
/// populate newly reserved storage within the array. The function
/// is called with an empty output span of capacity `newCount`,
/// and it must fully populate it before returning.
///
/// - Complexity: O(`self.count` + `newCount`)
@inlinable
public nonisolated(nonsending) mutating func replaceSubrange<Result: ~Copyable>(
_ subrange: Range<Int>,
newCount: Int,
initializingWith body: nonisolated(nonsending) (inout OutputSpan<Element>) async -> Result
) async -> Result {
// FIXME: Should we allow throwing (and a partially filled output span)?
// FIXME: Should we have a version of this with two closures, to allow custom-consuming the old items?
// replaceSubrange(5..<10, newCount: 3, consumingWith: {...}, initializingWith: {...})
let target = _gapForReplacement(of: subrange, withNewCount: newCount)
var span = OutputSpan(buffer: target, initializedCount: 0)
defer {
let c = span.finalize(for: target)
precondition(c == newCount, "Inserted fewer items than promised")
span = OutputSpan()
}
return await body(&span)
}
}

@available(SwiftStdlib 5.0, *)
Expand Down
29 changes: 29 additions & 0 deletions Sources/BasicContainers/RigidArray.swift
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,35 @@ extension RigidArray where Element: ~Copyable {
return try body(&span)
}

/// Arbitrarily edit the storage underlying this array by invoking a
/// user-supplied async closure with a mutable `OutputSpan` view over it.
/// This method calls its function argument precisely once, allowing it to
/// arbitrarily modify the contents of the output span it is given.
/// The argument is free to add, remove or reorder any items; however,
/// it is not allowed to replace the span or change its capacity.
///
/// When the function argument finishes (whether by returning or throwing an
/// error) the rigid array instance is updated to match the final contents of
/// the output span.
///
/// - Parameter body: A function that edits the contents of this array through
/// an `OutputSpan` argument. This method invokes this function
/// precisely once.
/// - Returns: This method returns the result of its function argument.
/// - Complexity: Adds O(1) overhead to the complexity of the function
/// argument.
@inlinable
public nonisolated(nonsending) mutating func edit<E: Error, R: ~Copyable>(
_ body: nonisolated(nonsending) (inout OutputSpan<Element>) async throws(E) -> R
) async throws(E) -> R {
var span = OutputSpan(buffer: _storage, initializedCount: _count)
defer {
_count = span.finalize(for: _storage)
span = OutputSpan()
}
return try await body(&span)
}

// FIXME: Stop using and remove this in favor of `edit`
@unsafe
@inlinable
Expand Down
25 changes: 25 additions & 0 deletions Sources/BasicContainers/UniqueArray+Append.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,31 @@ extension UniqueArray where Element: ~Copyable {
_ensureFreeCapacity(count)
return try _storage.append(count: count, initializingWith: body)
}

/// Append a given number of items to the end of this array by populating
/// an output span with an async closure.
///
/// If the array does not have sufficient capacity to hold the requested
/// number of new elements, then this reallocates the array's storage to
/// grow its capacity, using a geometric growth rate.
///
/// - Parameters
/// - count: The number of items to append to the array.
/// - body: A callback that gets called precisely once to directly
/// populate newly reserved storage within the array. The function
/// is allowed to initialize fewer than `count` items. The array is
/// appended however many items the callback adds to the output span
/// before it returns (or before it throws an error).
///
/// - Complexity: O(`count`)
@_alwaysEmitIntoClient
public nonisolated(nonsending) mutating func append<E: Error, Result: ~Copyable>(
count: Int,
initializingWith body: nonisolated(nonsending) (inout OutputSpan<Element>) async throws(E) -> Result
) async throws(E) -> Result {
_ensureFreeCapacity(count)
return try await _storage.append(count: count, initializingWith: body)
}
}

@available(SwiftStdlib 5.0, *)
Expand Down
19 changes: 19 additions & 0 deletions Sources/BasicContainers/UniqueArray+Initializers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,25 @@ extension UniqueArray where Element: ~Copyable {
self.init(capacity: capacity)
try edit(body)
}

/// Creates a new array with the specified capacity, directly initializing
/// its storage using an async closure that populates an output span.
///
/// - Parameters:
/// - capacity: The storage capacity of the new array.
/// - body: A callback that gets called precisely once to directly
/// populate newly reserved storage within the array. The function
/// is allowed to add fewer than `capacity` items. The array is
/// initialized with however many items the callback adds to the
/// output span before it returns (or before it throws an error).
@inlinable
public nonisolated(nonsending) init<E: Error>(
capacity: Int,
initializingWith body: nonisolated(nonsending) (inout OutputSpan<Element>) async throws(E) -> Void
) async throws(E) {
self.init(capacity: capacity)
try await edit(body)
}
}

@available(SwiftStdlib 5.0, *)
Expand Down
32 changes: 32 additions & 0 deletions Sources/BasicContainers/UniqueArray+Insertions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,38 @@ extension UniqueArray where Element: ~Copyable {
_ensureFreeCapacity(count)
return _storage.insert(count: count, at: index, initializingWith: body)
}

/// Inserts a given number of new items into this array at the specified
/// position, using an async callback to directly initialize array storage by
/// populating an output span.
///
/// All existing elements at or following the specified position are moved to
/// make room for the new items.
///
/// If the array does not have sufficient capacity to hold the new elements,
/// then this reallocates storage to extend its capacity, using a geometric
/// growth rate.
///
/// - Parameters:
/// - count: The number of items to insert into the array.
/// - index: The position at which to insert the new items.
/// `index` must be a valid index in the array.
/// - body: A callback that gets called precisely once to directly
/// populate newly reserved storage within the array. The function
/// is called with an empty output span of capacity matching the
/// supplied count, and it must fully populate it before returning.
///
/// - Complexity: O(`self.count` + `count`)
@inlinable
public nonisolated(nonsending) mutating func insert<Result: ~Copyable>(
count: Int,
at index: Int,
initializingWith body: nonisolated(nonsending) (inout OutputSpan<Element>) async -> Result
) async -> Result {
// FIXME: This does not allow `body` to throw, to prevent having to move the tail twice. Is that okay?
_ensureFreeCapacity(count)
return await _storage.insert(count: count, at: index, initializingWith: body)
}
}

@available(SwiftStdlib 5.0, *)
Expand Down
41 changes: 41 additions & 0 deletions Sources/BasicContainers/UniqueArray+Replacements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,47 @@ extension UniqueArray where Element: ~Copyable {
return _storage.replaceSubrange(
subrange, newCount: newCount, initializingWith: body)
}

/// Replace the specified subrange of elements with new items populated by an
/// async callback function directly into the newly allocated storage.
///
/// This method first removes any existing items from the target subrange.
/// If the array does not have sufficient capacity to accommodate the new
/// elements after the removal, then this reallocates the array's storage to
/// grow its capacity, using a geometric growth rate.
///
/// If you pass a zero-length range as the `subrange` parameter, then
/// this method is equivalent to calling
/// `insert(count: newCount, initializingWith: body)`.
///
/// Likewise, if you pass a zero for `newCount`, then this method
/// removes the elements in the given subrange without any replacement.
/// Calling `removeSubrange(subrange)` is preferred in this case.
///
/// - Parameters
/// - subrange: The subrange of the array to replace. The bounds of
/// the range must be valid indices in the array.
/// - newCount: the number of items to replace the old subrange.
/// - body: A callback that gets called precisely once to directly
/// populate newly reserved storage within the array. The function
/// is called with an empty output span of capacity `newCount`,
/// and it must fully populate it before returning.
///
/// - Complexity: O(`self.count` + `newCount`)
@inlinable
public nonisolated(nonsending) mutating func replaceSubrange<Result: ~Copyable>(
_ subrange: Range<Int>,
newCount: Int,
initializingWith body: nonisolated(nonsending) (inout OutputSpan<Element>) async -> Result
) async -> Result {
// FIXME: Should we allow throwing (and a partially filled output span)?
// FIXME: Should we have a version of this with two closures, to allow custom-consuming the old items?
// replaceSubrange(5..<10, newCount: 3, consumingWith: {...}, initializingWith: {...})
// FIXME: Avoid moving the subsequent elements twice.
_ensureFreeCapacity(newCount - subrange.count)
return await _storage.replaceSubrange(
subrange, newCount: newCount, initializingWith: body)
}
}

@available(SwiftStdlib 5.0, *)
Expand Down
24 changes: 24 additions & 0 deletions Sources/BasicContainers/UniqueArray.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,30 @@ extension UniqueArray where Element: ~Copyable {
) throws(E) -> R {
try _storage.edit(body)
}

/// Arbitrarily edit the storage underlying this array by invoking a
/// user-supplied async closure with a mutable `OutputSpan` view over it.
/// This method calls its function argument precisely once, allowing it to
/// arbitrarily modify the contents of the output span it is given.
/// The argument is free to add, remove or reorder any items; however,
/// it is not allowed to replace the span or change its capacity.
///
/// When the function argument finishes (whether by returning or throwing an
/// error) the rigid array instance is updated to match the final contents of
/// the output span.
///
/// - Parameter body: A function that edits the contents of this array through
/// an `OutputSpan` argument. This method invokes this function
/// precisely once.
/// - Returns: This method returns the result of its function argument.
/// - Complexity: Adds O(1) overhead to the complexity of the function
/// argument.
@inlinable @inline(__always)
public nonisolated(nonsending) mutating func edit<E: Error, R: ~Copyable>(
_ body: nonisolated(nonsending) (inout OutputSpan<Element>) async throws(E) -> R
) async throws(E) -> R {
try await _storage.edit(body)
}
}

//MARK: - Container primitives
Expand Down
Loading
Loading