[Accepted] SE-0467: MutableSpan

Hi everyone. The review for SE-0467: MutableSpan concluded on March 25, 2025, and the Language Steering Group has decided to accept the proposal with some minor modifications.

Reviewers raised a few questions about the proposal:

  • The conformance of MutableSpan to Sendable ought to be unconstrained by the default Copyable and Escapable constraints:

    extension MutableSpan: @unchecked Sendable
        where Element: Sendable & ~Copyable & ~Escapable {}
    

    The authors acknowledge that this was an oversight in the proposal text, and the proposal should be updated to suppress those implicit constraints.

  • It might be interesting to extend the MutableCollection protocol with a requirement to conditionally access a mutable span if the collection consists of contiguous storage, as a safe alternative to the existing withContiguousStorageIfAvailable requirement. With the acceptance of SE-0465: Standard Library Primitives for Nonescapable Types, we now have the foundational support in Optional to express this requirement:

    protocol MutableCollection {
      ...
      var mutableSpan: MutableSpan<Element>?
    }
    

    We leave this design direction for future proposals to explore.

  • Reviewers raised the potential of introducing a subtyping or implicit conversion relationship from MutableSpan to Span, by analogy to the existing implicit conversion for function arguments from UnsafeMutablePointer to UnsafePointer. MutableSpan cannot formally be a subtype of Span, because Span is Copyable but MutableSpan is not. Fully general implicit conversions also have the potential to lead to surprising second-order behavior due to their different interactions with exclusivity. Since a Span only requires shared borrowed access to its underlying memory, but MutableSpan requires fully exclusive access, source changes that lead to changes in where implicit conversions occur could lead to nonobvious ownership knock-on errors.

    That said, the LSG acknowledges that there is likely to be a related broader issue with APIs that want to work with Spans. The most natural way to write a function that wants to read in a contiguous block of memory, agnostic to how the memory is owned, is to take a Span or RawSpan:

    func process(elements: Span<Int>)
    

    However, this passes burden onto callers who do own the memory to project out the Span:

    var info: Array<Int>
    process(elements: data.span)
    
    var mutableInfo: MutableSpan<Int>
    process(elements: mutableInfo.span)
    

    and MutableSpan could be seen as one instance of this more general problem. It's tempting to introduce a protocol with a var span: Span { get } requirement, and have people write generic functions over that protocol, but the generic interface will add overhead compared to only taking a Span, because of either protocol dispatch or unnecessary generic specialization. A mechanism to allow for the ABI of taking a Span but the caller ergonomics of the generic interface could be interesting, and MutableSpan may be one instance of that broader problem. We are OK with leaving these alternatives to be explored as future directions.

  • There was discussion as to whether it is safe for MutableRawSpan (and RawSpan) to conform to Sendable. These types allows for values of any BitwiseCopyable & Escapable type to be loaded or stored as raw bytes from the referenced memory, and it is desirable for these types to be Sendable in order to facilitate divide-and-conquer operations over raw memory. However, since neither the store nor unsafeLoad operation require the type being stored or loaded to be Sendable, this could be used as a mechanism to pass non-Sendable values across isolation boundaries.

    On the other hand, the unsafeLoad operation is unsafe: even setting aside concurrency isolation concerns, not every BitwiseCopyable & Escapable type can be loaded from an arbitrary bit pattern.

    SE-0447 left it as a future direction to introduce another layout constraint for "fully inhabited" types, which can be safely formed from arbitrary bit patterns in memory. This constraint could then in turn be used as the constraint for a safe load operation. We can further constrain that safe load operation to only work with types that are also Sendable, either by having Sendable be a prerequisite to being considered "fully inhabited", or having Sendable be an independent constraint on load itself.

    With that restriction in place, it should not be possible to use MutableRawSpan and RawSpan together to break isolation boundaries with safe code, and thus it should ultimately be safe to have both types conform to Sendable. The LSG feels that this is the right tradeoff to make, since we foresee binary encoding and decoding to be an important use case for these types, and that task will typically use byte and numeric types which are always Sendable and fully inhabited. Saddling these use cases with the need for unsafe escape hatches in order to distribute work among tasks would be burdensome and introduce the potential for unintentional unsafety in the common case.

    The most prominent case of BitwiseCopyable & Escapable types that are not also Sendable are the Unsafe*Pointer family of types, which would never be safe to load from an arbitrary bit pattern, and although we don't want to preemptively rule out the possibility, it is likely to be rare for a fully inhabited safe-to-load type not to be Sendable, and so requiring unsafeLoad to work with those types is unlikely to be a burden.

Thank you to everyone who participated in the review!

14 Likes