Are nim exceptions zero-cost (on error-free execution) ?
for more details on meaning of "zero-cost", see https://mortoray.com/2013/09/12/the-true-cost-of-zero-cost-exceptions/ > Thus was born the zero-cost exception model. It offers zero runtime overhead for an error-free execution, which surpasses even that of C return-value error handling. As a trade-off, the propagation of an exception has a large overhead
If so, there are many places where using "defer:" would provide more safety over what we have, eg:
pushInfoContext(p.config, n.info)
result = replaceTypeVarsN(cl, n)
popInfoContext(p.config)
should become:
pushInfoContext(p.config, n.info)
defer: popInfoContext(p.config)
result = replaceTypeVarsN(cl, n)
so that code remains correct in case popInfoContext throws; maybe in this particular example popInfoContext cannot throw but that was just an illustration.Yeah, they are not. Which is why try finally is underused in the compiler. (There are some workarounds in place in the compiler though, so it's not that bad.)
Which is also why I'm pushing for making C++ the default compilation target. Destructors also will introduce many more implicit try-finallys. FYI please don't use defer in the compiler, I don't like it anymore...
thanks for the answer!
Even worse, it means we need to mark locals with volatile
hmmm not seeing any volatile in .c output (only in void (*volatile inner)(void); but not for the locals from test)
proc test()=
var b=1
try:
var a=1
echo a
except:
b=2
echo b
test()
I just looked at the generated code for nim c and nim cpp` and indeed, setjmp is used for C target, but try/catch is used for C++ target so that should result in zero-cost exceptions.
So with that in mind, I actually don't see any reason not to use exceptions and defer : when user wants performance, he can just use cpp compilation target instead of c and we could properly document that. Or is there currently any drawback in using C++ compilation target instead of C target ?
defer leads to less buggy code (and it's D analog, scope(exit), is loved and used extensively), eg:
#this is exception safe (in case bar throws)
proc foo()=
setupFoo()
defer: cleanupFoo()
bar()
#this is not exception safe (in case bar throws)
proc foo()=
setupFoo()
bar()
cleanupFoo()
Sadly, defer leads to more buggy code when compared to good old try finally:
proc foo()=
setupFoo()
defer: cleanupFoo()
bar()
Turns into:
proc foo()=
try:
setupFoo()
bar()
finally:
cleanupFoo()
But this is wrong! If setupFoo raises, cleanupFoo should not be called. (If fopen fails, fclose is not to be called.) It should have been:
proc foo()=
setupFoo()
try:
bar()
finally:
cleanupFoo()
What you really want is to pair the construction with the destruction like
proc foo() {.destructiveArrow.} =
setupFoo() ~> cleanupFoo()
stuffInBetween
moreHere ~> cleanupMore()
moreStuff
Which would be transformed into:
proc foo() =
setupFoo()
try:
stuffInBetween
moreHere
try:
moreStuff
finally:
cleanupMore()
finally:
cleanupFoo()
Well I doubt the manual is clear about it but I'm sure people also use this idiom which means it's impossible to fix:
proc foo =
var f: File
defer: close(f)
f = open(f)
Does defer still have the utterly counterintuitive behavior that it gets called if a statement before it raises an exception?
no, this was debunked, see https://forum.nim-lang.org/t/4022#25088, as you can also see here:
when true:
# prints a1 then raises foo
block:
echo "a1"
raise newException(ValueError, "foo")
echo "a2"
defer: echo "a3"
echo "a4"
when true:
block:
template main1 =
echo "ok1"
defer: echo "ok3"
echo "ok2"
main1()
main1()
> ok1 ok2 ok1 ok2 ok3 ok3
But that's not what states the Nim Manual where it is written that defer: statements are rewritten to try: ... finally:. That's the reason why I never used defer: as I thought that the try: ... finally: scope rules were more clear.
With that semantic, defer: is really different from try: ... finally: and the manual paragraph should be changed.
That's the reason why I never used defer: as I thought that the try: ... finally: scope rules were more clear. > With that semantic, defer: is really different from try: ... finally: and the manual paragraph should be changed.
fixed, see https://github.com/nim-lang/Nim/pull/16010
Great update! However, I still don't like to see defer in the Nim compiler code and I dislike it so much that I thought of adding a switch --araqstyle (which forbids defer and continue) that is enabled for the compiler's code.
But great update, thanks for writing it.
@timothee, I know that this thread is talking about exception but perhaps the revised documentation should precise that defer works with exception scope. defer code won't run at closure iterator end of scope like in the following example.
import os
proc bar(name: string): iterator: string =
iterator foo: string {.closure.} =
echo "Open file ", name
var file = open(name)
defer:
echo "Close file ", name
close(file)
var line: string
while readLine(file, line):
yield line
result = foo
echo "Program starts"
var iter = bar(paramStr(1))
var i = 0
for l in iter():
inc(i)
echo "Read line #", i, "=", l
if i > 5:
break;
echo "Program completed"
Run it with nim c -r bug.nim bug.nim. It would have been nice though for universal and simple resources cleaning...
@spip a library solution that honors defer and try/finally is possible as mentioned in https://github.com/nim-lang/Nim/issues/15501#issuecomment-730231149
when true:
import std/iterates
iterator foo(name: string): string {.genIter.} =
echo "Open file ", name
var file = open(name)
try:
var line: string
while readLine(file, line):
yield line
finally:
echo "Close file ", name
close(file)
for l in iterate foo("/tmp/z04.txt"):
echo "inside"
break
echo "Program completed"