changeset 34126:da670b1549b3 v9.1.0027

patch 9.1.0027: Vim is missing a foreach() func Commit: https://github.com/vim/vim/commit/e79e2077607e8f829ba823308c91104a795736ba Author: Ernie Rael <errael@raelity.com> Date: Sat Jan 13 11:47:33 2024 +0100 patch 9.1.0027: Vim is missing a foreach() func Problem: Vim is missing a foreach() func Solution: Implement foreach({expr1}, {expr2}) function, which applies {expr2} for each item in {expr1} without changing it (Ernie Rael) closes: #12166 Signed-off-by: Ernie Rael <errael@raelity.com> Signed-off-by: Christian Brabandt <cb@256bit.org>
author Christian Brabandt <cb@256bit.org>
date Sat, 13 Jan 2024 12:00:06 +0100
parents 93171f4925c5
children d42de932962e
files runtime/doc/builtin.txt runtime/doc/tags runtime/doc/usr_41.txt src/blob.c src/dict.c src/evalfunc.c src/list.c src/proto/list.pro src/strings.c src/structs.h src/testdir/test_filter_map.vim src/version.c
diffstat 12 files changed, 303 insertions(+), 61 deletions(-) [+]
line wrap: on
line diff
--- a/runtime/doc/builtin.txt
+++ b/runtime/doc/builtin.txt
@@ -1,4 +1,4 @@
-*builtin.txt*	For Vim version 9.1.  Last change: 2024 Jan 05
+*builtin.txt*	For Vim version 9.1.  Last change: 2024 Jan 13
 
 
 		  VIM REFERENCE MANUAL	  by Bram Moolenaar
@@ -198,6 +198,8 @@ foldclosedend({lnum})		Number	last line 
 foldlevel({lnum})		Number	fold level at {lnum}
 foldtext()			String	line displayed for closed fold
 foldtextresult({lnum})		String	text for closed fold at {lnum}
+foreach({expr1}, {expr2})	List/Dict/Blob/String
+					for each item in {expr1} call {expr2}
 foreground()			Number	bring the Vim window to the foreground
 fullcommand({name} [, {vim9}])	String	get full command from {name}
 funcref({name} [, {arglist}] [, {dict}])
@@ -2995,6 +2997,45 @@ foldtextresult({lnum})					*foldtextresu
 
 		Can also be used as a |method|: >
 			GetLnum()->foldtextresult()
