So here’s my counter-proposal, fleshed out:
Specially, on resiliency:
## Internally to a module
Compiler can use generated Error-list metadata to:
- provide compile-errors with specific uncaught errors (better diagnostics)
- allow omitting catch-alls
- optimise away Error existential allocations
All of that would automatically apply to all throwing/rethrowing functions, without any additional developer effort.
## Cross-module
Compiler can use generated Error-list metadata to:
- Inform users about errors that might get thrown by this version of the function (purely documentation)
- Allow omitting catch-alls for specific functions which opt-in to that contract.
And that’s it. Notice there is no behavioural change; the Error-list metadata is entirely optional.
### Exhaustive catching cross-module
Resilient functions can _additionally_ promise to never throw new Errors. It should be an additional promise. From an error-list perspective, the function makes an additional promise that later versions of the error-list will never get more inclusive.
We can’t check that resilience at compile-time, though. If you change the function signature, you will get a error in the dynamic linker. Similarly, if somebody just adds a case to their @fixed enum, you won’t know until runtime when it gets thrown and nobody’s there to catch it.
- It would be cool if we failed gracefully in that case; if the caller wasn’t catching exhaustively, the new error should fall in to the existing catch-all.
- Otherwise, if the caller was assuming the library author kept their promise and omitted a catch-all, we should still synthesise one to provide a unique trap location (something like swift_resilience_unexpectedError()).
- It means we can't optimise away the Error existential cross-module (maybe in -Ounchecked?), but that seems to me like an acceptable cost.
The big problem with this is that it relies on unwritten, generated metadata. For resilient functions promising resilient error-lists, it’s helpful to have the errors you’re promising written down and easily manually inspectable. That’s why I initially suggested having the compiler validate the documentation comments. We could still do that - so if you have a @versioned function and you additionally say that the errors it throws are also @versioned, you have to write a comment listing every Error (and the compiler will check it).
Small example:
enum FailureReason {
case deviceBusy
case networkDown
case notFound
}
//% [Error-list]: FailureReason.notFound
func openFile(_ path: String) throws { … }
//% @versioned [Error-list]: FailureReason.notFound, FailureReason.deviceBusy
@versioned(includingErrors)
func read(_ path: String, range: Range<Int>) -> Data { … }
//% [Error-list]: <rethrows from arg1>
func retrying(attempts n: Int, _ work: ()throws -> Void) rethrows -> Bool {
for _ in 0..<attempts {
do { try work(); return true }
catch FailureReason.deviceBusy { continue }
catch FailureReason.networkDown { continue }
catch { throw error }
}
return false
}
//% [Error-list]: <rethrows from arg0, masks: FailureReason.notFound>
func mustBeFound(_ work: ()throws -> Void) rethrows {
do { try work() }
catch FailureReason.notFound { fatalError(“This thing must be found") }
catch { throw error }
}
mustBeFound { openFile(“test.txt”) } // can be proven not to throw (in same module), because mustBeFound handles all errors
// cross-module
// Function does not have a versioned error-list; catch-all is mandatory.
do { try openFile(“test.txt”) }
catch .notFound { print(“not found!”) }
catch { print(“other error: \(error}”) }
do { try mustBeFound { open(“test.txt”) } }
catch { print(“other error: \(error}”) }
// Function has a versioned error-list; catch-all is optional.
do { try readFile(“test.txt”, range: 0..<64) }
catch .notFound { print(“file not found!”) }
catch .deviceBusy { /* maybe retry? */ }
// [implicit] catch { _swift_reslience_unexpectedError() }
···
On 21 Feb 2017, at 00:34, Karl Wagner <karl.swift@springsup.com> wrote:
On 19 Feb 2017, at 21:04, Anton Zhilin <antonyzhilin@gmail.com <mailto:antonyzhilin@gmail.com>> wrote:
It’s expected that if you need resilience, then you will throw an “open” enum. Essentially, we pass resilience of typed throws on to those who will hopefully establish resilience of enums.
If you prefer separate error types, then declare a base protocol for all your error types and throw a protocol existential. You won’t even need default case in switches, if closed protocols make it into the language.
I don’t like any solution that is based on comments. I think that compiler should always ignore comments.
Open enums can only add cases, not remove them. That means that new versions of a function will similarly only be able to add errors, and won’t be able to communicate that certain errors are no longer thrown. Protocols aren’t a solution, because you will need to write verbose conformances for all of your Error types.
Let me put this in another (perhaps more palatable) way. Forget comments, let’s say its part of some hidden metadata:
- The compiler lists every error which can be thrown by a function (it’s easily able to do that)
- Rather than making this part of the signature, the signature only says “throws an Error” and the actual Error list is written somewhere in the module as documentation/metadata.
Here’s why it’s so good:
- It’s completely free (you don’t write anything). The compiler generates everything for you.
- It’s **completely optional**: it won’t make your structure your Errors in a way that's less usable in a type-system sense for clients who don’t care about exhaustive catching.
- Exhaustive catching within your own module for free (you can omit a catch-all, the compiler won’t complain)
It’s good for resiliency, too:
- Non-resilient functions always reserve the right to throw new Errors in later versions. No system will get exhaustive catching for them anyway.
- If you stop throwing a specific Error, nothing breaks - it simply vanishes from the documentation/metadata. The compiler can simply warn about the redundant catch.
- Resilient (@versioned) functions can still offer exhaustive catching if we want to offer that. We might decide to make that opt-in/opt-out, because it would mean they would be limited to removing Errors, and never adding new ones.
—> As with any ABI promise, we will trap or it will be UB if you break the contract. We couldn’t validate it when compiling, but theoretically a validator could be built which compared two library versions.
- Karl