Thanks Doug. This is a large addition, so I'll probably have more comments later, but so far:
Is there a way to create an Aliased* from an AliasedMutable*?
AliasedMutableSpan's mutating methods are actually non-mutating, but most of MutableSpan's mutating members are not changing the shape of the MutableSpan either. I thought the superseding reason would be that in the Atomics proposal, we found that "var" means "participates in exclusivity checks" and we don't want that here, but this isn't referenced in the proposal here. Is that still accurate?
@safe
func withUnsafeBufferPointer<E: Error, Result: ~Copyable>(
I understand the argument that AliasedSpan.withUnsafeBufferPointer is "safe" because the unsafety belongs on the pointer you get in the closure, which already exists. For AliasedSpan.withUnsafeBytes, I don't think that the argument holds. In the following code:
var strings = ["a", "b", "c"]
let x = string.span.aliased.withUnsafeBytes {
let bytes = unsafe $0.bytes
return bytes.load(atByteOffset: 0, as: Int.self)
}
I believe it's understood that UnsafeRawBufferPointer.bytes is safe under the assumption that you don't escape the original pointer and promise no mutable aliases. We don't need to be concerned with whether the buffer pointer was created over a range of CopyableFromRawBytes elements because that's the problem of whoever created the unsafe raw buffer pointer: it's the last chance the author had to verify this is OK, and that piece of code should be flagged as needing extra scrutiny. However, the piece of code which does that is withUnsafeBytes, and it is @safe.
I'm not sure if/how this is applied to spans and other types already. Unless I missed something, the rules for whether it's OK that these functions is @safe were never discussed.
I am pretty sure it's a pitch oversight that AliasedMutableSpan's unchecked subscript is not @unsafe:
extension AliasedMutableSpan {
subscript(_ position: Index) -> Element {
get
nonmutating set
}
subscript(unchecked position: Index) -> Element {
get
nonmutating set
}
}
As noted in the Proposed Solution section, the subscripts for AliasedSpan use get accessors rather than borrow accessors to force the caller to copy the result.
This is correct, but I think it's missing some discussion: my understanding is that a copying accessor does not require a copy, it only requires that the result is semantically equivalent to a copy after the compiler is done optimizing. This raises several questions for me:
- If the optimizer sees two consecutive loads of the same index, can they be combined? I think the answer is: probably? Aliased* appear intended to be memory-safe in the face of race conditions, which does not require ordering memory accesses on multiple threads in any specific way (which is fine for Aliased* since Sendable conformance is gated behind FullyInhabited–also, side note, that's pretty neat).
- If the optimizer sees two adjacent accesses, can it combine them into one? I think the answer is "yes", again because Aliased* doesn't try to order memory accesses in any specific way and it also doesn't try to be the right type for MMIO regions. (This is a little hypothetical at this time because my experience is that LLVM is unable to do that.)
- If the optimizer needs to stash a value it loaded from an Aliased*, instead of spilling to the stack and reload from there, can it choose to reload from the Aliased*? It definitely cannot.
I'd like to make sure this has been considered and the parts where we put hard "cannot" constraints are verified to generate code that will never lead to these outcomes.