[swift-evolution] Enums and Source Compatibility

Jordan Rose jordan_rose at apple.com
Wed Sep 13 13:28:38 CDT 2017


> On Sep 13, 2017, at 05:21, Brent Royal-Gordon <brent at architechies.com> wrote:
> 
>> On Sep 12, 2017, at 6:30 PM, Jordan Rose <jordan_rose at apple.com <mailto:jordan_rose at apple.com>> wrote:
>> 
>> It gets a little tricky if layout matters—Optional<AnyObject> fits exactly in a single word-sized value, but Optional<Optional<AnyObject>> does not on Apple platforms—but that just means it should be opt-in or tied to -enable-testing in some way.
> 
> 
> I forgot to state this explicitly, but I agree—unless the module was compiled with -enable-testing, the generated code should not permit #invalid values and would be identical to a version without any @testable parameters/types.
> 
> Here's a more explicit sketch of a design for this feature (albeit one that has some impact on the type system and a couple weird corners):
> 
> 	• `@testable T` is a supertype of `T` which, when the module is compiled with `-enable-testing`, has an additional `#invalid` inhabitant. (We can bikeshed `@testable` and `#invalid` some other time.) Notionally, `@testable` is sort of like an enum which has one case (`valid(Wrapped)`) in a non-`-enable-testing` build, and an additional case (`invalid`) in an `-enable-testing` build.
> 
> 	• `T` implicitly converts to `@testable T`; `@testable T` can be explicitly downcast to `T`.* When `-enable-testing` is *not* provided, these downcasts will always succeed, and the trap in `as!` or the code for a `nil` result from `as?` are unreachable. We should ignore and potentially optimize away this unreachable code without warning about it.
> 
> 	• Any pattern that matches against `T` can also match against `@testable T` with no alteration. Only `_` or a capture can match `#invalid`.** Otherwise, `#invalid` values will be handled by the `default` case of a `switch` or the `else` block of an `if` or `guard`.
> 
> 	• A given `@testable T` value (i.e. property, variable, subscript, parameter, return value, etc.) may only be assigned `#invalid` if it is either in the current module or is in a module imported with `@testable import`.
> 
> 	• When `-enable-testing` is *not* provided, all code which creates an `#invalid` value must be unreachable. This is even true in `default:` cases and other constructs which could be reached by unknown future values of a type. Only constructs like `guard let t = testableT as? T else { return #invalid }` can be successfully compiled with `-enable-testing` disabled.
> 
> The memory representation of `#invalid` does not have to be the same for all types, so it could try to find a spare bit or bit pattern that's unused in the original type (as long as, for non-exhaustive enums, it also avoids using any bit pattern a future version of the type *might* use). Or, for simplicity, we could just add a tag byte unconditionally. This tag byte would only be needed when built with `-enable-testing`, so basically only debug builds would pay this price, and only in places where the author explicitly asked to be able to test with `#invalid` values.
> 
> 
> * There's an argument to be made for an IUO-style implicit conversion from `@testable Foo` to `Foo` which traps on `#invalid`. This seems dangerous to me, but on the other hand, you should only ever encounter it in testing or development, never in production.
> 
> ** I'm not sure captures can work here—wouldn't they still be the `@testable` type?—so I'm actually wondering if we should introduce a subtle distinction between `case _`/`case let x` and `default`: the former cannot match `#invalid`, while the latter can. That would be a little bit…odd, though.

Thanks for working this out. This matches the intuitions I was having, and also finds a point that’s pretty concerning:

> * There's an argument to be made for an IUO-style implicit conversion from `@testable Foo` to `Foo` which traps on `#invalid`. This seems dangerous to me, but on the other hand, you should only ever encounter it in testing or development, never in production.

It’s going to be very common to have a future value and hand it right back to the framework without looking at it, for example:

override func process(_ transaction: @testable Transaction) {
  switch transaction {
  case .deposit(let amount):
    // …
  case .withdrawal(let amount):
    // …
  default:
    super.process(transaction) // hmm…
  }
}

So just making it to the ‘default’ case doesn’t guarantee that it’s testable in practice.

In any case, a model like this can be added later without breaking source or binary compatibility, so I think I’m going to leave it out of the proposal for now. I’ll mention it in “Alternatives considered”.

Jordan
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170913/2d5a2c7f/attachment.html>


More information about the swift-evolution mailing list