Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Pitch] MutableSpan and MutableRawSpan #2681

Merged
merged 9 commits into from
Mar 12, 2025
Merged

Conversation

glessard
Copy link
Contributor

@glessard glessard commented Feb 5, 2025

Pitch MutableSpan and MutableRawSpan, non-escapable types to model mutations of containers. This will begin replacing uses of the withUnsafeMutableBufferPointer() and withUnsafeMutableBytes() functions to mutate standard library types.

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.

Choose a reason for hiding this comment

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

Whitespace: should have a newline between heading and paragraph?


#### 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.

Choose a reason for hiding this comment

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

Strongly recommend using the more accessible terms "bounds safety" and "lifetime safety" instead of "spatial safety" and "temporal safety".

@unsafe
subscript(unchecked position: Index) -> Element { borrow; mutate }

/// Exchange the elements at the two given offsets

Choose a reason for hiding this comment

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

Whitespace: this is a tab

```
##### 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.

Choose a reason for hiding this comment

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

It's surprising to me that you can specify a start index on all of these, but not an end index, especially for update(startingAt:repeating:).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We're going with the extracting() functions coupled with full-span bulk operations. The ergonomics of this aren't optimal at this time, but we expect them to improve in the future.

) -> Index
}
```
##### Interoperability with unsafe code:

Choose a reason for hiding this comment

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

You can pass an Array to a function that takes an UnsafePointer/UnsafeMutablePointer using &array as the argument. Does that also work with MutableSpan?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No it doesn't. There is other work to import C function signatures with Span or MutableSpan when applicable. In the meantime we need to use withUnsafe[Mutable]Pointer(). The &array syntax suffers greatly from not having a strongly-associated count argument.

var isEmpty: Bool { get }

/// The range of valid byte offsets into this `RawSpan`
var byteOffsets: Range<Int> { get }

Choose a reason for hiding this comment

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

Is this basically indices? I assume it is because MutableRawSpan requires its content to be initialized?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This fulfills the same purpose as indices. MutableRawSpan doesn't have a subscript with an index, but does have byte offsets from which to unsafeLoad() or to storeBytes(). This naming is from the Span proposal (SE-0447).


```swift
extension MutableRawSpan {
mutating func storeBytes<T: BitwiseCopyable>(

Choose a reason for hiding this comment

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

Does BitwiseCopyable imply Sendable?

Copy link
Contributor Author

@glessard glessard Mar 6, 2025

Choose a reason for hiding this comment

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

It doesn't. However, a safe-loading abstraction would require something like "FullyInhabited & Sendable". We can use UnsafeRawPointer as an example here:

  • Int.init(bitPattern: UnsafeRawPointer) is safe
  • Storing that Int is safe.
  • Loading that Int is safe.
  • UnsafeRawPointer.init?(bitPattern: Int) is clearly unsafe.
    There is an unsafe operation here, and I believe its presence is sufficient. We don't really need two or more indications of unsafety. We could attempt to do more to impede pointer smuggling like this, but while would it not really improve safety, it would probably impede expressiveness.

}
```

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.

Choose a reason for hiding this comment

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

Same as above, I'm surprised there's a start index but no end index. In the absence of slicing on Span types, it seems to mean there's no way to bulk-modify part of a MutableRawSpan from part of another span. This is functionality I would use in a project that I'm working towards migrating to Span/RawSpan. (I could still do it by hand, I guess?)

}
```

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:

Choose a reason for hiding this comment

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

Is it intended that these are not mutating get accessors?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes; mutations via pointers cannot be enforced by exclusive access.


#### <a name="performance"></a>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`.

Choose a reason for hiding this comment

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

Spelling:

and if the backing buffer is not uniquely reference a copy must
                                          ^~~~~~~~~ referenced

@rjmccall rjmccall added the LSG Contains topics under the domain of the Language Steering Group label Feb 24, 2025
@jckarter jckarter merged commit 6ef8cf3 into swiftlang:main Mar 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LSG Contains topics under the domain of the Language Steering Group
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants