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)
dict_free_contents(dict_T *d)
{
hashtab_free_contents(&d->dv_hashtab);
+ free_type(d->dv_type);
+ d->dv_type = NULL;
}
/*
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)
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));
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
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)
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)
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);
}
/*
* 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);
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);
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
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
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<number> = {a: 1}
+ extend(d, {b: 2})
+ END
+ CheckDefAndScriptSuccess(lines)
+
+ lines =<< trim END
+ var d: dict<number> = {a: 1}
+ extend(d, {b: 'x'})
+ END
+ CheckDefFailure(lines, 'E1013: Argument 2: type mismatch, expected dict<number> but got dict<string>', 2)
+ CheckScriptFailure(['vim9script'] + lines, 'E1012:', 3)
+
+ lines =<< trim END
+ var d: dict<number> = {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<number> = [1]
+ extend(l, [2])
+ END
+ CheckDefAndScriptSuccess(lines)
+
+ lines =<< trim END
+ var l: list<number> = [1]
+ extend(l, ['x'])
+ END
+ CheckDefFailure(lines, 'E1013: Argument 2: type mismatch, expected list<number> but got list<string>', 2)
+ CheckScriptFailure(['vim9script'] + lines, 'E1012:', 3)
+
+ lines =<< trim END
+ var l: list<number> = [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<number>): list<number>
return filter(items, (_, val) => get({[val]: 1}, 'x'))
assert_match('<SNR>\d*_ScriptFuncStoreMember\_s*' ..
'var locallist: list<number> = []\_s*' ..
'\d NEWLIST size 0\_s*' ..
+ '\d SETTYPE list<number>\_s*' ..
'\d STORE $0\_s*' ..
'locallist\[0\] = 123\_s*' ..
'\d PUSHNR 123\_s*' ..
'\d STORELIST\_s*' ..
'var localdict: dict<number> = {}\_s*' ..
'\d NEWDICT size 0\_s*' ..
+ '\d SETTYPE dict<number>\_s*' ..
'\d STORE $1\_s*' ..
'localdict\["a"\] = 456\_s*' ..
'\d\+ PUSHNR 456\_s*' ..
assert_match('<SNR>\d*_ListAdd\_s*' ..
'var l: list<number> = []\_s*' ..
'\d NEWLIST size 0\_s*' ..
+ '\d SETTYPE list<number>\_s*' ..
'\d STORE $0\_s*' ..
'add(l, 123)\_s*' ..
'\d LOAD $0\_s*' ..
assert_match('ForLoop\_s*' ..
'var res: list<number>\_s*' ..
'\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 LOADG g:number\_s*' ..
'\d CHECKTYPE number stack\[-1\]\_s*' ..
'\d NEWLIST size 2\_s*' ..
+ '\d SETTYPE list<number>\_s*' ..
'\d STORE $0\_s*' ..
'\d PUSHNR 0\_s*' ..
'\d RETURN\_s*',
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 2272,
/**/
2271,
/**/
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.
// ":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,
break;
case ISN_CHECKTYPE:
+ case ISN_SETTYPE:
free_type(isn->isn_arg.type.ct_type);
break;
}
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:
{
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);
* 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;
}
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.