[vim/vim] No closures for the for loop [var1, var2] variables? (Issue #11094)

104 views
Skip to first unread message

Maxim Kim

unread,
Sep 9, 2022, 5:55:21 AM9/9/22
to vim/vim, Subscribed

Steps to reproduce

I am not sure if this is a bug or intended behavior...

  1. vim -Nu NONE
  2. Add following script:
vim9script
for [idx, val] in "hello world!"->items()
    timer_start(idx * 1000, (_) => {
        echow $"{idx}: {val}"
    })
endfor
  1. source

Expected behaviour

I would expect message window to show every letter of hello world with an index of a letter.
Instead, I can see the last char and no indices at all (plus if you do visual mode while timer is active, -- VISUAL -- appears in message window:
2022-09-09_12-43-08

Looks like lambda in timer_start doesn't "enclosure" outer variables defined in for loop.

I was assuming it should work, looking to the help example:

This can be useful for a timer, for example: >
	var count = 0
 	var timer = timer_start(500, (_) => {
		 count += 1
		 echom 'Handler called ' .. count
	     }, {repeat: 3})

Version of Vim

9.0.0420

Environment

  • Fedora 36
  • simple terminal/gnome terminal
  • xterm-256color
  • bash

Logs and stack traces

No response


Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094@github.com>

Maxim Kim

unread,
Sep 9, 2022, 11:03:43 AM9/9/22
to vim/vim, Subscribed

If you put the whole for loop into function, it shows the latest idx and val:

image


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1242088812@github.com>

Maxim Kim

unread,
Sep 9, 2022, 11:05:32 AM9/9/22
to vim/vim, Subscribed

And as a side note, :echow for sure has an issue with the visual line mode:

image


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1242090988@github.com>

Maxim Kim

unread,
Sep 9, 2022, 11:12:58 AM9/9/22
to vim/vim, Subscribed

I think closure part works fine in the variant of a function -- all timers use the same idx and val that by the time of execution have the latest values of the items().

So it is just inconsistent with the plain for loop on the script level.


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1242105222@github.com>

lacygoill

unread,
Sep 9, 2022, 12:42:13 PM9/9/22
to vim/vim, Subscribed

I think closure part works fine in the variant of a function -- all timers use the same idx and val that by the time of execution have the latest values of the items().

I think there is a bug, because according to the help (somewhere around :help E1271), that should work:

def GetClosure(idx: number, val: string): func

    return () => {

        echowindow $'{idx}: {val}'

    }

enddef



for [idx, val] in 'hello world!'->items()

    timer_start(idx * 1'000, (_) => {

        GetClosure(idx, val)()

    })

endfor

But it does not. idx is not printed, and val is always the last character (!).


Something else is weird, if we try to capture idx and val in a global variable, we can't print its value:

vim9script

def GetClosure(idx: number, val: string): func

    g:var = get(g:, 'var', []) + [[idx, val]]

    return () => {

        echowindow $'{idx}: {val}'

    }

enddef

for [idx, val] in 'hello world!'->items
()

    timer_start(idx * 1'000, (_) => {

        GetClosure(idx, val)()

    })

endfor

echo g:var

The last :echo does not print anything. Even g:var->string() does not print anything. However, json_encode() does print something:

[[,"!"],[,"!"],[,"!"],[,"!"],[,"!"],[,"!"],[,"!"],[,"!"],[,"!"],[,"!"],[,"!"],[,"!"]]

Notice that in each nested list, the first idx item is missing:

[,"!"]

 ^

 ✘

That looks wrong.


There is also a crash:

vim9script

for n in [0]

    timer_start(0, (_) => {

        echo n

    })

endfor

And as a side note, :echow for sure has an issue with the visual line mode:

Yes, I don't think the "-- VISUAL LINE --" message should also be printed inside the window.


Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1242209447@github.com>

Bram Moolenaar

unread,
Sep 9, 2022, 4:40:32 PM9/9/22
to vim/vim, Subscribed

I fixed the problem with putting the mode message in the message window and the crash.

When a closure is defined it remembers variables used from its context. These variables are then accessed at the time the closure is executed. Thus you get the latest value, not the value from when the closure was defined.

I understand that in a loop you may want to use the current value, not a reference to the variable. I don't know a simple solution for this. I wonder how this is done in other languages.


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1242445847@github.com>

lacygoill

