Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to me as the review manager by DM. When contacting the review manager directly, please put "SE-0426" in the subject line.
Trying it out
If you'd like to try this proposal out, you can download a toolchain supporting it from Swift.org; the most recent Trunk Development (main) snapshots support the feature. You will need to add -enable-experimental-feature BitwiseCopyable to your build flags.
What goes into a review?
The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:
What is your evaluation of the proposal?
Is the problem being addressed significant enough to warrant a change to Swift?
Does this proposal fit well with the feel and direction of Swift?
If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
More information about the Swift evolution process is available at:
Long overdue feature overall, happy to see it take shape.
Just an initial point of feedback to revisit the point brought up in the pitch that this feature is proposed as a Marker Protocol—and not just a @_marker protocol as an implementation detail.
While BitwiseCopyable won't have required members and is non-retroactive, that we are contemplating (even as a future direction) ways of checking that a type is bitwise copyable at runtime means that it's not really a Marker Protocol par excellence. (The same feedback was given by @beccadax, as I recall, regarding the design for opt-in Reflectable when it was to be our second-ever Marker Protocol.)
Would we lose something here by specifying instead that BitwiseCopyable is a non-retroactive protocol with no required members, with runtime querying to be designed later?
as the syntax to suppress the inferred conformance?
Sendable uses the former but Copyable uses the later.
I find the ~BitwiseCopyable a lot more intuitive. It is also nice to be able to suppress the conformance at the declaration of the type. The @available syntax feels more like a trick.
Also, does the unavailability of BitwiseCopyable need to be declared in the same file or module? I guess yes, but the separate @available annotation would make it possible to move it to another file or even module.
That may be reflecting the state of the implementation rather than where we really want to end up. We also ultimately want to allow ~Protocol to suppress assumed conformances in other contexts, including ~Sendable on local types, ~Equatable and ~Hashable on no-payload enums, and so on.
Finally! I'm also not sure if it should be a marker protocol though. It's really more like Copyable, but I definitely understand wanting to allow it to be a type alias for the more niche lower level constraints. I'm definitely for this addition.
I maintain that there’s nothing “Copyable” about this type; any type that is bitwise-movable, trivially destructed, and Copyable is going to be bitwise-copyable. I don’t actually have a better name, but I think if we introduce BitwiseCopyable, then we’ll soon have BitwiseCopyable & ~Copyable, and we should admit that now rather than hypothesizing about BitwiseMovable being a separate thing.
I don’t quite get the inference section. Is BitwiseCopyable inferred on public types or not? How does -enable-library-evolution change that answer? Is it that dynamic casts will succeed even though static type checks don’t have such a guarantee? That seems risky to me but I can understand why it’s useful for optimization…
Since this isn't a real protocol, and the runtime encoding doesn't change, I think it ends up being a compatible change if later we have to replace it with a typealias for a composition of more primitive generic constraints. For BitwiseMovable in particular though, BitwiseCopyable would still be a stronger property than BitwiseMovable & Copyable, since the latter includes most types with nontrivial destructors. And while BitwiseMovable and BitwiseCopyable admit new capabilities (the former lets you borrow-by-memcpy, allowing aggregate borrows to be derived without formally copying the value, and the latter lets you mmap, write(2), store-unaligned, and do other raw memory manipulations), from the pitch discussion it wasn't clear that NoOpDeinit by itself carried its weight as a generic constraint, since there isn't any API that comes to mind that would require it. It's still an interesting property to test for optimization purposes, but that doesn't necessarily mean it has to be available as a constraint.
Ah, I agree that a BitwiseMovable that does not include NoOpDeinit remains interesting—this includes very common types like String and Array, at least today. What I suspect is not interesting is types that are BitwiseMovable & NoOpDeinit & !Copyable. So I'd rather have "BitwiseCopyable" mean BitwiseMovable & NoOpDeinit & ?Copyable, rather than BitwiseMovable & NoOpDeinit & Copyable, which together with SE-0427 implies that "BitwiseCopyable & ~Copyable" is a valid and useful combination.
Back in the pitch thread I tried to run through all the real-world cases, finding that they were all precedented except BitwiseMovable & NoOpDeinit & !Copyable. But we could say Swift doesn't need to distinguish all of them. What if the defining feature today was focused on the "cleanup" side rather than the "move" side? Then we could say there's two ways storage gets cleaned up in Swift at the low level: it can be moved, or it can be deinitialized. Storage that requires no "cleanup" may or may not be copyable, but if it is, that operation is always going to be treated as trivial as well.
The main problem here is that C++ does let you violate this rule, but we would say a type with a non-trivial move or copy constructor does not get the "trivial cleanup" property when imported into Swift because of the optimizations Swift wants to do on such types.
I think the "marker protocol" concept hasn't completely stood up as a general idea. We should move away from talking about whether protocols are "marker protocols" and instead be explicit about the language properties we want from them.
SE-0302 defined marker protocols as having four key properties:
They cannot have requirements of any kind.
They cannot inherit from non-marker protocols.
A marker protocol cannot be named as the type in an is or as? check (e.g., x as? Sendable is an error).
A marker protocol cannot be used in a generic constraint for a conditional protocol conformance to a non-marker protocol.
That was defined in the context of Sendable, where we had a bunch of specific constraints:
Sendable doesn't need function or associated type requirements.
Sendable conformances are very common and shouldn't impose significant runtime overheads.
Sendable needs to not rely on runtime or type-specific support to avoid a back-deployment nightmare.
Sendable needs to be addable retroactively to a type in order to solve practical problems with pre-concurrency library dependencies.
So the restrictions were built around neither needing nor wanting any runtime representation of Sendable conformances because it'd be too heavyweight and would make clients too reliant on cooperation from their dependencies.
For BitwiseCopyable, we still definitely don't want to pay for unnecessary witness tables for every conformance, but we already have a (much more lightweight) runtime representation that's (AFAIK) perfectly reliable in back-deployed cases. The only questions with back-deployment are representing the BitwiseCopyable constraint itself and having some direct runtime support for querying it in e.g. dynamic casts.
So I think we want a somewhat different concept here. As I understand it, we're already building out some very similar runtime support for Copyable.