changeset 30269:42a6345b91fd v9.0.0470

patch 9.0.0470: in :def function all closures in loop get the same variables Commit: https://github.com/vim/vim/commit/b46c083a5ed9e0c4ac5f3aec577946dcbe8c9dc5 Author: Bram Moolenaar <Bram@vim.org> Date: Thu Sep 15 17:19:37 2022 +0100 patch 9.0.0470: in :def function all closures in loop get the same variables Problem: In a :def function all closures in a loop get the same variables. Solution: When in a loop and a closure refers to a variable declared in the loop, prepare for making a copy of variables for each closure.
author Bram Moolenaar <Bram@vim.org>
date Thu, 15 Sep 2022 18:30:04 +0200
parents 82cfdc85861d
children 92c9073d3500
files src/proto/vim9instr.pro src/testdir/test_vim9_disassemble.vim src/version.c src/vim9.h src/vim9cmds.c src/vim9compile.c src/vim9execute.c src/vim9instr.c
diffstat 8 files changed, 315 insertions(+), 58 deletions(-) [+]
line wrap: on
line diff
--- a/src/proto/vim9instr.pro
+++ b/src/proto/vim9instr.pro
@@ -43,14 +43,15 @@ int generate_FUNCREF(cctx_T *cctx, ufunc
 int generate_NEWFUNC(cctx_T *cctx, char_u *lambda_name, char_u *func_name);
 int generate_DEF(cctx_T *cctx, char_u *name, size_t len);
 int generate_JUMP(cctx_T *cctx, jumpwhen_T when, int where);
+int generate_WHILE(cctx_T *cctx, int funcref_idx);
 int generate_JUMP_IF_ARG_SET(cctx_T *cctx, int arg_off);
 int generate_FOR(cctx_T *cctx, int loop_idx);
+int generate_ENDLOOP(cctx_T *cctx, int funcref_idx, int prev_local_count);
 int generate_TRYCONT(cctx_T *cctx, int levels, int where);
 int check_internal_func_args(cctx_T *cctx, int func_idx, int argcount, int method_call, type2_T **argtypes, type2_T *shuffled_argtypes);
 int generate_BCALL(cctx_T *cctx, int func_idx, int argcount, int method_call);
 int generate_LISTAPPEND(cctx_T *cctx);
 int generate_BLOBAPPEND(cctx_T *cctx);
-int check_args_on_stack(cctx_T *cctx, ufunc_T *ufunc, int argcount);
 int generate_CALL(cctx_T *cctx, ufunc_T *ufunc, int pushed_argcount);
 int generate_UCALL(cctx_T *cctx, char_u *name, int argcount);
 int check_func_args_from_type(cctx_T *cctx, type_T *type, int argcount, int at_top, char_u *name);
--- a/src/testdir/test_vim9_disassemble.vim
+++ b/src/testdir/test_vim9_disassemble.vim
@@ -1466,17 +1466,20 @@ def Test_disassemble_for_loop()
         '\d NEWLIST size 0\_s*' ..
         '\d SETTYPE list<number>\_s*' ..
         '\d STORE $0\_s*' ..
+
         'for i in range(3)\_s*' ..
         '\d STORE -1 in $1\_s*' ..
         '\d PUSHNR 3\_s*' ..
         '\d BCALL range(argc 1)\_s*' ..
         '\d FOR $1 -> \d\+\_s*' ..
-        '\d STORE $2\_s*' ..
+        '\d STORE $3\_s*' ..
+
         'res->add(i)\_s*' ..
         '\d LOAD $0\_s*' ..
-        '\d LOAD $2\_s*' ..
+        '\d LOAD $3\_s*' ..
         '\d\+ LISTAPPEND\_s*' ..
         '\d\+ DROP\_s*' ..
+
         'endfor\_s*' ..
         '\d\+ JUMP -> \d\+\_s*' ..
         '\d\+ DROP',
@@ -1498,21 +1501,25 @@ def Test_disassemble_for_loop_eval()
         'var res = ""\_s*' ..
         '\d PUSHS ""\_s*' ..
         '\d STORE $0\_s*' ..
+
         'for str in eval(''\["one", "two"\]'')\_s*' ..
         '\d STORE -1 in $1\_s*' ..
         '\d PUSHS "\["one", "two"\]"\_s*' ..
         '\d BCALL eval(argc 1)\_s*' ..
         '\d FOR $1 -> \d\+\_s*' ..
-        '\d STORE $2\_s*' ..
+        '\d STORE $3\_s*' ..
+
         'res ..= str\_s*' ..
         '\d\+ LOAD $0\_s*' ..
-        '\d\+ LOAD $2\_s*' ..
+        '\d\+ LOAD $3\_s*' ..
         '\d 2STRING_ANY stack\[-1\]\_s*' ..
         '\d\+ CONCAT size 2\_s*' ..
         '\d\+ STORE $0\_s*' ..
+
         'endfor\_s*' ..
         '\d\+ JUMP -> 5\_s*' ..
         '\d\+ DROP\_s*' ..
+
         'return res\_s*' ..
         '\d\+ LOAD $0\_s*' ..
         '\d\+ RETURN',
@@ -1539,12 +1546,14 @@ def Test_disassemble_for_loop_unpack()
         '\d\+ NEWLIST size 2\_s*' ..
         '\d\+ FOR $0 -> 16\_s*' ..
         '\d\+ UNPACK 2\_s*' ..
-        '\d\+ STORE $1\_s*' ..
         '\d\+ STORE $2\_s*' ..
+        '\d\+ STORE $3\_s*' ..
+
         'echo x1 x2\_s*' ..
-        '\d\+ LOAD $1\_s*' ..
         '\d\+ LOAD $2\_s*' ..
+        '\d\+ LOAD $3\_s*' ..
         '\d\+ ECHO 2\_s*' ..
+
         'endfor\_s*' ..
         '\d\+ JUMP -> 8\_s*' ..
         '\d\+ DROP\_s*' ..
@@ -1576,32 +1585,43 @@ def Test_disassemble_for_loop_continue()
         '2 PUSHNR 2\_s*' ..
         '3 NEWLIST size 2\_s*' ..
         '4 FOR $0 -> 22\_s*' ..
-        '5 STORE $1\_s*' ..
+        '5 STORE $2\_s*' ..
+
         'try\_s*' ..
         '6 TRY catch -> 17, endtry -> 20\_s*' ..
+
         'echo "ok"\_s*' ..
         '7 PUSHS "ok"\_s*' ..
         '8 ECHO 1\_s*' ..
+
         'try\_s*' ..
         '9 TRY catch -> 13, endtry -> 15\_s*' ..
+
         'echo "deeper"\_s*' ..
         '10 PUSHS "deeper"\_s*' ..
         '11 ECHO 1\_s*' ..
+
         'catch\_s*' ..
         '12 JUMP -> 15\_s*' ..
         '13 CATCH\_s*' ..
+
         'continue\_s*' ..
         '14 TRY-CONTINUE 2 levels -> 4\_s*' ..
+
         'endtry\_s*' ..
         '15 ENDTRY\_s*' ..
+
         'catch\_s*' ..
         '16 JUMP -> 20\_s*' ..
         '17 CATCH\_s*' ..
+
         'echo "not ok"\_s*' ..
         '18 PUSHS "not ok"\_s*' ..
         '19 ECHO 1\_s*' ..
+
         'endtry\_s*' ..
         '20 ENDTRY\_s*' ..
+
         'endfor\_s*' ..
         '21 JUMP -> 4\_s*' ..
         '\d\+ DROP\_s*' ..
@@ -2478,7 +2498,8 @@ def Test_silent_for()
         '\d NEWLIST size 1\_s*' ..
         '\d CMDMOD_REV\_s*' ..
         '5 FOR $0 -> 8\_s*' ..
-        '\d STORE $1\_s*' ..
+        '\d STORE $2\_s*' ..
+
         'endfor\_s*' ..
         '\d JUMP -> 5\_s*' ..
         '8 DROP\_s*' ..
@@ -2499,7 +2520,7 @@ def Test_silent_while()
         '\d LOADG g:not\_s*' ..
         '\d COND2BOOL\_s*' ..
         '\d CMDMOD_REV\_s*' ..
-        '\d JUMP_IF_FALSE -> 6\_s*' ..
+        '\d WHILE $0 -> 6\_s*' ..
 
         'endwhile\_s*' ..
         '\d JUMP -> 0\_s*' ..
@@ -2691,17 +2712,17 @@ def Test_debug_for()
           '4 STORE -1 in $0\_s*' ..
           '5 PUSHNR 0\_s*' ..
           '6 NEWLIST size 1\_s*' ..
-          '7 DEBUG line 2-2 varcount 2\_s*' ..
+          '7 DEBUG line 2-2 varcount 3\_s*' ..
           '8 FOR $0 -> 15\_s*' ..
-          '9 STORE $1\_s*' ..
+          '9 STORE $2\_s*' ..
 
           'echo a\_s*' ..
-          '10 DEBUG line 3-3 varcount 2\_s*' ..
-          '11 LOAD $1\_s*' ..
+          '10 DEBUG line 3-3 varcount 3\_s*' ..
+          '11 LOAD $2\_s*' ..
           '12 ECHO 1\_s*' ..
 
           'endfor\_s*' ..
-          '13 DEBUG line 4-4 varcount 2\_s*' ..
+          '13 DEBUG line 4-4 varcount 3\_s*' ..
           '14 JUMP -> 7\_s*' ..
           '15 DROP\_s*' ..
           '16 RETURN void*',
--- a/src/version.c
+++ b/src/version.c
@@ -704,6 +704,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    470,
+/**/
     469,
 /**/
     468,
--- a/src/vim9.h
+++ b/src/vim9.h
@@ -122,6 +122,9 @@ typedef enum {
 
     // loop
     ISN_FOR,	    // get next item from a list, uses isn_arg.forloop
+    ISN_WHILE,	    // jump if condition false, store funcref count, uses
+		    // isn_arg.whileloop
+    ISN_ENDLOOP,    // handle variables for closures, uses isn_arg.endloop
 
     ISN_TRY,	    // add entry to ec_trystack, uses isn_arg.tryref
     ISN_THROW,	    // pop value of stack, store in v:exception
@@ -240,6 +243,7 @@ typedef enum {
     JUMP_ALWAYS,
     JUMP_NEVER,
     JUMP_IF_FALSE,		// pop and jump if false
+    JUMP_WHILE_FALSE,		// pop and jump if false for :while
     JUMP_AND_KEEP_IF_TRUE,	// jump if top of stack is truthy, drop if not
     JUMP_IF_COND_TRUE,		// jump if top of stack is true, drop if not
     JUMP_IF_COND_FALSE,		// jump if top of stack is false, drop if not
@@ -263,6 +267,19 @@ typedef struct {
     int	    for_end;	    // position to jump to after done
 } forloop_T;
 
+// arguments to ISN_WHILE
+typedef struct {
+    int	    while_funcref_idx;  // variable index for funcref count
+    int	    while_end;		// position to jump to after done
+} whileloop_T;
+
+// arguments to ISN_ENDLOOP
+typedef struct {
+    short    end_funcref_idx;	// variable index of funcrefs.ga_len
+    short    end_var_idx;	// first variable declared in the loop
+    short    end_var_count;	// number of variables declared in the loop
+} endloop_T;
+
 // indirect arguments to ISN_TRY
 typedef struct {
     int	    try_catch;	    // position to jump to on throw
@@ -446,6 +463,8 @@ struct isn_S {
 	jump_T		    jump;
 	jumparg_T	    jumparg;
 	forloop_T	    forloop;
+	whileloop_T	    whileloop;
+	endloop_T	    endloop;
 	try_T		    tryref;
 	trycont_T	    trycont;
 	cbfunc_T	    bfunc;
@@ -597,6 +616,9 @@ typedef struct {
 typedef struct {
     int		ws_top_label;	    // instruction idx at WHILE
     endlabel_T	*ws_end_label;	    // instructions to set end
+    int		ws_funcref_idx;	    // index of var that holds funcref count
+    int		ws_local_count;	    // ctx_locals.ga_len at :while
+    int		ws_closure_count;   // ctx_closure_count at :while
 } whilescope_T;
 
 /*
@@ -605,6 +627,9 @@ typedef struct {
 typedef struct {
     int		fs_top_label;	    // instruction idx at FOR
     endlabel_T	*fs_end_label;	    // break instructions
+    int		fs_funcref_idx;	    // index of var that holds funcref count
+    int		fs_local_count;	    // ctx_locals.ga_len at :for
+    int		fs_closure_count;   // ctx_closure_count at :for
 } forscope_T;
 
 /*
@@ -726,8 +751,10 @@ struct cctx_S {
 
     garray_T	ctx_locals;	    // currently visible local variables
 
-    int		ctx_has_closure;    // set to one if a closure was created in
-				    // the function
+    int		ctx_has_closure;    // set to one if a FUNCREF was used in the
+				    // function
+    int		ctx_closure_count;  // incremented for each closure created in
+				    // the function.
 
     skip_T	ctx_skip;
     scope_T	*ctx_scope;	    // current scope, NULL at toplevel
--- a/src/vim9cmds.c
+++ b/src/vim9cmds.c
@@ -278,10 +278,15 @@ compile_unletlock(char_u *arg, exarg_T *
 }
 
 /*
- * generate a jump to the ":endif"/":endfor"/":endwhile"/":finally"/":endtry".
+ * Generate a jump to the ":endif"/":endfor"/":endwhile"/":finally"/":endtry".
+ * "funcref_idx" is used for JUMP_WHILE_FALSE
  */
     static int
-compile_jump_to_end(endlabel_T **el, jumpwhen_T when, cctx_T *cctx)
+compile_jump_to_end(
+	endlabel_T  **el,
+	jumpwhen_T  when,
+	int	    funcref_idx,
+	cctx_T	    *cctx)
 {
     garray_T	*instr = &cctx->ctx_instr;
     endlabel_T  *endlabel = ALLOC_CLEAR_ONE(endlabel_T);
@@ -292,7 +297,10 @@ compile_jump_to_end(endlabel_T **el, jum
     *el = endlabel;
     endlabel->el_end_label = instr->ga_len;
 
-    generate_JUMP(cctx, when, 0);
+    if (when == JUMP_WHILE_FALSE)
+	generate_WHILE(cctx, funcref_idx);
+    else
+	generate_JUMP(cctx, when, 0);
     return OK;
 }
 
@@ -564,7 +572,7 @@ compile_elseif(char_u *arg, cctx_T *cctx
 	}
 
 	if (compile_jump_to_end(&scope->se_u.se_if.is_end_label,
-						    JUMP_ALWAYS, cctx) == FAIL)
+						 JUMP_ALWAYS, 0, cctx) == FAIL)
 	    return NULL;
 	// previous "if" or "elseif" jumps here
 	isn = ((isn_T *)instr->ga_data) + scope->se_u.se_if.is_if_label;
@@ -695,7 +703,7 @@ compile_else(char_u *arg, cctx_T *cctx)
 	{
 	    if (!cctx->ctx_had_return
 		    && compile_jump_to_end(&scope->se_u.se_if.is_end_label,
-						    JUMP_ALWAYS, cctx) == FAIL)
+						 JUMP_ALWAYS, 0, cctx) == FAIL)
 		return NULL;
 	}
 
@@ -771,16 +779,17 @@ compile_endif(char_u *arg, cctx_T *cctx)
  * Compile "for var in expr":
  *
  * Produces instructions:
- *       PUSHNR -1
- *       STORE loop-idx		Set index to -1
- *       EVAL expr		result of "expr" on top of stack
+ *       STORE -1 in loop-idx	Set index to -1
+ *       EVAL expr		Result of "expr" on top of stack
  * top:  FOR loop-idx, end	Increment index, use list on bottom of stack
  *				- if beyond end, jump to "end"
  *				- otherwise get item from list and push it
+ *				- store ec_funcrefs in var "loop-idx" + 1
  *       STORE var		Store item in "var"
  *       ... body ...
- *       JUMP top		Jump back to repeat
- * end:	 DROP			Drop the result of "expr"
+ *       ENDLOOP funcref-idx off count	Only if closure uses local var
+ *       JUMP top			Jump back to repeat
+ * end:	 DROP				Drop the result of "expr"
  *
  * Compile "for [var1, var2] in expr" - as above, but instead of "STORE var":
  *	 UNPACK 2		Split item in 2
@@ -801,7 +810,9 @@ compile_for(char_u *arg_start, cctx_T *c
     size_t	varlen;
     garray_T	*instr = &cctx->ctx_instr;
     scope_T	*scope;
+    forscope_T	*forscope;
     lvar_T	*loop_lvar;	// loop iteration variable
+    lvar_T	*funcref_lvar;
     lvar_T	*var_lvar;	// variable for "var"
     type_T	*vartype;
     type_T	*item_type = &t_any;
@@ -845,18 +856,28 @@ compile_for(char_u *arg_start, cctx_T *c
     scope = new_scope(cctx, FOR_SCOPE);
     if (scope == NULL)
 	return NULL;
+    forscope = &scope->se_u.se_for;
 
     // Reserve a variable to store the loop iteration counter and initialize it
     // to -1.
     loop_lvar = reserve_local(cctx, (char_u *)"", 0, FALSE, &t_number);
     if (loop_lvar == NULL)
     {
-	// out of memory
 	drop_scope(cctx);
-	return NULL;
+	return NULL;  // out of memory
     }
     generate_STORENR(cctx, loop_lvar->lv_idx, -1);
 
+    // Reserve a variable to store ec_funcrefs.ga_len, used in ISN_ENDLOOP.
+    // The variable index is always the loop var index plus one.
+    // It is not used when no closures are encountered, we don't know yet.
+    funcref_lvar = reserve_local(cctx, (char_u *)"", 0, FALSE, &t_number);
+    if (funcref_lvar == NULL)
+    {
+	drop_scope(cctx);
+	return NULL;  // out of memory
+    }
+
     // compile "expr", it remains on the stack until "endfor"
     arg = p;
     if (compile_expr0(&arg, cctx) == FAIL)
@@ -901,7 +922,7 @@ compile_for(char_u *arg_start, cctx_T *c
 	generate_undo_cmdmods(cctx);
 
 	// "for_end" is set when ":endfor" is found
-	scope->se_u.se_for.fs_top_label = current_instr_idx(cctx);
+	forscope->fs_top_label = current_instr_idx(cctx);
 
 	if (cctx->ctx_compile_type == CT_DEBUG)
 	{
@@ -1019,6 +1040,11 @@ compile_for(char_u *arg_start, cctx_T *c
 	    arg = skipwhite(p);
 	    vim_free(name);
 	}
+
+	forscope->fs_funcref_idx = funcref_lvar->lv_idx;
+	// remember the number of variables and closures, used in :endfor
+	forscope->fs_local_count = cctx->ctx_locals.ga_len;
+	forscope->fs_closure_count = cctx->ctx_closure_count;
     }
 
     return arg_end;
@@ -1030,6 +1056,23 @@ failed:
 }
 
 /*
+ * At :endfor and :endwhile: Generate an ISN_ENDLOOP instruction if any
+ * variable was declared that could be used by a new closure.
+ */
+    static int
+compile_loop_end(
+	int	prev_local_count,
+	int	prev_closure_count,
+	int	funcref_idx,
+	cctx_T	*cctx)
+{
+    if (cctx->ctx_locals.ga_len > prev_local_count
+	    && cctx->ctx_closure_count > prev_closure_count)
+	return generate_ENDLOOP(cctx, funcref_idx, prev_local_count);
+    return OK;
+}
+
+/*
  * compile "endfor"
  */
     char_u *
@@ -1052,6 +1095,14 @@ compile_endfor(char_u *arg, cctx_T *cctx
     cctx->ctx_scope = scope->se_outer;
     if (cctx->ctx_skip != SKIP_YES)
     {
+	// Handle the case that any local variables were declared that might be
+	// used in a closure.
+	if (compile_loop_end(forscope->fs_local_count,
+				forscope->fs_closure_count,
+				forscope->fs_funcref_idx,
+				cctx) == FAIL)
+	    return NULL;
+
 	unwind_locals(cctx, scope->se_local_count);
 
 	// At end of ":for" scope jump back to the FOR instruction.
@@ -1080,25 +1131,42 @@ compile_endfor(char_u *arg, cctx_T *cctx
  * compile "while expr"
  *
  * Produces instructions:
- * top:  EVAL expr		Push result of "expr"
- *       JUMP_IF_FALSE end	jump if false
- *       ... body ...
- *       JUMP top		Jump back to repeat
+ * top:  EVAL expr			Push result of "expr"
+ *	 WHILE funcref-idx  end		Jump if false
+ *	 ... body ...
+ *       ENDLOOP funcref-idx off count	only if closure uses local var
+ *	 JUMP top			Jump back to repeat
  * end:
  *
  */
     char_u *
 compile_while(char_u *arg, cctx_T *cctx)
 {
-    char_u	*p = arg;
-    scope_T	*scope;
+    char_u	    *p = arg;
+    scope_T	    *scope;
+    whilescope_T    *whilescope;
+    lvar_T	    *funcref_lvar;
 
     scope = new_scope(cctx, WHILE_SCOPE);
     if (scope == NULL)
 	return NULL;
+    whilescope = &scope->se_u.se_while;
 
     // "endwhile" jumps back here, one before when profiling or using cmdmods
-    scope->se_u.se_while.ws_top_label = current_instr_idx(cctx);
+    whilescope->ws_top_label = current_instr_idx(cctx);
+
+    // Reserve a variable to store ec_funcrefs.ga_len, used in ISN_ENDLOOP.
+    // It is not used when no closures are encountered, we don't know yet.
+    funcref_lvar = reserve_local(cctx, (char_u *)"", 0, FALSE, &t_number);
+    if (funcref_lvar == NULL)
+    {
+	drop_scope(cctx);
+	return NULL;  // out of memory
+    }
+    whilescope->ws_funcref_idx = funcref_lvar->lv_idx;
+    // remember the number of variables and closures, used in :endwhile
+    whilescope->ws_local_count = cctx->ctx_locals.ga_len;
+    whilescope->ws_closure_count = cctx->ctx_closure_count;
 
     // compile "expr"
     if (compile_expr0(&p, cctx) == FAIL)
@@ -1119,8 +1187,8 @@ compile_while(char_u *arg, cctx_T *cctx)
 	generate_undo_cmdmods(cctx);
 
 	// "while_end" is set when ":endwhile" is found
-	if (compile_jump_to_end(&scope->se_u.se_while.ws_end_label,
-						  JUMP_IF_FALSE, cctx) == FAIL)
+	if (compile_jump_to_end(&whilescope->ws_end_label,
+			 JUMP_WHILE_FALSE, funcref_lvar->lv_idx, cctx) == FAIL)
 	    return FAIL;
     }
 
@@ -1146,6 +1214,16 @@ compile_endwhile(char_u *arg, cctx_T *cc
     cctx->ctx_scope = scope->se_outer;
     if (cctx->ctx_skip != SKIP_YES)
     {
+	whilescope_T	*whilescope = &scope->se_u.se_while;
+
+	// Handle the case that any local variables were declared that might be
+	// used in a closure.
+	if (compile_loop_end(whilescope->ws_local_count,
+				whilescope->ws_closure_count,
+				whilescope->ws_funcref_idx,
+				cctx) == FAIL)
+	    return NULL;
+
 	unwind_locals(cctx, scope->se_local_count);
 
 #ifdef FEAT_PROFILE
@@ -1250,7 +1328,7 @@ compile_break(char_u *arg, cctx_T *cctx)
 
     // Jump to the end of the FOR or WHILE loop.  The instruction index will be
     // filled in later.
-    if (compile_jump_to_end(el, JUMP_ALWAYS, cctx) == FAIL)
+    if (compile_jump_to_end(el, JUMP_ALWAYS, 0, cctx) == FAIL)
 	return FAIL;
 
     return arg;
@@ -1397,7 +1475,7 @@ compile_catch(char_u *arg, cctx_T *cctx 
 #endif
 	// Jump from end of previous block to :finally or :endtry
 	if (compile_jump_to_end(&scope->se_u.se_try.ts_end_label,
-						    JUMP_ALWAYS, cctx) == FAIL)
+						 JUMP_ALWAYS, 0, cctx) == FAIL)
 	    return NULL;
 
 	// End :try or :catch scope: set value in ISN_TRY instruction
--- a/src/vim9compile.c
+++ b/src/vim9compile.c
@@ -3449,8 +3449,14 @@ nextline:
 	}
 	dfunc->df_varcount = dfunc->df_var_names.ga_len;
 	dfunc->df_has_closure = cctx.ctx_has_closure;
+
 	if (cctx.ctx_outer_used)
+	{
 	    ufunc->uf_flags |= FC_CLOSURE;
+	    if (outer_cctx != NULL)
+		++outer_cctx->ctx_closure_count;
+	}
+
 	ufunc->uf_def_status = UF_COMPILED;
     }
 
--- a/src/vim9execute.c
+++ b/src/vim9execute.c
@@ -504,7 +504,8 @@ call_dfunc(
     // - if needed: a counter for number of closures created in
     //   ectx->ec_funcrefs.
     varcount = dfunc->df_varcount + dfunc->df_has_closure;
-    if (GA_GROW_FAILS(&ectx->ec_stack, arg_to_add + STACK_FRAME_SIZE + varcount))
+    if (GA_GROW_FAILS(&ectx->ec_stack,
+				     arg_to_add + STACK_FRAME_SIZE + varcount))
 	return FAIL;
 
     // If depth of calling is getting too high, don't execute the function.
@@ -553,6 +554,8 @@ call_dfunc(
     {
 	typval_T *tv = STACK_TV_BOT(STACK_FRAME_SIZE + dfunc->df_varcount);
 
+	// Initialize the variable that counts how many closures were created.
+	// This is used in handle_closure_in_use().
 	tv->v_type = VAR_NUMBER;
 	tv->vval.v_number = 0;
     }
@@ -1821,8 +1824,8 @@ fill_partial_and_closure(partial_T *pt, 
 	dfunc_T	*dfunc = ((dfunc_T *)def_functions.ga_data)
 							  + ectx->ec_dfunc_idx;
 
-	// The closure may need to find arguments and local variables in the
-	// current stack.
+	// The closure may need to find arguments and local variables of the
+	// current function in the stack.
 	pt->pt_outer.out_stack = &ectx->ec_stack;
 	pt->pt_outer.out_frame_idx = ectx->ec_frame_idx;
 	if (ectx->ec_outer_ref != NULL)
@@ -1836,8 +1839,9 @@ fill_partial_and_closure(partial_T *pt, 
 	    }
 	}
 
-	// If this function returns and the closure is still being used, we
-	// need to make a copy of the context (arguments and local variables).
+	// If the function currently executing returns and the closure is still
+	// being referenced, we need to make a copy of the context (arguments
+	// and local variables) so that the closure can use it later.
 	// Store a reference to the partial so we can handle that.
 	if (GA_GROW_FAILS(&ectx->ec_funcrefs, 1))
 	{
@@ -2477,6 +2481,7 @@ execute_unletrange(isn_T *iptr, ectx_T *
 execute_for(isn_T *iptr, ectx_T *ectx)
 {
     typval_T	*tv;
+    int		jump = FALSE;
     typval_T	*ltv = STACK_TV_BOT(-1);
     typval_T	*idxtv =
 		   STACK_TV_VAR(iptr->isn_arg.forloop.for_idx);
@@ -2492,9 +2497,7 @@ execute_for(isn_T *iptr, ectx_T *ectx)
 	if (list == NULL
 		       || idxtv->vval.v_number >= list->lv_len)
 	{
-	    // past the end of the list, jump to "endfor"
-	    ectx->ec_iidx = iptr->isn_arg.forloop.for_end;
-	    may_restore_cmdmod(&ectx->ec_funclocal);
+	    jump = TRUE;
 	}
 	else if (list->lv_first == &range_list_item)
 	{
@@ -2524,9 +2527,7 @@ execute_for(isn_T *iptr, ectx_T *ectx)
 	++idxtv->vval.v_number;
 	if (str == NULL || str[idxtv->vval.v_number] == NUL)
 	{
-	    // past the end of the string, jump to "endfor"
-	    ectx->ec_iidx = iptr->isn_arg.forloop.for_end;
-	    may_restore_cmdmod(&ectx->ec_funclocal);
+	    jump = TRUE;
 	}
 	else
 	{
@@ -2557,12 +2558,9 @@ execute_for(isn_T *iptr, ectx_T *ectx)
 
 	// The index is for the previous byte.
 	++idxtv->vval.v_number;
-	if (blob == NULL
-		     || idxtv->vval.v_number >= blob_len(blob))
+	if (blob == NULL || idxtv->vval.v_number >= blob_len(blob))
 	{
-	    // past the end of the blob, jump to "endfor"
-	    ectx->ec_iidx = iptr->isn_arg.forloop.for_end;
-	    may_restore_cmdmod(&ectx->ec_funclocal);
+	    jump = TRUE;
 	}
 	else
 	{
@@ -2580,6 +2578,33 @@ execute_for(isn_T *iptr, ectx_T *ectx)
 				    vartype_name(ltv->v_type));
 	return FAIL;
     }
+
+    if (jump)
+    {
+	// past the end of the list/string/blob, jump to "endfor"
+	ectx->ec_iidx = iptr->isn_arg.forloop.for_end;
+	may_restore_cmdmod(&ectx->ec_funclocal);
+    }
+    else
+    {
+	// Store the current number of funcrefs, this may be used in
+	// ISN_LOOPEND.  The variable index is always one more than the loop
+	// variable index.
+	tv = STACK_TV_VAR(iptr->isn_arg.forloop.for_idx + 1);
+	tv->vval.v_number = ectx->ec_funcrefs.ga_len;
+    }
+
+    return OK;
+}
+
+/*
+ * End of a for or while loop: Handle any variables used by a closure.
+ */
+    static int
+execute_endloop(isn_T *iptr UNUSED, ectx_T *ectx UNUSED)
+{
+    // TODO
+
     return OK;
 }
 
@@ -3989,6 +4014,31 @@ exec_instructions(ectx_T *ectx)
 		}
 		break;
 
+	    // "while": jump to end if a condition is false
+	    case ISN_WHILE:
+		{
+		    int		error = FALSE;
+		    int		jump = TRUE;
+
+		    tv = STACK_TV_BOT(-1);
+		    SOURCING_LNUM = iptr->isn_lnum;
+		    jump = !tv_get_bool_chk(tv, &error);
+		    if (error)
+			goto on_error;
+		    // drop the value from the stack
+		    clear_tv(tv);
+		    --ectx->ec_stack.ga_len;
+		    if (jump)
+			ectx->ec_iidx = iptr->isn_arg.whileloop.while_end;
+
+		    // Store the current funccal count, may be used by
+		    // ISN_LOOPEND later
+		    tv = STACK_TV_VAR(
+				    iptr->isn_arg.whileloop.while_funcref_idx);
+		    tv->vval.v_number = ectx->ec_funcrefs.ga_len;
+		}
+		break;
+
 	    // Jump if an argument with a default value was already set and not
 	    // v:none.
 	    case ISN_JUMP_IF_ARG_SET:
@@ -4005,6 +4055,12 @@ exec_instructions(ectx_T *ectx)
 		    goto theend;
 		break;
 
+	    // end of a for or while loop
+	    case ISN_ENDLOOP:
+		if (execute_endloop(iptr, ectx) == FAIL)
+		    goto theend;
+		break;
+
 	    // start of ":try" block
 	    case ISN_TRY:
 		{
@@ -6185,6 +6241,9 @@ list_instructions(char *pfx, isn_T *inst
 			case JUMP_IF_FALSE:
 			    when = "JUMP_IF_FALSE";
 			    break;
+			case JUMP_WHILE_FALSE:
+			    when = "JUMP_WHILE_FALSE";  // unused
+			    break;
 			case JUMP_IF_COND_FALSE:
 			    when = "JUMP_IF_COND_FALSE";
 			    break;
@@ -6212,6 +6271,27 @@ list_instructions(char *pfx, isn_T *inst
 		}
 		break;
 
+	    case ISN_ENDLOOP:
+		{
+		    endloop_T *endloop = &iptr->isn_arg.endloop;
+
+		    smsg("%s%4d ENDLOOP $%d save $%d - $%d", pfx, current,
+			    endloop->end_funcref_idx,
+			    endloop->end_var_idx,
+			    endloop->end_var_idx + endloop->end_var_count - 1);
+		}
+		break;
+
+	    case ISN_WHILE:
+		{
+		    whileloop_T *whileloop = &iptr->isn_arg.whileloop;
+
+		    smsg("%s%4d WHILE $%d -> %d", pfx, current,
+					       whileloop->while_funcref_idx,
+					       whileloop->while_end);
+		}
+		break;
+
 	    case ISN_TRY:
 		{
 		    try_T *try = &iptr->isn_arg.tryref;
--- a/src/vim9instr.c
+++ b/src/vim9instr.c
@@ -1284,6 +1284,27 @@ generate_JUMP(cctx_T *cctx, jumpwhen_T w
 }
 
 /*
+ * Generate an ISN_WHILE instruction.  Similar to ISN_JUMP for :while
+ */
+    int
+generate_WHILE(cctx_T *cctx, int funcref_idx)
+{
+    isn_T	*isn;
+    garray_T	*stack = &cctx->ctx_type_stack;
+
+    RETURN_OK_IF_SKIP(cctx);
+    if ((isn = generate_instr(cctx, ISN_WHILE)) == NULL)
+	return FAIL;
+    isn->isn_arg.whileloop.while_funcref_idx = funcref_idx;
+    isn->isn_arg.whileloop.while_end = 0;  // filled in later
+
+    if (stack->ga_len > 0)
+	--stack->ga_len;
+
+    return OK;
+}
+
+/*
  * Generate an ISN_JUMP_IF_ARG_SET instruction.
  */
     int
@@ -1312,6 +1333,25 @@ generate_FOR(cctx_T *cctx, int loop_idx)
     // type doesn't matter, will be stored next
     return push_type_stack(cctx, &t_any);
 }
+
+    int
+generate_ENDLOOP(
+	cctx_T	*cctx,
+	int	funcref_idx,
+	int	prev_local_count)
+{
+    isn_T	*isn;
+
+    RETURN_OK_IF_SKIP(cctx);
+    if ((isn = generate_instr(cctx, ISN_ENDLOOP)) == NULL)
+	return FAIL;
+    isn->isn_arg.endloop.end_funcref_idx = funcref_idx;
+    isn->isn_arg.endloop.end_var_idx = prev_local_count;
+    isn->isn_arg.endloop.end_var_count =
+				    cctx->ctx_locals.ga_len - prev_local_count;
+    return OK;
+}
+
 /*
  * Generate an ISN_TRYCONT instruction.
  */
@@ -2295,6 +2335,7 @@ delete_instr(isn_T *isn)
 	case ISN_ECHOERR:
 	case ISN_ECHOMSG:
 	case ISN_ECHOWINDOW:
+	case ISN_ENDLOOP:
 	case ISN_ENDTRY:
 	case ISN_EXECCONCAT:
 	case ISN_EXECUTE:
@@ -2341,10 +2382,10 @@ delete_instr(isn_T *isn)
 	case ISN_RETURN_VOID:
 	case ISN_SHUFFLE:
 	case ISN_SLICE:
+	case ISN_SOURCE:
 	case ISN_STORE:
 	case ISN_STOREINDEX:
 	case ISN_STORENR:
-	case ISN_SOURCE:
 	case ISN_STOREOUTER:
 	case ISN_STORERANGE:
 	case ISN_STOREREG:
@@ -2357,6 +2398,7 @@ delete_instr(isn_T *isn)
 	case ISN_UNLETRANGE:
 	case ISN_UNPACK:
 	case ISN_USEDICT:
+	case ISN_WHILE:
 	// nothing allocated
 	break;
     }