unread,
Sep 9, 2022, 5:17:30 PM9/9/22
to vim/vim, Subscribed

according to the help (somewhere around :help E1271), that should work:

I was wrong. This code can't work as expected because GetClosure() is called too late.
OTOH, this works as expected:

vim9script
def GetClosure(idx: number, val: string): func
    return (_) => {
        echowindow $'{idx}: {val}'
    }
enddef
for [idx, val] in 'hello world!'->items
()
    timer_start(idx * 1'000, GetClosure(idx, val))
endfor
0: h
1: e
2: l
3: l
4: o
5:
6: w
7: o
8: r
9: l
10: d
11: !

The help which explains how to use function calls to create separate contexts could be improved a little. Just a missing comma:

diff --git a/runtime/doc/vim9.txt b/runtime/doc/vim9.txt
index aba0f0051..9939555b5 100644
--- a/runtime/doc/vim9.txt
+++ b/runtime/doc/vim9.txt
@@ -1311,7 +1311,7 @@ Make sure to define the breakpoint before compiling the outer function.
 The "inloop" variable will exist only once, all closures put in the list refer
 to the same instance, which in the end will have the value 4.  This is
 efficient, also when looping many times.  If you do want a separate context
-for each closure call a function to define it: >
+for each closure, call a function to define it: >
 	def GetClosure(i: number): func
 	  var infunc = i
 	  return () => infunc


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1242487187@github.com>

lacygoill

unread,
Sep 9, 2022, 5:20:20 PM9/9/22
to vim/vim, Subscribed

OTOH, this works as expected:

It works because the lambda is no longer created directly inside the loop, but from a function. Vim uses the same context only in the former case (for better performance); not in the latter. IOW, the current design privileges performance.


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1242489233@github.com>

Maxim Kim

unread,
Sep 10, 2022, 2:15:49 AM9/10/22
to vim/vim, Subscribed

Now with patches if I do the same as described in first message:

vim9script
for [idx, val] in "hello world!"->items()
    timer_start(idx * 1000, (_) => {
        echow $"{idx}: {val}"
    })
endfor

image


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1242642043@github.com>

Maxim Kim

unread,
Sep 10, 2022, 2:22:00 AM9/10/22
to vim/vim, Subscribed

And apparently you can't pre-define those variables to avoid deletion:

vim9script

var idx: number
var val: string

for [idx, val] in "hello world!"->items()
    timer_start(idx * 1000, (_) => {
        echow $"{idx}: {val}"
    })
endfor

image


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1242643400@github.com>

Maxim Kim

unread,
Sep 10, 2022, 2:26:23 AM9/10/22
to vim/vim, Subscribed

When a closure is defined it remembers variables used from its context. These variables are then accessed at the time the closure is executed. Thus you get the latest value, not the value from when the closure was defined.

Indeed, it makes sense. Thank you!


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1242644311@github.com>

lacygoill

unread,
Sep 10, 2022, 2:40:29 AM9/10/22
to vim/vim, Subscribed

Now with patches if I do the same as described in first message:

I think this is working as intended (at least for now). idx and val are defined in a block at the script-level. They live as long as the block is being processed. As soon as the execution leaves the block, they are deleted. When the timers' callbacks are executed, the execution has left the block. It can't find the variables, hence the error.

And apparently you can't pre-define those variables to avoid deletion:

We can't shadow variables. That is, the same name can't be declared in 2 different scopes. If idx is declared in the script-local scope, then it can't be declared in the block-local scope too (for idx in ... declares idx implicitly). If that was allowed, then we wouldn't be able to access the idx declared in the script-local scope from the block-local one. It's not fundamentally impossible, but it was chosen to be disallowed, presumably to avoid writing confusing code (FWIW, after having read/written too much code which is hard to debug because of poorly named variables, I tend to agree).

Here is the current solution to the issue:

vim9script
def GetClosure(idx: number, val: string): func
    return (_) => {
        echowindow $'{idx}: {val}'
    }
enddef
for [idx, val] in 'hello world!'->items
()
    
timer_start(idx * 1'000, GetClosure(idx, val))
endfor
0: h
1: e
2: l
3: l
4: o
5:
6: w
7: o
8: r
9: l
10: d
11: !


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1242647262@github.com>

Maxim Kim

unread,
Sep 10, 2022, 2:46:14 AM9/10/22
to vim/vim, Subscribed

Closed #11094 as completed.


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issue/11094/issue_event/7360512184@github.com>

Maxim Kim

unread,
Sep 10, 2022, 2:46:16 AM9/10/22
to vim/vim, Subscribed

Thx, @lacygoill, I get it that a new context has to be created with a separate function.


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1242648509@github.com>

bfrg

unread,
Sep 10, 2022, 8:44:31 AM9/10/22
to vim/vim, Subscribed

If you want it more compact, use a lambda and call it immediately within the timer function:

for [idx, val] in 'hello world!'->items
()
    timer_start(idx * 1000, ((i: number, v: string) => (_) => {
        echowindow $'{i}: {v}'
    })(idx, val))
endfor

Or slightly more readable:

for [idx, val] in 'hello world!'->items
()
    timer_start(idx * 1000, (i: number, v: string) => {
        return (_) => {
            echowindow $'{i}: {v}'
        }
    }(idx, val))
endfor


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1242721377@github.com>

Bram Moolenaar

unread,
Sep 10, 2022, 9:10:41 AM9/10/22
to vim/vim, Subscribed


> If you want it more compact, use a lambda and call it immediately
> within the timer function:
> ```vim

> for [idx, val] in 'hello world!'->items()
> timer_start(idx * 1000, ((i: number, v: string) => (_) => {
> echowindow $'{i}: {v}'
> })(idx, val))
> endfor
> ```
>
> Or slightly more readable:
> ```vim

> for [idx, val] in 'hello world!'->items()
> timer_start(idx * 1000, (i: number, v: string) => {
> return (_) => {
> echowindow $'{i}: {v}'
> }
> }(idx, val))
> endfor
> ```

More compact, but harder to find mistakes (there is no function name)
and slightly less efficient (a new lambda is created every time, instaed
of using one function. But it also doesn't leave a function behind.

Still wondering if there is a simpler solution we can borrow from
another language.

--
Change is inevitable, except from a vending machine.

/// Bram Moolenaar -- ***@***.*** -- http://www.Moolenaar.net \\\
/// \\\
\\\ sponsor Vim, vote for features -- http://www.Vim.org/sponsor/ ///
\\\ help me help AIDS victims -- http://ICCF-Holland.org ///


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1242725217@github.com>

Doug Kearns

unread,
Sep 10, 2022, 2:00:50 PM9/10/22
to vim/vim, Subscribed

You can currently achieve this by creating a new block scope as well but I think it really should work like this without the extra scope.

for [idx, val] in "hello world!"->items
()
  {
    var i = idx
    var v = val
    
timer_start(idx * 1000, (_) =
> {
      echow $"{i}: {v}"
    })
  }
endfor

It seems to me there are currently two surprising features of the implementation, assuming I understand correctly:

  1. It's not possible to capture the loop variables directly -> "Script variable was deleted" error. Why?
  2. Any variables declared in the loop block reference persistent objects for the duration of the loop. This prevents simply copying the loop variable to a block-local variable which can then be captured with the current value.

Ignoring any implementation difficulties I think these semantics are confusing.

The scope of the loop variable is less important but generally these for loops are desugared to either (using the example from :help E1271):

var list = range(5)
var flist: list<func>
                                                                                                                                                                                                                                                           
{
  var val: number
  var i = 0
  while i < len(list)
    val = list[i]
    flist[i] = () => val
    i = i + 1
  endwhile
}

echo range(5)->map((j, _) => flist[j]()) # =>  [4, 4, 4, 4, 4]                                                                                                                                                                                                                    
var list = range(5)
var flist: list<func>                                                                                                                                                                                                                                                           

var i = 0
while i < len(list)
  {
    var val = list[i]
    flist[i] = () => val
    i = i + 1
  }
endwhile

echo range(5)->map((j, _) => flist[j]())  # =>  [0, 1, 2, 3, 4]

I think the latter form is becoming more popular as it's become clear that it's what most users expect. C# made a breaking change to modify the behaviour of their foreach loop from the first to the second form some years ago and in JavaScript it was considered a gotcha until ES6.

Amongst some of the other languages you've surveyed when designing Vim9, Dart, Java and Lua use the second form, while Go and Ruby use the first.


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1242777557@github.com>

bfrg

unread,
Sep 10, 2022, 2:53:28 PM9/10/22
to vim/vim, Subscribed

Still wondering if there is a simpler solution we can borrow from another language.

This is what the official Python documentation recommends: Why do lambdas defined in a loop with different values all return the same result?

In C++ it is possible to capture by reference or by copy. More detailed explanation can be found on cppreference.

Here's an example where the variable is captured by copy:

#include <cstdio>
#include <functional>
#include <vector>

int main()
{
    std::vector<std::function<int()>> squares;

    for (int i = 0; i < 5; i++) {
        // capture variable 'i' by copy
        squares.push_back([i](){ return i * i; });
    }

    for (int i = 0; i < 5; i++) {
        printf("%d ", squares[i]()); // output: 0 1 4 9 16
    }
}

Same as above but capture variable by reference:

#include <cstdio>
#include <functional>
#include <vector>

int main()
{
    std::vector<std::function<int()>> squares;

    for (int i = 0; i < 5; i++) {
        // capture variable 'i' by reference
        squares.push_back([&i](){ return i * i; });
    }

    for (int i = 0; i < 5; i++) {
        printf("%d ", squares[i]()); // output: 25 25 25 25 25
    }
}


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1242787708@github.com>

Bram Moolenaar

unread,
Sep 10, 2022, 5:14:11 PM9/10/22
to vim/vim, Subscribed


Doug Kearns wrote:

> You can currently achieve this by creating a new block scope as well but
> I think it really should work like this without the extra scope.
>
> ```vim

> for [idx, val] in "hello world!"->items()
> {
> var i = idx
> var v = val
> timer_start(idx * 1000, (_) => {
> echow $"{i}: {v}"
> })
> }
> endfor
> ```

That's defenitely better than calling a function to create a closure.
Your Python reference also recommends something similar.
We can ignore the C++ example, we don't have references (and C++ is not
to be used as an example for "good language design" anyway).


> It seems to me there are currently two surprising features of the
> implementation, assuming I understand correctly:
> 1. It's not possible to capture the loop variables directly -> "Script

> variable was deleted" error. Why?

So you can use "for i in ..." twice in a row. The scope of "i" is really
only the for loop. If we would make an exception for when "i" is used
in a closure things get weird. I mean that you can only use a second
"for i in ..." if the first for loop didn't use "i" in a closure. Let's
not do that.

> 2. Any variables declared in the loop block reference persistent

> objects for the duration of the loop. This prevents simply copying
> the loop variable to a block-local variable which can then be captured
> with the current value.
>
> Ignoring any implementation difficulties I think these semantics are
> confusing.

Well, I think it's actually consistent, although in the case of a for
loop variable probably not what you wanted or expected. It appears
Python does the same, so maybe it's not so bad?

[...]


> I think the latter form is becoming more popular as it's become clear
> that it's what most users expect.

It's totally inconsistent though. One moment you refer to a variable
"by reference" and the closure can change the variable so that another
invocation can change the value. There is only one variable that is
shared, so that the value can change over time.


> C# made a breaking change to modify the behaviour of their `foreach`
> loop from the first to the second form some years ago and in
> JavaScript it was considered a gotcha until ES6.
>
> Amongst some of the other languages you've surveyed when designing
> Vim9, Dart, Java and Lua use the second form, while Go and Ruby use
> the first.

I think there is one possibility that might be useful without making it
weird: consider each invocation of the block inside the for loop getting
a separate loop variable. It's like making a copy in the block, which
is what this message started with. So:

var closures: list<func> = []
for n in range(3)
closures[n] = () => {
n += 1
return n
}
endfor
echo closures[0]() # 5
echo closures[0]() # 10
echo closures[1]() # 6
echo closures[1]() # 11

This works like what you would currently get with:

var closures: list<func> = []
for n in range(3)
{
var nr = n
closures[nr] = () => {
nr += 5
return nr
}
}
endfor

This would mean a change in semantics, but since currently "n" would not
be usable after the loop anyway, perhaps that doesn't matter?

The implementation will be a bit tricky if we want to keep it efficient
(only let the variable survive after the block when it was actually used
in a closure).

On the other hand, just keeping what we have now and explaining the user
how to make it work with the block variable might not be so bad.

--
I wonder how much deeper the ocean would be without sponges.


/// Bram Moolenaar -- ***@***.*** -- http://www.Moolenaar.net \\\
/// \\\
\\\ sponsor Vim, vote for features -- http://www.Vim.org/sponsor/ ///
\\\ help me help AIDS victims -- http://ICCF-Holland.org ///


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1242805583@github.com>

bfrg

unread,
Sep 10, 2022, 7:25:50 PM9/10/22
to vim/vim, Subscribed

The following example throws an error at script level but works fine inside a def function:

var squares: list<func(): number> = []
for x in range(5)
    squares->add(() => x * x)
endfor

for F in squares
    echo F()
endfor

Result:

Error detected while processing /tmp/test.vim[43]..function <lambda>16:
line    1:
E1302: Script variable was deleted

Doing the same inside a function works:

vim9script

def Test()
    var squares: list<func(): number> = []
    for x in range(5)
        squares->add(() => x * x)
    endfor

    for F in squares
        echo F()
    endfor
enddef

Test()

Result:

16
16
16
16
16

I would have expected the same behavior.


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1242819204@github.com>

bfrg

unread,
Sep 11, 2022, 7:59:19 AM9/11/22
to vim/vim, Subscribed

The following two identical code snippets (one at script level, the other one in a def function) give two different results.

First example, code is executed at script level:

var squares: list<func> = []
for n in range(5)
    {
        const nr = n
        squares[nr] = () => nr * nr
    }
endfor

for F in squares
    echo F()
endfor

Output: 0 1 4 9 16

Second example, code is executed with a def function:

def Test()
    var squares: list<func> = []
    for n in range(5)
        {
            const nr = n
            squares[nr] = () => nr * nr
        }
    
endfor

    for F in squares
        echo F()
    endfor
enddef

Test()

Output: 16 16 16 16 16

Should I open a separate issue for this?


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1242948880@github.com>

Doug Kearns

unread,
Sep 11, 2022, 2:03:35 PM9/11/22
to vim/vim, Subscribed

On Sun, 11 Sept 2022 at 07:13, Bram Moolenaar ***@***.***> wrote:
> Doug Kearns wrote:

<snip>


> Your Python reference also recommends something similar.
> We can ignore the C++ example, we don't have references (and C++ is not to be used as an example for "good language design" anyway).

Those were actually someone else's comments. I'd never consult C++ or
Python on "good language design". :)


> > It seems to me there are currently two surprising features of the
> > implementation, assuming I understand correctly:
> > 1. It's not possible to capture the loop variables directly -> "Script
> > variable was deleted" error. Why?
>
> So you can use "for i in ..." twice in a row. The scope of "i" is really
> only the for loop. If we would make an exception for when "i" is used
> in a closure things get weird. I mean that you can only use a second
> "for i in ..." if the first for loop didn't use "i" in a closure. Let's
> not do that.

Then "i" should really be properly scoped to the loop with a wrapper
scope like form 1 from my earlier post. It seems we're essentially
doing that anyway with the ad-hoc variable deletion rather than just
letting it fall out of scope. Then the second loop gets a new
variable.

However, I'd probably prefer form 2 as you describe later.


> > 2. Any variables declared in the loop block reference persistent
> > objects for the duration of the loop. This prevents simply copying
> > the loop variable to a block-local variable which can then be captured
> > with the current value.
> >
> > Ignoring any implementation difficulties I think these semantics are
> > confusing.
>
> Well, I think it's actually consistent, although in the case of a for
> loop variable probably not what you wanted or expected. It appears
> Python does the same, so maybe it's not so bad?

Python doesn't have block scope so it makes sense there. Here it
appears to be a performance optimization informing the unusual
semantics.


> > I think the latter form is becoming more popular as it's become clear
> > that it's what most users expect.
>
> It's totally inconsistent though. One moment you refer to a variable
> "by reference" and the closure can change the variable so that another
> invocation can change the value. There is only one variable that is
> shared, so that the value can change over time.

I don't think there's anything notable or inconsistent about multiple
closures referencing the same variable. That happens for any free
variables captured from outside the scope of the loop but this second
form doesn't have that problem for the loop variable because it's
declared in the loop. Well, unless there's multiple closures created
per iteration.

<snip>


> I think there is one possibility that might be useful without making it
> weird: consider each invocation of the block inside the for loop getting
> a separate loop variable. It's like making a copy in the block, which
> is what this message started with. So:

RIght, I think that's exactly what I was proposing as form 2 above.

<snip>


> This would mean a change in semantics, but since currently "n" would not
> be usable after the loop anyway, perhaps that doesn't matter?

I don't think it does.


> The implementation will be a bit tricky if we want to keep it efficient
> (only let the variable survive after the block when it was actually used
> in a closure).
>
> On the other hand, just keeping what we have now and explaining the user
> how to make it work with the block variable might not be so bad.

Well, one of the stated aims was to make Vim script "less weird" so I
feel the need to push back and say I find both of the issues I
mentioned to be pretty weird. :) I, otherwise knowing the syntax,
wouldn't expect to need to read the docs to implement the code from
the initial report.

