strict effects (#18777)

* fixes #17369
* megatest is green for --cpu:arm64
* docgen output includes more tags/raises
* implemented 'effectsOf' 
* algorithm.nim: uses new effectsOf annotation
* closes #18376
* closes #17475
* closes #13905
* allow effectsOf: [a, b]
* added a test case
* parameters that are not ours cannot be declared as .effectsOf
* documentation
* manual: added the 'sort' example
* bootstrap with the new better options
This commit is contained in:
Andreas Rumpf 2021-09-02 12:10:14 +02:00 • committed by GitHub
commit e0ef859130
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 482 additions and 203 deletions

View file

@ -4721,6 +4721,11 @@ and rely on functionality of the `x` object to get exception details.
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
------------------
@ -4770,35 +4775,24 @@ possibly raised exceptions; 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, if the call is of the form `f(...)` where `f` is a parameter of the currently analyzed routine it is ignored. The call is optimistically assumed to have no effect. Rule 2 compensates for this case.
2. 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
However, if the call is of the form `f(...)` where `f` is 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.
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 or an `importc` pragma) is assumed to
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.
6. For determining a `raises` list, the `raise` and `try` statements
of `p` are taken into consideration.
Rules 1-2 ensure the following works:
.. code-block:: nim
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
@ -4823,6 +4817,60 @@ with `--panics:on`: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:
.. code-block:: nim
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:
.. code-block:: nim
:test: "nim c $1"
:status: 1
{.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
------------
@ -4831,7 +4879,7 @@ 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:
.. code-block:: nim
:test: "nim c $1"
:test: "nim c --warningAsError:Effect:on $1"
:status: 1
type IO = object ## input/output effect
@ -4848,6 +4896,78 @@ 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
<system.html#debugEcho,varargs[typed,]>`_ 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:
.. code-block:: nim
func `+` (x, y: int): int
To override the compiler's side effect analysis a `{.noSideEffect.}`
`cast` pragma block can be used:
.. code-block:: nim
func f() =
{.cast(noSideEffect).}:
echo "test"
**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`:idx: 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`:idx: 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:
.. code-block:: nim
var
someGlobal: string = "some string here"
perThread {.threadvar.}: string
proc setPerThread() =
{.cast(gcsafe).}:
deepCopy(perThread, someGlobal)
See also:
- `Shared heap memory management <gc.html>`_.
Effects pragma
--------------
@ -6366,55 +6486,6 @@ This pragma can also take in an optional warning string to relay to developers.
proc thing(x: bool) {.deprecated: "use thong instead".}
noSideEffect pragma
-------------------
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.
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
<system.html#debugEcho,varargs[typed,]>`_ 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:
.. code-block:: nim
func `+` (x, y: int): int
To override the compiler's side effect analysis a `{.noSideEffect.}`
`cast` pragma block can be used:
.. code-block:: nim
func f() =
{.cast(noSideEffect).}:
echo "test"
When a `noSideEffect` proc has proc params `bar`, whether it can be used inside a `noSideEffect` context
depends on what the compiler knows about `bar`:
.. code-block:: nim
:test: "nim c $1"
func foo(bar: proc(): int): int = bar()
var count = 0
proc fn1(): int = 1
proc fn2(): int = (count.inc; count)
func fun1() = discard foo(fn1) # ok because fn1 is inferred as `func`
# func fun2() = discard foo(fn2) # would give: Error: 'fun2' can have side effects
# with callbacks, the compiler is conservative, ie that bar will have side effects
var foo2: type(foo) = foo
func main() =
discard foo(fn1) # ok
# discard foo2(fn1) # now this errors
compileTime pragma
------------------
@ -7861,6 +7932,10 @@ collected) heap, and sharing of memory is restricted to global variables. This
helps to prevent race conditions. GC efficiency is improved quite a lot,
because the GC never has to stop other threads and see what they reference.
The only way to create a thread is via `spawn` or
`createThread`. The invoked proc must not use `var` parameters nor must
any of its parameters contain a `ref` or `closure` type. This enforces
the *no heap sharing restriction*.
Thread pragma
-------------
@ -7875,43 +7950,6 @@ A thread proc is passed to `createThread` or `spawn` and invoked
indirectly; so the `thread` pragma implies `procvar`.
GC safety
---------
We call a proc `p` `GC safe`:idx: 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 `gcsafe`:idx: annotation can be used to mark a proc to be gcsafe,
otherwise this property is inferred by the compiler. Note that `noSideEffect`
implies `gcsafe`. The only way to create a thread is via `spawn` or
`createThread`. The invoked proc must not use `var` parameters nor must
any of its parameters contain a `ref` or `closure` type. This enforces
the *no heap sharing restriction*.
Routines that are imported from C are always assumed to be `gcsafe`.
To disable the GC-safety checking the `--threadAnalysis:off`:option: command-line
switch can be used. This is a temporary workaround to ease the porting effort
from old code to the new threading model.
To override the compiler's gcsafety analysis a `{.cast(gcsafe).}` pragma block can
be used:
.. code-block:: nim
var
someGlobal: string = "some string here"
perThread {.threadvar.}: string
proc setPerThread() =
{.cast(gcsafe).}:
deepCopy(perThread, someGlobal)
See also:
- `Shared heap memory management <gc.html>`_.
Threadvar pragma
----------------