Skip to content

Commit 74e54fc

Browse files
committed
patch 8.2.2658: :for cannot loop over a string
Problem: :for cannot loop over a string. Solution: Accept a string argument and iterate over its characters.
1 parent 522eefd commit 74e54fc

File tree

9 files changed

+163
-32
lines changed

9 files changed

+163
-32
lines changed

runtime/doc/eval.txt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -439,8 +439,8 @@ Changing the order of items in a list: >
439439

440440
For loop ~
441441

442-
The |:for| loop executes commands for each item in a list. A variable is set
443-
to each item in the list in sequence. Example: >
442+
The |:for| loop executes commands for each item in a List, String or Blob.
443+
A variable is set to each item in sequence. Example with a List: >
444444
:for item in mylist
445445
: call Doit(item)
446446
:endfor
@@ -457,7 +457,7 @@ If all you want to do is modify each item in the list then the |map()|
457457
function will be a simpler method than a for loop.
458458

459459
Just like the |:let| command, |:for| also accepts a list of variables. This
460-
requires the argument to be a list of lists. >
460+
requires the argument to be a List of Lists. >
461461
:for [lnum, col] in [[1, 3], [2, 8], [3, 0]]
462462
: call Doit(lnum, col)
463463
:endfor
@@ -473,6 +473,14 @@ It is also possible to put remaining items in a List variable: >
473473
: endif
474474
:endfor
475475

476+
For a Blob one byte at a time is used.
477+
478+
For a String one character, including any composing characters, is used as a
479+
String. Example: >
480+
for c in text
481+
echo 'This character is ' .. c
482+
endfor
483+
476484

477485
List functions ~
478486
*E714*

src/errors.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,3 +389,5 @@ EXTERN char e_non_empty_string_required_for_argument_nr[]
389389
INIT(= N_("E1175: Non-empty string required for argument %d"));
390390
EXTERN char e_misplaced_command_modifier[]
391391
INIT(= N_("E1176: Misplaced command modifier"));
392+
EXTERN char e_for_loop_on_str_not_supported[]
393+
INIT(= N_("E1177: For loop on %s not supported"));

src/eval.c

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ typedef struct
4141
list_T *fi_list; // list being used
4242
int fi_bi; // index of blob
4343
blob_T *fi_blob; // blob being used
44+
char_u *fi_string; // copy of string being used
45+
int fi_byte_idx; // byte index in fi_string
4446
} forinfo_T;
4547

4648
static int tv_op(typval_T *tv1, typval_T *tv2, char_u *op);
@@ -1738,6 +1740,14 @@ eval_for_line(
17381740
}
17391741
clear_tv(&tv);
17401742
}
1743+
else if (tv.v_type == VAR_STRING)
1744+
{
1745+
fi->fi_byte_idx = 0;
1746+
fi->fi_string = tv.vval.v_string;
1747+
tv.vval.v_string = NULL;
1748+
if (fi->fi_string == NULL)
1749+
fi->fi_string = vim_strsave((char_u *)"");
1750+
}
17411751
else
17421752
{
17431753
emsg(_(e_listreq));
@@ -1790,7 +1800,23 @@ next_for_item(void *fi_void, char_u *arg)
17901800
tv.vval.v_number = blob_get(fi->fi_blob, fi->fi_bi);
17911801
++fi->fi_bi;
17921802
return ex_let_vars(arg, &tv, TRUE, fi->fi_semicolon,
1793-
fi->fi_varcount, flag, NULL) == OK;
1803+
fi->fi_varcount, flag, NULL) == OK;
1804+
}
1805+
1806+
if (fi->fi_string != NULL)
1807+
{
1808+
typval_T tv;
1809+
int len;
1810+
1811+
len = mb_ptr2len(fi->fi_string + fi->fi_byte_idx);
1812+
if (len == 0)
1813+
return FALSE;
1814+
tv.v_type = VAR_STRING;
1815+
tv.v_lock = VAR_FIXED;
1816+
tv.vval.v_string = vim_strnsave(fi->fi_string + fi->fi_byte_idx, len);
1817+
fi->fi_byte_idx += len;
1818+
return ex_let_vars(arg, &tv, TRUE, fi->fi_semicolon,
1819+
fi->fi_varcount, flag, NULL) == OK;
17941820
}
17951821

17961822
item = fi->fi_lw.lw_item;
@@ -1800,7 +1826,7 @@ next_for_item(void *fi_void, char_u *arg)
18001826
{
18011827
fi->fi_lw.lw_item = item->li_next;
18021828
result = (ex_let_vars(arg, &item->li_tv, TRUE, fi->fi_semicolon,
1803-
fi->fi_varcount, flag, NULL) == OK);
1829+
fi->fi_varcount, flag, NULL) == OK);
18041830
}
18051831
return result;
18061832
}
@@ -1813,13 +1839,17 @@ free_for_info(void *fi_void)
18131839
{
18141840
forinfo_T *fi = (forinfo_T *)fi_void;
18151841

1816-
if (fi != NULL && fi->fi_list != NULL)
1842+
if (fi == NULL)
1843+
return;
1844+
if (fi->fi_list != NULL)
18171845
{
18181846
list_rem_watch(fi->fi_list, &fi->fi_lw);
18191847
list_unref(fi->fi_list);
18201848
}
1821-
if (fi != NULL && fi->fi_blob != NULL)
1849+
else if (fi->fi_blob != NULL)
18221850
blob_unref(fi->fi_blob);
1851+
else
1852+
vim_free(fi->fi_string);
18231853
vim_free(fi);
18241854
}
18251855