Regards,
Doug


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1243013436@github.com>

Bram Moolenaar

unread,
Sep 11, 2022, 3:14:30 PM9/11/22
to vim...@googlegroups.com, Doug Kearns

Let's try to summarize:

1. The loop variable in a Vim9 script and a compiled function behave
differently, we expect them to work the same way. In a script the
variable can't be found after the loop, it has been deleted. In a
compiled function it can be used by a closure executed later.
-> In a script make the loop variable available also after the loop
has finished.


2. Assuming the loop variable can be used, the value is what it was last
set to, thus when using "for i in range(4)" it will be 3. Other
language do this too, thus it would be acceptable. But it is not
very useful (esp. since the variable is read-only, a closure cannot
change it). One exception: if the loop can be exited early, then the
last value could be useful:
for i in range(100)
if i == 0
timer_start(1000, (_) => {
echowin 'last count is ' i
})
endif
if i == 5
break
endif
endfor
-> Documenting this would be sufficient.


3. It is common to use the current value of the loop variable, not
the final value. There should be an easy way to do that. That would
be making a copy in a block-local variable (Python also mentions
this).
At the script level this works, but only by explicitly using a {}
block, not by declaring the variable in the for-endfor block.
In a compiled function this doesn't currently work.
-> Make this work both in a script and compiled function:
for i in range(4)
var ii = i
timer_start(1000, (_) => {
echowin 'current count is ' ii
})
endfor
This would show the values 0, 1, 2 and 3.

