Effect System
Effect system
Note: The rules for effect tracking changed with the release of
version 1.6 of the Nim compiler. This section describes the new rules
that are activated via --experimental:strictEffects.
Exception tracking
Nim supports exception tracking. The raises{.interpreted-text
role="idx"} 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 CatchableError:
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
Tis assumed to raisesystem.Exception(the base type of the exception hierarchy) and thus any exception unlessThas an explicitraiseslist. However, if the call is of the formf(...)wherefis a parameter of the currently analyzed routine it is ignored that is marked as.effectsOf: f. The call is optimistically assumed to have no effect. Rule 2 compensates for this case. - Every expression
eof some proc type within a call that is passed to parameter marked as.effectsOfis assumed to be called indirectly and thus its raises list is added top\'s raises list. - Every call to a proc
qwhich has an unknown body (due to a forward declaration) is assumed to raisesystem.Exceptionunlessqhas an explicitraiseslist. Procs that areimportc\'ed are assumed to have.raises: [], unless explicitly declared otherwise. - Every call to a method
mis assumed to raisesystem.Exceptionunlessmhas an explicitraiseslist. - For every other call, the analysis can determine an exact
raiseslist. - For determining a
raiseslist, theraiseandtrystatements ofpare taken into consideration.
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:
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{.interpreted-text role="option"} Defects become
unrecoverable errors. (Since version 1.4 of the language.)
EffectsOf annotation
Rules 1-2 of the exception tracking inference rules (see the previous section) ensure the following works:
proc weDontRaiseButMaybeTheCallback(callback: proc()) {.raises: [], effectsOf: callback.} =
callback()
proc doRaise() {.raises: [IOError].} =
raise newException(IOError, "IO")
proc use() {.raises: [].} =
# doesn't compile! Can raise IOError!
weDontRaiseButMaybeTheCallback(doRaise)
As can be seen from the example, a parameter of type proc (...) can be
annotated as .effectsOf. Such a parameter allows for effect
polymorphism: The proc weDontRaiseButMaybeTheCallback raises the
exceptions that callback raises.
So in many cases a callback does not cause the compiler to be overly conservative in its effect analysis:
{.push warningAsError[Effect]: on.}
{.experimental: "strictEffects".}
import algorithm
type
MyInt = distinct int
var toSort = @[MyInt 1, MyInt 2, MyInt 3]
proc cmpN(a, b: MyInt): int =
cmp(a.int, b.int)
proc harmless {.raises: [].} =
toSort.sort cmpN
proc cmpE(a, b: MyInt): int {.raises: [Exception].} =
cmp(a.int, b.int)
proc harmfull {.raises: [].} =
# does not compile, `sort` can now raise Exception
toSort.sort cmpE
Tag tracking
Exception tracking is part of Nim\'s effect system{.interpreted-text
role="idx"}. Raising an exception is an effect. Other effects can also
be defined. A user defined effect is a means to tag a routine and to
perform checks against this tag:
type IO = object ## input/output effect
proc readLine(): string {.tags: [IO].} = discard
proc no_IO_please() {.tags: [].} =
# the compiler prevents this:
let x = readLine()
A tag has to be a type name. A tags list - like a raises list - can
also be attached to a proc type. This affects type compatibility.
The inference for tag tracking is analogous to the inference for exception tracking.
Side effects
The noSideEffect pragma is used to mark a proc/iterator that can have
only side effects through parameters. This means that the proc/iterator
only changes locations that are reachable from its parameters and the
return value only depends on the parameters. If none of its parameters
have the type var, ref, ptr, cstring, or proc, then no
locations are modified.
In other words, a routine has no side effects if it does not access a threadlocal or global variable and it does not call any routine that has a side effect.
It is a static error to mark a proc/iterator to have no side effect if the compiler cannot verify this.
As a special semantic rule, the built-in
debugEcho pretends to be
free of side effects so that it can be used for debugging routines
marked as noSideEffect.
func is syntactic sugar for a proc with no side effects:
To override the compiler\'s side effect analysis a {.noSideEffect.}
cast pragma block can be used:
Side effects are usually inferred. The inference for side effects is analogous to the inference for exception tracking.
GC safety effect
We call a proc p GC safe when it
doesn\'t access any global variable that contains GC\'ed memory
(string, seq, ref or a closure) either directly or indirectly
through a call to a GC unsafe proc.
The GC safety property is usually inferred. The inference for GC safety is analogous to the inference for exception tracking.
The gcsafe annotation can be used to
mark a proc to be gcsafe, otherwise this property is inferred by the
compiler. Note that noSideEffect implies gcsafe.
Routines that are imported from C are always assumed to be gcsafe.
To override the compiler\'s gcsafety analysis a {.cast(gcsafe).}
pragma block can be used:
var
someGlobal: string = "some string here"
perThread {.threadvar.}: string
proc setPerThread() =
{.cast(gcsafe).}:
deepCopy(perThread, someGlobal)
See also:
Effects pragma
The effects pragma has been designed to assist the programmer with the
effects analysis. It is a statement that makes the compiler output all
inferred effects up to the effects\'s position:
proc p(what: bool) =
if what:
raise newException(IOError, "IO")
{.effects.}
else:
raise newException(OSError, "OS")
The compiler produces a hint message that IOError can be raised.
OSError is not listed as it cannot be raised in the branch the
effects pragma appears in.