According to https://nim-lang.org/docs/destructors.html#destructor-removal, when a variable was moved the compiler does not call it's destructor.
I wanted to see if I can exploit this to implement "higher RAII" aka "linear types", but first I tested destructor removal with the following program:
type
Widget = object
i : int
proc newWidget(): Widget =
result.i = 1
proc `=destroy`(x: var Widget) =
echo "Destroyed ", x.i
proc `=sink`(dest: var Widget; source: Widget) =
echo "Moved ", source.i
proc `=copy`(dest: var Widget; source: Widget) =
echo "Copied ", source.i
proc use(x: sink Widget) =
echo "Used"
# A `=destroy` here as expected
proc test() =
echo "Begin test"
var a = newWidget()
use(a)
# I expect a wasMoved(a) here so it cancels the `=destroy`
echo "End test"
# But it calls `=destroy` here
test()
echo "End program"
The output is not what I expected:
Begin test
Used
Destroyed 1
End test
Destroyed 0
End program
It calls =destroy at the end of test but I expected that it would produce a wasMoved =destroy pair that cancels each other. Even when I manually add wasMoved it still calls the destructor.
I tried with nim 1.9.3 2e4ba4ad93c6d9021b6de975cf7ac78e67acba26 and 1.6.12 1aa9273640c0c51486cf3a7b67282fe58f360e91.
It is optimized as soon as you remove the echo:
proc test() =
var a = newWidget()
use(a)
Compile with --expandArc:test to see it in effect. echo destroys the optimizers efforts currently because it can raise an exception and the optimizer is not smart enough to move the =destroy call to above the echo call in order to combine it with the wasMoved operation.
Thank you --expandArc:test explains well why it's not optimized. For some reason the hidden control flow is still an issue with --exceptions:quirky --panics:on.
Wouldn't moving the =destroy break some RAII use cases, for example by releasing a mutex too early?
Wouldn't moving the =destroy break some RAII use cases, for example by releasing a mutex too early?
Good point.
So starting with this:
var a
# Here a is default-initialized
try:
echo ["Begin test"] # May throw
a = newWidget()
# a is assigned
use:
let blitTmp = a
wasMoved(a)
blitTmp
# Use may throw but "a" is default-initialized
echo ["End test"] # May throw
# Can also reach the finally block from here, "a" is default-initialized
finally:
# In all cases "a" is default-initialized, can remove the `=destroy`
`=destroy`(a) # To be removed
After removing the =destroy call:
var a
# If the first echo throws it's never used, if not it's assigned.
# No need to default-initialize it
try:
echo ["Begin test"]
a = newWidget()
use:
let blitTmp = a
# From that point "a" is never used again (including destructor calls) whatever if the following throws or not.
# No need to default-initialize it
wasMoved(a) # To be removed
blitTmp
echo ["End test"]
finally:
discard
a =destroy is removed if the object is (un|default|zero)initialized in all paths leading to it
Something similar is already done by the "cursor inference" and in practice the C(++) codegen can do the elisions too, they are usually obvious enough after inlining. Unless some benchmarks show benefits in adding more rules to the current system, it should remain as it is. And probably even the current elision rules are worthless and subsumed by a decent optimizer.