We could change the choice in the second one, but implementing that
efficiently actually isn't so easy.


--
The future isn't what it used to be.

/// Bram Moolenaar -- Br...@Moolenaar.net -- http://www.Moolenaar.net \\\

Doug Kearns

unread,
Sep 12, 2022, 9:43:38 AM9/12/22
to vim...@googlegroups.com, Bram Moolenaar
On Mon, 12 Sept 2022 at 05:14, Bram Moolenaar <Br...@moolenaar.net> wrote:
> Let's try to summarize:

<snip>

This sounds good to me.

Thanks,
Doug

bfrg

unread,
Sep 13, 2022, 11:16:00 AM9/13/22
to vim/vim, Subscribed

For some reasons Bram's message didn't make it to github.

Here's the link: https://groups.google.com/g/vim_dev/c/gTJ2N8A-txQ/m/ML6wYx5cAQAJ


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1245562768@github.com>

lacygoill

unread,
Sep 14, 2022, 3:42:12 AM9/14/22
to vim/vim, Subscribed

The following example throws an error at script level but works fine inside a def function:

This has been fixed by patches 9.0.0459 and 9.0.0460.


The following two identical code snippets (one at script level, the other one in a def function) give two different results.

They still give different results.


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1246371028@github.com>

Bram Moolenaar

