nim-lang / Nim

Nim is a statically typed compiled systems programming language. It combines successful concepts from mature languages like Python, Ada and Modula. Its design focuses on efficiency, expressiveness, and elegance (in that order of priority).
https://nim-lang.org
Other
16.47k stars 1.47k forks source link

Closures capture scope is confusing, leading to unexpective behaviour #23684

Open Alogani opened 3 months ago

Alogani commented 3 months ago

Description

Hello,

There can be multiple scopes inside a same functions thanks to blocks and templates. But this scope aren't captured as is when creating a closure. This can result in pointing to the wrong variable inside the closure.

I think an example will be more explicit :

type MyRef = ref object
var myClosures: seq[proc()]

echo "DECEPTIVE EXAMPLE - with template"
template generateClosure(): proc() =
    let myRef = MyRef()
    echo "OUTSIDE:", cast[int](myRef)
    proc closure() =
        echo "INSIDE:", cast[int](myRef)
    closure

for i in 0..<2:
    myClosures.add generateClosure()
for p in myClosures:
    p()
myClosures.setLen(0)

echo "DECEPTIVE EXAMPLE - direct substitution"
for i in 0..<2:
    myClosures.add block:
        let myRef = MyRef()
        echo "OUTSIDE:", cast[int](myRef)
        proc closure() =
            echo "INSIDE:", cast[int](myRef)
        closure

for p in myClosures:
    p()
myClosures.setLen(0)

echo "WORKING EXAMPLE"
for i in 0..<2:
    myClosures.add block:
        proc closureGenerator(): proc() =
            let myRef = MyRef()
            echo "OUTSIDE:", cast[int](myRef)
            proc closure() =
                echo "INSIDE:", cast[int](myRef)
            closure
        closureGenerator()

for p in myClosures:
    p()

Nim Version

On fedora, v2.0.4

Current Output

DECEPTIVE EXAMPLE - with template
OUTSIDE:139977855484000
OUTSIDE:139977855484096
INSIDE:139977855484096
INSIDE:139977855484096

DECEPTIVE EXAMPLE - direct subsitution
OUTSIDE:139977855484064
OUTSIDE:139977855484192
INSIDE:139977855484192
INSIDE:139977855484192

WORKING EXAMPLE
OUTSIDE:139977855484256
OUTSIDE:139977855484288
INSIDE:139977855484256
INSIDE:139977855484288

Possible Solution

I'm not sure if it is possible to capture a nested scope correctly (blocks/templates) without implying a closure. So I don't think a correction is possible without overhead or breaking how nim actually works.

However, it might certainly be possible for the compiler to detect when a referenced scope will be erased and should emit a warning or an error.