In addition to its type system Nim also offers an effect system. The goal of the effect system is to prevent even more mistakes at compile-time. The effect system is concerned with the question "what is routine X allowed to do?". In most programming languages a routine is a black box; when you see a call to function f without knowing its implementation f can perform any arbitrary task: It could perform any kind of IO, it could raise an exception, it could acquire locks that you also need to acquire causing deadlocks and it could run an arbitrary shell script, including a script that it itself generated. Thus Nim’s effect system covers: • Which types of exceptions a routine can raise, if any. • Which locks a routine can acquire, if any. • If a routine accesses any global state. • Which custom effects like ExecProgram or UsesDatabase a routine can have. When we talk about routine in the context of the effect system, it means proc, func, iterator, converter, method but not template or macro. 26.1. Exception tracking The raises pragma can be used to explicitly define which exceptions a routine 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:
171
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, inference rules to determine the set of possibly raised exceptions are used; the algorithm operates on p's call graph:
For determining a raises list, the raise and try statements of p are 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: 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:option: Defects become unrecoverable errors. 26.2. 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: [].} =
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:
173
{.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 harmful {.raises: [].} =
sort can now raise ExceptiontoSort.sort cmpE 26.3. Tag tracking Exception tracking is part of Nim’s effect system. 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: [].} =
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.
174
26.4. Side effects
The noSideEffect pragma is used to mark a routine that can have only side
effects through parameters. This means that the routine only changes
locations that are reachable from its parameters and the return value only
depends on the parameters. If none of its parameters' types contain 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 thread-
local or global variable and it does not call any routine that has a side effect.
It is a static error to mark a routine to have no side effect if the compiler
cannot verify this.
As a special semantic rule, the built-in system.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:
func +(x, y: int): int
To override the compiler’s side effect analysis a {.noSideEffect.} cast
pragma block can be used:
func f() =
{.cast(noSideEffect).}:
echo "test"
Side effects are usually inferred. The inference for side effects is analogous to
the inference for exception tracking.
26.5. 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.
175
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 GC safety 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)