unread,
Sep 14, 2022, 11:11:24 AM9/14/22
to vim/vim, Subscribed


> > The following example throws an error at script level but works fine inside a def function:
>
> This has been fixed by patches 9.0.0459 and 9.0.0460.
>
> ---

>
> > The following two identical code snippets (one at script level, the other one in a def function) give two different results.
>
> They still give different results.

Yes, this is actually quite hard to implement. In a script we can
dynamically add a variable, and when it's used in a closure (well, we
only check if a function was defined in the loop) then keep it around.
This all happens at runtime and the variables are script-local.

In compiled code we have one position on the stack for a local variable.
When it turns out it is (or might be) used in a closure, how do we keep
it around in a way that the closure can find it? There is a mechanism
to make a copy of all the function items, including the arguments, but
now we only need the variables in the block of the for loop. And that
might happen thousands of times, thus it should be reasonably
efficient...

--
I got a new desk stapler. It broke on the first desk I tried.

/// Bram Moolenaar -- ***@***.*** -- http://www.Moolenaar.net \\\

/// \\\
\\\ sponsor Vim, vote for features -- http://www.Vim.org/sponsor/ ///
\\\ help me help AIDS victims -- http://ICCF-Holland.org ///


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1246915365@github.com>

Bram Moolenaar

unread,
Sep 17, 2022, 10:52:38 AM9/17/22
to vim/vim, Subscribed

