From f1c871df5c37da6bb316f0db75d5f3118da55609 Mon Sep 17 00:00:00 2001 From: Yegappan Lakshmanan Date: Sat, 3 Jan 2026 12:50:00 -0800 Subject: [PATCH] When adding a new item at the end of a list in vim9script, use the proper item type --- runtime/doc/eval.txt | 8 +- src/eval.c | 12 + src/list.c | 76 ++++++- src/proto/vim9class.pro | 1 + src/testdir/test_listdict.vim | 416 ++++++++++++++++++++++++++++++++++ src/vim9class.c | 37 +++ src/vim9execute.c | 15 +- 7 files changed, 549 insertions(+), 16 deletions(-) diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index 7ba70e307c55f..5792653be3e74 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -467,7 +467,13 @@ Changing the order of items in a list: > :call reverse(list) " reverse the order of items :call uniq(sort(list)) " sort and remove duplicates - +In a Vim9 script or a def method, a new item can be appended to a List by +using the list length as the index: > + vim9script + var l: list + l[0] = 'a' + l[1] = 'b' +< For loop ~ The |:for| loop executes commands for each item in a List, Tuple, String or diff --git a/src/eval.c b/src/eval.c index 254a0c6083828..4a9f5821ba654 100644 --- a/src/eval.c +++ b/src/eval.c @@ -2470,6 +2470,18 @@ set_var_lval( NULL, 0) == FAIL) return; + // If the lval is a List and the type of the list is not yet set, + // then set the item type from the declared type of the variable. + if (in_vim9script() && rettv->v_type == VAR_LIST + && rettv->vval.v_list != NULL + && rettv->vval.v_list->lv_type == NULL) + { + if (lp->ll_tv->v_type == VAR_LIST + && lp->ll_tv->vval.v_list != NULL + && lp->ll_tv->vval.v_list->lv_type != NULL) + set_tv_type(rettv, lp->ll_tv->vval.v_list->lv_type); + } + if (lp->ll_newkey != NULL) { if (op != NULL && *op != '=') diff --git a/src/list.c b/src/list.c index 7d6793c59e2f1..4579d3dadbe5c 100644 --- a/src/list.c +++ b/src/list.c @@ -764,6 +764,63 @@ list_insert(list_T *l, listitem_T *ni, listitem_T *item) } } +/* + * Append a new empty item into "l" based on the list item type. Used when + * adding a new element to a List in Vim9script by using the current list + * length as the index. + */ + static int +list_append_new_item(list_T *l) +{ + typval_T tv; + + if (l->lv_type != NULL && l->lv_type->tt_member != NULL) + tv.v_type = l->lv_type->tt_member->tt_type; + else + tv.v_type = VAR_NUMBER; + tv.v_lock = 0; + + switch (tv.v_type) + { + case VAR_BOOL: tv.vval.v_number = VVAL_FALSE; break; + case VAR_SPECIAL: tv.vval.v_number = 0; break; + case VAR_NUMBER: tv.vval.v_number = 0; break; + case VAR_FLOAT: tv.vval.v_float = 0; break; + case VAR_STRING: tv.vval.v_string = NULL; break; + case VAR_BLOB: tv.vval.v_blob = blob_alloc(); break; + case VAR_FUNC: tv.vval.v_string = NULL; break; + case VAR_PARTIAL: tv.vval.v_partial = NULL; break; + case VAR_LIST: tv.vval.v_list = list_alloc(); break; + case VAR_DICT: tv.vval.v_dict = dict_alloc(); break; + case VAR_JOB: tv.vval.v_job = NULL; break; + case VAR_CHANNEL: tv.vval.v_channel = NULL; break; + case VAR_CLASS: + if (l->lv_type != NULL && l->lv_type->tt_member != NULL) + tv.vval.v_class = l->lv_type->tt_member->tt_class; + else + tv.vval.v_class = NULL; + break; + case VAR_OBJECT: + if (l->lv_type != NULL && l->lv_type->tt_member != NULL) + tv.vval.v_object = + alloc_object(l->lv_type->tt_member->tt_class); + else + tv.vval.v_object = NULL; + break; + case VAR_TYPEALIAS: tv.vval.v_typealias = NULL; break; + case VAR_TUPLE: tv.vval.v_tuple = tuple_alloc(); break; + default: + tv.v_type = VAR_NUMBER; + tv.vval.v_number = 0; + break; + } + + if (l->lv_type != NULL && l->lv_type->tt_member != NULL) + set_tv_type(&tv, l->lv_type->tt_member); + + return list_append_tv(l, &tv); +} + /* * Get the list item in "l" with index "n1". "n1" is adjusted if needed. * In Vim9, it is at the end of the list, add an item if "can_append" is TRUE. @@ -782,7 +839,8 @@ check_range_index_one(list_T *l, long *n1, int can_append, int quiet) if (can_append && in_vim9script() && *n1 == l->lv_len && l->lv_lock == 0) { - list_append_number(l, 0); + if (list_append_new_item(l) == FAIL) + return NULL; li = list_find_index(l, n1); } if (li == NULL) @@ -1265,7 +1323,21 @@ list_slice_or_index( { // copy the item to "var1" to avoid that freeing the list makes it // invalid. - copy_tv(&list_find(list, n1)->li_tv, &var1); + listitem_T *li = check_range_index_one(list, (long *)&n1, TRUE, TRUE); + if (li == NULL) + return FAIL; + copy_tv(&li->li_tv, &var1); + + // If "var1" is a List and the List item type is not set, then set it + // from the declared list item type. + if (in_vim9script() && var1.v_type == VAR_LIST + && var1.vval.v_list != NULL + && var1.vval.v_list->lv_type == NULL) + { + if (list->lv_type != NULL && list->lv_type->tt_member != NULL) + set_tv_type(&var1, list->lv_type->tt_member); + } + clear_tv(rettv); *rettv = var1; } diff --git a/src/proto/vim9class.pro b/src/proto/vim9class.pro index d40e71af0ac28..a6e5f6772e78f 100644 --- a/src/proto/vim9class.pro +++ b/src/proto/vim9class.pro @@ -22,6 +22,7 @@ ufunc_T *method_lookup(class_T *cl, vartype_T v_type, char_u *name, size_t namel int inside_class(cctx_T *cctx_arg, class_T *cl); int oc_var_check_ro(class_T *cl, ocmember_T *m); void obj_lock_const_vars(object_T *obj); +object_T *alloc_object(class_T *cl); void copy_object(typval_T *from, typval_T *to); void copy_class(typval_T *from, typval_T *to); void class_unref(class_T *cl); diff --git a/src/testdir/test_listdict.vim b/src/testdir/test_listdict.vim index 6d18b9179c121..069c257793576 100644 --- a/src/testdir/test_listdict.vim +++ b/src/testdir/test_listdict.vim @@ -1657,4 +1657,420 @@ def Test_id_with_dict() assert_equal('', id(null_channel)) assert_equal('', id(null_job)) enddef + +" Test for adding an item to a List with type list by using the list +" length as the index +def Test_list_length_as_index() + # Append a new item to a list (vim9script) + var lines =<< trim END + var l = [] + l[0] = 'abc' + assert_equal(['abc'], l) + END + v9.CheckDefAndScriptSuccess(lines) + + # Type check when adding a new item + lines =<< trim END + var l: list + l[0] = 10 + END + v9.CheckDefAndScriptFailure(lines, 'E1012: Type mismatch; expected string but got number', 2) + + # In legacy script, appending a new item to a list should fail. + lines =<< trim END + let l = [] + let l[0] = 'abc' + END + v9.CheckLegacyFailure(lines, 'E684: List index out of range: 0') + + # Append a new item to a dict which is part of another list + lines =<< trim END + var inner: dict = {} + var outer: list = [inner] + + outer[0] = {a: 'xxx'} + assert_equal([{'a': 'xxx'}], outer) + END + v9.CheckDefAndScriptSuccess(lines) + + # Append a new item to a list which is part of another tuple + lines =<< trim END + var inner: list + var outer: tuple = (inner,) + + outer[0][0] = 'aaa' + assert_equal((['aaa'],), outer) + END + v9.CheckDefAndScriptSuccess(lines) + + # Append a new item to a list which is part of another list + lines =<< trim END + var inner: list> + var outer: list = [inner] + + outer[0][0] = {a: ''} + assert_equal([[{'a': ''}]], outer) + END + v9.CheckDefAndScriptSuccess(lines) + + # Type check + lines =<< trim END + var inner: list> + var outer: list = [inner] + outer[0][0] = {a: 0z10} + END + v9.CheckDefExecAndScriptFailure(lines, 'E1012: Type mismatch; expected dict but got dict', 3) + + # In legacy script, appending a new item to a list which is part of another + # list should fail. + lines =<< trim END + let inner = [] + let outer = [inner] + + let outer[0][0] = {'a': ''} + END + v9.CheckLegacyFailure(lines, 'E684: List index out of range: 0') + + # appending an item to a nested list + lines =<< trim END + var inner: list>> + var outer: list = [inner] + + outer[0][0] = [] + outer[0][0][0] = {a: 'abc'} + assert_equal([[[{'a': 'abc'}]]], outer) + END + v9.CheckDefAndScriptSuccess(lines) + + # type check + lines =<< trim END + var inner: list>> + var outer: list = [inner] + outer[0][0] = [] + outer[0][0][0] = {a: 0z10} + END + v9.CheckDefExecAndScriptFailure(lines, 'E1012: Type mismatch; expected dict but got dict', 4) + + # legacy script + lines =<< trim END + let inner = [[]] + let outer = [inner] + let outer[0][0][0] = {'a': 'abc'} + END + v9.CheckLegacyFailure(lines, 'E684: List index out of range: 0') + + # adding a new blob to a list of blobs + lines =<< trim END + var inner: list + var outer: list = [inner] + outer[0][0] = 0z10 + assert_equal([[0z10]], outer) + END + v9.CheckDefAndScriptSuccess(lines) + + # type check + lines =<< trim END + var inner: list + var outer: list = [inner] + outer[0][0] = 10 + END + v9.CheckDefExecAndScriptFailure(lines, 'E1012: Type mismatch; expected blob but got number', 3) + + # adding a new tuple to a list of tuples + lines =<< trim END + var inner: list> + var outer: list = [inner] + outer[0][0] = ('abc',) + assert_equal([[('abc',)]], outer) + END + v9.CheckDefAndScriptSuccess(lines) + + # type check + lines =<< trim END + var inner: list> + var outer: list = [inner] + outer[0][0] = ['abc'] + END + v9.CheckDefExecAndScriptFailure(lines, 'E1012: Type mismatch; expected tuple but got list', 3) + + # adding a new string to a list of strings + lines =<< trim END + var inner: list + var outer: list = [inner] + outer[0][0] = 'xx' + assert_equal([['xx']], outer) + END + v9.CheckDefAndScriptSuccess(lines) + + # type check + lines =<< trim END + var inner: list + var outer: list = [inner] + outer[0][0] = 10 + END + v9.CheckDefExecAndScriptFailure(lines, 'E1012: Type mismatch; expected string but got number', 3) + + # adding a new number to a list of numbers + lines =<< trim END + var inner: list + var outer: list = [inner] + outer[0][0] = 1234 + assert_equal([[1234]], outer) + END + v9.CheckDefAndScriptSuccess(lines) + + # type check + lines =<< trim END + var inner: list + var outer: list = [inner] + outer[0][0] = '' + END + v9.CheckDefExecAndScriptFailure(lines, 'E1012: Type mismatch; expected number but got string', 3) + + # adding a new float to a list of floats + lines =<< trim END + var inner: list + var outer: list = [inner] + outer[0][0] = 2.0 + assert_equal([[2.0]], outer) + END + v9.CheckDefAndScriptSuccess(lines) + + # type check + lines =<< trim END + var inner: list + var outer: list = [inner] + outer[0][0] = '' + END + v9.CheckDefExecAndScriptFailure(lines, 'E1012: Type mismatch; expected float but got string', 3) + + # adding a new bool to a list of bools + lines =<< trim END + var inner: list + var outer: list = [inner] + outer[0][0] = true + assert_equal([[true]], outer) + END + v9.CheckDefAndScriptSuccess(lines) + + # type check + lines =<< trim END + var inner: list + var outer: list = [inner] + outer[0][0] = 'a' + END + v9.CheckDefExecAndScriptFailure(lines, 'E1012: Type mismatch; expected bool but got string', 3) + + # adding a new funcref to a list of funcrefs + lines =<< trim END + var inner: list + var outer: list = [inner] + outer[0][0] = function('min') + assert_equal([[function('min')]], outer) + END + v9.CheckDefAndScriptSuccess(lines) + + # adding a new lambda to a list of funcrefs + lines =<< trim END + var inner: list + var outer: list = [inner] + outer[0][0] = () => 'x' + assert_match("\[\[function('\d\+')]]", string(outer)) + END + v9.CheckDefAndScriptSuccess(lines) + + # type check + lines =<< trim END + var inner: list + var outer: list = [inner] + outer[0][0] = 'min' + END + v9.CheckDefExecAndScriptFailure(lines, 'E1012: Type mismatch; expected func(...): unknown but got string', 3) + + # adding a new job to a list of jobs + lines =<< trim END + var inner: list + var outer: list = [inner] + outer[0][0] = test_null_job() + assert_equal('[[''no process'']]', string(outer)) + END + v9.CheckDefAndScriptSuccess(lines) + + # type check + lines =<< trim END + var inner: list + var outer: list = [inner] + outer[0][0] = test_null_channel() + END + v9.CheckDefExecAndScriptFailure(lines, 'E1012: Type mismatch; expected job but got channel', 3) + + # adding a new channel to a list of channels + lines =<< trim END + var inner: list + var outer: list = [inner] + outer[0][0] = test_null_channel() + assert_equal('[[''channel fail'']]', string(outer)) + END + v9.CheckDefAndScriptSuccess(lines) + + # type check + lines =<< trim END + var inner: list + var outer: list = [inner] + outer[0][0] = test_null_job() + END + v9.CheckDefExecAndScriptFailure(lines, 'E1012: Type mismatch; expected channel but got job', 3) + + # adding a new object to a list of objects + lines =<< trim END + vim9script + class A + endclass + var inner: list + var outer: list = [inner] + outer[0][0] = A.new() + assert_equal('[[object of A {}]]', string(outer)) + def Fn() + var Finner: list + var Fouter: list = [Finner] + Fouter[0][0] = A.new() + assert_equal('[[object of A {}]]', string(Fouter)) + enddef + Fn() + END + v9.CheckSourceSuccess(lines) + + # type check (in a script) + lines =<< trim END + vim9script + class A + endclass + class B + endclass + var inner: list + var outer: list = [inner] + outer[0][0] = B.new() + END + v9.CheckSourceFailure(lines, 'E1012: Type mismatch; expected object but got object', 8) + + # type check (in a method) + lines =<< trim END + vim9script + class A + endclass + class B + endclass + def Fn() + var inner: list + var outer: list = [inner] + outer[0][0] = B.new() + enddef + Fn() + END + v9.CheckSourceFailure(lines, 'E1012: Type mismatch; expected object but got object', 3) + + # adding a new enum to a list of enums + lines =<< trim END + vim9script + enum Color + RED, + BLUE, + GREEN + endenum + var inner: list + var outer: list = [inner] + outer[0][0] = Color.BLUE + assert_equal("[[enum Color.BLUE {name: 'BLUE', ordinal: 1}]]", string(outer)) + def Fn() + var Finner: list + var Fouter: list = [Finner] + Fouter[0][0] = Color.GREEN + assert_equal("[[enum Color.GREEN {name: 'GREEN', ordinal: 2}]]", string(Fouter)) + enddef + Fn() + END + v9.CheckSourceSuccess(lines) + + # type check (in a script) + lines =<< trim END + vim9script + enum Color + RED, + BLUE, + GREEN + endenum + enum Shape + CIRCLE, + SQUARE + endenum + var inner: list + var outer: list = [inner] + outer[0][0] = Shape.CIRCLE + END + v9.CheckSourceFailure(lines, 'E1012: Type mismatch; expected enum but got enum', 13) + + # type check (in a method) + lines =<< trim END + vim9script + enum Color + RED, + BLUE, + GREEN + endenum + enum Shape + CIRCLE, + SQUARE + endenum + def Fn() + var inner: list + var outer: list = [inner] + outer[0][0] = Shape.CIRCLE + enddef + Fn() + END + v9.CheckSourceFailure(lines, 'E1012: Type mismatch; expected enum but got enum', 3) + + # adding a new string to a list of strings (using type alias) + lines =<< trim END + vim9script + type StrType = string + var inner: list + var outer: list = [inner] + outer[0][0] = 'abc' + assert_equal([['abc']], outer) + + def Fn() + var Finner: list + var Fouter: list = [Finner] + Fouter[0][0] = 'abc' + assert_equal([['abc']], Fouter) + enddef + Fn() + END + v9.CheckSourceScriptSuccess(lines) + + # type check (using type alias in a script) + lines =<< trim END + vim9script + type StrType = string + var inner: list + var outer: list = [inner] + outer[0][0] = 10 + END + v9.CheckSourceScriptFailure(lines, 'E1012: Type mismatch; expected string but got number', 5) + + # type check (using type alias in a method) + lines =<< trim END + vim9script + type StrType = string + def Fn() + var inner: list + var outer: list = [inner] + outer[0][0] = 10 + enddef + Fn() + END + v9.CheckSourceScriptFailure(lines, 'E1012: Type mismatch; expected string but got number', 3) +enddef + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/vim9class.c b/src/vim9class.c index 9d0521a42e181..3dc4ddf8608b9 100644 --- a/src/vim9class.c +++ b/src/vim9class.c @@ -1895,6 +1895,13 @@ enum_set_internal_obj_vars(class_T *en, object_T *enval) break; } + if (i >= en->class_class_member_count) + // When adding enum values to an enum, the index value should be less + // than the member count. It will be greater only when appending a + // new item to a list of enums. In this case, skip setting the enum + // name and ordinal (as this is a dummy enum object) + return; + // First object variable is the name ocmember_T *value_ocm = en->class_class_members + i; typval_T *name_tv = (typval_T *)(enval + 1); @@ -3633,6 +3640,36 @@ obj_lock_const_vars(object_T *obj) } } +/* + * Create a new instance of class "cl" + */ + object_T * +alloc_object(class_T *cl) +{ + object_T *obj; + int sz; + + if (cl == NULL) + return NULL; + + sz = sizeof(object_T) + cl->class_obj_member_count * sizeof(typval_T); + obj = alloc_clear(sz); + if (obj == NULL) + return NULL; + + obj->obj_class = cl; + ++cl->class_refcount; + obj->obj_refcount = 1; + object_created(obj); + + // When creating an enum value object, initialize the name and ordinal + // object variables. + if (IS_ENUM(cl)) + enum_set_internal_obj_vars(cl, obj); + + return obj; +} + /* * Make a copy of an object. */ diff --git a/src/vim9execute.c b/src/vim9execute.c index 25e56005bcb34..2b2d59ad51d5a 100644 --- a/src/vim9execute.c +++ b/src/vim9execute.c @@ -3708,19 +3708,8 @@ exec_instructions(ectx_T *ectx) // "this" is always the local variable at index zero tv = STACK_TV_VAR(0); tv->v_type = VAR_OBJECT; - tv->vval.v_object = alloc_clear( - iptr->isn_arg.construct.construct_size); - tv->vval.v_object->obj_class = - iptr->isn_arg.construct.construct_class; - ++tv->vval.v_object->obj_class->class_refcount; - tv->vval.v_object->obj_refcount = 1; - object_created(tv->vval.v_object); - - // When creating an enum value object, initialize the name and - // ordinal object variables. - class_T *en = tv->vval.v_object->obj_class; - if (IS_ENUM(en)) - enum_set_internal_obj_vars(en, tv->vval.v_object); + tv->vval.v_object = + alloc_object(iptr->isn_arg.construct.construct_class); break; // execute Ex command line