# HG changeset patch # User Bram Moolenaar # Date 1639853103 -3600 # Node ID 6fd15d82e89848929bb02faa4b7e4622e1c740b3 # Parent eeac85e187e7639b20bd1e7dfa31b37f679ddb99 patch 8.2.3848: cannot use reduce() for a string Commit: https://github.com/vim/vim/commit/0ccb5842f5fb103763d106c7aa364d758343c35a Author: rbtnn Date: Sat Dec 18 18:33:46 2021 +0000 patch 8.2.3848: cannot use reduce() for a string Problem: Cannot use reduce() for a string. Solution: Make reduce() work with a string. (Naruhiko Nishino, closes https://github.com/vim/vim/issues/9366) diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -8959,9 +8959,9 @@ readfile({fname} [, {type} [, {max}]]) reduce({object}, {func} [, {initial}]) *reduce()* *E998* {func} is called for every item in {object}, which can be a - |List| or a |Blob|. {func} is called with two arguments: the - result so far and current item. After processing all items - the result is returned. + |String|, |List| or a |Blob|. {func} is called with two arguments: + the result so far and current item. After processing all + items the result is returned. {initial} is the initial result. When omitted, the first item in {object} is used and {func} is first called for the second @@ -8972,6 +8972,7 @@ reduce({object}, {func} [, {initial}]) echo reduce([1, 3, 5], { acc, val -> acc + val }) echo reduce(['x', 'y'], { acc, val -> acc .. val }, 'a') echo reduce(0z1122, { acc, val -> 2 * acc + val }) + echo reduce('xyz', { acc, val -> acc .. ',' .. val }) < Can also be used as a |method|: > echo mylist->reduce({ acc, val -> acc + val }, 0) diff --git a/src/errors.h b/src/errors.h --- a/src/errors.h +++ b/src/errors.h @@ -843,4 +843,8 @@ EXTERN char e_highlight_group_name_too_l EXTERN char e_argument_of_str_must_be_list_string_dictionary_or_blob[] INIT(= N_("E1250: Argument of %s must be a List, String, Dictionary or Blob")); EXTERN char e_list_dict_blob_or_string_required_for_argument_nr[] - INIT(= N_("E1228: List, Dictionary, Blob or String required for argument %d")); + INIT(= N_("E1251: List, Dictionary, Blob or String required for argument %d")); +EXTERN char e_string_list_or_blob_required_for_argument_nr[] + INIT(= N_("E1252: String, List or Blob required for argument %d")); +EXTERN char e_string_expected_for_argument_nr[] + INIT(= N_("E1253: String expected for argument %d")); diff --git a/src/evalfunc.c b/src/evalfunc.c --- a/src/evalfunc.c +++ b/src/evalfunc.c @@ -465,6 +465,21 @@ arg_list_or_dict_or_blob_or_string(type_ } /* + * Check "type" is a list of 'any' or a blob or a string. + */ + static int +arg_string_list_or_blob(type_T *type, argcontext_T *context) +{ + if (type->tt_type == VAR_ANY + || type->tt_type == VAR_LIST + || type->tt_type == VAR_BLOB + || type->tt_type == VAR_STRING) + return OK; + arg_type_mismatch(&t_list_any, type, context->arg_idx + 1); + return FAIL; +} + +/* * Check "type" is a job. */ static int @@ -817,7 +832,7 @@ static argcheck_T arg2_mapfilter[] = {ar static argcheck_T arg25_matchadd[] = {arg_string, arg_string, arg_number, arg_number, arg_dict_any}; static argcheck_T arg25_matchaddpos[] = {arg_string, arg_list_any, arg_number, arg_number, arg_dict_any}; static argcheck_T arg119_printf[] = {arg_string_or_nr, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL}; -static argcheck_T arg23_reduce[] = {arg_list_or_blob, NULL, NULL}; +static argcheck_T arg23_reduce[] = {arg_string_list_or_blob, NULL, NULL}; static argcheck_T arg24_remote_expr[] = {arg_string, arg_string, arg_string, arg_number}; static argcheck_T arg23_remove[] = {arg_list_or_dict_or_blob, arg_remove2, arg_number}; static argcheck_T arg2_repeat[] = {arg_repeat1, arg_number}; diff --git a/src/list.c b/src/list.c --- a/src/list.c +++ b/src/list.c @@ -314,6 +314,28 @@ listitem_alloc(void) } /* + * Make a typval_T of the first character of "input" and store it in "output". + * Return OK or FAIL. + */ + static int +tv_get_first_char(char_u *input, typval_T *output) +{ + char_u buf[MB_MAXBYTES + 1]; + int len; + + if (input == NULL || output == NULL) + return FAIL; + + len = has_mbyte ? mb_ptr2len(input) : 1; + STRNCPY(buf, input, len); + buf[len] = NUL; + output->v_type = VAR_STRING; + output->vval.v_string = vim_strsave(buf); + + return output->vval.v_string == NULL ? FAIL : OK; +} + +/* * Free a list item, unless it was allocated together with the list itself. * Does not clear the value. Does not notify watchers. */ @@ -2492,7 +2514,6 @@ filter_map(typval_T *argvars, typval_T * char_u *p; typval_T tv; garray_T ga; - char_u buf[MB_MAXBYTES + 1]; int len; // set_vim_var_nr() doesn't set the type @@ -2503,16 +2524,9 @@ filter_map(typval_T *argvars, typval_T * { typval_T newtv; - if (has_mbyte) - len = mb_ptr2len(p); - else - len = 1; - - STRNCPY(buf, p, len); - buf[len] = NUL; - - tv.v_type = VAR_STRING; - tv.vval.v_string = vim_strsave(buf); + if (tv_get_first_char(p, &tv) == FAIL) + break; + len = STRLEN(tv.vval.v_string); set_vim_var_nr(VV_KEY, idx); if (filter_map_one(&tv, expr, filtermap, &newtv, &rem) == FAIL @@ -3248,12 +3262,17 @@ f_reduce(typval_T *argvars, typval_T *re partial_T *partial = NULL; funcexe_T funcexe; typval_T argv[3]; - - if (argvars[0].v_type != VAR_LIST && argvars[0].v_type != VAR_BLOB) - { - emsg(_(e_listblobreq)); + int r; + int called_emsg_start = called_emsg; + + if (in_vim9script() + && check_for_string_or_list_or_blob_arg(argvars, 0) == FAIL) return; - } + + if (argvars[0].v_type != VAR_STRING + && argvars[0].v_type != VAR_LIST + && argvars[0].v_type != VAR_BLOB) + semsg(_(e_string_list_or_blob_required), "reduce()"); if (argvars[1].v_type == VAR_FUNC) func_name = argvars[1].vval.v_string; @@ -3278,8 +3297,6 @@ f_reduce(typval_T *argvars, typval_T *re { list_T *l = argvars[0].vval.v_list; listitem_T *li = NULL; - int r; - int called_emsg_start = called_emsg; if (l != NULL) CHECK_LIST_MATERIALIZE(l); @@ -3319,6 +3336,43 @@ f_reduce(typval_T *argvars, typval_T *re l->lv_lock = prev_locked; } } + else if (argvars[0].v_type == VAR_STRING) + { + char_u *p = tv_get_string(&argvars[0]); + int len; + + if (argvars[2].v_type == VAR_UNKNOWN) + { + if (*p == NUL) + { + semsg(_(e_reduceempty), "String"); + return; + } + if (tv_get_first_char(p, rettv) == FAIL) + return; + p += STRLEN(rettv->vval.v_string); + } + else if (argvars[2].v_type != VAR_STRING) + { + semsg(_(e_string_expected_for_argument_nr), 3); + return; + } + else + copy_tv(&argvars[2], rettv); + + for ( ; *p != NUL; p += len) + { + argv[0] = *rettv; + if (tv_get_first_char(p, &argv[1]) == FAIL) + break; + len = STRLEN(argv[1].vval.v_string); + r = call_func(func_name, -1, rettv, 2, argv, &funcexe); + clear_tv(&argv[0]); + clear_tv(&argv[1]); + if (r == FAIL || called_emsg != called_emsg_start) + break; + } + } else { blob_T *b = argvars[0].vval.v_blob; diff --git a/src/proto/typval.pro b/src/proto/typval.pro --- a/src/proto/typval.pro +++ b/src/proto/typval.pro @@ -34,6 +34,7 @@ int check_for_opt_lnum_arg(typval_T *arg int check_for_opt_string_or_number_arg(typval_T *args, int idx); int check_for_string_or_blob_arg(typval_T *args, int idx); int check_for_string_or_list_arg(typval_T *args, int idx); +int check_for_string_or_list_or_blob_arg(typval_T *args, int idx); int check_for_opt_string_or_list_arg(typval_T *args, int idx); int check_for_string_or_dict_arg(typval_T *args, int idx); int check_for_string_or_number_or_list_arg(typval_T *args, int idx); diff --git a/src/testdir/test_listdict.vim b/src/testdir/test_listdict.vim --- a/src/testdir/test_listdict.vim +++ b/src/testdir/test_listdict.vim @@ -1,4 +1,5 @@ " Tests for the List and Dict types +scriptencoding utf-8 source vim9.vim @@ -936,7 +937,7 @@ func Test_reverse_sort_uniq() call assert_fails("call sort([1, 2], function('min'))", "E118:") endfunc -" reduce a list or a blob +" reduce a list, blob or string func Test_reduce() let lines =<< trim END call assert_equal(1, reduce([], LSTART acc, val LMIDDLE acc + val LEND, 1)) @@ -959,6 +960,16 @@ func Test_reduce() call assert_equal(0xff, reduce(0zff, LSTART acc, val LMIDDLE acc + val LEND)) call assert_equal(2 * (2 * 0xaf + 0xbf) + 0xcf, reduce(0zAFBFCF, LSTART acc, val LMIDDLE 2 * acc + val LEND)) + + call assert_equal('x,y,z', 'xyz'->reduce(LSTART acc, val LMIDDLE acc .. ',' .. val LEND)) + call assert_equal('', ''->reduce(LSTART acc, val LMIDDLE acc .. ',' .. val LEND, '')) + call assert_equal('あ,い,う,え,お,😊,💕', 'あいうえお😊💕'->reduce(LSTART acc, val LMIDDLE acc .. ',' .. val LEND)) + call assert_equal('😊,あ,い,う,え,お,💕', 'あいうえお💕'->reduce(LSTART acc, val LMIDDLE acc .. ',' .. val LEND, '😊')) + call assert_equal('ऊ,ॠ,ॡ', reduce('ऊॠॡ', LSTART acc, val LMIDDLE acc .. ',' .. val LEND)) + call assert_equal('c,à,t', reduce('càt', LSTART acc, val LMIDDLE acc .. ',' .. val LEND)) + call assert_equal('Å,s,t,r,ö,m', reduce('Åström', LSTART acc, val LMIDDLE acc .. ',' .. val LEND)) + call assert_equal('Å,s,t,r,ö,m', reduce('Åström', LSTART acc, val LMIDDLE acc .. ',' .. val LEND)) + call assert_equal(',a,b,c', reduce('abc', LSTART acc, val LMIDDLE acc .. ',' .. val LEND, test_null_string())) END call CheckLegacyAndVim9Success(lines) @@ -967,13 +978,23 @@ func Test_reduce() call assert_fails("call reduce([], { acc, val -> acc + val })", 'E998: Reduce of an empty List with no initial value') call assert_fails("call reduce(0z, { acc, val -> acc + val })", 'E998: Reduce of an empty Blob with no initial value') + call assert_fails("call reduce('', { acc, val -> acc + val })", 'E998: Reduce of an empty String with no initial value') + call assert_fails("call reduce(test_null_string(), { acc, val -> acc + val })", 'E998: Reduce of an empty String with no initial value') - call assert_fails("call reduce({}, { acc, val -> acc + val }, 1)", 'E897:') - call assert_fails("call reduce(0, { acc, val -> acc + val }, 1)", 'E897:') - call assert_fails("call reduce('', { acc, val -> acc + val }, 1)", 'E897:') + call assert_fails("call reduce({}, { acc, val -> acc + val }, 1)", 'E1098:') + call assert_fails("call reduce(0, { acc, val -> acc + val }, 1)", 'E1098:') call assert_fails("call reduce([1, 2], 'Xdoes_not_exist')", 'E117:') call assert_fails("echo reduce(0z01, { acc, val -> 2 * acc + val }, '')", 'E39:') + call assert_fails("vim9 reduce(0, (acc, val) => (acc .. val), '')", 'E1252:') + call assert_fails("vim9 reduce({}, (acc, val) => (acc .. val), '')", 'E1252:') + call assert_fails("vim9 reduce(0.1, (acc, val) => (acc .. val), '')", 'E1252:') + call assert_fails("vim9 reduce(function('tr'), (acc, val) => (acc .. val), '')", 'E1252:') + call assert_fails("call reduce('', { acc, val -> acc + val }, 1)", 'E1253:') + call assert_fails("call reduce('', { acc, val -> acc + val }, {})", 'E1253:') + call assert_fails("call reduce('', { acc, val -> acc + val }, 0.1)", 'E1253:') + call assert_fails("call reduce('', { acc, val -> acc + val }, function('tr'))", 'E1253:') + let g:lut = [1, 2, 3, 4] func EvilRemove() call remove(g:lut, 1) diff --git a/src/testdir/test_vim9_builtin.vim b/src/testdir/test_vim9_builtin.vim --- a/src/testdir/test_vim9_builtin.vim +++ b/src/testdir/test_vim9_builtin.vim @@ -1232,7 +1232,7 @@ def Wrong_dict_key_type(items: list but got float', 'E1228: List, Dictionary, Blob or String required for argument 1') + CheckDefAndScriptFailure2(['filter(1.1, "1")'], 'E1013: Argument 1: type mismatch, expected list but got float', 'E1251: List, Dictionary, Blob or String required for argument 1') assert_equal([], filter([1, 2, 3], '0')) assert_equal([1, 2, 3], filter([1, 2, 3], '1')) assert_equal({b: 20}, filter({a: 10, b: 20}, 'v:val == 20')) @@ -2028,9 +2028,9 @@ enddef def Test_map() if has('channel') - CheckDefAndScriptFailure2(['map(test_null_channel(), "1")'], 'E1013: Argument 1: type mismatch, expected list but got channel', 'E1228: List, Dictionary, Blob or String required for argument 1') + CheckDefAndScriptFailure2(['map(test_null_channel(), "1")'], 'E1013: Argument 1: type mismatch, expected list but got channel', 'E1251: List, Dictionary, Blob or String required for argument 1') endif - CheckDefAndScriptFailure2(['map(1, "1")'], 'E1013: Argument 1: type mismatch, expected list but got number', 'E1228: List, Dictionary, Blob or String required for argument 1') + CheckDefAndScriptFailure2(['map(1, "1")'], 'E1013: Argument 1: type mismatch, expected list but got number', 'E1251: List, Dictionary, Blob or String required for argument 1') enddef def Test_map_failure() @@ -2147,9 +2147,9 @@ enddef def Test_mapnew() if has('channel') - CheckDefAndScriptFailure2(['mapnew(test_null_job(), "1")'], 'E1013: Argument 1: type mismatch, expected list but got job', 'E1228: List, Dictionary, Blob or String required for argument 1') + CheckDefAndScriptFailure2(['mapnew(test_null_job(), "1")'], 'E1013: Argument 1: type mismatch, expected list but got job', 'E1251: List, Dictionary, Blob or String required for argument 1') endif - CheckDefAndScriptFailure2(['mapnew(1, "1")'], 'E1013: Argument 1: type mismatch, expected list but got number', 'E1228: List, Dictionary, Blob or String required for argument 1') + CheckDefAndScriptFailure2(['mapnew(1, "1")'], 'E1013: Argument 1: type mismatch, expected list but got number', 'E1251: List, Dictionary, Blob or String required for argument 1') enddef def Test_mapset() @@ -2682,7 +2682,7 @@ def Test_readfile() enddef def Test_reduce() - CheckDefAndScriptFailure2(['reduce({a: 10}, "1")'], 'E1013: Argument 1: type mismatch, expected list but got dict', 'E897: List or Blob required') + CheckDefAndScriptFailure2(['reduce({a: 10}, "1")'], 'E1013: Argument 1: type mismatch, expected list but got dict', 'E1252: String, List or Blob required for argument 1') assert_equal(6, [1, 2, 3]->reduce((r, c) => r + c, 0)) assert_equal(11, 0z0506->reduce((r, c) => r + c, 0)) enddef diff --git a/src/typval.c b/src/typval.c --- a/src/typval.c +++ b/src/typval.c @@ -663,6 +663,23 @@ check_for_string_or_list_arg(typval_T *a } /* + * Give an error and return FAIL unless "args[idx]" is a string, a list or a + * blob. + */ + int +check_for_string_or_list_or_blob_arg(typval_T *args, int idx) +{ + if (args[idx].v_type != VAR_STRING + && args[idx].v_type != VAR_LIST + && args[idx].v_type != VAR_BLOB) + { + semsg(_(e_string_list_or_blob_required_for_argument_nr), idx + 1); + return FAIL; + } + return OK; +} + +/* * Check for an optional string or list argument at 'idx' */ int @@ -697,10 +714,7 @@ check_for_string_or_number_or_list_arg(t && args[idx].v_type != VAR_NUMBER && args[idx].v_type != VAR_LIST) { - if (idx >= 0) - semsg(_(e_string_number_or_list_required_for_argument_nr), idx + 1); - else - emsg(_(e_stringreq)); + semsg(_(e_string_number_or_list_required_for_argument_nr), idx + 1); return FAIL; } return OK; @@ -742,10 +756,7 @@ check_for_list_or_blob_arg(typval_T *arg { if (args[idx].v_type != VAR_LIST && args[idx].v_type != VAR_BLOB) { - if (idx >= 0) - semsg(_(e_list_or_blob_required_for_argument_nr), idx + 1); - else - emsg(_(e_listreq)); + semsg(_(e_list_or_blob_required_for_argument_nr), idx + 1); return FAIL; } return OK; diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -750,6 +750,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 3848, +/**/ 3847, /**/ 3846,