It now also works in compiled code. Please watch out for any problems or memory leaks.

I'm not sure yet what to do with nested loops. Currently only variables declared in the loop block itself are copied.
Is there a good example of what doesn't work but should work?


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1250084988@github.com>

Bram Moolenaar

unread,
Sep 19, 2022, 12:09:15 PM9/19/22
to vim/vim, Subscribed

And now it also works in compiled code for nested loops. This was complicated!
Most of it implemented with patch 9.0.0502 (you also need the next two).

I hope it doesn't slow down normal loops. And hopefully this hasn't introduced memory leaks.

Let me know if you still spot a problem.


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1251230449@github.com>

Doug Kearns

unread,
Sep 21, 2022, 11:11:56 AM9/21/22
to vim/vim, Subscribed

It seems the for [{var1}, {var2}, ...] in {listlist} is currently using a different scope for {var1} than for the rest of the variables when executed in a script. It works as expected when wrapped in a compiled function.

So the original example from this report is printing:

11: h
11: e
...
11: d
11: !

rather than

11: !
...
11: !


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1253851483@github.com>

Bram Moolenaar

unread,
Sep 21, 2022, 12:09:56 PM9/21/22
to vim/vim, Subscribed

Right, it miscomputes the location of the second loop variable and assumes it's declared inside the block.


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1253924172@github.com>