+
+foreach({expr1}, {expr2})					*foreach()*
+		{expr1} must be a |List|, |String|, |Blob| or |Dictionary|.
+		For each item in {expr1} execute {expr2}. {expr1} is not
+		modified; its values may be, as with |:lockvar| 1. *E741*
+		See |map()| and |filter()| to modify {expr1}.
+
+		{expr2} must be a |string| or |Funcref|.
+
+		If {expr2} is a |string|, inside {expr2} |v:val| has the value
+		of the current item.  For a |Dictionary| |v:key| has the key
+		of the current item and for a |List| |v:key| has the index of
+		the current item.  For a |Blob| |v:key| has the index of the
+		current byte. For a |String| |v:key| has the index of the
+		current character.
+		Examples: >
+			call foreach(mylist, 'used[v:val] = true')
+<		This records the items that are in the {expr1} list.
+
+		Note that {expr2} is the result of expression and is then used
+		as a command.  Often it is good to use a |literal-string| to
+		avoid having to double backslashes.
+
+		If {expr2} is a |Funcref| it must take two arguments:
+			1. the key or the index of the current item.
+			2. the value of the current item.
+		With a legacy script lambda you don't get an error if it only
+		accepts one argument, but with a Vim9 lambda you get "E1106:
+		One argument too many", the number of arguments must match.
+		If the function returns a value, it is ignored.
+
+		Returns {expr1} in all cases.
+		When an error is encountered while executing {expr2} no
+		further items in {expr1} are processed.
+		When {expr2} is a Funcref errors inside a function are ignored,
+		unless it was defined with the "abort" flag.
+
+		Can also be used as a |method|: >
+			mylist->foreach(expr2)
 <
 							*foreground()*
 foreground()	Move the Vim window to the foreground.  Useful when sent from
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -5167,6 +5167,7 @@ E738	eval.txt	/*E738*
 E739	builtin.txt	/*E739*
 E74	message.txt	/*E74*
 E740	userfunc.txt	/*E740*
+E741	builtin.txt	/*E741*
 E741	eval.txt	/*E741*
 E742	userfunc.txt	/*E742*
 E743	eval.txt	/*E743*
@@ -7148,6 +7149,7 @@ foldtextresult()	builtin.txt	/*foldtextr
 font-sizes	gui_x11.txt	/*font-sizes*
 fontset	mbyte.txt	/*fontset*
 forced-motion	motion.txt	/*forced-motion*
+foreach()	builtin.txt	/*foreach()*
 foreground()	builtin.txt	/*foreground()*
 fork	os_unix.txt	/*fork*
 form.vim	syntax.txt	/*form.vim*
--- a/runtime/doc/usr_41.txt
+++ b/runtime/doc/usr_41.txt
@@ -1,4 +1,4 @@
-*usr_41.txt*	For Vim version 9.1.  Last change: 2023 May 06
+*usr_41.txt*	For Vim version 9.1.  Last change: 2024 Jan 13
 
 		     VIM USER MANUAL - by Bram Moolenaar
 
@@ -798,6 +798,7 @@ List manipulation:					*list-functions*
 	filter()		remove selected items from a List
 	map()			change each List item
 	mapnew()		make a new List with changed items
+	foreach()		apply function to List items
 	reduce()		reduce a List to a value
 	slice()			take a slice of a List
 	sort()			sort a List
@@ -829,6 +830,7 @@ Dictionary manipulation:				*dict-functi
 	filter()		remove selected entries from a Dictionary
 	map()			change each Dictionary entry
 	mapnew()		make a new Dictionary with changed items
+	foreach()		apply function to Dictionary items
 	keys()			get List of Dictionary keys
 	values()		get List of Dictionary values
 	items()			get List of Dictionary key-value pairs
--- a/src/blob.c
+++ b/src/blob.c
@@ -641,25 +641,28 @@ blob_filter_map(
 	if (filter_map_one(&tv, expr, filtermap, fc, &newtv, &rem) == FAIL
 		|| did_emsg)
 	    break;
-	if (newtv.v_type != VAR_NUMBER && newtv.v_type != VAR_BOOL)
-	{
-	    clear_tv(&newtv);
-	    emsg(_(e_invalid_operation_for_blob));
-	    break;
-	}
-	if (filtermap != FILTERMAP_FILTER)
+	if (filtermap != FILTERMAP_FOREACH)
 	{
-	    if (newtv.vval.v_number != val)
-		blob_set(b_ret, i, newtv.vval.v_number);
-	}
-	else if (rem)
-	{
-	    char_u *p = (char_u *)blob_arg->bv_ga.ga_data;
+	    if (newtv.v_type != VAR_NUMBER && newtv.v_type != VAR_BOOL)
+	    {
+		clear_tv(&newtv);
+		emsg(_(e_invalid_operation_for_blob));
+		break;
+	    }
+	    if (filtermap != FILTERMAP_FILTER)
+	    {
+		if (newtv.vval.v_number != val)
+		    blob_set(b_ret, i, newtv.vval.v_number);
+	    }
+	    else if (rem)
+	    {
+		char_u *p = (char_u *)blob_arg->bv_ga.ga_data;
 
-	    mch_memmove(p + i, p + i + 1,
-		    (size_t)b->bv_ga.ga_len - i - 1);
-	    --b->bv_ga.ga_len;
-	    --i;
+		mch_memmove(p + i, p + i + 1,
+			    (size_t)b->bv_ga.ga_len - i - 1);
+		--b->bv_ga.ga_len;
+		--i;
+	    }
 	}
 	++idx;
     }
--- a/src/dict.c
+++ b/src/dict.c
@@ -1329,8 +1329,8 @@ dict_extend_func(
 }
 
 /*
- * Implementation of map() and filter() for a Dict.  Apply "expr" to every
- * item in Dict "d" and return the result in "rettv".
+ * Implementation of map(), filter(), foreach() for a Dict.  Apply "expr" to
+ * every item in Dict "d" and return the result in "rettv".
  */
     void
 dict_filter_map(
@@ -1392,7 +1392,6 @@ dict_filter_map(
 			    arg_errmsg, TRUE)))
 		break;
 	    set_vim_var_string(VV_KEY, di->di_key, -1);
