Exception tracking
Nim supports exception tracking. The raises pragma can be used to explicitly define which exceptions a proc/iterator/method/converter is allowed to raise. The compiler verifies this:
proc p(what: bool) {.raises: [IOError, OSError].} =
if what: raise newException(IOError, "IO")
else: raise newException(OSError, "OS")
An empty raises list (raises: []) means that no exception may be raised:
proc p(): bool {.raises: [].} =
try:
unsafeCall()
result = true
except:
result = false
A raises list can also be attached to a proc type. This affects type compatibility:
type
Callback = proc (s: string) {.raises: [IOError].}
var
c: Callback
proc p(x: string) =
raise newException(OSError, "OS")
c = p # type error
For a routine p the compiler uses inference rules to determine the set of possibly raised exceptions; the algorithm operates on p’s call graph:
- Every indirect call via some proc type T is assumed to raise system.Exception (the base type of the exception hierarchy) and thus any exception unless T has an explicit raises list. However if the call is of the form f(…) where f is a parameter of the currently analysed routine it is ignored. The call is optimistically assumed to have no effect. Rule 2 compensates for this case.
- Every expression of some proc type within a call that is not a call itself (and not nil) is assumed to be called indirectly somehow and thus its raises list is added to p’s raises list.
- Every call to a proc q which has an unknown body (due to a forward declaration or an importc pragma) is assumed to raise system.Exception unless q has an explicit raises list.
- Every call to a method m is assumed to raise system.Exception unless m has an explicit raises list.
- For every other call the analysis can determine an exact raises list.
- For determining a raises list, the raise and try statements of p are taken into consideration.
Rules 1-2 ensure the following works:
proc noRaise(x: proc()) {.raises: [].} =
# unknown call that might raise anything, but valid:
x()
proc doRaise() {.raises: [IOError].} =
raise newException(IOError, "IO")
proc use() {.raises: [].} =
# doesn't compile! Can raise IOError!
noRaise(doRaise)
So in many cases a callback does not cause the compiler to be overly conservative in its effect analysis.
Exceptions inheriting from system.Defect are not tracked with the .raises: [] exception tracking mechanism. This is more consistent with the built-in operations. The following code is valid::
proc mydiv(a, b): int {.raises: [].} =
a div b # can raise an DivByZeroDefect
And so is::
proc mydiv(a, b): int {.raises: [].} =
if b == 0: raise newException(DivByZeroDefect, "division by zero")
else: result = a div b
The reason for this is that DivByZeroDefect inherits from Defect and with --panics:on Defects become unrecoverable errors. (Since version 1.4 of the language.)