Bram Moolenaar

unread,
Sep 21, 2022, 2:42:36 PM9/21/22
to vim...@googlegroups.com, Bram Moolenaar

> Right, it miscomputes the location of the second loop variable and
> assumes it's declared inside the block.

Fixed with patch 9.0.0535

--
"Microsoft is like Coke. It's a secret formula, all the money is from
distribution, and their goal is to get Coke everywhere. Open source is like
selling water. There are water companies like Perrier and Poland Spring, but
you're competing with something that's free." -- Carl Howe


/// Bram Moolenaar -- Br...@Moolenaar.net -- http://www.Moolenaar.net \\\

Doug Kearns

unread,
Sep 28, 2022, 10:21:45 AM9/28/22
to vim/vim, Subscribed

Thanks for working on this @brammool - looks good now.


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1260989645@github.com>

Bram Moolenaar

unread,
Oct 11, 2022, 4:35:05 AM10/11/22
to vim/vim, Subscribed


> The following example throws an error at script level but works fine inside a `def` function:
> ```vim

> var squares: list<func(): number> = []
> for x in range(5)
> squares->add(() => x * x)
> endfor
>
> for F in squares
> echo F()
> endfor
> ```

> Result:
> ```
> Error detected while processing /tmp/test.vim[43]..function <lambda>16:
> line 1:
> E1302: Script variable was deleted
> ```
>
> Doing the same inside a function works:
> ```vim
> vim9script
>
> def Test()

> var squares: list<func(): number> = []
> for x in range(5)
> squares->add(() => x * x)
> endfor
>
> for F in squares
> echo F()
> endfor
> enddef
>
> Test()
> ```
>
> Result:
> ```

> 16
> 16
> 16
> 16
> 16
> ```

>
> I would have expected the same behavior.

Well, should we break the compiled version because it doesn't work at
script level? I don't think that is an improvement.

The main difference is that in a compiled function it actually uses a
different variable. If you add another for loop using "x" you can see
that the value of "x" used in the first for loop isn't changed, it is
still 4.

In a script we normally have one "x" that is re-used. That's not set in
stone though. We do have the mechanism that "x" can be declared twice:

vim9script

var squares: list<func(): number> = []

if 1
var x = 1

squares->add(() => x * x)
endif
if 1
var x = 3

squares->add(() => x * x)
endif


for F in squares
echo F()
endfor

So, if it works in an "if" block then maybe it should also work in a
"for" block?

--
Don't believe everything you hear or anything you say.

/// Bram Moolenaar -- ***@***.*** -- http://www.Moolenaar.net \\\

/// \\\
\\\ sponsor Vim, vote for features -- http://www.Vim.org/sponsor/ ///
\\\ help me help AIDS victims -- http://ICCF-Holland.org ///


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1274310972@github.com>

Doug Kearns