src/testdir/test_vim9_disassemble.vim

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,7 +1061,6 @@ def Test_disassemble_for_loop_eval()
10611061
'\d STORE -1 in $1\_s*' ..
10621062
'\d PUSHS "\["one", "two"\]"\_s*' ..
10631063
'\d BCALL eval(argc 1)\_s*' ..
1064-
'\d CHECKTYPE list<any> stack\[-1\]\_s*' ..
10651064
'\d FOR $1 -> \d\+\_s*' ..
10661065
'\d STORE $2\_s*' ..
10671066
'res ..= str\_s*' ..
@@ -1071,7 +1070,7 @@ def Test_disassemble_for_loop_eval()
10711070
'\d\+ CONCAT\_s*' ..
10721071
'\d\+ STORE $0\_s*' ..
10731072
'endfor\_s*' ..
1074-
'\d\+ JUMP -> 6\_s*' ..
1073+
'\d\+ JUMP -> 5\_s*' ..
10751074
'\d\+ DROP\_s*' ..
10761075
'return res\_s*' ..
10771076
'\d\+ LOAD $0\_s*' ..

src/testdir/test_vim9_script.vim

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2322,6 +2322,25 @@ def Test_for_loop()
23222322
res ..= n .. s
23232323
endfor
23242324
assert_equal('1a2b', res)
2325+
2326+
# loop over string
2327+
res = ''
2328+
for c in 'aéc̀d'
2329+
res ..= c .. '-'
2330+
endfor
2331+
assert_equal('a-é-c̀-d-', res)
2332+
2333+
res = ''
2334+
for c in ''
2335+
res ..= c .. '-'
2336+
endfor
2337+
assert_equal('', res)
2338+
2339+
res = ''
2340+
for c in test_null_string()
2341+
res ..= c .. '-'
2342+
endfor
2343+
assert_equal('', res)
23252344
enddef
23262345

23272346
def Test_for_loop_fails()
@@ -2333,10 +2352,17 @@ def Test_for_loop_fails()
23332352
CheckDefFailure(['var x = 5', 'for x in range(5)'], 'E1017:')
23342353
CheckScriptFailure(['def Func(arg: any)', 'for arg in range(5)', 'enddef', 'defcompile'], 'E1006:')
23352354
delfunc! g:Func
2336-
CheckDefFailure(['for i in "text"'], 'E1012:')
23372355
CheckDefFailure(['for i in xxx'], 'E1001:')
23382356
CheckDefFailure(['endfor'], 'E588:')
23392357
CheckDefFailure(['for i in range(3)', 'echo 3'], 'E170:')
2358+
2359+
# wrong type detected at compile time
2360+
CheckDefFailure(['for i in {a: 1}', 'echo 3', 'endfor'], 'E1177: For loop on dict not supported')
2361+
2362+
# wrong type detected at runtime
2363+
g:adict = {a: 1}
2364+
CheckDefExecFailure(['for i in g:adict', 'echo 3', 'endfor'], 'E1177: For loop on dict not supported')
2365+
unlet g:adict
23402366
enddef
23412367

23422368
def Test_for_loop_script_var()

src/testdir/test_vimscript.vim

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7484,6 +7484,26 @@ func Test_trinary_expression()
74847484
call assert_equal(v:false, eval(string(v:false)))
74857485
endfunction
74867486

7487+
func Test_for_over_string()
7488+
let res = ''
7489+
for c in 'aéc̀d'
7490+
let res ..= c .. '-'
7491+
endfor
7492+
call assert_equal('a-é-c̀-d-', res)
7493+
7494+
let res = ''
7495+
for c in ''
7496+
let res ..= c .. '-'
7497+
endfor
7498+
call assert_equal('', res)
7499+
7500+
let res = ''
7501+
for c in test_null_string()
7502+
let res ..= c .. '-'
7503+
endfor
7504+
call assert_equal('', res)
7505+
endfunc
7506+
74877507
"-------------------------------------------------------------------------------
74887508
" Modelines {{{1
74897509
" vim: ts=8 sw=2 sts=2 expandtab tw=80 fdm=marker

src/version.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,8 @@ static char *(features[]) =
750750

751751
static int included_patches[] =
752752
{ /* Add new patch number below this line */
753+
/**/
754+
2658,
753755
/**/
754756
2657,
755757
/**/

src/vim9compile.c

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7264,11 +7264,15 @@ compile_for(char_u *arg_start, cctx_T *cctx)
72647264
}
72657265
arg_end = arg;
72667266

