> On Sep 13, 2017, at 05:21, Brent Royal-Gordon <[email protected]> wrote:
> 
>> On Sep 12, 2017, at 6:30 PM, Jordan Rose <[email protected] 
>> <mailto:[email protected]>> 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
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to