unread,
Oct 11, 2022, 11:53:38 AM10/11/22
to vim/vim, Subscribed

@brammool, are you quoting the right message here? The examples you quoted now both work correctly without error, producing the same result.


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1274919243@github.com>

Doug Kearns

unread,
Oct 11, 2022, 12:04:40 PM10/11/22
to vim/vim, Subscribed

However, in the next and final report from @bfrg, with the now unnecessary extra {...} block in each of the for loops, the two examples still give different results.

The following two identical code snippets (one at script level, the other one in a def function) give two different results.

First example, code is executed at script level:

vim9script

var squares: list<func> = []
for n in range(5)
    {
        const nr = n
        squares[nr] = () => nr * nr
    }
endfor

for F in squares
    echo F()
endfor

Output: 0 1 4 9 16

Second example, code is executed with a def function:

vim9script

def Test()
    var squares: list<func> = []
    for n in range(5)
        {
            const nr = n
            squares[nr] = () => nr *
 nr
        }
    endfor

    for F in squares
        echo F()
    endfor
enddef

Test()

Output: 16 16 16 16 16

If you remove the unnecessary {...} block then the result is the same as the first example.

The extra block has, correctly, no effect in the first example.


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1274934123@github.com>

bfrg

unread,
Oct 11, 2022, 12:27:58 PM10/11/22
to vim/vim, Subscribed

Personally, I think vimscript should behave the same inside a def function and on a script level.

Here's another simple for-loop example (without any closures):

vim9script

# Filter out all odd numbers in each sublist
var list: list<list<number>> = [[1], [1, 2], [1, 2, 3], [1, 2, 3, 4], [1, 2, 3, 4, 5]]
for i in list
    filter(i, (_, n: number): bool => n % 2 == 0)
endfor

# [[], [2], [2], [2, 4], [2, 4]]
echo list

Everything works as expected. However, inside a def function, the above snippet throws an error:

vim9script

def Test()
    var items: list<list<number>> = [[1], [1, 2], [1, 2, 3], [1, 2, 3, 4], [1, 2, 3, 4, 5]]
    for i in items
        filter(i, (_, n: number): bool => n % 2 == 0)
    endfor
    echo items
enddef

Test()

Result:

Error detected while compiling /tmp/test.vim[34]..function <SNR>22_Test:
line    3:
E1307: Argument 1: Trying to modify a const list<number>

What does this mean? Does vim copy each item in list inside the for-loop? That would be quite inefficient. The items could be anything.

The following works but looks very complicated:

def Test()
    var items: list<list<number>> = [[1], [1, 2], [1, 2, 3], [1, 2, 3, 4], [1, 2, 3, 4, 5]]
    for i in items
        {
            var item: list<number> = i
            filter(item, (_, n: number): bool => n % 2 == 0)
        }
    endfor
    echo items
enddef

I feel like the old way was more intuitive where the for-loop variable was a reference to the current item in the list.


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1274963516@github.com>

Doug Kearns

unread,
Oct 11, 2022, 12:57:04 PM10/11/22
to vim/vim, Subscribed

The following works but looks very complicated:

def Test()
    var items: list<list<number>> = [[1], [1, 2], [1, 2, 3], [1, 2, 3, 4], [1, 2, 3, 4, 5]]
    for i in items
        {
            var item: list<number> = i
            filter(item, (_, n: number): bool => n % 2 == 0)
        }
    endfor
    echo items
enddef

That doesn't need the extra {...} block in the for loop.


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1274999027@github.com>

Doug Kearns

unread,
Oct 11, 2022, 1:00:10 PM10/11/22
to vim/vim, Subscribed

The Go people seem to agree with the C# people that this is confusing enough to users that it's worth introducing a breaking change. I don't feel strongly about it one way or the other but apparently I'm in the minority or just plain wrong.

golang/go#56010


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1275002293@github.com>

bfrg

unread,
Oct 11, 2022, 1:08:21 PM10/11/22
to vim/vim, Subscribed

That doesn't need the extra {...} block in the for loop.

Thanks, I didn't notice that.


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1275011023@github.com>

Bram Moolenaar

unread,
Oct 11, 2022, 2:25:46 PM10/11/22
to vim/vim, Subscribed

This issue is getting complicated, and it actually is already closed. Please create a separate issue for each problem.


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11094/1275102685@github.com>

Reply all
Reply to author
Forward
0 new messages