Yeah, the reason this is needed is that Span and MutableSpan have reference semantics (even though they are not classes): a var span means you can reassign the span, not that you can modify the span, just like how that would be for a var reference. Value semantics are impossible because the goal is to not copy the data.
I suppose it would be the var s which would be illegal in that example, but it looks like all the information would be present for the compiler to be able to diagnose that - it can see which accessors are available when you call array.span. Perhaps some kind of attribute would be necessary on Span. I'm not seeing anything truly insurmountable (maybe there still is a reason this can't work, I don't know).
I'm open to other options at the usage site. That's not really my concern - what I really want to know is what would it take to support a different model? I don't look forward to having to write or use a bunch of X/MutableX pairs.
Did you investigate other approaches to the one being presented? Or was this just a straightforward extension from the unsafe pointer types, adding escaping and copyability constraints to provide safety, and basically modelling this around the existing withUBP? Because I think deeper consideration is needed if this is going to become such a fundamental building block.
I've been using Swift as my main language pretty much since the project was announced. That's like, what, over 10 years now? You could say it has grown on me. Whenever I need to use another language, there is one thing I really have difficulty getting used to: the pervasive use of reference semantics. Even for simple things like arrays! It feels weeeeird to have to worry about things like spooky modifications at a distance.
Value semantics are the single best feature of Swift. I could live without a lot of the other things - concurrency, even generics - but not without value semantics.
Value semantics are impossible because the goal is to not copy the data.
How do you figure? Why does not copying hinder value semantics?
My understanding of value semantics is that it means modifications to one variable cannot affect another variable. That is even easier to achieve with non-copyable types, not harder.
Taking your example:
var mutStr = "foo"
mutStr.unicodeScalars.first // "f"
mutStr.unicodeScalars.append("t") // ā
OK - because the string is a 'var'
print(mutStr) // "foot"
This is enabled by the uniquely value-type ability to "put back" unicodeScalars into mutStr through a setter.
var mutStr = "foo"
var scalars = mutStr.unicodeScalars
scalars.append("t")
mut.unicodeScalars = scalars
Without a setter, you don't have value semantics. If a get-only property allows you to modify the state of the original object, it means you have reference semantics.
You cannot have a setter for Span.
var array = [1, 2, 3]
var span = array.span
span[1] = 3 // does this change `array` in your ideal Span implementation?
array.span = span // this is impossible, `array` is inaccessible until `span` is dead
var contiguous: ContiguousArray = [3, 4, 5]
contiguous.span = span // does this work? could this work?
Aside from it being impossible to have a setter for Span, value types can move between mutable and immutable:
let array = [1, 2, 3]
var span = array.span // cannot work, but prevented by what mechanism?
let span = array.span // OK in your ideal Span implementation
var span2 = consume span // what prevents this?
If these things sound like technicalities, you can try to implement Span at home. I ran into these issues trying to implement a value-like Span a while back. Maybe you can do better than me?
Then don't use a mutating get? That is a choice that we are making, and if one of the consequences of that choice is that we need to accept reference semantics and split mutable/immutable types for everything in Swift that is built upon the Span family, then we need to make that choice explicit to the community and thoroughly investigate the alternatives.
I think you're all coming at this backwards - like you're taking the existing language semantics (plus the extensions you've already implemented), getting stuck trying to work this problem, giving up and accepting reference semantics as the only solution. Because the problems you're listing don't seem like fundamental conceptual problems, they're more implementation limitations. I'm coming at this from the other direction - looking at the model I want, how well it composes, and working back to see what the language needs to do to support the model I want.
There are certainly many special behaviours that will need to be accounted for - for example, an immutable Span should be copyable, but a mutable Span should not be. That is a consequence of the rule of exclusivity, and the fact that these values conceptually represent borrows (perhaps we need explicit borrow and mutating borrow variables? If so, fine, let's do that then). But none of that seems like an insurmountable problem to me - we've added custom attributes before, to support special semantics on Atomic and Mutex, to support all kinds of concurrency features, etc. We can model this behaviour in the language.
I only have limited time to consider all of this - after all, my participation on these forums is a hobby, and I don't have all day to sit and ponder the details - so I'm willing to accept that perhaps there is some reason this model truly can never work, but I don't think that argument has been presented yet.
One more thing: this is not true - at least, not on MutableSpan.
All of the methods on MutableSpan which modify the span's contents, such as update and storeBytes, are marked mutating (and have to be for exclusivity). Therefore, a MutableSpan must be stored in a var in order to modify its contents, even if you never reassign it.
If itās not only acceptable for let atomic: Atomic<Int> to be mutable, but itās in fact illegal to make a var of Atomic type, it sure seems strange forMutableSpan to be treated the opposite.
While it's definitely a bit confusing, the rationale for Atomic is laid out at some length here and I think the difference from MutableSpan is perfectly reasonable: the entire point of Atomic is to have highly-performant, non-exclusive access to an underlying mutable value. In Swift, var asserts exclusivity of access, which is enforced by runtime checks in cases where the compiler cannot prove exclusivity.
MutableSpan's purpose is differentāindeed, it requires exclusive access for the duration of its lifetime:
Bleargh, this is so confusing and full of special cases:
- SE-0410 introduces a special attribute that, when applied to types like
Atomic, forces all bindings to belet, even though allAtomicvalues are āmutableā by any colloquial definition. - SE-0456 introduces two new special behaviors for getters that return values of
~Escapable & Copyabletype:- If the callee is
Escapableor~Escapable & ~Copyable, then the lifetime of the binding to the callee is extended by the lifetime of the returned value. - If the callee is
~Escapable & Copyable, then the lifetime of the returned value is a copy of the lifetime of the binding to the callee.
- The only purpose of this rule I can imagine is for chains of calls: if in
a.b.c, all ofa,b, andcare~Escapable & Copyable, thenbis allowed to die beforec, instead of having its own lifetime extended by that ofc.
- What about
MutableSpans created ex nihilo? Do they depend on the lifetime ofselffor the duration of the getterās execution between creation and being returned? Or does this logic only kick in on the callee side? What about static and global functions?
- If the callee is
- This pitch now proposes a revision to the immediately preceding special behavior: now the mutability of the newly created lifetime matches the mutability of the originating lifetime.
- Authors of types must mark their getters that return
MutableSpan-like types asmutating, and the authors ofMutableSpan-like types must mark all their useful methods asmutating.- Does the compiler or language enforce this?
- What if the callee is
~Escapable & Copyable(subcase 2 above)? The returned value does not extend the calleeās lifetime; it is an independent copy. Does that mean the return value can be stored in alet?
- Authors of types must mark their getters that return
Edit: interestingly, doesnāt this mean that itās impossible to define a mutableSpan property on Atomic?
Even if you could define one, using it would violate the semantic requirements of Atomic, so this isnāt a bad thing. (It would be essentially equivalent to passing an Atomic inout, which is also forbidden).
Right. The law of exclusivity was amended specifically for atomic operations:
we propose to introduce the concept of atomic access, and to amend the Law of Exclusivity as follows:
Two accesses to the same variable aren't allowed to overlap unless both accesses are reads or both accesses are atomic.
SE-282 Clarify the Swift memory consistency model
This is also why Atomic<T> does not need dynamic enforcement to expose an API free of undefined behaviour - it only exposes atomic operations, so all of its operations may overlap and there is nothing to enforce.
If it had any nonatomic operations, it would all become unsafe unless we guaranteed they never overlapped.
The piecemeal presentation probably creates a worse impression than necessary. Here is a more succinct presentation of every case in the two proposals:
-
Returned type is a borrow:
- If the source is owned, the returned type borrows the source.
- Most cases returning
SpanorRawSpan, as well as nonmutating accesses returningMutableSpanorMutableRawSpan
- Most cases returning
- If the source is a borrow, then the returned type copies the lifetime dependency of the source
- This is arguably an exception, but it makes little sense to borrow a borrowed binding.
- If the source is owned, the returned type borrows the source.
-
Returned type is an exclusive borrow:
- The returned type exclusively borrows the source
- When the source is an exclusive borrow, this allows e.g. temporary slicing of a
MutableSpan
- When the source is an exclusive borrow, this allows e.g. temporary slicing of a
- The returned type exclusively borrows the source
These are edges around the whole dependent lifetimes system; we can define a default behaviour more readily because these cases involve one parameter and one return value. As soon as we have two parameters, I think we'll need explicit annotations.
This need was foreseen, but according to @Andrew_Trick it will involve an entirely new alternative syntax.
I stand by my assessment of the rules for this syntax as being confusing. If Swift is going to need a second, more expressive syntax anyway, why introduce this first implicit syntax?
Some cases admit default behaviours, and defaults will be part of the full system. These are the defaults. Waiting longer won't help imo.
The prohibition on var/inout Atomics is not actually airtight and is more of a guardrail against easy misuse than a strict rule. You can very easily have:
struct Foo: ~Copyable {
var state: State
let cache: Atomic<Cache>
}
var foo = Foo(...)
foo = Foo(...) // replace the entire value, including (nonatomically, exclusively) the Atomic field
and it could be legitimate to do so, if the Atomic is serving the purpose of a cache or something where you might've used a mutable field in C++ to hide mutable state in an immutable-presenting value, while still expecting exclusive access for formal mutations. So it's not so much that you can't form an exclusive-access Atomic as it is you probably don't want to, at least to first order. Additionally, we could conceivably later added APIs to Atomic to take advantage of exclusive access when possible, such as to do tearing load/stores when we can statically prove it's safe to disregard ordering with other writers (though we'd still probably want the attribute to avoid inadvertent dynamic exclusivity checks in that case). By contrast, the exclusive access demands on MutableSpan are a core part of maintaining its contract.
I assert that the current behaviors are not easily understandable nor documentable. They are implied by a combination of:
- Whether the return type of a property is
~Escapable & Copyable - Whether the thing whose property is accessed:
- ā¦is borrowed exclusively (which itself is contingent on whether any upstream bindings are
varorlet), and - ā¦is itself
~Escapableand/or~Copyable.
- ā¦is borrowed exclusively (which itself is contingent on whether any upstream bindings are
As I pointed out above, they are also not completely defined:
struct S: ~Escapable : Copyable {
init()
}
func f() -> S {
var s = S()
// has f() now exclusively borrowed the world?
return s
}
struct Foo {
var prop: S {
let s = S()
// Is it ok that `s` is a `let` even though `prop` is a `var`?
return s
}
}
}
I am concerned that these corner cases exist because the plan has been to focus on the ādefaultsā before designing a robust algebraic lifetime system. I have seen such āhappy path syndromeā play out many times.
So it's not so much that you can't form an exclusive-access
Atomicas it is you probably don't want to, at least to first order.
For what itās worth, I have seen this done intentionally in ObjC code I work with. I tend to insist on rewriting it to use atomic operations, but itās technically possible to have atomic and non-atomic views on the same storage as long as you issue the appropriate memory barriers for nonatomic accesses.
We would have to define the extensions on the concrete types themselves, which is possible but somewhat onerous.
Would the following protocol extension work?
#if SWIFT_STDLIB_ENABLE_VECTOR_TYPES
extension SIMD where Scalar: _ExpressibleByBuiltinIntegerLiteral {
public var span: Span<Scalar> { borrowing get }
public var mutableSpan: MutableSpan<Scalar> { mutating get }
}
#endif
Conforming types will have inline and contiguous storage (unless a third-party can import Builtin using the experimental feature).
Conforming types will have inline and contiguous storage
Is that so? StaticBigInt conforms to _ExpressibleByBuiltinIntegerLiteral.
StaticBigIntconforms to_ExpressibleByBuiltinIntegerLiteral.
But it doesn't conform to SIMDScalar. (Neither do {U}Int128 and Float80.)