From e8c98f0f12fd5b3c4d184944982149be22db503b Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Tue, 21 Jan 2025 13:09:11 -0800 Subject: [PATCH 1/8] Propose MutableSpan and MutableRawSpan --- proposals/nnnn-MutableSpan.md | 498 ++++++++++++++++++++++++++++++++++ 1 file changed, 498 insertions(+) create mode 100644 proposals/nnnn-MutableSpan.md diff --git a/proposals/nnnn-MutableSpan.md b/proposals/nnnn-MutableSpan.md new file mode 100644 index 0000000000..9627e80ca5 --- /dev/null +++ b/proposals/nnnn-MutableSpan.md @@ -0,0 +1,498 @@ +# MutableSpan and MutableRawSpan: delegate mutations of contiguous memory + +* Proposal: TBD +* Author: [Guillaume Lessard](https://github.com/glessard) +* Review Manager: TBD +* Status: **Pitch** +* Roadmap: [BufferView Roadmap](https://forums.swift.org/t/66211) +* Implementation: "Future" target of [swift-collections](https://github.com/apple/swift-collections/tree/future) +* Review: [Pitch](https://forums.swift.org/) + +[SE-0446]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0446-non-escapable.md +[SE-0447]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0447-span-access-shared-contiguous-storage.md +[SE-0456]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0456-stdlib-span-properties.md +[PR-2305]: https://github.com/swiftlang/swift-evolution/pull/2305 +[SE-0453]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0453-vector.md +[SE-0223]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0223-array-uninitialized-initializer.md +[SE-0176]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0176-enforce-exclusive-access-to-memory.md + +## Introduction + +We recently [introduced][SE-0447] the `Span` and `RawSpan` types, providing read-only access to borrowed memory shared. This proposal adds mutations of exclusively-borrowed memory with `MutableSpan` and `MutableRawSpan`. + +## Motivation + +Many standard library container types can provide direct access to modify their internal representation. Up to now, it has only been possible to do so in an unsafe way. The standard library provides this unsafe functionality with closure-taking functions such as `withUnsafeMutableBufferPointer()` and `withContiguousMutableStorageIfAvailable()`. + +These functions have a few different drawbacks, most prominently their reliance on unsafe types, which makes them unpalatable in security-conscious environments. We continue addressing these issues with `MutableSpan` and `MutableRawSpan`, new non-copyable and non-escapable types that manage respectively mutations of typed and untyped memory. + +In addition to the new types, we will propose adding new API some standard library types to take advantage of `MutableSpan` and `MutableRawSpan`. + +## Proposed solution +We previously introduced `Span` to provide shared read-only access to containers. A question can be raised as to whether this same type could be used for mutations. We cannot, due to the [law of exclusivity][SE-0176]. `Span` is copyable, and it should be copyable in order to properly model read access under the law of exclusivity: a value can be simultaneously accessed through multiple read-only accesses. Mutations, on the other hand, require _exclusive access_. Exclusive access cannot be modeled through a copyable type, since a copy of the value representing the access would violate exclusivity. We therefore need a type separate from `Span` in order to model mutations. + +#### MutableSpan + +`MutableSpan` allows delegating mutations of a type's contiguous internal representation, by providing access to an exclusively-borrowed view of a range of contiguous, initialized memory. `MutableSpan` relies on guarantees that it has exclusive access to the range of memory it represents, and that the memory it represents will remain valid for the duration of the access. These guarantee data race safety and temporal safety. Like `Span`, `MutableSpan` performs bounds-checking on every access to preserve spatial safety. + +A `MutableSpan` provided by a container represents a mutation of that container, via an exclusive borrow. Mutations are implemented by mutating operations, which let the compiler statically enforce exclusivity. + +#### MutableRawSpan + +`MutableRawSpan` allows delegating mutations to memory representing possibly heterogeneously-typed values, such as memory intended for encoding. It makes the same safety guarantees as `MutableSpan`. A `MutableRawSpan` can be obtained from a `MutableSpan` whose `Element` is `BitwiseCopyable`. + +#### Extensions to standard library types + +The standard library will provide `mutableSpan` computed properties. These return lifetime-dependent `MutableSpan` instances, and represent a mutation of the instance that provided them. These computed properties are the safe and composable replacements for the existing `withUnsafeMutableBufferPointer` closure-taking functions. For example, + +```swift +func(_ array: inout Array) { + var ms = array.mutableSpan + modify(&ms) // call function that mutates a MutableSpan + // array.append(2) // attempt to modify `array` would be an error here + _ = consume ms // access to `array` via `ms` ends here + array.append(1) +} +``` + +These computed properties represent a case of lifetime relationships between two bindings that wasn't covered in [SE-0456][SE-0456]. There we defined lifetime relationships for computed property getters of non-escapable and copyable types (`~Escapable & Copyable`). We now need to define them for properties of non-escapable and non-copyable types (`~Escapable & ~Copyable`). A `~Escapable & ~Copyable` value borrows another binding; if this borrow is also a mutation then it is an exclusive borrow. The scope of the borrow, whether or not it is exclusive, extends until the last use of the dependent binding. + +## Detailed Design + +#### MutableSpan + +`MutableSpan` is a simple representation of a region of initialized memory. It is non-copyable in order to enforce exclusive access for mutations of its memory, as required by the law of exclusivity: + +````swift +@frozen +public struct MutableSpan: ~Copyable, ~Escapable { + internal var _start: UnsafeMutableRawPointer? + internal var _count: Int +} + +extension MutableSpan: @unchecked Sendable where Element: Sendable {} +```` + +We store a `UnsafeMutableRawPointer` value internally in order to explicitly support reinterpreted views of memory as containing different types of `BitwiseCopyable` elements. Note that the the optionality of the pointer does not affect usage of `MutableSpan`, since accesses are bounds-checked and the pointer is only dereferenced when the `MutableSpan` isn't empty, when the pointer cannot be `nil`. + +```swift +extension MutableSpan where Element: ~Copyable { + /// The number of initialized elements in this `MutableSpan`. + var count: Int { get } + + /// A Boolean value indicating whether the span is empty. + var isEmpty: Bool { get } + + /// The type that represents a position in a `MutableSpan`. + typealias Index = Int + + /// The range of indices valid for this `MutableSpan`. + var indices: Range { get } + + /// Accesses the element at the specified position. + subscript(_ index: Index) -> Element { borrowing read; mutate } + + /// Exchange the elements at the two given offsets + mutating func swapAt(_ i: Index, _ j: Index) +} +``` + +Like `Span` before it, `MutableSpan` does not conform to `Collection` or `MutableCollection`. These two protocols assume their conformers and elements are copyable, and as such are not compatible with a non-copyable type such as `MutableSpan`. A later proposal will consider generalized containers. + +The subscript uses a borrowing accessor for read-only element access, and a mutate accessor for element mutation. The read-only borrow is a read access to the entire `MutableSpan` for the duration of the access to the element. The `mutate` accessor is an exclusive access to the entire `MutableSpan` for the duration of the mutation of the element. + +`MutableSpan` uses offset-based indexing. The first element of a given span is always at offset 0, and its last element is always at position `count-1`. + +As a side-effect of not conforming to `Collection` or `Sequence`, `MutableSpan` is not directly supported by `for` loops at this time. It is, however, easy to use in a `for` loop via indexing: + +```swift +for i in myMutableSpan.indices { + mutatingFunction(&myMutableSpan[i]) +} +``` + +##### `MutableSpan` API: + +Initializers, required for library adoption, will be proposed alongside [lifetime annotations][PR-2305]; for details, see "[Initializers](#initializers)" in the [future directions](#Directions) section. + +```swift +extension MutableSpan where Element: ~Copyable { + @unsafe + subscript(unchecked position: Index) -> Element { borrowing read; mutate } + + @unsafe + mutating func swapAt(unchecked i: Index, unchecked j: Index) + + var storage: Span { borrowing get } +} +``` +##### Bulk updating of a `MutableSpan`'s elements: + +We include functions to perform bulk copies into the memory represented by a `MutableSpan`. Updating a `MutableSpan` from known-sized sources (such as `Collection` or `Span`) copies every element of a source. It is an error to do so when there is the span is too short to contain every element from the source. Updating a `MutableSpan` from `Sequence` or `IteratorProtocol` instances will copy as many items as possible, either until the input is empty or until the operation has updated the item at the last index. + +```swift +extension MutableSpan { + mutating func update( + startingAt offset: Index = 0, + repeating repeatedValue: Element + ) + + mutating func update( + startingAt offset: Index = 0, + from source: S + ) -> (unwritten: S.Iterator, index: Index) where S.Element == Element + + mutating func update( + startingAt offset: Index = 0, + from elements: inout some IteratorProtocol + ) -> Index + + mutating func update( + startingAt offset: Index = 0, + fromContentsOf source: some Collection + ) -> Index + + mutating func update( + startingAt offset: Index = 0, + fromContentsOf source: Span + ) -> Index + + mutating func update( + startingAt offset: Index = 0, + fromContentsOf source: borrowing Self + ) -> Index +} + + mutating func moveUpdate( + startingAt offset: Index = 0, + fromContentsOf source: UnsafeMutableBufferPointer + ) -> Index +} + +extension MutableSpan { + mutating func moveUpdate( + startingAt offset: Index = 0, + fromContentsOf source: Slice> + ) -> Index +} +``` +##### Interoperability with unsafe code: + +```swift +extension MutableSpan where Element: ~Copyable { + func withUnsafeBufferPointer( + _ body: (_ buffer: UnsafeBufferPointer) throws(E) -> Result + ) throws(E) -> Result + + mutating func withUnsafeMutableBufferPointer( + _ body: (_ buffer: UnsafeMutableBufferPointer) throws(E) -> Result + ) throws(E) -> Result +} + +extension MutableSpan where Element: BitwiseCopyable { + func withUnsafeBytes( + _ body: (_ buffer: UnsafeRawBufferPointer) throws(E) -> Result + ) throws(E) -> Result + + mutating func withUnsafeMutableBytes( + _ body: (_ buffer: UnsafeMutableRawBufferPointer) throws(E) -> Result + ) throws(E) -> Result +} +``` +These functions use a closure to define the scope of validity of `buffer`, ensuring that the underlying `MutableSpan` and the binding it depends on both remain valid through the end of the closure. They have the same shape as the equivalents on `Array` because they fulfill the same function, namely to keep the underlying binding alive. + +#### MutableRawSpan + +`MutableRawSpan` is similar to `MutableSpan`, but reperesents untyped initialized bytes. `MutableRawSpan` specifically supports encoding and decoding applications. Its API supports `unsafeLoad(as:)` and `storeBytes(of: as:)`, as well as a variety of bulk copying operations. + +##### `MutableRawSpan` API: + +```swift +@frozen +public struct MutableRawSpan: ~Copyable, ~Escapable { + internal var _start: UnsafeMutableRawPointer? + internal var _count: Int +} + +extension MutableRawSpan: @unchecked Sendable +``` + +Initializers, required for library adoption, will be proposed alongside [lifetime annotations][PR-2305]; for details, see "[Initializers](#initializers)" in the [future directions](#Directions) section. + +```swift +extension MutableRawSpan { + /// The number of bytes in the span. + var byteCount: Int { get } + + /// A Boolean value indicating whether the span is empty. + var isEmpty: Bool { get } + + /// The range of valid byte offsets into this `RawSpan` + var byteOffsets: Range { get } +} + +``` + +##### Accessing and modifying the memory of a `MutableRawSpan`: + +The basic operations available on `RawSpan` are available for `MutableRawSpan`. These operations are not type-safe, in that the loaded value returned by the operation can be invalid, and violate type invariants. Some types have a property that makes the `unsafeLoad(as:)` function safe, but we don't have a way to [formally identify](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0447-span-access-shared-contiguous-storage.md#SurjectiveBitPattern) such types at this time. + +```swift +extension MutableRawSpan { + @unsafe + func unsafeLoad( + fromByteOffset offset: Int = 0, as: T.Type + ) -> T + + @unsafe + func unsafeLoadUnaligned( + fromByteOffset offset: Int = 0, as: T.Type + ) -> T + + @unsafe + func unsafeLoad( + fromUncheckedByteOffset offset: Int, as: T.Type + ) -> T + + @unsafe + func unsafeLoadUnaligned( + fromUncheckedByteOffset offset: Int, as: T.Type + ) -> T +} +``` + +To these, `MutableRawSpan` adds functions to store the bytes of a `BitwiseCopyable` value: + +```swift +extension MutableRawSpan { + func storeBytes( + of value: T, toByteOffset offset: Int = 0, as type: T.Type + ) + + @unsafe + func storeBytes( + of value: T, toUncheckedByteOffset offset: Int, as type: T.Type + ) +} +``` + +We include functions to perform bulk copies into the memory represented by a `MutableRawSpan`. Updating a `MutableRawSpan` from a `Collection` or a `Span` copies every element of a source. It is an error to do so when there is are not enough bytes in the span to contain every element from the source. Updating `MutableRawSpan` from `Sequence` or `IteratorProtocol` instance copies as many items as possible, either until the input is empty or until there are not enough bytes in the span to store another element. +```swift +extension MutableRawSpan { + mutating func update( + startingAt byteOffset: Int = 0, + from source: S + ) -> (unwritten: S.Iterator, byteOffset: Int) where S.Element: BitwiseCopyable + + mutating func update( + startingAt byteOffset: Int = 0, + from elements: inout some IteratorProtocol + ) -> Int + + mutating func update( + startingAt byteOffset: Int = 0, + fromContentsOf source: C + ) -> Int where C.Element: BitwiseCopyable + + mutating func update( + startingAt byteOffset: Int = 0, + fromContentsOf source: Span + ) -> Int + + mutating func update( + startingAt byteOffset: Int = 0, + fromContentsOf source: borrowing MutableSpan + ) -> Int + + mutating func update( + startingAt byteOffset: Int = 0, + fromContentsOf source: RawSpan + ) -> Int + + mutating func update( + startingAt byteOffset: Int = 0, + fromContentsOf source: borrowing MutableRawSpan + ) -> Int +} +``` + +##### Interoperability with unsafe code: + +```swift +extension MutableRawSpan { + func withUnsafeBytes( + _ body: (_ buffer: UnsafeRawBufferPointer) throws(E) -> Result + ) throws(E) -> Result + + mutating func withUnsafeMutableBytes( + _ body: (_ buffer: UnsafeMutableRawBufferPointer) throws(E) -> Result + ) throws(E) -> Result +} +``` +These functions use a closure to define the scope of validity of `buffer`, ensuring that the underlying `MutableSpan` and the binding it depends on both remain valid through the end of the closure. They have the same shape as the equivalents on `Array` because they fulfill the same purpose, namely to keep the underlying binding alive. + +##### Accessing and mutating the raw bytes of a `MutableSpan` + +```swift +extension MutableSpan where Element: BitwiseCopyable { + var mutableBytes: MutableRawSpan { mutating get } +} +``` + + + +#### Extensions to Standard Library types + +A `mutating` computed property getter defined on any type and returning a `~Escapable & ~Copyable` value establishes an exclusive borrowing lifetime relationship of the returned value on the callee's binding. As long as the returned value exists, then the callee's binding remains borrowed and cannot be accessed in any other way. + +A `nonmutating` computed property getter returning a `~Escapable & ~Copyable` value establishes a borrowing lifetime relationship, as if returning a `~Escapable & Copyable` value (see [SE-0456][SE-0456].) + +The standard library will provide `mutableSpan` computed properties. These return lifetime-dependent `MutableSpan` instances. These computed properties are the safe and composable replacements for the existing `withUnsafeMutableBufferPointer` closure-taking functions. + +```swift +extension Array { + var mutableSpan: MutableSpan { mutating get } +} + +extension ContiguousArray { + var mutableSpan: MutableSpan { mutating get } +} + +extension ArraySlice { + var mutableSpan: MutableSpan { mutating get } +} + +extension InlineArray { + var mutableSpan: MutableSpan { mutating get } +} + +extension CollectionOfOne { + var mutableSpan: MutableSpan { mutating get } +} +``` + + + +#### Extensions to unsafe buffer types + +We hope that `MutableSpan` and `MutableRawSpan` will become the standard ways to delegate mutations of shared contiguous memory in Swift. Many current API delegate mutations with closure-based functions that receive an `UnsafeMutableBufferPointer` parameter to do this. We will provide ways to unsafely obtain `MutableSpan` instances from `UnsafeMutableBufferPointer` and `MutableRawSpan` instances from `UnsafeMutableRawBufferPointer`, in order to bridge these unsafe types to newer, safer contexts. + +```swift +extension UnsafeMutableBufferPointer { + var mutableSpan: MutableSpan { get } +} + +extension UnsafeMutableRawBufferPointer { + var mutableBytes: MutableRawSpan { get } +} +``` + +These unsafe conversions returns a value whose lifetime is dependent on the _binding_ of the `UnsafeMutable[Raw]BufferPointer`. This dependency does not keep the underlying memory alive. As is usual where the `UnsafePointer` family of types is involved, the programmer must ensure the memory remains allocated while it is in use. Additionally, the following invariants must remain true for as long as the `MutableSpan` or `MutableRawSpan` value exists: + + - The underlying memory remains initialized. + - The underlying memory is not accessed through another means. + +Failure to maintain these invariants results in undefined behaviour. + +#### Extensions to `Foundation.Data` + +While the `swift-foundation` package and the `Foundation` framework are not governed by the Swift evolution process, `Data` is similar in use to standard library types, and the project acknowledges that it is desirable for it to have similar API when appropriate. Accordingly, we plan to propose the following additions to `Foundation.Data`: + +```swift +extension Foundation.Data { + // Mutate this `Data`'s bytes through a `MutableSpan` + var mutableSpan: MutableSpan { mutating get } + + // Mutate this `Data`'s bytes through a `MutableRawSpan` + var mutableBytes: MutableRawSpan { mutating get } +} +``` + +#### Performance + +The `mutableSpan` and `mutableBytes` properties should be performant and return their `MutableSpan` or `MutableRawSpan` with very little work, in O(1) time. In copy-on-write types, however, obtaining a `MutableSpan` is the start of the mutation, and if the backing buffer is not uniquely reference a copy must be made ahead of returning the `MutableSpan`. + +Note that `MutableSpan` incurs no special behaviour for bridged types, since mutations always require a defensive copy of data bridged from Objective-C data structures. + +## Source compatibility + +This proposal is additive and source-compatible with existing code. + +## ABI compatibility + +This proposal is additive and ABI-compatible with existing code. + +## Implications on adoption + +The additions described in this proposal require a new version of the Swift standard library and runtime. + +## Alternatives considered + +#### Adding `withMutableSpan()` closure-taking functions + +The `mutableSpan` and `mutableBytes` properties aim to be safe replacements for the `withUnsafeMutableBufferPointer()` and `withUnsafeMutableBytes()` closure-taking functions. We could consider `withMutableSpan()` and `withMutableBytes()` closure-taking functions that would provide a quicker migration away from the older unsafe functions. We do not believe the closure-taking functions are desirable in the long run. In the short run, there may be a desire to clearly mark the scope where a `MutableSpan` instance is used. The default method would be to explicitly consume a `MutableSpan` instance: + +```swift +var a = ContiguousArray(0..<8) +var span = a.mutableSpan +modify(&span) +_ = consume span +a.append(8) +``` + +During the evolution of Swift, we have learned that closure-based API are difficult to compose, especially with one another. They can also require alterations to support new language features. For example, the generalization of closure-taking API for non-copyable values as well as typed throws is ongoing; adding more closure-taking API may make future feature evolution more labor-intensive. By instead relying on returned values, whether from computed properties or functions, we build for **greater** composability. Use cases where this approach falls short should be reported as enhancement requests or bugs. + +#### Omitting extensions to `UnsafeBufferPointer` and related types + +We could omit the extensions to `UnsafeMutableBufferPointer` and related types, and rely instead of future `MutableSpan` and `MutableRawSpan` initializers. The initializers can have the advantage of being able to communicate semantics (somewhat) through their parameter labels. However, they also have a very different shape than the `storage` computed properties we are proposing for the safe types such as `Array`. We believe that the adding the same API on both safe and unsafe types is advantageous, even if the preconditions for the properties cannot be statically enforced. + +## Future directions + +Note: The future directions stated in [SE-0447](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0447-span-access-shared-contiguous-storage.md#Directions) apply here as well. + +#### Initializing and returning `MutableSpan` instances + +`MutableSpan` represents a region of memory and, as such, must be initialized using an unsafe pointer. This is an unsafe operation which will typically be performed internally to a container's implementation. In order to bridge to safe code, these initializers require new annotations that indicate to the compiler how the newly-created `Span` can be used safely. + +These annotations have been [pitched][PR-2305-pitch] and, after revision, are expected to be pitched again soon. `MutableSpan` initializers using lifetime annotations will be proposed alongside the annotations themselves. + +#### Functions providing variants of `MutableRawSpan` to `MutableSpan` + +`MutableSpan`s representing subsets of consecutive elements could be extracted out of a larger `MutableSpan` with an API similar to the `extracting()` functions recently added to `UnsafeBufferPointer` in support of non-copyable elements: + +```swift +extension MutableSpan where Element: ~Copyable { + public mutating func extracting(_ bounds: Range) -> Self +} +``` + +These functions would require a lifetime dependency annotation. + +Similarly, a `MutableRawSpan` could provide a function to mutate a range of its bytes as a typed `MutableSpan`: + +```swift +extension MutableRawSpan { + @unsafe + public mutating func unsafeMutableView(as type: T.Type) -> MutableSpan +} +``` +We are subsetting functions that require lifetime annotations until such annotations are [proposed][PR-2305]. + +#### Splitting `MutableSpan` instances + +It is desirable to have a way to split a `MutableSpan` in multiple parts, for divide-and-conquer algorithms or other reasons: + +```swift +extension MutableSpan where Element: ~Copyable { + func split(at index: Index) -> (part1: Self, part2: Self) +} +``` + +Unfortunately, tuples do not support non-copyable values yet. We may be able to use the new `Vector`/`Slab`/`InlineArray` type proposed in [SE-0453][SE-0453], but destructuring its non-copyable elements remains a challenge. Solving this issue for `Span` as well as `MutableSpan` is a top priority. + +#### Delegated initialization with `OutputSpan` + +Some data structures can delegate initialization of parts of their owned memory. The standard library added the `Array` initializer `init(unsafeUninitializedCapacity:initializingWith:)` in [SE-0223][SE-0223]. This initializer relies on `UnsafeMutableBufferPointer` and correct usage of initialization primitives. We should present a simpler and safer model of initialization by leveraging non-copyability and non-escapability. + +## Acknowledgements + From 2f625f9cb3b54f2759850622059a937a16720ab0 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Fri, 7 Feb 2025 13:54:35 -0800 Subject: [PATCH 2/8] Edits, feedback responses --- proposals/nnnn-MutableSpan.md | 95 +++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 37 deletions(-) diff --git a/proposals/nnnn-MutableSpan.md b/proposals/nnnn-MutableSpan.md index 9627e80ca5..70a76f6172 100644 --- a/proposals/nnnn-MutableSpan.md +++ b/proposals/nnnn-MutableSpan.md @@ -12,6 +12,7 @@ [SE-0447]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0447-span-access-shared-contiguous-storage.md [SE-0456]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0456-stdlib-span-properties.md [PR-2305]: https://github.com/swiftlang/swift-evolution/pull/2305 +[SE-0437]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0437-noncopyable-stdlib-primitives.md [SE-0453]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0453-vector.md [SE-0223]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0223-array-uninitialized-initializer.md [SE-0176]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0176-enforce-exclusive-access-to-memory.md @@ -29,13 +30,13 @@ These functions have a few different drawbacks, most prominently their reliance In addition to the new types, we will propose adding new API some standard library types to take advantage of `MutableSpan` and `MutableRawSpan`. ## Proposed solution -We previously introduced `Span` to provide shared read-only access to containers. A question can be raised as to whether this same type could be used for mutations. We cannot, due to the [law of exclusivity][SE-0176]. `Span` is copyable, and it should be copyable in order to properly model read access under the law of exclusivity: a value can be simultaneously accessed through multiple read-only accesses. Mutations, on the other hand, require _exclusive access_. Exclusive access cannot be modeled through a copyable type, since a copy of the value representing the access would violate exclusivity. We therefore need a type separate from `Span` in order to model mutations. +We introduced `Span` to provide shared read-only access to containers. We cannot use `Span` to also model container mutations, due to the [law of exclusivity][SE-0176]. `Span` is copyable, and must be copyable in order to properly model read access under the law of exclusivity: a value can be simultaneously accessed through multiple read-only accesses. Mutations, on the other hand, require _exclusive access_. Exclusive access cannot be modeled with a copyable type, since a copy of the value representing the access would violate exclusivity by adding a second access. We therefore need a non-copyable type separate from `Span` in order to model mutations. #### MutableSpan -`MutableSpan` allows delegating mutations of a type's contiguous internal representation, by providing access to an exclusively-borrowed view of a range of contiguous, initialized memory. `MutableSpan` relies on guarantees that it has exclusive access to the range of memory it represents, and that the memory it represents will remain valid for the duration of the access. These guarantee data race safety and temporal safety. Like `Span`, `MutableSpan` performs bounds-checking on every access to preserve spatial safety. +`MutableSpan` allows delegating mutations of a type's contiguous internal representation, by providing access to an exclusively-borrowed view of a range of contiguous, initialized memory. `MutableSpan` relies on guarantees that it has exclusive access to the range of memory it represents, and that the memory it represents will remain valid for the duration of the access. These provide data race safety and temporal safety. Like `Span`, `MutableSpan` performs bounds-checking on every access to preserve spatial safety. -A `MutableSpan` provided by a container represents a mutation of that container, via an exclusive borrow. Mutations are implemented by mutating operations, which let the compiler statically enforce exclusivity. +A `MutableSpan` provided by a container represents a mutation of that container, via an exclusive borrow. Mutations are implemented by mutating functions and subscripts, which let the compiler statically enforce exclusivity. #### MutableRawSpan @@ -55,7 +56,7 @@ func(_ array: inout Array) { } ``` -These computed properties represent a case of lifetime relationships between two bindings that wasn't covered in [SE-0456][SE-0456]. There we defined lifetime relationships for computed property getters of non-escapable and copyable types (`~Escapable & Copyable`). We now need to define them for properties of non-escapable and non-copyable types (`~Escapable & ~Copyable`). A `~Escapable & ~Copyable` value borrows another binding; if this borrow is also a mutation then it is an exclusive borrow. The scope of the borrow, whether or not it is exclusive, extends until the last use of the dependent binding. +These computed properties represent a case of lifetime relationships not covered in [SE-0456][SE-0456]. In SE-0456 we defined lifetime relationships for computed property getters of non-escapable and copyable types (`~Escapable & Copyable`). We propose defining them for properties of non-escapable and non-copyable types (`~Escapable & ~Copyable`). A `~Escapable & ~Copyable` value borrows another binding; if this borrow is also a mutation then it is an exclusive borrow. The scope of the borrow, whether or not it is exclusive, extends until the last use of the dependent binding. ## Detailed Design @@ -75,6 +76,8 @@ extension MutableSpan: @unchecked Sendable where Element: Sendable {} We store a `UnsafeMutableRawPointer` value internally in order to explicitly support reinterpreted views of memory as containing different types of `BitwiseCopyable` elements. Note that the the optionality of the pointer does not affect usage of `MutableSpan`, since accesses are bounds-checked and the pointer is only dereferenced when the `MutableSpan` isn't empty, when the pointer cannot be `nil`. +Initializers, required for library adoption, will be proposed alongside [lifetime annotations][PR-2305]; for details, see "[Initializers](#initializers)" in the [future directions](#Directions) section. + ```swift extension MutableSpan where Element: ~Copyable { /// The number of initialized elements in this `MutableSpan`. @@ -90,10 +93,14 @@ extension MutableSpan where Element: ~Copyable { var indices: Range { get } /// Accesses the element at the specified position. - subscript(_ index: Index) -> Element { borrowing read; mutate } + subscript(_ index: Index) -> Element { borrow; mutate } + // accessor syntax from accessors roadmap (https://forums.swift.org/t/76707) /// Exchange the elements at the two given offsets mutating func swapAt(_ i: Index, _ j: Index) + + /// Borrow the underlying memory for read-only access + var span: Span { borrowing get } } ``` @@ -111,24 +118,33 @@ for i in myMutableSpan.indices { } ``` -##### `MutableSpan` API: +##### Unchecked access to elements: -Initializers, required for library adoption, will be proposed alongside [lifetime annotations][PR-2305]; for details, see "[Initializers](#initializers)" in the [future directions](#Directions) section. +The `subscript` mentioned above always checks the bounds of the `MutableSpan` before allowing access to the memory, preventing out-of-bounds accesses. We also provide an unchecked variant of the `subscript` and of the `swapAt` function as an alternative for situations where bounds-checking is costly and has already been performed: ```swift extension MutableSpan where Element: ~Copyable { + /// Accesses the element at the specified `position`. + /// + /// This subscript does not validate `position`; this is an unsafe operation. + /// + /// - Parameter position: The offset of the element to access. `position` + /// must be greater or equal to zero, and less than `count`. @unsafe - subscript(unchecked position: Index) -> Element { borrowing read; mutate } + subscript(unchecked position: Index) -> Element { borrow; mutate } + /// Exchange the elements at the two given offsets + /// + /// This function does not validate `i` or `j`; this is an unsafe operation. @unsafe - mutating func swapAt(unchecked i: Index, unchecked j: Index) - - var storage: Span { borrowing get } + mutating func swapAt(unchecked i: Index, unchecked j: Index) } ``` ##### Bulk updating of a `MutableSpan`'s elements: -We include functions to perform bulk copies into the memory represented by a `MutableSpan`. Updating a `MutableSpan` from known-sized sources (such as `Collection` or `Span`) copies every element of a source. It is an error to do so when there is the span is too short to contain every element from the source. Updating a `MutableSpan` from `Sequence` or `IteratorProtocol` instances will copy as many items as possible, either until the input is empty or until the operation has updated the item at the last index. +We include functions to perform bulk copies of elements into the memory represented by a `MutableSpan`. Updating a `MutableSpan` from known-sized sources (such as `Collection` or `Span`) copies every element of a source. It is an error to do so when there is the span is too short to contain every element from the source. Updating a `MutableSpan` from `Sequence` or `IteratorProtocol` instances will copy as many items as possible, either until the input is empty or until the operation has updated the item at the last index. + +**Note:** This set of functions is sufficiently complete in functionality, but uses a minimal approach to slicing. This is only one of many possible approaches to slicing `MutableSpan`. We could revive the option of using a `some RangeExpression` parameter, or we could use the return value of a `func extracting(_: some RangeExpression)` such as was [recently added][SE-0437] to `UnsafeBufferPointer`. The latter option in combination with `mutating` functions requires the use of intermediate bindings. This section may change in response to feedback and our investigations. ```swift extension MutableSpan { @@ -163,6 +179,7 @@ extension MutableSpan { ) -> Index } +extension MutableSpan where Element: ~Copyable { mutating func moveUpdate( startingAt offset: Index = 0, fromContentsOf source: UnsafeMutableBufferPointer @@ -203,7 +220,7 @@ These functions use a closure to define the scope of validity of `buffer`, ensur #### MutableRawSpan -`MutableRawSpan` is similar to `MutableSpan`, but reperesents untyped initialized bytes. `MutableRawSpan` specifically supports encoding and decoding applications. Its API supports `unsafeLoad(as:)` and `storeBytes(of: as:)`, as well as a variety of bulk copying operations. +`MutableRawSpan` is similar to `MutableSpan`, but represents untyped initialized bytes. `MutableRawSpan` specifically supports encoding and decoding applications. Its API supports `unsafeLoad(as:)` and `storeBytes(of: as:)`, as well as a variety of bulk copying operations. ##### `MutableRawSpan` API: @@ -230,12 +247,26 @@ extension MutableRawSpan { /// The range of valid byte offsets into this `RawSpan` var byteOffsets: Range { get } } - ``` ##### Accessing and modifying the memory of a `MutableRawSpan`: -The basic operations available on `RawSpan` are available for `MutableRawSpan`. These operations are not type-safe, in that the loaded value returned by the operation can be invalid, and violate type invariants. Some types have a property that makes the `unsafeLoad(as:)` function safe, but we don't have a way to [formally identify](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0447-span-access-shared-contiguous-storage.md#SurjectiveBitPattern) such types at this time. +`MutableRawSpan` supports storing the bytes of a `BitwiseCopyable` value to its underlying memory: + +```swift +extension MutableRawSpan { + mutating func storeBytes( + of value: T, toByteOffset offset: Int = 0, as type: T.Type + ) + + @unsafe + mutating func storeBytes( + of value: T, toUncheckedByteOffset offset: Int, as type: T.Type + ) +} +``` + +Additionally, the basic loading operations available on `RawSpan` are available for `MutableRawSpan`. These operations are not type-safe, in that the loaded value returned by the operation can be invalid, and violate type invariants. Some types have a property that makes the `unsafeLoad(as:)` function safe, but we don't have a way to [formally identify](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0447-span-access-shared-contiguous-storage.md#SurjectiveBitPattern) such types at this time. ```swift extension MutableRawSpan { @@ -261,22 +292,10 @@ extension MutableRawSpan { } ``` -To these, `MutableRawSpan` adds functions to store the bytes of a `BitwiseCopyable` value: - -```swift -extension MutableRawSpan { - func storeBytes( - of value: T, toByteOffset offset: Int = 0, as type: T.Type - ) +We include functions to perform bulk copies into the memory represented by a `MutableRawSpan`. Updating a `MutableRawSpan` from a `Collection` or a `Span` copies every element of a source. It is an error to do so when there is are not enough bytes in the span to contain every element from the source. Updating `MutableRawSpan` from `Sequence` or `IteratorProtocol` instance copies as many items as possible, either until the input is empty or until there are not enough bytes in the span to store another element. - @unsafe - func storeBytes( - of value: T, toUncheckedByteOffset offset: Int, as type: T.Type - ) -} -``` +**Note:** This set of functions is sufficiently complete in functionality, but uses a minimal approach to slicing. This is only one of many possible approaches to slicing `MutableRawSpan`. (See the note above for more details on the same considerations.) -We include functions to perform bulk copies into the memory represented by a `MutableRawSpan`. Updating a `MutableRawSpan` from a `Collection` or a `Span` copies every element of a source. It is an error to do so when there is are not enough bytes in the span to contain every element from the source. Updating `MutableRawSpan` from `Sequence` or `IteratorProtocol` instance copies as many items as possible, either until the input is empty or until there are not enough bytes in the span to store another element. ```swift extension MutableRawSpan { mutating func update( @@ -347,7 +366,7 @@ A `mutating` computed property getter defined on any type and returning a `~Esca A `nonmutating` computed property getter returning a `~Escapable & ~Copyable` value establishes a borrowing lifetime relationship, as if returning a `~Escapable & Copyable` value (see [SE-0456][SE-0456].) -The standard library will provide `mutableSpan` computed properties. These return lifetime-dependent `MutableSpan` instances. These computed properties are the safe and composable replacements for the existing `withUnsafeMutableBufferPointer` closure-taking functions. +The standard library will provide `mutating` computed properties providing lifetime-dependent `MutableSpan` instances. These `mutableSpan` computed properties are intended as the safe and composable replacements for the existing `withUnsafeMutableBufferPointer` closure-taking functions. ```swift extension Array { @@ -371,8 +390,6 @@ extension CollectionOfOne { } ``` - - #### Extensions to unsafe buffer types We hope that `MutableSpan` and `MutableRawSpan` will become the standard ways to delegate mutations of shared contiguous memory in Swift. Many current API delegate mutations with closure-based functions that receive an `UnsafeMutableBufferPointer` parameter to do this. We will provide ways to unsafely obtain `MutableSpan` instances from `UnsafeMutableBufferPointer` and `MutableRawSpan` instances from `UnsafeMutableRawBufferPointer`, in order to bridge these unsafe types to newer, safer contexts. @@ -412,7 +429,7 @@ extension Foundation.Data { The `mutableSpan` and `mutableBytes` properties should be performant and return their `MutableSpan` or `MutableRawSpan` with very little work, in O(1) time. In copy-on-write types, however, obtaining a `MutableSpan` is the start of the mutation, and if the backing buffer is not uniquely reference a copy must be made ahead of returning the `MutableSpan`. -Note that `MutableSpan` incurs no special behaviour for bridged types, since mutations always require a defensive copy of data bridged from Objective-C data structures. +Note that `MutableSpan` incurs no special behaviour for bridged types, since mutable bindings always require a defensive copy of data bridged from Objective-C data structures. ## Source compatibility @@ -424,7 +441,7 @@ This proposal is additive and ABI-compatible with existing code. ## Implications on adoption -The additions described in this proposal require a new version of the Swift standard library and runtime. +The additions described in this proposal require a new version of the Swift standard library. ## Alternatives considered @@ -478,7 +495,7 @@ extension MutableRawSpan { ``` We are subsetting functions that require lifetime annotations until such annotations are [proposed][PR-2305]. -#### Splitting `MutableSpan` instances +#### Splitting `MutableSpan` instances – `MutableSpan` in divide-and-conquer algorithms It is desirable to have a way to split a `MutableSpan` in multiple parts, for divide-and-conquer algorithms or other reasons: @@ -488,11 +505,15 @@ extension MutableSpan where Element: ~Copyable { } ``` -Unfortunately, tuples do not support non-copyable values yet. We may be able to use the new `Vector`/`Slab`/`InlineArray` type proposed in [SE-0453][SE-0453], but destructuring its non-copyable elements remains a challenge. Solving this issue for `Span` as well as `MutableSpan` is a top priority. +Unfortunately, tuples do not support non-copyable values yet. We may be able to use `InlineArray` ([SE-0453][SE-0453]), or a bespoke type, but destructuring the non-copyable constituent part remains a challenge. Solving this issue for `Span` and `MutableSpan` is a top priority. + +#### Mutating algorithms + +Algorithms defined on `MutableCollection` such as `sort(by:)` and `partition(by:)` could be defined on `MutableSpan`. We believe we will be able to define these more generally once we have a generalized container protocol hierarchy. #### Delegated initialization with `OutputSpan` Some data structures can delegate initialization of parts of their owned memory. The standard library added the `Array` initializer `init(unsafeUninitializedCapacity:initializingWith:)` in [SE-0223][SE-0223]. This initializer relies on `UnsafeMutableBufferPointer` and correct usage of initialization primitives. We should present a simpler and safer model of initialization by leveraging non-copyability and non-escapability. -## Acknowledgements +We expect to propose an `OutputSpan` type to represent partially-initialized memory, and to support to the initialization of memory by appending to the initialized portion of the underlying storage. From 356900ad58f726cdb2ce99d6ba50b5b0af681ccf Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Mon, 10 Feb 2025 07:50:18 -0800 Subject: [PATCH 3/8] Fix word salad --- proposals/nnnn-MutableSpan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/nnnn-MutableSpan.md b/proposals/nnnn-MutableSpan.md index 70a76f6172..a76ddba350 100644 --- a/proposals/nnnn-MutableSpan.md +++ b/proposals/nnnn-MutableSpan.md @@ -19,7 +19,7 @@ ## Introduction -We recently [introduced][SE-0447] the `Span` and `RawSpan` types, providing read-only access to borrowed memory shared. This proposal adds mutations of exclusively-borrowed memory with `MutableSpan` and `MutableRawSpan`. +We recently [introduced][SE-0447] the `Span` and `RawSpan` types, providing shared read-only access to borrowed memory. This proposal adds helper types to delegate mutations of exclusively-borrowed memory: `MutableSpan` and `MutableRawSpan`. ## Motivation From 3d7202fc88a5bfc1e98c8ac912a7f8a00dc9fb4e Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Mon, 10 Feb 2025 08:59:55 -0800 Subject: [PATCH 4/8] Improve solution introduction --- proposals/nnnn-MutableSpan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/nnnn-MutableSpan.md b/proposals/nnnn-MutableSpan.md index a76ddba350..f128749662 100644 --- a/proposals/nnnn-MutableSpan.md +++ b/proposals/nnnn-MutableSpan.md @@ -30,7 +30,7 @@ These functions have a few different drawbacks, most prominently their reliance In addition to the new types, we will propose adding new API some standard library types to take advantage of `MutableSpan` and `MutableRawSpan`. ## Proposed solution -We introduced `Span` to provide shared read-only access to containers. We cannot use `Span` to also model container mutations, due to the [law of exclusivity][SE-0176]. `Span` is copyable, and must be copyable in order to properly model read access under the law of exclusivity: a value can be simultaneously accessed through multiple read-only accesses. Mutations, on the other hand, require _exclusive access_. Exclusive access cannot be modeled with a copyable type, since a copy of the value representing the access would violate exclusivity by adding a second access. We therefore need a non-copyable type separate from `Span` in order to model mutations. +We introduced `Span` to provide shared read-only access to containers. The natural next step is to provide a similar capability for mutable access. Mutability requires exclusive access, per Swift's [law of exclusivity][SE-0176]. `Span` is copyable, and must be copyable in order to properly model read access under the law of exclusivity: a value can be simultaneously accessed through multiple read-only accesses. Exclusive access cannot be modeled with a copyable type, since a copy would represent an additional access, in violation of the law of exclusivity. We therefore need a non-copyable type separate from `Span` in order to model mutations. #### MutableSpan From 0344e4f3c3393981437d2bc9197817aeaac687e4 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Thu, 6 Mar 2025 17:55:35 -0800 Subject: [PATCH 5/8] Lot of edits --- proposals/nnnn-MutableSpan.md | 375 ++++++++++++++++++++++++++-------- 1 file changed, 289 insertions(+), 86 deletions(-) diff --git a/proposals/nnnn-MutableSpan.md b/proposals/nnnn-MutableSpan.md index f128749662..fce33b83e7 100644 --- a/proposals/nnnn-MutableSpan.md +++ b/proposals/nnnn-MutableSpan.md @@ -30,13 +30,19 @@ These functions have a few different drawbacks, most prominently their reliance In addition to the new types, we will propose adding new API some standard library types to take advantage of `MutableSpan` and `MutableRawSpan`. ## Proposed solution -We introduced `Span` to provide shared read-only access to containers. The natural next step is to provide a similar capability for mutable access. Mutability requires exclusive access, per Swift's [law of exclusivity][SE-0176]. `Span` is copyable, and must be copyable in order to properly model read access under the law of exclusivity: a value can be simultaneously accessed through multiple read-only accesses. Exclusive access cannot be modeled with a copyable type, since a copy would represent an additional access, in violation of the law of exclusivity. We therefore need a non-copyable type separate from `Span` in order to model mutations. + +We introduced `Span` to provide shared read-only access to containers. The natural next step is to provide a similar capability for mutable access. A library whose API provides access to its internal storage makes a decision regarding the type of access it provides; it may provide read-only access or provide the ability to mutate its storage. That decision is made by the API author. If mutations were enabled by simply binding a `Span` value to a mutable binding (`var` binding or `inout` parameter), that decision would rest with the user of the API instead of its author. This explains why mutations must be modeled by a type separate from `Span`. + +Mutability requires exclusive access, per Swift's [law of exclusivity][SE-0176]. `Span` is copyable, and must be copyable in order to properly model read access under the law of exclusivity: a value can be simultaneously accessed through multiple read-only accesses. Exclusive access cannot be modeled with a copyable type, since a copy would represent an additional access, in violation of the law of exclusivity. This explains why the type which models mutations must be non-copyable. #### MutableSpan -`MutableSpan` allows delegating mutations of a type's contiguous internal representation, by providing access to an exclusively-borrowed view of a range of contiguous, initialized memory. `MutableSpan` relies on guarantees that it has exclusive access to the range of memory it represents, and that the memory it represents will remain valid for the duration of the access. These provide data race safety and temporal safety. Like `Span`, `MutableSpan` performs bounds-checking on every access to preserve spatial safety. +`MutableSpan` allows delegating mutations of a type's contiguous internal representation, by providing access to an exclusively-borrowed view of a range of contiguous, initialized memory. `MutableSpan`'s memory safety' relies on guarantees that: +- it has exclusive access to the range of memory it represents, providing data race safety and enforced by `~Copyable`. +- the memory it represents will remain valid for the duration of the access, providing lifetime safety and enforced by `~Escapable`. +- each access is guarded by bounds checking, providing bounds safety. -A `MutableSpan` provided by a container represents a mutation of that container, via an exclusive borrow. Mutations are implemented by mutating functions and subscripts, which let the compiler statically enforce exclusivity. +A `MutableSpan` provided by a container represents a mutation of that container, as an extended mutation access. Mutations are implemented by mutating functions and subscripts, which let the compiler statically enforce exclusivity. #### MutableRawSpan @@ -44,7 +50,7 @@ A `MutableSpan` provided by a container represents a mutation of that container, #### Extensions to standard library types -The standard library will provide `mutableSpan` computed properties. These return lifetime-dependent `MutableSpan` instances, and represent a mutation of the instance that provided them. These computed properties are the safe and composable replacements for the existing `withUnsafeMutableBufferPointer` closure-taking functions. For example, +The standard library will provide `mutableSpan` computed properties. These return a new lifetime-dependent `MutableSpan` instance, and that `MutableSpan` represents a mutation of the instance that provided it. The `mutableSpan` computed properties are the safe and composable replacements for the existing `withUnsafeMutableBufferPointer` closure-taking functions. For example, ```swift func(_ array: inout Array) { @@ -56,7 +62,47 @@ func(_ array: inout Array) { } ``` -These computed properties represent a case of lifetime relationships not covered in [SE-0456][SE-0456]. In SE-0456 we defined lifetime relationships for computed property getters of non-escapable and copyable types (`~Escapable & Copyable`). We propose defining them for properties of non-escapable and non-copyable types (`~Escapable & ~Copyable`). A `~Escapable & ~Copyable` value borrows another binding; if this borrow is also a mutation then it is an exclusive borrow. The scope of the borrow, whether or not it is exclusive, extends until the last use of the dependent binding. +The `mutableSpan` computed property represents a case of lifetime relationships not covered until now. The `mutableSpan` computed properties proposed here will represent mutations of their callee. This relationship will be illustrated with a hypothetical `@_lifetime` attribute, which ties the lifetime of a return value to an input parameter in a specific way. + +Note: The `@_lifetime` attribute is not real; it is a placeholder. The eventual lifetime annotations proposal may or may not propose syntax along these lines. We expect that, as soon as Swift adopts a syntax do describe lifetime dependencies, the Standard Library will be modified to adopt that new syntax. + +```swift +extension Array { + public var mutableSpan: MutableSpan { + @_lifetime(inout self) + mutating get { ... } + } +} +``` + +Here, the lifetime of the returned `MutableSpan` is tied to an `inout` access of `self` (the `Array`.) As long as the returned instance exists, the source `Array` is being mutated, and no other access to the `Array` can occur. + +This lifetime relationship will apply to all the safe `var mutableSpan: MutableSpan` and `var mutableBytes: MutableRawSpan` properties described in this proposal. + +#### Slicing `MutableSpan` or `MutableRawSpan` instances + +An important category of use cases for `MutableSpan` and `MutableRawSpan` consists of bulk copying operations. Often times, such bulk operations do not necessarily start at the beginning of the span, thus having a method to select a sub-span is necessary. This means producing an instance derived from the callee instance. We adopt the nomenclature already introduced in [SE-0437][SE-0437], with a family of `extracting()` methods. + +```swift +extension MutableSpan where Element: ~Copyable & ~Escapable { + @_lifetime(inout self) + public mutating func extracting(_ range: Range) -> Self +} +``` + +This function returns an instance of `MutableSpan` that represents a mutation of the same memory as represented by the callee. The callee can therefore no longer be mutated while the returned value exists: + +```swift +var array = [1, 2, 3, 4, 5] +var span1 = array.mutableSpan +var span2 = span1.extracting(3..<5) +// span1 cannot be accessed here +span2.swapAt(0, 1) +_ = consume span2 // explicitly end scope for `span2` +print(array) // [1, 2, 3, 5, 4] +``` + +As established in [SE-0437][SE-0437], the instance returned by the `extracting()` function does not share indices with the function's callee. ## Detailed Design @@ -79,7 +125,7 @@ We store a `UnsafeMutableRawPointer` value internally in order to explicitly sup Initializers, required for library adoption, will be proposed alongside [lifetime annotations][PR-2305]; for details, see "[Initializers](#initializers)" in the [future directions](#Directions) section. ```swift -extension MutableSpan where Element: ~Copyable { +extension MutableSpan where Element: ~Copyable & ~Escapable { /// The number of initialized elements in this `MutableSpan`. var count: Int { get } @@ -100,7 +146,7 @@ extension MutableSpan where Element: ~Copyable { mutating func swapAt(_ i: Index, _ j: Index) /// Borrow the underlying memory for read-only access - var span: Span { borrowing get } + var span: Span { @_lifetime(borrow self) borrowing get } } ``` @@ -118,99 +164,164 @@ for i in myMutableSpan.indices { } ``` -##### Unchecked access to elements: +##### Bulk updates of a `MutableSpan`'s elements: -The `subscript` mentioned above always checks the bounds of the `MutableSpan` before allowing access to the memory, preventing out-of-bounds accesses. We also provide an unchecked variant of the `subscript` and of the `swapAt` function as an alternative for situations where bounds-checking is costly and has already been performed: +We include functions to perform bulk copies of elements into the memory represented by a `MutableSpan`. Updating a `MutableSpan` from known-sized sources (such as `Collection` or `Span`) copies every element of a source. It is an error to do so when there is the span is too short to contain every element from the source. Updating a `MutableSpan` from `Sequence` or `IteratorProtocol` instances will copy as many items as possible, either until the input is empty or until the operation has updated the item at the last index. The bulk operations return the index following the last element updated. ```swift -extension MutableSpan where Element: ~Copyable { - /// Accesses the element at the specified `position`. - /// - /// This subscript does not validate `position`; this is an unsafe operation. - /// - /// - Parameter position: The offset of the element to access. `position` - /// must be greater or equal to zero, and less than `count`. - @unsafe - subscript(unchecked position: Index) -> Element { borrow; mutate } - - /// Exchange the elements at the two given offsets - /// - /// This function does not validate `i` or `j`; this is an unsafe operation. - @unsafe - mutating func swapAt(unchecked i: Index, unchecked j: Index) -} -``` -##### Bulk updating of a `MutableSpan`'s elements: - -We include functions to perform bulk copies of elements into the memory represented by a `MutableSpan`. Updating a `MutableSpan` from known-sized sources (such as `Collection` or `Span`) copies every element of a source. It is an error to do so when there is the span is too short to contain every element from the source. Updating a `MutableSpan` from `Sequence` or `IteratorProtocol` instances will copy as many items as possible, either until the input is empty or until the operation has updated the item at the last index. - -**Note:** This set of functions is sufficiently complete in functionality, but uses a minimal approach to slicing. This is only one of many possible approaches to slicing `MutableSpan`. We could revive the option of using a `some RangeExpression` parameter, or we could use the return value of a `func extracting(_: some RangeExpression)` such as was [recently added][SE-0437] to `UnsafeBufferPointer`. The latter option in combination with `mutating` functions requires the use of intermediate bindings. This section may change in response to feedback and our investigations. - -```swift -extension MutableSpan { +extension MutableSpan where Element: Copyable{ + /// Updates every element of this span's to the given value. mutating func update( - startingAt offset: Index = 0, repeating repeatedValue: Element ) - + + /// Updates the span's elements with the elements from the source mutating func update( - startingAt offset: Index = 0, from source: S ) -> (unwritten: S.Iterator, index: Index) where S.Element == Element + /// Updates the span's elements with the elements from the source mutating func update( - startingAt offset: Index = 0, - from elements: inout some IteratorProtocol + from source: inout some IteratorProtocol ) -> Index + /// Updates the span's elements with every element of the source. mutating func update( - startingAt offset: Index = 0, fromContentsOf source: some Collection ) -> Index - +} + +extension MutableSpan where Element: ~Copyable + /// Updates the span's elements with every element of the source. mutating func update( - startingAt offset: Index = 0, fromContentsOf source: Span ) -> Index + /// Updates the span's elements with every element of the source. mutating func update( - startingAt offset: Index = 0, - fromContentsOf source: borrowing Self + fromContentsOf source: borrowing MutableSpan ) -> Index -} - -extension MutableSpan where Element: ~Copyable { + + /// Updates the span's elements with every element of the source, + /// leaving the source uninitialized. mutating func moveUpdate( - startingAt offset: Index = 0, fromContentsOf source: UnsafeMutableBufferPointer ) -> Index } -extension MutableSpan { +extension MutableSpan where Element: Copyable { + /// Updates the span's elements with every element of the source, + /// leaving the source uninitialized. mutating func moveUpdate( - startingAt offset: Index = 0, fromContentsOf source: Slice> ) -> Index } ``` + +##### Extracting sub-spans +These functions extract sub-spans of the callee. The first two perform strict bounds-checking. The last four return prefixes or suffixes, where the number of elements in the returned sub-span is bounded by the number of elements in the parent `MutableSpan`. + +```swift +extension MutableSpan where Element: ~Copable & ~Escapable { + /// Returns a span over the items within the supplied range of + /// positions within this span. + @_lifetime(inout self) + mutating public func extracting(_ bounds: Range) -> Self + + /// Returns a span over the items within the supplied range of + /// positions within this span. + @_lifetime(inout self) + mutating public func extracting(_ bounds: some RangeExpression) -> Self + + /// Returns a span containing the initial elements of this span, + /// up to the specified maximum length. + @_lifetime(inout self) + mutating public func extracting(first maxLength: Int) -> Self + + /// Returns a span over all but the given number of trailing elements. + @_lifetime(inout self) + mutating public func extracting(droppingLast k: Int) -> Self + + /// Returns a span containing the final elements of the span, + /// up to the given maximum length. + @_lifetime(inout self) + mutating public func extracting(last maxLegnth: Int) -> Self + + /// Returns a span over all but the given number of initial elements. + @_lifetime(inout self) + mutating public func extracting(droppingFirst k: Int) -> Self +} +``` + +##### Unchecked access to elements or sub-spans: + +The `subscript` and index-taking functions mentioned above always check the bounds of the `MutableSpan` before allowing access to the memory, preventing out-of-bounds accesses. We also provide unchecked variants of the `subscript`, the `swapAt()` and `extracting()` functions as alternatives in situations where repeated bounds-checking is costly and has already been performed: + +```swift +extension MutableSpan where Element: ~Copyable { + /// Accesses the element at the specified `position`. + /// + /// This subscript does not validate `position`; this is an unsafe operation. + /// + /// - Parameter position: The offset of the element to access. `position` + /// must be greater or equal to zero, and less than `count`. + @unsafe + subscript(unchecked position: Index) -> Element { borrow; mutate } + + /// Exchange the elements at the two given offsets + /// + /// This function does not validate `i` or `j`; this is an unsafe operation. + @unsafe + mutating func swapAt(unchecked i: Index, unchecked j: Index) + + /// Constructs a new span over the items within the supplied range of + /// positions within this span. + /// + /// This function does not validate `bounds`; this is an unsafe operation. + @unsafe + @_lifetime(inout self) + mutating func extracting(unchecked bounds: Range) -> Self + + /// Constructs a new span over the items within the supplied range of + /// positions within this span. + /// + /// This function does not validate `bounds`; this is an unsafe operation. + @unsafe + @_lifetime(inout self) + mutating func extracting(unchecked bounds: ClosedRange) -> Self +} +``` + + ##### Interoperability with unsafe code: ```swift extension MutableSpan where Element: ~Copyable { + /// Calls a closure with a pointer to the viewed contiguous storage. func withUnsafeBufferPointer( _ body: (_ buffer: UnsafeBufferPointer) throws(E) -> Result ) throws(E) -> Result + /// Calls a closure with a pointer to the viewed mutable contiguous + /// storage. mutating func withUnsafeMutableBufferPointer( _ body: (_ buffer: UnsafeMutableBufferPointer) throws(E) -> Result ) throws(E) -> Result } extension MutableSpan where Element: BitwiseCopyable { + /// Calls a closure with a pointer to the underlying bytes of + /// the viewed contiguous storage. func withUnsafeBytes( _ body: (_ buffer: UnsafeRawBufferPointer) throws(E) -> Result ) throws(E) -> Result + /// Calls a closure with a pointer to the underlying bytes of + /// the viewed mutable contiguous storage. + /// + /// Note: mutating the bytes may result in the violation of + /// invariants in the internal representation of `Element` + @unsafe mutating func withUnsafeMutableBytes( _ body: (_ buffer: UnsafeMutableRawBufferPointer) throws(E) -> Result ) throws(E) -> Result @@ -255,10 +366,14 @@ extension MutableRawSpan { ```swift extension MutableRawSpan { + /// Stores the given value's bytes into raw memory at the specified offset. mutating func storeBytes( of value: T, toByteOffset offset: Int = 0, as type: T.Type ) + /// Stores the given value's bytes into raw memory at the specified offset. + /// + /// This function does not validate `offset`; this is an unsafe operation. @unsafe mutating func storeBytes( of value: T, toUncheckedByteOffset offset: Int, as type: T.Type @@ -270,21 +385,29 @@ Additionally, the basic loading operations available on `RawSpan` are available ```swift extension MutableRawSpan { + /// Returns a new instance of the given type, constructed from the raw memory + /// at the specified offset. @unsafe func unsafeLoad( fromByteOffset offset: Int = 0, as: T.Type ) -> T + /// Returns a new instance of the given type, constructed from the raw memory + /// at the specified offset. @unsafe func unsafeLoadUnaligned( fromByteOffset offset: Int = 0, as: T.Type ) -> T + /// Returns a new instance of the given type, constructed from the raw memory + /// at the specified offset. @unsafe func unsafeLoad( fromUncheckedByteOffset offset: Int, as: T.Type ) -> T + /// Returns a new instance of the given type, constructed from the raw memory + /// at the specified offset. @unsafe func unsafeLoadUnaligned( fromUncheckedByteOffset offset: Int, as: T.Type @@ -294,55 +417,116 @@ extension MutableRawSpan { We include functions to perform bulk copies into the memory represented by a `MutableRawSpan`. Updating a `MutableRawSpan` from a `Collection` or a `Span` copies every element of a source. It is an error to do so when there is are not enough bytes in the span to contain every element from the source. Updating `MutableRawSpan` from `Sequence` or `IteratorProtocol` instance copies as many items as possible, either until the input is empty or until there are not enough bytes in the span to store another element. -**Note:** This set of functions is sufficiently complete in functionality, but uses a minimal approach to slicing. This is only one of many possible approaches to slicing `MutableRawSpan`. (See the note above for more details on the same considerations.) - ```swift extension MutableRawSpan { + /// Updates the span's bytes with the bytes of the elements from the source mutating func update( - startingAt byteOffset: Int = 0, from source: S ) -> (unwritten: S.Iterator, byteOffset: Int) where S.Element: BitwiseCopyable + /// Updates the span's bytes with the bytes of the elements from the source mutating func update( - startingAt byteOffset: Int = 0, from elements: inout some IteratorProtocol ) -> Int + /// Updates the span's bytes with every byte of the source. mutating func update( - startingAt byteOffset: Int = 0, fromContentsOf source: C ) -> Int where C.Element: BitwiseCopyable + /// Updates the span's bytes with every byte of the source. mutating func update( - startingAt byteOffset: Int = 0, fromContentsOf source: Span ) -> Int + /// Updates the span's bytes with every byte of the source. mutating func update( - startingAt byteOffset: Int = 0, fromContentsOf source: borrowing MutableSpan ) -> Int + /// Updates the span's bytes with every byte of the source. mutating func update( - startingAt byteOffset: Int = 0, fromContentsOf source: RawSpan ) -> Int + /// Updates the span's bytes with every byte of the source. mutating func update( - startingAt byteOffset: Int = 0, fromContentsOf source: borrowing MutableRawSpan ) -> Int } ``` +##### Extracting sub-spans +These functions extract sub-spans of the callee. The first two perform strict bounds-checking. The last four return prefixes or suffixes, where the number of elements in the returned sub-span is bounded by the number of elements in the parent `MutableRawSpan`. + +```swift +extension MutableRawSpan { + /// Returns a span over the items within the supplied range of + /// positions within this span. + @_lifetime(inout self) + mutating public func extracting(_ byteOffsets: Range) -> Self + + /// Returns a span over the items within the supplied range of + /// positions within this span. + @_lifetime(inout self) + mutating public func extracting(_ byteOffsets: some RangeExpression) -> Self + + /// Returns a span containing the initial elements of this span, + /// up to the specified maximum length. + @_lifetime(inout self) + mutating public func extracting(first maxLength: Int) -> Self + + /// Returns a span over all but the given number of trailing elements. + @_lifetime(inout self) + mutating public func extracting(droppingLast k: Int) -> Self + + /// Returns a span containing the final elements of the span, + /// up to the given maximum length. + @_lifetime(inout self) + mutating public func extracting(last maxLegnth: Int) -> Self + + /// Returns a span over all but the given number of initial elements. + @_lifetime(inout self) + mutating public func extracting(droppingFirst k: Int) -> Self +} +``` + +We also provide unchecked variants of the `extracting()` functions as alternatives in situations where repeated bounds-checking is costly and has already been performed: + +```swift +extension MutableRawSpan { + /// Constructs a new span over the items within the supplied range of + /// positions within this span. + /// + /// This function does not validate `byteOffsets`; this is an unsafe operation. + @unsafe + @_lifetime(inout self) + mutating func extracting(unchecked byteOffsets: Range) -> Self + + /// Constructs a new span over the items within the supplied range of + /// positions within this span. + /// + /// This function does not validate `byteOffsets`; this is an unsafe operation. + @unsafe + @_lifetime(inout self) + mutating func extracting(unchecked byteOffsets: ClosedRange) -> Self +} +``` + + + ##### Interoperability with unsafe code: ```swift extension MutableRawSpan { + /// Calls a closure with a pointer to the underlying bytes of + /// the viewed contiguous storage. func withUnsafeBytes( _ body: (_ buffer: UnsafeRawBufferPointer) throws(E) -> Result ) throws(E) -> Result + /// Calls a closure with a pointer to the underlying bytes of + /// the viewed mutable contiguous storage. mutating func withUnsafeMutableBytes( _ body: (_ buffer: UnsafeMutableRawBufferPointer) throws(E) -> Result ) throws(E) -> Result @@ -350,57 +534,69 @@ extension MutableRawSpan { ``` These functions use a closure to define the scope of validity of `buffer`, ensuring that the underlying `MutableSpan` and the binding it depends on both remain valid through the end of the closure. They have the same shape as the equivalents on `Array` because they fulfill the same purpose, namely to keep the underlying binding alive. +#### Properties providing `MutableSpan` or `MutableRawSpan` instances + ##### Accessing and mutating the raw bytes of a `MutableSpan` +When a `MutableSpan`'s element is `BitwiseCopyable`, we allow mutations of the underlying storage as raw bytes, as a `MutableRawSpan`. + ```swift extension MutableSpan where Element: BitwiseCopyable { - var mutableBytes: MutableRawSpan { mutating get } + /// Access the underlying raw bytes of this `MutableSpan`'s elements + /// + /// Note: mutating the bytes may result in the violation of + /// invariants in the internal representation of `Element` + @unsafe + var mutableBytes: MutableRawSpan { @_lifetime(inout self) mutating get } } ``` +The standard library will provide `mutating` computed properties providing lifetime-dependent `MutableSpan` instances. These `mutableSpan` computed properties are intended as the safe and composable replacements for the existing `withUnsafeMutableBufferPointer` closure-taking functions. - -#### Extensions to Standard Library types - -A `mutating` computed property getter defined on any type and returning a `~Escapable & ~Copyable` value establishes an exclusive borrowing lifetime relationship of the returned value on the callee's binding. As long as the returned value exists, then the callee's binding remains borrowed and cannot be accessed in any other way. - -A `nonmutating` computed property getter returning a `~Escapable & ~Copyable` value establishes a borrowing lifetime relationship, as if returning a `~Escapable & Copyable` value (see [SE-0456][SE-0456].) - -The standard library will provide `mutating` computed properties providing lifetime-dependent `MutableSpan` instances. These `mutableSpan` computed properties are intended as the safe and composable replacements for the existing `withUnsafeMutableBufferPointer` closure-taking functions. +##### Extensions to Standard Library types ```swift extension Array { - var mutableSpan: MutableSpan { mutating get } + /// Access this Array's elements as mutable contiguous storage. + var mutableSpan: MutableSpan { @_lifetime(inout self) mutating get } } extension ContiguousArray { - var mutableSpan: MutableSpan { mutating get } + /// Access this Array's elements as mutable contiguous storage. + var mutableSpan: MutableSpan { @_lifetime(inout self) mutating get } } extension ArraySlice { - var mutableSpan: MutableSpan { mutating get } + /// Access this Array's elements as mutable contiguous storage. + var mutableSpan: MutableSpan { @_lifetime(inout self) mutating get } } extension InlineArray { - var mutableSpan: MutableSpan { mutating get } + /// Access this Array's elements as mutable contiguous storage. + var mutableSpan: MutableSpan { @_lifetime(inout self) mutating get } } extension CollectionOfOne { - var mutableSpan: MutableSpan { mutating get } + /// Access this Collection's element as mutable contiguous storage. + var mutableSpan: MutableSpan { @_lifetime(inout self) mutating get } } ``` -#### Extensions to unsafe buffer types +##### Extensions to unsafe buffer types -We hope that `MutableSpan` and `MutableRawSpan` will become the standard ways to delegate mutations of shared contiguous memory in Swift. Many current API delegate mutations with closure-based functions that receive an `UnsafeMutableBufferPointer` parameter to do this. We will provide ways to unsafely obtain `MutableSpan` instances from `UnsafeMutableBufferPointer` and `MutableRawSpan` instances from `UnsafeMutableRawBufferPointer`, in order to bridge these unsafe types to newer, safer contexts. +We hope that `MutableSpan` and `MutableRawSpan` will become the standard ways to delegate mutations of shared contiguous memory in Swift. Many current API delegate mutations via closure-based functions that receive an `UnsafeMutableBufferPointer` parameter. We will provide ways to unsafely obtain `MutableSpan` instances from `UnsafeMutableBufferPointer` and `MutableRawSpan` instances from `UnsafeMutableRawBufferPointer`, in order to bridge these unsafe types to newer, safer contexts. ```swift extension UnsafeMutableBufferPointer { - var mutableSpan: MutableSpan { get } + /// Unsafely access this buffer as a MutableSpan + @unsafe + var mutableSpan: MutableSpan { @_lifetime(borrow self) get } } extension UnsafeMutableRawBufferPointer { - var mutableBytes: MutableRawSpan { get } + /// Unsafely access this buffer as a MutableRawSpan + @unsafe + var mutableBytes: MutableRawSpan { @_lifetime(borrow self) get } } ``` @@ -411,23 +607,23 @@ These unsafe conversions returns a value whose lifetime is dependent on the _bin Failure to maintain these invariants results in undefined behaviour. -#### Extensions to `Foundation.Data` +##### Extensions to `Foundation.Data` While the `swift-foundation` package and the `Foundation` framework are not governed by the Swift evolution process, `Data` is similar in use to standard library types, and the project acknowledges that it is desirable for it to have similar API when appropriate. Accordingly, we plan to propose the following additions to `Foundation.Data`: ```swift extension Foundation.Data { - // Mutate this `Data`'s bytes through a `MutableSpan` - var mutableSpan: MutableSpan { mutating get } + // Access this instance's bytes as mutable contiguous storage + var mutableSpan: MutableSpan { @_lifetime(inout self) mutating get } - // Mutate this `Data`'s bytes through a `MutableRawSpan` - var mutableBytes: MutableRawSpan { mutating get } + // Access this instance's bytes as mutable contiguous bytes + var mutableBytes: MutableRawSpan { @_lifetime(inout self) mutating get } } ``` #### Performance -The `mutableSpan` and `mutableBytes` properties should be performant and return their `MutableSpan` or `MutableRawSpan` with very little work, in O(1) time. In copy-on-write types, however, obtaining a `MutableSpan` is the start of the mutation, and if the backing buffer is not uniquely reference a copy must be made ahead of returning the `MutableSpan`. +The `mutableSpan` and `mutableBytes` properties should be performant and return their `MutableSpan` or `MutableRawSpan` with very little work, in O(1) time. In copy-on-write types, however, obtaining a `MutableSpan` is the start of the mutation. When the backing buffer is not uniquely referenced then a full copy must be made ahead of returning the `MutableSpan`. Note that `MutableSpan` incurs no special behaviour for bridged types, since mutable bindings always require a defensive copy of data bridged from Objective-C data structures. @@ -509,11 +705,18 @@ Unfortunately, tuples do not support non-copyable values yet. We may be able to #### Mutating algorithms -Algorithms defined on `MutableCollection` such as `sort(by:)` and `partition(by:)` could be defined on `MutableSpan`. We believe we will be able to define these more generally once we have a generalized container protocol hierarchy. +Algorithms defined on `MutableCollection` such as `sort(by:)` and `partition(by:)` could be defined on `MutableSpan`. We believe we will be able to define these more generally once we have a generalized container protocol hierarchy. + +#### Exclusive Access + +The `mutating` functions in this proposal generally do not represent mutations of the binding itself, but of memory being referenced. `mutating` is necessary in order to model the necessary exclusive access to the memory. We could conceive of an access level between "shared" (`let`) and "exclusive" (`var`) that would model an exclusive access while allowing the pointer and count information to be stored in registers. + +#### Harmonizing `extracting()` functions across types + +The range of `extracting()` functions proposed here expands upon the range accepted in [SE-0437][SE-0437]. If the prefix and suffix variants are accepted, we should add them to `UnsafeBufferPointer` types as well. `Span` and `RawSpan` should also have `extracting()` functions with appropriate lifetime dependencies. #### Delegated initialization with `OutputSpan` Some data structures can delegate initialization of parts of their owned memory. The standard library added the `Array` initializer `init(unsafeUninitializedCapacity:initializingWith:)` in [SE-0223][SE-0223]. This initializer relies on `UnsafeMutableBufferPointer` and correct usage of initialization primitives. We should present a simpler and safer model of initialization by leveraging non-copyability and non-escapability. -We expect to propose an `OutputSpan` type to represent partially-initialized memory, and to support to the initialization of memory by appending to the initialized portion of the underlying storage. - +We expect to propose an `OutputSpan` type to represent partially-initialized memory, and to support to the initialization of memory by appending to the initialized portion of the underlying storage. From 5340db1e42756b6738efaaee3ef9a90adedeb262 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Tue, 11 Mar 2025 13:34:11 -0700 Subject: [PATCH 6/8] editorial fixes --- proposals/nnnn-MutableSpan.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/proposals/nnnn-MutableSpan.md b/proposals/nnnn-MutableSpan.md index fce33b83e7..803fdd0424 100644 --- a/proposals/nnnn-MutableSpan.md +++ b/proposals/nnnn-MutableSpan.md @@ -37,7 +37,7 @@ Mutability requires exclusive access, per Swift's [law of exclusivity][SE-0176]. #### MutableSpan -`MutableSpan` allows delegating mutations of a type's contiguous internal representation, by providing access to an exclusively-borrowed view of a range of contiguous, initialized memory. `MutableSpan`'s memory safety' relies on guarantees that: +`MutableSpan` allows delegating mutations of a type's contiguous internal representation, by providing access to an exclusively-borrowed view of a range of contiguous, initialized memory. `MutableSpan`'s memory safety relies on guarantees that: - it has exclusive access to the range of memory it represents, providing data race safety and enforced by `~Copyable`. - the memory it represents will remain valid for the duration of the access, providing lifetime safety and enforced by `~Escapable`. - each access is guarded by bounds checking, providing bounds safety. @@ -90,7 +90,7 @@ extension MutableSpan where Element: ~Copyable & ~Escapable { } ``` -This function returns an instance of `MutableSpan` that represents a mutation of the same memory as represented by the callee. The callee can therefore no longer be mutated while the returned value exists: +This function returns an instance of `MutableSpan` that represents a mutation of the same memory as represented by the callee. The callee can therefore no longer be accessed (read or mutated) while the returned value exists: ```swift var array = [1, 2, 3, 4, 5] @@ -713,7 +713,7 @@ The `mutating` functions in this proposal generally do not represent mutations o #### Harmonizing `extracting()` functions across types -The range of `extracting()` functions proposed here expands upon the range accepted in [SE-0437][SE-0437]. If the prefix and suffix variants are accepted, we should add them to `UnsafeBufferPointer` types as well. `Span` and `RawSpan` should also have `extracting()` functions with appropriate lifetime dependencies. +The range of `extracting()` functions proposed here expands upon the range accepted in [SE-0437][SE-0437]. If the prefix and suffix variants are accepted, we should add them to `UnsafeBufferPointer` types as well. `Span` and `RawSpan` should also have `extracting()` functions with appropriate lifetime dependencies. #### Delegated initialization with `OutputSpan` From b15d39a8d9def33623bf904f2bd1892b5500550c Mon Sep 17 00:00:00 2001 From: Joe Groff Date: Tue, 11 Mar 2025 17:13:37 -0700 Subject: [PATCH 7/8] more typos --- proposals/nnnn-MutableSpan.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/proposals/nnnn-MutableSpan.md b/proposals/nnnn-MutableSpan.md index 803fdd0424..7ba38b7a00 100644 --- a/proposals/nnnn-MutableSpan.md +++ b/proposals/nnnn-MutableSpan.md @@ -96,10 +96,12 @@ This function returns an instance of `MutableSpan` that represents a mutation of var array = [1, 2, 3, 4, 5] var span1 = array.mutableSpan var span2 = span1.extracting(3..<5) -// span1 cannot be accessed here +// neither array nor span1 can be accessed here span2.swapAt(0, 1) _ = consume span2 // explicitly end scope for `span2` -print(array) // [1, 2, 3, 5, 4] +span1.swapAt(0, 1) +_ = consume span1 // explicitly end scope for `span1` +print(array) // [2, 1, 3, 5, 4] ``` As established in [SE-0437][SE-0437], the instance returned by the `extracting()` function does not share indices with the function's callee. @@ -169,7 +171,7 @@ for i in myMutableSpan.indices { We include functions to perform bulk copies of elements into the memory represented by a `MutableSpan`. Updating a `MutableSpan` from known-sized sources (such as `Collection` or `Span`) copies every element of a source. It is an error to do so when there is the span is too short to contain every element from the source. Updating a `MutableSpan` from `Sequence` or `IteratorProtocol` instances will copy as many items as possible, either until the input is empty or until the operation has updated the item at the last index. The bulk operations return the index following the last element updated. ```swift -extension MutableSpan where Element: Copyable{ +extension MutableSpan where Element: Copyable { /// Updates every element of this span's to the given value. mutating func update( repeating repeatedValue: Element @@ -179,12 +181,12 @@ extension MutableSpan where Element: Copyable{ mutating func update( from source: S ) -> (unwritten: S.Iterator, index: Index) where S.Element == Element - + /// Updates the span's elements with the elements from the source mutating func update( from source: inout some IteratorProtocol ) -> Index - + /// Updates the span's elements with every element of the source. mutating func update( fromContentsOf source: some Collection @@ -196,7 +198,7 @@ extension MutableSpan where Element: ~Copyable mutating func update( fromContentsOf source: Span ) -> Index - + /// Updates the span's elements with every element of the source. mutating func update( fromContentsOf source: borrowing MutableSpan @@ -245,7 +247,7 @@ extension MutableSpan where Element: ~Copable & ~Escapable { /// Returns a span containing the final elements of the span, /// up to the given maximum length. @_lifetime(inout self) - mutating public func extracting(last maxLegnth: Int) -> Self + mutating public func extracting(last maxLength: Int) -> Self /// Returns a span over all but the given number of initial elements. @_lifetime(inout self) @@ -292,8 +294,7 @@ extension MutableSpan where Element: ~Copyable { } ``` - -##### Interoperability with unsafe code: +##### Interoperability with unsafe code ```swift extension MutableSpan where Element: ~Copyable { @@ -457,6 +458,7 @@ extension MutableRawSpan { ``` ##### Extracting sub-spans + These functions extract sub-spans of the callee. The first two perform strict bounds-checking. The last four return prefixes or suffixes, where the number of elements in the returned sub-span is bounded by the number of elements in the parent `MutableRawSpan`. ```swift @@ -513,8 +515,6 @@ extension MutableRawSpan { } ``` - - ##### Interoperability with unsafe code: ```swift @@ -697,11 +697,11 @@ It is desirable to have a way to split a `MutableSpan` in multiple parts, for di ```swift extension MutableSpan where Element: ~Copyable { - func split(at index: Index) -> (part1: Self, part2: Self) + public mutating func split(at index: Index) -> (part1: Self, part2: Self) } ``` -Unfortunately, tuples do not support non-copyable values yet. We may be able to use `InlineArray` ([SE-0453][SE-0453]), or a bespoke type, but destructuring the non-copyable constituent part remains a challenge. Solving this issue for `Span` and `MutableSpan` is a top priority. +Unfortunately, tuples do not support non-copyable or non-escapable values yet. We may be able to use `InlineArray` ([SE-0453][SE-0453]), or a bespoke type, but destructuring the non-copyable constituent part remains a challenge. Solving this issue for `Span` and `MutableSpan` is a top priority. #### Mutating algorithms From 7a3ed493f68ed49770fa932173d35960cb2784dd Mon Sep 17 00:00:00 2001 From: Joe Groff Date: Tue, 11 Mar 2025 17:16:05 -0700 Subject: [PATCH 8/8] christen MutableSpan as SE-0467 and begin review --- proposals/{nnnn-MutableSpan.md => 0467-MutableSpan.md} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename proposals/{nnnn-MutableSpan.md => 0467-MutableSpan.md} (99%) diff --git a/proposals/nnnn-MutableSpan.md b/proposals/0467-MutableSpan.md similarity index 99% rename from proposals/nnnn-MutableSpan.md rename to proposals/0467-MutableSpan.md index 7ba38b7a00..0ed4faecc7 100644 --- a/proposals/nnnn-MutableSpan.md +++ b/proposals/0467-MutableSpan.md @@ -1,12 +1,12 @@ # MutableSpan and MutableRawSpan: delegate mutations of contiguous memory -* Proposal: TBD +* Proposal: [SE-0467](0467-MutableSpan.md) * Author: [Guillaume Lessard](https://github.com/glessard) -* Review Manager: TBD -* Status: **Pitch** +* Review Manager: [Joe Groff](https://github.com/jckarter) +* Status: **Active review (March 11...25, 2025)** * Roadmap: [BufferView Roadmap](https://forums.swift.org/t/66211) * Implementation: "Future" target of [swift-collections](https://github.com/apple/swift-collections/tree/future) -* Review: [Pitch](https://forums.swift.org/) +* Review: [Pitch](https://forums.swift.org/t/pitch-mutablespan/77790) [SE-0446]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0446-non-escapable.md [SE-0447]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0447-span-access-shared-contiguous-storage.md