> On 4. Oct 2017, at 01:37, Karl Wagner <razie...@gmail.com> wrote:
> 
> 
> 
>> On 30. Sep 2017, at 20:23, Xiaodi Wu via swift-dev <swift-dev@swift.org 
>> <mailto:swift-dev@swift.org>> wrote:
>> 
>> On Sat, Sep 30, 2017 at 11:58 AM,  <swift-dev-requ...@swift.org 
>> <mailto:swift-dev-requ...@swift.org>> wrote:
>> Message: 2
>> Date: Fri, 29 Sep 2017 18:21:44 -0700
>> From: Jordan Rose <jordan_r...@apple.com <mailto:jordan_r...@apple.com>>
>> To: swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>>
>> Subject: [swift-dev] What can you change in a non-exhaustive enum?
>> Message-ID: <31df689e-1ad3-47cb-9fce-6cbc7e34b...@apple.com 
>> <mailto:31df689e-1ad3-47cb-9fce-6cbc7e34b...@apple.com>>
>> Content-Type: text/plain; charset="utf-8"
>> 
>> Hello again, swift-dev! This is a sort of follow-up to "What can you change 
>> in a fixed-contents struct" from a few weeks ago, but this time concerning 
>> enums. Worryingly, this seems to be an important consideration even for 
>> non-exhaustive enums, which are normally the ones where we'd want to allow a 
>> developer to do anything and everything that doesn't break source 
>> compatibility.
>> 
>> [This only affects libraries with binary compatibility concerns. Libraries 
>> distributed with an app can always allow the app to access the enum's 
>> representation directly. That makes this an Apple-centric problem in the 
>> near term.]
>> 
>> So, what's the issue? We want to make it efficient to switch over a 
>> non-exhaustive enum, even from a client library that doesn't have access to 
>> the enum's guts. We do this by asking for the enum's tag separately from its 
>> payload (pseudocode):
>> 
>> switch getMyOpaqueEnumTag(&myOpaqueEnum) {
>> case 0:
>>   var payload: Int
>>   getMyOpaqueEnumPayload(&myOpaqueEnum, 0, &payload)
>>   doSomething(payload)
>> case 1:
>>   var payload: String
>>   getMyOpaqueEnumPayload(&myOpaqueEnum, 1, &payload)
>>   doSomethingElse(payload)
>> default:
>>   print("unknown case")
>> }
>> 
>> The tricky part is those constant values "0" and "1". We'd really like them 
>> to be constants so that the calling code can actually emit a jump table 
>> rather than a series of chained conditionals, but that means case tags are 
>> part of the ABI, even for non-exhaustive enums.
>> 
>> Like with struct layout, this means we need a stable ordering for cases. 
>> Since non-exhaustive enums can have new cases added at any time, we can't do 
>> a simple alphabetical sort, nor can we do some kind of ordering on the 
>> payload types. The naive answer, then, is that enum cases cannot be 
>> reordered, even in non-exhaustive enums. This isn't great, because people 
>> like being able to move deprecated enum cases to the end of the list, where 
>> they're out of the way, but it's at least explainable, and consistent with 
>> the idea of enums some day having a 'cases' property that includes all cases.
>> 
>> Slava and I aren't happy with this, but we haven't thought of another 
>> solution yet. The rest of this email will describe our previous idea, which 
>> has a fatal flaw.
>> 
>> 
>> Availability Ordering
>> 
>> In a library with binary compatibility concerns, any new API that gets added 
>> should always be explicitly annotated with an availability attribute. Today 
>> that looks like this:
>> 
>> @available(macOS 10.13, iOS 11, tvOS 11, watchOS 4, *)
>> 
>> It's a model we only support for Apple platforms, but in theory it's 
>> extendable to arbitrary "deployments". You ought to be able to say 
>> `@available(MagicKit 5)` and have the compiler actually check that.
>> 
>> Let's say we had this model, and we were using it like this:
>> 
>> public enum SpellKind {
>>   case hex
>>   case charm
>>   case curse
>>   @available(MagicKit 5)
>>   case blight
>>   @available(MagicKit 5.1)
>>   case jinx
>> }
>> 
>> "Availability ordering" says that we can derive a canonical ordering from 
>> the names of cases (which are API) combined with their versions. Since we 
>> "know" that newly-added cases will always have a newer version than existing 
>> cases, we can just put the older cases first. In this case, that would give 
>> us a canonical ordering of [charm, curse, hex, blight, jinx].
>> 
>> 
>> The Fatal Flaw
>> 
>> It's time for MagicKit 6 to come out, and we're going to add a new SpellKind:
>> 
>> @available(MagicKit 6)
>> case summoning
>> // [charm, curse, hex, blight, jinx, summoning]
>> 
>> We ship out a beta to our biggest clients, but realize we forgot a vital 
>> feature. Beta 2 comes with another new SpellKind:
>> 
>> @available(MagicKit 6)
>> case banishing
>> // [charm, curse, hex, blight, jinx, banishing, summoning]
>> 
>> And now we're in trouble: anything built against the first beta expects 
>> 'summoning' to have tag 5, not 6. Our clients have to recompile everything 
>> before they can even try out the new version of the library.
>> 
>> Can this be fixed? Sure. We could add support for beta versions to 
>> `@available`, or fake it somehow with the existing version syntax. But in 
>> both of these cases, it means you have to know what constitutes a "release", 
>> so that you can be sure to use a higher number than the previous "release". 
>> This could be made to work for a single library, but falls down for an 
>> entire Apple OS. If the Foundation team wants to add a second new enum case 
>> while macOS is still in beta, they're not going to stop and recompile all of 
>> /System/Library/Frameworks just to try out their change.
>> 
>> So, availability ordering is great when you have easily divisible 
>> "releases", but falls down when you want to make a change "during a release".
>> 
>> 
>> Salvaging Availability Ordering?
>> 
>> - We could still sort by availability, so that you can reorder the sections 
>> but not the individual cases in them. That doesn't seem very useful, though.
>> 
>> - We could say "this is probably rare", and state that anything added "in 
>> the same release" needs to get an explicit annotation for ordering purposes. 
>> (This is equivalent to the `@abi(2)` Dave Zarzycki mentioned in the previous 
>> thread—it's not the default but it's there if you need it.)
>> 
>> - We could actually require libraries to annotate all of their "releases", 
>> but in order to apply that within Apple we'd need some translation from 
>> library versions (like "Foundation 1258") to OS versions ("macOS 10.11.4"), 
>> and then we'd still need to figure out what to do about betas. (And there's 
>> a twist, at least at Apple, where a release's version number isn't decided 
>> until the new source code is submitted.)
>> 
>> - There might be something clever that I haven't thought of yet.
>> 
>> 
>> This kind of known ordering isn't just good for enum cases; it could also be 
>> applied to protocol witnesses, so that those could be directly dispatched 
>> like C++ vtables. (I don't think we want to restrict reordering of protocol 
>> requirements, as much as it would make our lives easier.) So if anyone has 
>> any brilliant ideas, Slava and I would love to hear them!
>> 
>> Jordan
>> 
>> Kind of a hybrid idea, but hopefully one that circumvents the internal 
>> issues that you outline here and simplifies the mental model. I'll introduce 
>> it stepwise:
>> 
>> Currently, the @available annotation is supported for platforms and for 
>> Swift versions; you propose extending support to arbitrary deployments 
>> (e.g., MagicKit). Instead, suppose you extended support to arbitrary 
>> versioned types (open, public, and @_versioned internal):
>> 
>> @_versioned(abi: 2)
>> public enum SpellKind {
>>   case hex, charm, curse
>>   @available(abi: SpellKind 1) case blight
>>   @available(abi: SpellKind 1.1) case jinx
>> }
>> 
>> Now, clearly, the value of "abi" is arbitrary inside the @_versioned 
>> annotation; it's not really necessary for our limited purposes, and if 
>> there's a typo and it's lower than the highest ABI version referenced in an 
>> @available annotation, things get wonky. So, drop it:
>> 
>> public enum SpellKind {
>>   case hex, charm, curse
>>   @available(abi: 1) case blight
>>   @available(abi: 1.1) case jinx
>> }
>> 
>> This is looking like the original @abi(2) proposal that Dave Zarzycki 
>> brought up. However, a key difference here: multiple cases can have the same 
>> ABI "version" and would be ordered relative to each other by name; that is, 
>> a user would only be annotating when new cases are added and doesn't have to 
>> think about memory layout; Swift takes care of the rest.
>> 
>> This can be simplified further; these ABI version numbers are entirely 
>> arbitrary. Suppose we instead extended the "@available(introduced:)" syntax 
>> to allow dates or timestamps:
>> 
>> public enum SpellKind {
>>   case hex, charm, curse
>>   @available(*, introduced: 2017-09-30): case blight
>>   @available(*, introduced: 2017-10-12): case jinx
>> }
>> 
>> I suspect there are wrinkles to this scheme, but the overall idea here is to 
>> salvage availability ordering but have some way to version a type in a 
>> low-mental-overhead way instead of resorting to a syntax for manually 
>> ordering cases.
>> 
> 
> Pretty much. If you want a stable order, you need to manually annotate each 
> change to the enum’s layout with some incrementing identifier which can be 
> sorted. Ideally, we would use the module’s version number, but as Jordan 
> points out, during development layout can change even between versions.
> 
> So the simplest answer to me is to give each enum it’s own mini version 
> number. You could write them out, as in your examples. Another approach might 
> be to introduce a version-break statement in the case-list, with the 
> condition that those version-breaks cannot be reordered, but everything 
> inside of them can be:
> 
> enum SpellKind {
>     case hex, charm, curse
>     @new-version case blight
>     @new-version
>      case jinx
>      case summoning
>      case ...
> }
> 
> A library developer could add as many of those as they need for internal 
> testing, and selectively remove them as appropriate for each Beta/GM release 
> (or they might keep them, if they decide to promise binary compatibility 
> between beta and GM).
> 
> - Karl
> 
>> 
>> _______________________________________________
>> swift-dev mailing list
>> swift-dev@swift.org <mailto:swift-dev@swift.org>
>> https://lists.swift.org/mailman/listinfo/swift-dev 
>> <https://lists.swift.org/mailman/listinfo/swift-dev>
Ah I suppose this wouldn’t really work with @available since you can’t group 
cases in to a single availability context to provide a stable ordering within. 
So you will have to write the number out.

Still, I don’t think there is any way to do it other than an arbitrary, 
enum-scoped build/release number. It doesn’t mean anything other than to 
provide a level of ABI versioning below the module’s version number, like 
@abi(2). I don’t agree with supporting arbitrary tags or dates: they need to 
obviously increment, with minimal opportunities to get it wrong.

enum SpellKind {
    case hex
    case charm
    case curse
    @available(MagicKit 5) case blight
    @available(MagicKit 5.1) case jinx
    @available(MagicKit 6) case summoning
    @available(MagicKit 6, build: 2) case banishing
}

- Karl
_______________________________________________
swift-dev mailing list
swift-dev@swift.org
https://lists.swift.org/mailman/listinfo/swift-dev

Reply via email to