7267-
// Now that we know the type of "var", check that it is a list, now or at
7268-
// runtime.
7267+
// If we know the type of "var" and it is a not a list or string we can
7268+
// give an error now.
72697269
vartype = ((type_T **)stack->ga_data)[stack->ga_len - 1];
7270-
if (need_type(vartype, &t_list_any, -1, 0, cctx, FALSE, FALSE) == FAIL)
7270+
if (vartype->tt_type != VAR_LIST && vartype->tt_type != VAR_STRING
7271+
&& vartype->tt_type != VAR_ANY)
72717272
{
7273+
// TODO: support Blob
7274+
semsg(_(e_for_loop_on_str_not_supported),
7275+
vartype_name(vartype->tt_type));
72727276
drop_scope(cctx);
72737277
return NULL;
72747278
}

src/vim9execute.c

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2741,36 +2741,76 @@ call_def_function(
27412741
// top of a for loop
27422742
case ISN_FOR:
27432743
{
2744-
list_T *list = STACK_TV_BOT(-1)->vval.v_list;
2744+
typval_T *ltv = STACK_TV_BOT(-1);
27452745
typval_T *idxtv =
27462746
STACK_TV_VAR(iptr->isn_arg.forloop.for_idx);
27472747

2748-
// push the next item from the list
27492748
if (GA_GROW(&ectx.ec_stack, 1) == FAIL)
27502749
goto failed;
2751-
++idxtv->vval.v_number;
2752-
if (list == NULL || idxtv->vval.v_number >= list->lv_len)
2750+
if (ltv->v_type == VAR_LIST)
27532751
{
2754-
// past the end of the list, jump to "endfor"
2755-
ectx.ec_iidx = iptr->isn_arg.forloop.for_end;
2756-
may_restore_cmdmod(&funclocal);
2752+
list_T *list = ltv->vval.v_list;
2753+
2754+
// push the next item from the list
2755+
++idxtv->vval.v_number;
2756+
if (list == NULL
2757+
|| idxtv->vval.v_number >= list->lv_len)
2758+
{
2759+
// past the end of the list, jump to "endfor"
2760+
ectx.ec_iidx = iptr->isn_arg.forloop.for_end;
2761+
may_restore_cmdmod(&funclocal);
2762+
}
2763+
else if (list->lv_first == &range_list_item)
2764+
{
2765+
// non-materialized range() list
2766+
tv = STACK_TV_BOT(0);
2767+
tv->v_type = VAR_NUMBER;
2768+
tv->v_lock = 0;
2769+
tv->vval.v_number = list_find_nr(
2770+
list, idxtv->vval.v_number, NULL);
2771+
++ectx.ec_stack.ga_len;
2772+
}
2773+
else
2774+
{
2775+
listitem_T *li = list_find(list,
2776+
idxtv->vval.v_number);
2777+
2778+
copy_tv(&li->li_tv, STACK_TV_BOT(0));
2779+
++ectx.ec_stack.ga_len;
2780+
}
27572781
}
2758-
else if (list->lv_first == &range_list_item)
2782+
else if (ltv->v_type == VAR_STRING)
27592783
{
2760-
// non-materialized range() list
2761-
tv = STACK_TV_BOT(0);
2762-
tv->v_type = VAR_NUMBER;
2763-
tv->v_lock = 0;
2764-
tv->vval.v_number = list_find_nr(
2765-
list, idxtv->vval.v_number, NULL);
2766-
++ectx.ec_stack.ga_len;
2784+
char_u *str = ltv->vval.v_string;
2785+
int len = str == NULL ? 0 : (int)STRLEN(str);
2786+
2787+
// Push the next character from the string. The index
2788+
// is for the last byte of the previous character.
2789+
++idxtv->vval.v_number;
2790+
if (idxtv->vval.v_number >= len)
2791+
{
2792+
// past the end of the string, jump to "endfor"
2793+
ectx.ec_iidx = iptr->isn_arg.forloop.for_end;
2794+
may_restore_cmdmod(&funclocal);
2795+
}
2796+
else
2797+
{
2798+
int clen = mb_ptr2len(str + idxtv->vval.v_number);
2799+
2800+
tv = STACK_TV_BOT(0);
2801+
tv->v_type = VAR_STRING;
2802+
tv->vval.v_string = vim_strnsave(
2803+
str + idxtv->vval.v_number, clen);
2804+
++ectx.ec_stack.ga_len;
2805+
idxtv->vval.v_number += clen - 1;
2806+
}
27672807
}
27682808
else
27692809
{
2770-
listitem_T *li = list_find(list, idxtv->vval.v_number);
2771-
2772-
copy_tv(&li->li_tv, STACK_TV_BOT(0));
2773-
++ectx.ec_stack.ga_len;
2810+
// TODO: support Blob
2811+
semsg(_(e_for_loop_on_str_not_supported),
2812+
vartype_name(ltv->v_type));
2813+
goto failed;
27742814
}
27752815
}
27762816
break;

0 commit comments

Comments
 (0)