-	    newtv.v_type = VAR_UNKNOWN;
 	    r = filter_map_one(&di->di_tv, expr, filtermap, fc, &newtv, &rem);
 	    clear_tv(get_vim_var_tv(VV_KEY));
 	    if (r == FAIL || did_emsg)
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -607,10 +607,11 @@ arg_list_or_dict_or_blob_or_string_mod(
 }
 
 /*
- * Check second argument of map() or filter().
+ * Check second argument of map(), filter(), foreach().
  */
     static int
-check_map_filter_arg2(type_T *type, argcontext_T *context, int is_map)
+check_map_filter_arg2(type_T *type, argcontext_T *context,
+							filtermap_T filtermap)
 {
     type_T *expected_member = NULL;
     type_T *(args[2]);
@@ -663,12 +664,14 @@ check_map_filter_arg2(type_T *type, argc
     {
 	where_T where = WHERE_INIT;
 
-	if (is_map)
+	if (filtermap == FILTERMAP_MAP)
 	    t_func_exp.tt_member = expected_member == NULL
 					|| type_any_or_unknown(type->tt_member)
 				? &t_any : expected_member;
-	else
+	else if (filtermap == FILTERMAP_FILTER)
 	    t_func_exp.tt_member = &t_bool;
+	else // filtermap == FILTERMAP_FOREACH
+	    t_func_exp.tt_member = &t_unknown;
 	if (args[0] == NULL)
 	    args[0] = &t_unknown;
 	if (type->tt_argcount == -1)
@@ -693,7 +696,7 @@ arg_filter_func(type_T *type, type_T *de
 	return OK;
 
     if (type->tt_type == VAR_FUNC)
-	return check_map_filter_arg2(type, context, FALSE);
+	return check_map_filter_arg2(type, context, FILTERMAP_FILTER);
     semsg(_(e_string_or_function_required_for_argument_nr), 2);
     return FAIL;
 }
@@ -710,7 +713,24 @@ arg_map_func(type_T *type, type_T *decl_
 	return OK;
 
     if (type->tt_type == VAR_FUNC)
-	return check_map_filter_arg2(type, context, TRUE);
+	return check_map_filter_arg2(type, context, FILTERMAP_MAP);
+    semsg(_(e_string_or_function_required_for_argument_nr), 2);
+    return FAIL;
+}
+
+/*
+ * Check second argument of foreach(), the function.
+ */
+    static int
+arg_foreach_func(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
+{
+    if (type->tt_type == VAR_STRING
+	    || type->tt_type == VAR_PARTIAL
+	    || type_any_or_unknown(type))
+	return OK;
+
+    if (type->tt_type == VAR_FUNC)
+	return check_map_filter_arg2(type, context, FILTERMAP_FOREACH);
     semsg(_(e_string_or_function_required_for_argument_nr), 2);
     return FAIL;
 }
@@ -1173,6 +1193,7 @@ static argcheck_T arg1_len[] = {arg_len1
 static argcheck_T arg3_libcall[] = {arg_string, arg_string, arg_string_or_nr};
 static argcheck_T arg14_maparg[] = {arg_string, arg_string, arg_bool, arg_bool};
 static argcheck_T arg2_filter[] = {arg_list_or_dict_or_blob_or_string_mod, arg_filter_func};
+static argcheck_T arg2_foreach[] = {arg_list_or_dict_or_blob_or_string, arg_foreach_func};
 static argcheck_T arg2_instanceof[] = {arg_object, varargs_class, NULL };
 static argcheck_T arg2_map[] = {arg_list_or_dict_or_blob_or_string_mod, arg_map_func};
 static argcheck_T arg2_mapnew[] = {arg_list_or_dict_or_blob_or_string, arg_any};
@@ -2013,6 +2034,8 @@ static funcentry_T global_functions[] =
 			ret_string,	    f_foldtext},
     {"foldtextresult",	1, 1, FEARG_1,	    arg1_lnum,
 			ret_string,	    f_foldtextresult},
+    {"foreach",		2, 2, FEARG_1,	    arg2_foreach,
+			ret_first_arg,	    f_foreach},
     {"foreground",	0, 0, 0,	    NULL,
 			ret_void,	    f_foreground},
     {"fullcommand",	1, 2, FEARG_1,	    arg2_string_bool,
--- a/src/list.c
+++ b/src/list.c
@@ -2325,7 +2325,7 @@ f_uniq(typval_T *argvars, typval_T *rett
 }
 
 /*
- * Handle one item for map() and filter().
+ * Handle one item for map(), filter(), foreach().
  * Sets v:val to "tv".  Caller must set v:key.
  */
     int
@@ -2341,6 +2341,17 @@ filter_map_one(
     int		retval = FAIL;
 
     copy_tv(tv, get_vim_var_tv(VV_VAL));
+
+    newtv->v_type = VAR_UNKNOWN;
+    if (filtermap == FILTERMAP_FOREACH && expr->v_type == VAR_STRING)
+    {
+	// foreach() is not limited to an expression
+	do_cmdline_cmd(expr->vval.v_string);
+	if (!did_emsg)
+	    retval = OK;
+	goto theend;
+    }
+
     argv[0] = *get_vim_var_tv(VV_KEY);
     argv[1] = *get_vim_var_tv(VV_VAL);
     if (eval_expr_typval(expr, FALSE, argv, 2, fc, newtv) == FAIL)
@@ -2360,6 +2371,8 @@ filter_map_one(
 	if (error)
 	    goto theend;
     }
+    else if (filtermap == FILTERMAP_FOREACH)
+	clear_tv(newtv);
     retval = OK;
 theend:
     clear_tv(get_vim_var_tv(VV_VAL));
@@ -2367,8 +2380,8 @@ theend:
 }
 
 /*
- * Implementation of map() and filter() for a List.  Apply "expr" to every item
- * in List "l" and return the result in "rettv".
+ * Implementation of map(), filter(), foreach() for a List.  Apply "expr" to
+ * every item in List "l" and return the result in "rettv".
  */
     static void
 list_filter_map(
@@ -2421,7 +2434,8 @@ list_filter_map(
 	int		stride = l->lv_u.nonmat.lv_stride;
 
 	// List from range(): loop over the numbers
-	if (filtermap != FILTERMAP_MAPNEW)
+	// NOTE: foreach() returns the range_list_item
+	if (filtermap != FILTERMAP_MAPNEW && filtermap != FILTERMAP_FOREACH)
 	{
 	    l->lv_first = NULL;
 	    l->lv_u.mat.lv_last = NULL;
@@ -2444,27 +2458,30 @@ list_filter_map(
 		clear_tv(&newtv);
 		break;
 	    }
-	    if (filtermap != FILTERMAP_FILTER)
+	    if (filtermap != FILTERMAP_FOREACH)
 	    {
-		if (filtermap == FILTERMAP_MAP && argtype != NULL
+		if (filtermap != FILTERMAP_FILTER)
+		{
+		    if (filtermap == FILTERMAP_MAP && argtype != NULL
 			&& check_typval_arg_type(
-			    argtype->tt_member, &newtv,
-			    func_name, 0) == FAIL)
-		{
-		    clear_tv(&newtv);
-		    break;
+						 argtype->tt_member, &newtv,
+						 func_name, 0) == FAIL)
+		    {
+			clear_tv(&newtv);
+			break;
+		    }
+		    // map(), mapnew(): always append the new value to the
+		    // list
+		    if (list_append_tv_move(filtermap == FILTERMAP_MAP
+					    ? l : l_ret, &newtv) == FAIL)
+			break;
 		}
-		// map(), mapnew(): always append the new value to the
-		// list
-		if (list_append_tv_move(filtermap == FILTERMAP_MAP
-			    ? l : l_ret, &newtv) == FAIL)
-		    break;
-	    }
-	    else if (!rem)
-	    {
-		// filter(): append the list item value when not rem
-		if (list_append_tv_move(l, &tv) == FAIL)
-		    break;
+		else if (!rem)
+		{
+		    // filter(): append the list item value when not rem
+		    if (list_append_tv_move(l, &tv) == FAIL)
+			break;
+		}
 	    }
 
 	    val += stride;
@@ -2508,7 +2525,7 @@ list_filter_map(
 		    break;
 	    }
 	    else if (filtermap == FILTERMAP_FILTER && rem)
-		listitem_remove(l, li);
+		    listitem_remove(l, li);
 	    ++idx;
 	}
     }
@@ -2519,7 +2536,7 @@ list_filter_map(
 }
 
 /*
- * Implementation of map() and filter().
+ * Implementation of map(), filter() and foreach().
  */
     static void
 filter_map(typval_T *argvars, typval_T *rettv, filtermap_T filtermap)
@@ -2527,16 +2544,19 @@ filter_map(typval_T *argvars, typval_T *
     typval_T	*expr;
     char	*func_name = filtermap == FILTERMAP_MAP ? "map()"
 				  : filtermap == FILTERMAP_MAPNEW ? "mapnew()"
-				  : "filter()";
+				  : filtermap == FILTERMAP_FILTER ? "filter()"
+				  : "foreach()";
     char_u	*arg_errmsg = (char_u *)(filtermap == FILTERMAP_MAP
 							 ? N_("map() argument")
 				       : filtermap == FILTERMAP_MAPNEW
 						      ? N_("mapnew() argument")
-						    : N_("filter() argument"));
+				       : filtermap == FILTERMAP_FILTER
+						      ? N_("filter() argument")
+						   : N_("foreach() argument"));
     int		save_did_emsg;
     type_T	*type = NULL;
 
-    // map() and filter() return the first argument, also on failure.
+    // map(), filter(), foreach() return the first argument, also on failure.
     if (filtermap != FILTERMAP_MAPNEW && argvars[0].v_type != VAR_STRING)
 	copy_tv(&argvars[0], rettv);
 
@@ -2630,6 +2650,15 @@ f_mapnew(typval_T *argvars, typval_T *re
 }
 
 /*
+ * "foreach()" function
+ */
+    void
+f_foreach(typval_T *argvars, typval_T *rettv)
+{
+    filter_map(argvars, rettv, FILTERMAP_FOREACH);
+}
+
+/*
  * "add(list, item)" function
  */
     static void
--- a/src/proto/list.pro
+++ b/src/proto/list.pro
@@ -56,6 +56,7 @@ int filter_map_one(typval_T *tv, typval_
 void f_filter(typval_T *argvars, typval_T *rettv);
 void f_map(typval_T *argvars, typval_T *rettv);
 void f_mapnew(typval_T *argvars, typval_T *rettv);
+void f_foreach(typval_T *argvars, typval_T *rettv);
 void f_add(typval_T *argvars, typval_T *rettv);
 void f_count(typval_T *argvars, typval_T *rettv);
 void f_extend(typval_T *argvars, typval_T *rettv);
--- a/src/strings.c
+++ b/src/strings.c
@@ -942,7 +942,6 @@ string_filter_map(
 	    break;
 	len = (int)STRLEN(tv.vval.v_string);
 
-	newtv.v_type = VAR_UNKNOWN;
 	set_vim_var_nr(VV_KEY, idx);
 	if (filter_map_one(&tv, expr, filtermap, fc, &newtv, &rem) == FAIL
 		|| did_emsg)
@@ -951,7 +950,7 @@ string_filter_map(
 	    clear_tv(&tv);
 	    break;
 	}
-	else if (filtermap != FILTERMAP_FILTER)
+	if (filtermap == FILTERMAP_MAP || filtermap == FILTERMAP_MAPNEW)
 	{
 	    if (newtv.v_type != VAR_STRING)
 	    {
@@ -963,7 +962,7 @@ string_filter_map(
 	    else
 		ga_concat(&ga, newtv.vval.v_string);
 	}
-	else if (!rem)
+	else if (filtermap == FILTERMAP_FOREACH || !rem)
 	    ga_concat(&ga, tv.vval.v_string);
 
 	clear_tv(&newtv);
--- a/src/structs.h
+++ b/src/structs.h
@@ -4879,11 +4879,12 @@ typedef struct {
     hashtab_T	sve_hashtab;
 } save_v_event_T;
 
-// Enum used by filter(), map() and mapnew()
+// Enum used by filter(), map(), mapnew() and foreach()
 typedef enum {
     FILTERMAP_FILTER,
     FILTERMAP_MAP,
-    FILTERMAP_MAPNEW
+    FILTERMAP_MAPNEW,
+    FILTERMAP_FOREACH
 } filtermap_T;
 
 // Structure used by switch_win() to pass values to restore_win()
--- a/src/testdir/test_filter_map.vim
+++ b/src/testdir/test_filter_map.vim
@@ -14,6 +14,18 @@ func Test_filter_map_list_expr_string()
   call assert_equal([0, 2, 4, 6], map([1, 2, 3, 4], 'v:key * 2'))
   call assert_equal([9, 9, 9, 9], map([1, 2, 3, 4], 9))
   call assert_equal([7, 7, 7], map([1, 2, 3], ' 7 '))
+
+  " foreach()
+  let list01 = [1, 2, 3, 4]
+  let list02 = []
+  call assert_equal([1, 2, 3, 4], foreach(list01, 'call add(list02, v:val * 2)'))
+  call assert_equal([2, 4, 6, 8], list02)
+  let list02 = []
+  call assert_equal([1, 2, 3, 4], foreach(list01, 'call add(list02, v:key * 2)'))
+  call assert_equal([0, 2, 4, 6], list02)
+  let list02 = []
+  call assert_equal([1, 2, 3, 4], foreach(list01, 'call add(list02, 9)'))
+  call assert_equal([9, 9, 9, 9], list02)
 endfunc
 
 " dict with expression string
@@ -29,6 +41,14 @@ func Test_filter_map_dict_expr_string()
   call assert_equal({"foo": 2, "bar": 4, "baz": 6}, map(copy(dict), 'v:val * 2'))
   call assert_equal({"foo": "f", "bar": "b", "baz": "b"}, map(copy(dict), 'v:key[0]'))
   call assert_equal({"foo": 9, "bar": 9, "baz": 9}, map(copy(dict), 9))
+
+  " foreach()
+  let dict01 = {}
+  call assert_equal(dict, foreach(copy(dict), 'let dict01[v:key] = v:val * 2'))
+  call assert_equal({"foo": 2, "bar": 4, "baz": 6}, dict01)
+  let dict01 = {}
+  call assert_equal(dict, foreach(copy(dict), 'let dict01[v:key] = v:key[0]'))
+  call assert_equal({"foo": "f", "bar": "b", "baz": "b"}, dict01)
 endfunc
 
 " list with funcref
@@ -54,6 +74,16 @@ func Test_filter_map_list_expr_funcref()
     return a:index * 2
   endfunc
   call assert_equal([0, 2, 4, 6], map([1, 2, 3, 4], function('s:filter4')))
+
+  " foreach()
+  func! s:foreach1(index, val) abort
+    call add(g:test_variable, a:val + 1)
+    return [ 11, 12, 13, 14 ]
+  endfunc
+  let g:test_variable = []
+  call assert_equal([0, 1, 2, 3, 4], foreach(range(5), function('s:foreach1')))
+  call assert_equal([1, 2, 3, 4, 5], g:test_variable)
+  call remove(g:, 'test_variable')
 endfunc
 
 func Test_filter_map_nested()
@@ -90,11 +120,46 @@ func Test_filter_map_dict_expr_funcref()
     return a:key[0]
   endfunc
   call assert_equal({"foo": "f", "bar": "b", "baz": "b"}, map(copy(dict), function('s:filter4')))
+
+  " foreach()
+  func! s:foreach1(key, val) abort
+    call extend(g:test_variable, {a:key: a:val * 2})
+    return [ 11, 12, 13, 14 ]
+  endfunc
+  let g:test_variable = {}
+  call assert_equal(dict, foreach(copy(dict), function('s:foreach1')))
+  call assert_equal({"foo": 2, "bar": 4, "baz": 6}, g:test_variable)
+  call remove(g:, 'test_variable')
+endfunc
+
+func Test_map_filter_locked()
+  let list01 = [1, 2, 3, 4]
+  lockvar 1 list01
+  call assert_fails('call filter(list01, "v:val > 1")', 'E741:')
+  call assert_equal([2, 4, 6, 8], map(list01, 'v:val * 2'))
+  call assert_equal([1, 2, 3, 4], map(list01, 'v:val / 2'))
+  call assert_equal([2, 4, 6, 8], mapnew(list01, 'v:val * 2'))
+  let g:test_variable = []
+  call assert_equal([1, 2, 3, 4], foreach(list01, 'call add(g:test_variable, v:val * 2)'))
+  call remove(g:, 'test_variable')
+  call assert_fails('call filter(list01, "v:val > 1")', 'E741:')
+  unlockvar 1 list01
+  lockvar! list01
+  call assert_fails('call filter(list01, "v:val > 1")', 'E741:')
+  call assert_fails('call map(list01, "v:val * 2")', 'E741:')
+  call assert_equal([2, 4, 6, 8], mapnew(list01, 'v:val * 2'))
+  let g:test_variable = []
+  call assert_equal([1, 2, 3, 4], foreach(list01, 'call add(g:test_variable, v:val * 2)'))
+  call assert_fails('call foreach(list01, "let list01[0] = -1")', 'E741:')
+  call assert_fails('call filter(list01, "v:val > 1")', 'E741:')
+  call remove(g:, 'test_variable')
+  unlockvar! list01
 endfunc
 
 func Test_map_filter_fails()
   call assert_fails('call map([1], "42 +")', 'E15:')
   call assert_fails('call filter([1], "42 +")', 'E15:')
+  call assert_fails('call foreach([1], "let a = }")', 'E15:')
   call assert_fails("let l = filter([1, 2, 3], '{}')", 'E728:')
   call assert_fails("let l = filter({'k' : 10}, '{}')", 'E728:')
   call assert_fails("let l = filter([1, 2], {})", 'E731:')
@@ -106,6 +171,8 @@ func Test_map_filter_fails()
   call assert_fails("let l = filter([1, 2], function('min'))", 'E118:')
   call assert_equal([1, 2, 3], filter([1, 2, 3], test_null_partial()))
   call assert_fails("let l = filter([1, 2], {a, b, c -> 1})", 'E119:')
+  call assert_fails('call foreach([1], "xyzzy")', 'E492:')
+  call assert_fails('call foreach([1], "let a = foo")', 'E121:')
 endfunc
 
 func Test_map_and_modify()
@@ -123,7 +190,7 @@ endfunc
 
 func Test_filter_and_modify()
   let l = [0]
-  " cannot change the list halfway a map()
+  " cannot change the list halfway thru filter()
   call assert_fails('call filter(l, "remove(l, 0)")', 'E741:')
 
   let d = #{a: 0, b: 0, c: 0}
@@ -133,6 +200,18 @@ func Test_filter_and_modify()
   call assert_fails('call filter(b, "remove(b, 0)")', 'E741:')
 endfunc
 
+func Test_foreach_and_modify()
+  let l = [0]
+  " cannot change the list halfway thru foreach()
+  call assert_fails('call foreach(l, "let a = remove(l, 0)")', 'E741:')
+
+  let d = #{a: 0, b: 0, c: 0}
+  call assert_fails('call foreach(d, "let a = remove(d, v:key)")', 'E741:')
+
+  let b = 0z1234
+  call assert_fails('call foreach(b, "let a = remove(b, 0)")', 'E741:')
+endfunc
+
 func Test_mapnew_dict()
   let din = #{one: 1, two: 2}
   let dout = mapnew(din, {k, v -> string(v)})
@@ -160,6 +239,36 @@ func Test_mapnew_blob()
   call assert_equal(0z129956, bout)
 endfunc
 
+func Test_foreach_blob()
+  let lines =<< trim END
+    LET g:test_variable = []
+    call assert_equal(0z0001020304, foreach(0z0001020304, 'call add(g:test_variable, v:val)'))
+    call assert_equal([0, 1, 2, 3, 4], g:test_variable)
+  END
+  call v9.CheckLegacyAndVim9Success(lines)
+
+  func! s:foreach1(index, val) abort
+    call add(g:test_variable, a:val)
+    return [ 11, 12, 13, 14 ]
+  endfunc
+  let g:test_variable = []
+  call assert_equal(0z0001020304, foreach(0z0001020304, function('s:foreach1')))
+  call assert_equal([0, 1, 2, 3, 4], g:test_variable)
+
+  let lines =<< trim END
+    def Foreach1(_, val: any): list<number>
+      add(g:test_variable, val)
+      return [ 11, 12, 13, 14 ]
+    enddef
+    g:test_variable = []
+    assert_equal(0z0001020304, foreach(0z0001020304, Foreach1))
+    assert_equal([0, 1, 2, 3, 4], g:test_variable)
+  END
+  call v9.CheckDefSuccess(lines)
+
+  call remove(g:, 'test_variable')
+endfunc
+
 " Test for using map(), filter() and mapnew() with a string
 func Test_filter_map_string()
   " filter()
@@ -219,6 +328,37 @@ func Test_filter_map_string()
   END
   call v9.CheckLegacyAndVim9Success(lines)
 
+  " foreach()
+  let lines =<< trim END
+    VAR s = "abc"
+    LET g:test_variable = []
+    call assert_equal(s, foreach(s, 'call add(g:test_variable, v:val)'))
+    call assert_equal(['a', 'b', 'c'], g:test_variable)
+    LET g:test_variable = []
+    LET s = 'あiうえお'
+    call assert_equal(s, foreach(s, 'call add(g:test_variable, v:val)'))
+    call assert_equal(['あ', 'i', 'う', 'え', 'お'], g:test_variable)
+  END
+  call v9.CheckLegacyAndVim9Success(lines)
+  func! s:foreach1(index, val) abort
+    call add(g:test_variable, a:val)
+    return [ 11, 12, 13, 14 ]
+  endfunc
+  let g:test_variable = []
+  call assert_equal('abcd', foreach('abcd', function('s:foreach1')))
+  call assert_equal(['a', 'b', 'c', 'd'], g:test_variable)
+  let lines =<< trim END
+    def Foreach1(_, val: string): list<number>
+      add(g:test_variable, val)
+      return [ 11, 12, 13, 14 ]
+    enddef
+    g:test_variable = []
+    assert_equal('abcd', foreach('abcd', Foreach1))
+    assert_equal(['a', 'b', 'c', 'd'], g:test_variable)
+  END
+  call v9.CheckDefSuccess(lines)
+  call remove(g:, 'test_variable')
+
   let lines =<< trim END
     #" map() and filter()
     call assert_equal('[あ][⁈][a][😊][⁉][💕][💕][b][💕]', map(filter('あx⁈ax😊x⁉💕💕b💕x', '"x" != v:val'), '"[" .. v:val .. "]"'))
--- a/src/version.c
+++ b/src/version.c
@@ -705,6 +705,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    27,
+/**/
     26,
 /**/
     25,