ch26.md 7.6 KB

Chapter 26. Effect systems

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:

  1. 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, an indirect call is ignored if the call is of the form f(...) where f is a parameter of the currently analyzed routine that is marked as .effectsOf: f. The call is optimistically assumed to have no effect. Rule 2 compensates for this case.
  2. Every expression e of some proc type within a call that is passed to parameter marked as .effectsOf is assumed to be called indirectly and thus its raises list is added to p's raises list.
  3. Every call to a proc q which has an unknown body (due to a forward declaration) is assumed to raise system.Exception unless q has an explicit raises list. Procs that are importc'ed are assumed to have .raises: [], unless explicitly declared otherwise.
  4. Every call to a method m is assumed to raise system.Exception unless m has an explicit raises list.
  5. For every other call, the analysis can determine an exact raises list. 172
  6. 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: [].} =

    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:

                                                                       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: [].} =

    does not compile, sort can now raise Exception

    toSort.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: [].} =

    not allowed:

    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)