Add documentation

This commit is contained in:
Zahary Karadjov 2020-04-08 16:26:36 +03:00
parent 03e9469b1d
commit b3a6b52c77
No known key found for this signature in database
GPG Key ID: C8936F8A3073D609
2 changed files with 202 additions and 1 deletions

View File

@ -23,10 +23,11 @@ respective folders
- `bitops2` - an updated version of `bitops.nim`, filling in gaps in original code
- `byteutils` - utilities that make working with the Nim `byte` type convenient
- `eh` - error-handling utils for working with tracked exceptions, `Result` or `Option` types. Please see [the dedicated docs](docs/error_handling.md).
- `endians2` - utilities for converting to and from little / big endian integers
- `objects` - get an object's base type at runtime, as a string
- `ptrops` - pointer arithmetic utilities
- `result` - friendly, exception-free value-or-error returns, similar to `Option[T]`, from [nim-result](https://github.com/arnetheduck/nim-result/)
- `results` - friendly, exception-free value-or-error returns, similar to `Option[T]`, from [nim-result](https://github.com/arnetheduck/nim-result/)
- `shims` - backports of nim `devel` code to the stable version that Status is using
## Layout

200
docs/error_handling.md Normal file
View File

@ -0,0 +1,200 @@
## The `errors` pragma
The `errors` pragma is similar to the `raises` pragma, but it uses
the Nim type system to ensure that the raised recoverable errors
will be handled at the call-sites of the annotated function.
To achieve this, it performs the following simple transformation:
```nim
proc foo(x: int, y: string): float {.errors: (ValueError, KeyError).} =
body
```
is re-written to the equivalent of:
```nim
type
Raising[ErrorsList, ResultType] = distinct ResultType
proc foo_original(x: int, y: string): float {.
raises: [Defect, ValueError, KeyError]
.} =
body
template foo(x: int, y: string): untyped =
Raising[(ValueError, KeyError), float](foo_original(x, y))
```
Please note that the original proc now features a `raises` annotation
that will guarantee that no other exceptions might be raised from it.
The `Defect` type was implicitly added to the list as a convenience.
The returned distinct type will be useless at the call-site unless
it is stripped-away through `raising`, `either` or `check` which are
the error-handling mechanisms provided by this library and discussed
further in this document.
If you accidentally forget to use one of the error-handling mechanisms,
you'll get a compilation error along these lines:
```
required type for x: float
but expression 'Raising[(ValueError, KeyError), float](foo_original(x, y))' is of type: Raising[tuple of (ValueError, KeyError), system.float]
```
Please note that if you have assigned the `Raising` result to a
variable, the compilation error might happen on a line where you
attempt to use that variable. To fix the error, please introduce
error handling as early as possible at the right call-site such
that no `Raising` variable is created at all.
`noerrors` is another pragma provided for convenience which is
equivalent to an empty `errors` pragma. The forced error handling
through the `Raising` type won't be applied.
Both pragmas can be combined with the `nodefects` pragma that
indicates that the specific proc should be proven to be Defect-free.
The transformation uses a template by default to promote efficiency,
but if you need to take the address the Raising proc, please add the
`addressable` pragma that will force the wrapper to be a regular proc.
Finally, `failing` is another pragma provided for convenience which
is equivalent to `{.errors: (CatchableError).}`.
## The `raising` annotation
The `raising` annotation is the simplest form of error tracking
similar to the `try` annotation made popular by the [Midori error model](http://joeduffyblog.com/2016/02/07/the-error-model/),
which is also [proposed for addition in C++](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0709r0.pdf) and [already available in Zig](https://ziglang.org/documentation/master/#try).
It merely marks the locations in the code where errors might be raised
and strips away the `Raising` type to disarm the compiler checks:
```nim
proc attachValidator(node: BeaconNode, keyFile: string) {.failing.} =
node.addValidator(raising ValidatorPrivKey.init(raising readFile(keyFile))
```
When applied to a `Result` or an `Option`, `raising` will use the
`tryGet` API to attempt to obtain the computed value.
## The capital `Try` API
The capital `Try` API is similar to a regular `try` expression
or a `try` statement. The only difference is that you must provide
exception handlers for all possible recoverable errors. If you fail
to do so, the compiler will point out the line in the `try` block
where an unhandled exception might be raised:
```nim
proc replaceValues(values: var openarray[string],
replacements: Table[MyEnum, string]) =
Try:
for v in mitems(values):
let
enumValue = parseEnum v
replacement = replacements[enumValue] # Error here
v = replacement
except ValueError:
echo "Invalid enum value"
```
The above example will fail to compile with an error indicating
that `replacements[enumValue]` may fail with an unhandled `KeyError`.
## The `either` expression
The `either` expression can be used with APIs based on `Option[T]`,
`Result[T, E]` or the `errors` pragma when it's appropriate to
discriminate only between successful execution and any type of
failure. Regardless of the error handling scheme being used,
`either` is used like this:
```nim
let x = either(foo(), fallbackValue)
```
On success, `either` returns the successfully computed value
and the failure side of the expression won't be evaluated at all.
Besides providing a substitute value, the failure side of the
expression may also feature a `noReturn` statement such as
`return`, `raise`, `quit` as long at it's used with the following
syntax:
```nim
let x = either foo():
return
```
Within the failure path, you can also use the `error` keyword to
refer to the raised exception or the `error` value of the failed
`Result`.
## The `check` expression
The `check` macro provides a general mechanism for handling
the failures of APIs based the `errors` pragma or `Result[T, E]`
where `E` is an `enum` or a case object type.
It takes an expression that might fail in multiple ways together
with a block of error handlers that will be executed in case of
failure. If the user failed to cover any of the possible failure
types, this will result in a compilation error.
On success, the `check` returns the successfully computed value
of the checked expression. In case of failure, the appropriate
error handler is executed. It may produce a substitute value or
it may return from the current function with a `return`, `raise`,
`quit` or any other `noReturn` API.
The syntax of the `check` expression is the following:
```nim
let x = check foo():
SomeError as err: defaultValue
AnotherError: return
_: raise
```
If the `foo()` function was using the `errors` pragma, the
above example will be re-written to:
```nim
let x = try:
raising foo()
except SomeError as err:
defaultValue
except AnotherError:
return
except CatchableError:
raise
```
Alternatively, if `foo()` was returning a `Result[T, E: enum]`, the
example will be re-written to:
```nim
let x = foo()
if x.isOk:
x.get
else:
case x.orror:
of SomeError:
let err = x.error
defaultValue
of AnotherError:
return
else:
raiseResultError x
```
The `Result` error type can also be a case object with a single `enum`
discriminator that will be considered the error type. The generated code
will be quite similar to the example above.
Please note that the special default case `_` is considered equivalent
to `CatchableError` or `else` when working with enums.