[swift-dev] What can you change in a non-exhaustive enum?

Greg Titus greg at omnigroup.com
Fri Sep 29 22:24:07 CDT 2017


I think that you are going to be better served by making things more explicit for non-exhaustive enums with binary compatibility concerns. I don’t think that there is a solution that salvages an ordering for a jump table while still allowing open enums to change during the same @availability version without at some point having the coder editing the enum needing to define or override the case tags (e.g. the "@abi(2)" workaround). 

So given that it’s going to come up and [Apple] library authors will need to deal with it, why not use the same rules/logic/syntax that already exists for RawRepresentable Int enums? I.e. tag value is the same as order in the enum, except that you can override it and following values by explicitly specifying. People familiar with C are going to already know instinctively how to correctly modify the enum to maintain the ABI when reordering it. That is, 

public enum SpellKind {
  case hex = 0
  case charm
  case curse
  case jinx = 4
  case summoning
  case banishing

  // deprecated, it’s more of a curse targeted on a locale than it is a separate kind
  case blight = 3
}

This doesn’t help any for the protocol witness example, of course. Adding “= 4” to the end of your function declaration in your protocol might looks a little odd. But for enums, at least, a large number of devs are going to just get it, and you automatically get the correct behavior (by explicitly defining “public enum SpellKind: Int”) for making this enum work for Objective-C inter-op.

- Greg

> On Sep 29, 2017, at 6:21 PM, Jordan Rose via swift-dev <swift-dev at swift.org> wrote:
> 
> 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
> _______________________________________________
> swift-dev mailing list
> swift-dev at swift.org
> https://lists.swift.org/mailman/listinfo/swift-dev

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-dev/attachments/20170929/ec3dae7a/attachment.html>


More information about the swift-dev mailing list