# HG changeset patch # User Bram Moolenaar # Date 1609598704 -3600 # Node ID d2b1269c2c68e7f6c0b4587b5338b7eb33e6414e # Parent 86b9697a8c634bb5d7453dbec0b95ea3a215b436 patch 8.2.2272: Vim9: extend() can violate the type of a variable Commit: https://github.com/vim/vim/commit/aa210a3aeccc33c6051978017959126b037f94af Author: Bram Moolenaar Date: Sat Jan 2 15:41:03 2021 +0100 patch 8.2.2272: Vim9: extend() can violate the type of a variable Problem: Vim9: extend() can violate the type of a variable. Solution: Add the type to the dictionary or list and check items against it. (closes #7593) diff --git a/src/dict.c b/src/dict.c --- a/src/dict.c +++ b/src/dict.c @@ -107,6 +107,8 @@ rettv_dict_set(typval_T *rettv, dict_T * dict_free_contents(dict_T *d) { hashtab_free_contents(&d->dv_hashtab); + free_type(d->dv_type); + d->dv_type = NULL; } /* @@ -1057,6 +1059,12 @@ dict_extend(dict_T *d1, dict_T *d2, char hashitem_T *hi2; int todo; char_u *arg_errmsg = (char_u *)N_("extend() argument"); + type_T *type; + + if (d1->dv_type != NULL && d1->dv_type->tt_member != NULL) + type = d1->dv_type->tt_member; + else + type = NULL; todo = (int)d2->dv_hashtab.ht_used; for (hi2 = d2->dv_hashtab.ht_array; todo > 0; ++hi2) @@ -1076,6 +1084,11 @@ dict_extend(dict_T *d1, dict_T *d2, char if (!valid_varname(hi2->hi_key, TRUE)) break; } + + if (type != NULL + && check_typval_type(type, &HI2DI(hi2)->di_tv, 0) == FAIL) + break; + if (di1 == NULL) { di1 = dictitem_copy(HI2DI(hi2)); diff --git a/src/evalvars.c b/src/evalvars.c --- a/src/evalvars.c +++ b/src/evalvars.c @@ -3147,9 +3147,9 @@ set_var_const( di->di_flags &= ~DI_FLAGS_RELOAD; // A Vim9 script-local variable is also present in sn_all_vars and - // sn_var_vals. + // sn_var_vals. It may set "type" from "tv". if (is_script_local && vim9script) - update_vim9_script_var(FALSE, di, tv, type); + update_vim9_script_var(FALSE, di, tv, &type); } // existing variable, need to clear the value @@ -3237,9 +3237,9 @@ set_var_const( di->di_flags |= DI_FLAGS_LOCK; // A Vim9 script-local variable is also added to sn_all_vars and - // sn_var_vals. + // sn_var_vals. It may set "type" from "tv". if (is_script_local && vim9script) - update_vim9_script_var(TRUE, di, tv, type); + update_vim9_script_var(TRUE, di, tv, &type); } if (copy || tv->v_type == VAR_NUMBER || tv->v_type == VAR_FLOAT) @@ -3251,6 +3251,14 @@ set_var_const( init_tv(tv); } + if (vim9script && type != NULL) + { + if (type->tt_type == VAR_DICT && di->di_tv.vval.v_dict != NULL) + di->di_tv.vval.v_dict->dv_type = alloc_type(type); + else if (type->tt_type == VAR_LIST && di->di_tv.vval.v_list != NULL) + di->di_tv.vval.v_list->lv_type = alloc_type(type); + } + // ":const var = value" locks the value // ":final var = value" locks "var" if (flags & ASSIGN_CONST) diff --git a/src/list.c b/src/list.c --- a/src/list.c +++ b/src/list.c @@ -270,6 +270,7 @@ list_free_list(list_T *l) if (l->lv_used_next != NULL) l->lv_used_next->lv_used_prev = l->lv_used_prev; + free_type(l->lv_type); vim_free(l); } @@ -689,13 +690,17 @@ list_append_number(list_T *l, varnumber_ /* * Insert typval_T "tv" in list "l" before "item". * If "item" is NULL append at the end. - * Return FAIL when out of memory. + * Return FAIL when out of memory or the type is wrong. */ int list_insert_tv(list_T *l, typval_T *tv, listitem_T *item) { - listitem_T *ni = listitem_alloc(); + listitem_T *ni; + if (l->lv_type != NULL && l->lv_type->tt_member != NULL + && check_typval_type(l->lv_type->tt_member, tv, 0) == FAIL) + return FAIL; + ni = listitem_alloc(); if (ni == NULL) return FAIL; copy_tv(tv, &ni->li_tv); diff --git a/src/proto/vim9script.pro b/src/proto/vim9script.pro --- a/src/proto/vim9script.pro +++ b/src/proto/vim9script.pro @@ -10,7 +10,7 @@ void ex_import(exarg_T *eap); int find_exported(int sid, char_u *name, ufunc_T **ufunc, type_T **type, cctx_T *cctx); char_u *handle_import(char_u *arg_start, garray_T *gap, int import_sid, evalarg_T *evalarg, void *cctx); char_u *vim9_declare_scriptvar(exarg_T *eap, char_u *arg); -void update_vim9_script_var(int create, dictitem_T *di, typval_T *tv, type_T *type); +void update_vim9_script_var(int create, dictitem_T *di, typval_T *tv, type_T **type); void hide_script_var(scriptitem_T *si, int idx, int func_defined); void free_all_script_vars(scriptitem_T *si); svar_T *find_typval_in_script(typval_T *dest); diff --git a/src/structs.h b/src/structs.h --- a/src/structs.h +++ b/src/structs.h @@ -1481,6 +1481,7 @@ struct listvar_S int lv_idx; // cached index of an item } mat; } lv_u; + type_T *lv_type; // allocated by alloc_type() list_T *lv_copylist; // copied list used by deepcopy() list_T *lv_used_next; // next list in used lists list list_T *lv_used_prev; // previous list in used lists list @@ -1544,6 +1545,7 @@ struct dictvar_S int dv_refcount; // reference count int dv_copyID; // ID used by deepcopy() hashtab_T dv_hashtab; // hashtab that refers to the items + type_T *dv_type; // allocated by alloc_type() dict_T *dv_copydict; // copied dict used by deepcopy() dict_T *dv_used_next; // next dict in used dicts list dict_T *dv_used_prev; // previous dict in used dicts list 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 @@ -252,6 +252,57 @@ def Test_extend_return_type() res->assert_equal(6) enddef +func g:ExtendDict(d) + call extend(a:d, #{xx: 'x'}) +endfunc + +def Test_extend_dict_item_type() + var lines =<< trim END + var d: dict = {a: 1} + extend(d, {b: 2}) + END + CheckDefAndScriptSuccess(lines) + + lines =<< trim END + var d: dict = {a: 1} + extend(d, {b: 'x'}) + END + CheckDefFailure(lines, 'E1013: Argument 2: type mismatch, expected dict but got dict', 2) + CheckScriptFailure(['vim9script'] + lines, 'E1012:', 3) + + lines =<< trim END + var d: dict = {a: 1} + g:ExtendDict(d) + END + CheckDefExecFailure(lines, 'E1012: Type mismatch; expected number but got string', 0) + CheckScriptFailure(['vim9script'] + lines, 'E1012:', 1) +enddef + +func g:ExtendList(l) + call extend(a:l, ['x']) +endfunc + +def Test_extend_list_item_type() + var lines =<< trim END + var l: list = [1] + extend(l, [2]) + END + CheckDefAndScriptSuccess(lines) + + lines =<< trim END + var l: list = [1] + extend(l, ['x']) + END + CheckDefFailure(lines, 'E1013: Argument 2: type mismatch, expected list but got list', 2) + CheckScriptFailure(['vim9script'] + lines, 'E1012:', 3) + + lines =<< trim END + var l: list = [1] + g:ExtendList(l) + END + CheckDefExecFailure(lines, 'E1012: Type mismatch; expected number but got string', 0) + CheckScriptFailure(['vim9script'] + lines, 'E1012:', 1) +enddef def Wrong_dict_key_type(items: list): list return filter(items, (_, val) => get({[val]: 1}, 'x')) diff --git a/src/testdir/test_vim9_disassemble.vim b/src/testdir/test_vim9_disassemble.vim --- a/src/testdir/test_vim9_disassemble.vim +++ b/src/testdir/test_vim9_disassemble.vim @@ -257,6 +257,7 @@ def Test_disassemble_store_member() assert_match('\d*_ScriptFuncStoreMember\_s*' .. 'var locallist: list = []\_s*' .. '\d NEWLIST size 0\_s*' .. + '\d SETTYPE list\_s*' .. '\d STORE $0\_s*' .. 'locallist\[0\] = 123\_s*' .. '\d PUSHNR 123\_s*' .. @@ -265,6 +266,7 @@ def Test_disassemble_store_member() '\d STORELIST\_s*' .. 'var localdict: dict = {}\_s*' .. '\d NEWDICT size 0\_s*' .. + '\d SETTYPE dict\_s*' .. '\d STORE $1\_s*' .. 'localdict\["a"\] = 456\_s*' .. '\d\+ PUSHNR 456\_s*' .. @@ -347,6 +349,7 @@ def Test_disassemble_list_add() assert_match('\d*_ListAdd\_s*' .. 'var l: list = []\_s*' .. '\d NEWLIST size 0\_s*' .. + '\d SETTYPE list\_s*' .. '\d STORE $0\_s*' .. 'add(l, 123)\_s*' .. '\d LOAD $0\_s*' .. @@ -1034,6 +1037,7 @@ def Test_disassemble_for_loop() assert_match('ForLoop\_s*' .. 'var res: list\_s*' .. '\d NEWLIST size 0\_s*' .. + '\d SETTYPE list\_s*' .. '\d STORE $0\_s*' .. 'for i in range(3)\_s*' .. '\d STORE -1 in $1\_s*' .. @@ -1137,6 +1141,7 @@ def Test_disassemble_typecast() '\d LOADG g:number\_s*' .. '\d CHECKTYPE number stack\[-1\]\_s*' .. '\d NEWLIST size 2\_s*' .. + '\d SETTYPE list\_s*' .. '\d STORE $0\_s*' .. '\d PUSHNR 0\_s*' .. '\d RETURN\_s*', diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -751,6 +751,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 2272, +/**/ 2271, /**/ 2270, diff --git a/src/vim9compile.c b/src/vim9compile.c --- a/src/vim9compile.c +++ b/src/vim9compile.c @@ -831,6 +831,20 @@ generate_TYPECHECK( return OK; } + static int +generate_SETTYPE( + cctx_T *cctx, + type_T *expected) +{ + isn_T *isn; + + RETURN_OK_IF_SKIP(cctx); + if ((isn = generate_instr(cctx, ISN_SETTYPE)) == NULL) + return FAIL; + isn->isn_arg.type.ct_type = alloc_type(expected); + return OK; +} + /* * Return TRUE if "actual" could be "expected" and a runtime typecheck is to be * used. Return FALSE if the types will never match. @@ -6025,6 +6039,15 @@ compile_assignment(char_u *arg, exarg_T // ":const var": lock the value, but not referenced variables generate_LOCKCONST(cctx); + if (is_decl + && (type->tt_type == VAR_DICT || type->tt_type == VAR_LIST) + && type->tt_member != NULL + && type->tt_member != &t_any + && type->tt_member != &t_unknown) + // Set the type in the list or dict, so that it can be checked, + // also in legacy script. + generate_SETTYPE(cctx, type); + if (dest != dest_local) { if (generate_store_var(cctx, dest, opt_flags, vimvaridx, @@ -8193,6 +8216,7 @@ delete_instr(isn_T *isn) break; case ISN_CHECKTYPE: + case ISN_SETTYPE: free_type(isn->isn_arg.type.ct_type); break; diff --git a/src/vim9execute.c b/src/vim9execute.c --- a/src/vim9execute.c +++ b/src/vim9execute.c @@ -2994,6 +2994,24 @@ call_def_function( } break; + case ISN_SETTYPE: + { + checktype_T *ct = &iptr->isn_arg.type; + + tv = STACK_TV_BOT(-1); + if (tv->v_type == VAR_DICT && tv->vval.v_dict != NULL) + { + free_type(tv->vval.v_dict->dv_type); + tv->vval.v_dict->dv_type = alloc_type(ct->ct_type); + } + else if (tv->v_type == VAR_LIST && tv->vval.v_list != NULL) + { + free_type(tv->vval.v_list->lv_type); + tv->vval.v_list->lv_type = alloc_type(ct->ct_type); + } + } + break; + case ISN_2BOOL: case ISN_COND2BOOL: { @@ -3890,6 +3908,15 @@ ex_disassemble(exarg_T *eap) iptr->isn_arg.checklen.cl_more_OK ? ">= " : "", iptr->isn_arg.checklen.cl_min_len); break; + case ISN_SETTYPE: + { + char *tofree; + + smsg("%4d SETTYPE %s", current, + type_name(iptr->isn_arg.type.ct_type, &tofree)); + vim_free(tofree); + break; + } case ISN_COND2BOOL: smsg("%4d COND2BOOL", current); break; case ISN_2BOOL: if (iptr->isn_arg.number) smsg("%4d INVERT (!val)", current); diff --git a/src/vim9script.c b/src/vim9script.c --- a/src/vim9script.c +++ b/src/vim9script.c @@ -661,10 +661,10 @@ vim9_declare_scriptvar(exarg_T *eap, cha * with a hashtable) and sn_var_vals (lookup by index). * When "create" is TRUE this is a new variable, otherwise find and update an * existing variable. - * When "type" is NULL use "tv" for the type. + * When "*type" is NULL use "tv" for the type and update "*type". */ void -update_vim9_script_var(int create, dictitem_T *di, typval_T *tv, type_T *type) +update_vim9_script_var(int create, dictitem_T *di, typval_T *tv, type_T **type) { scriptitem_T *si = SCRIPT_ITEM(current_sctx.sc_sid); hashitem_T *hi; @@ -715,10 +715,9 @@ update_vim9_script_var(int create, dicti } if (sv != NULL) { - if (type == NULL) - sv->sv_type = typval2type(tv, &si->sn_type_list); - else - sv->sv_type = type; + if (*type == NULL) + *type = typval2type(tv, &si->sn_type_list); + sv->sv_type = *type; } // let ex_export() know the export worked.