patch 9.2.0049: Vim9: typename() wrong for lists/dicts/tuples with shared references
Commit:
https://github.com/vim/vim/commit/b1d4b03058ca7f7c39959dcc8f9efae28ddad754
Author: Hirohito Higashi <
h.eas...@gmail.com>
Date: Tue Feb 24 21:22:38 2026 +0000
patch 9.2.0049: Vim9: typename() wrong for lists/dicts/tuples with shared references
Problem: Vim9: typename() returns wrong type for lists/dicts/tuples
with shared references (Mao-Yining).
Solution: Reset CopyID after processing the item so it can be
re-inspected if encountered again via a different reference
(Hirohito Higashi).
fixes: #19490
closes: #19492
Signed-off-by: Hirohito Higashi <
h.eas...@gmail.com>
Signed-off-by: Christian Brabandt <
c...@256bit.org>
diff --git a/src/testdir/test_tuple.vim b/src/testdir/test_tuple.vim
index 191546a5e..26609a20f 100644
--- a/src/testdir/test_tuple.vim
+++ b/src/testdir/test_tuple.vim
@@ -2238,6 +2238,14 @@ func Test_tuple_typename()
END
call v9.CheckSourceDefAndScriptSuccess(lines)
+ " Shared (non-circular) references must not be treated as circular.
+ " repeat() makes all elements point to the same inner tuple object.
+ let lines =<< trim END
+ call assert_equal('tuple<tuple<number, number>, tuple<number, number>, tuple<number, number>>', ((1, 2),)->repeat(3)->typename())
+ call assert_equal('list<tuple<number, number>>', [(1, 2)]->repeat(3)->typename())
+ END
+ call v9.CheckSourceLegacyAndVim9Success(lines)
+
" When a tuple item is used in a "for" loop, the type is tuple<any>
let lines =<< trim END
vim9script
diff --git a/src/testdir/test_vim9_builtin.vim b/src/testdir/test_vim9_builtin.vim
index d5521cf87..d3effd834 100644
--- a/src/testdir/test_vim9_builtin.vim
+++ b/src/testdir/test_vim9_builtin.vim
@@ -4983,6 +4983,22 @@ def Test_typename()
endif
var l: list<func(list<number>): any> = [function('min')]
assert_equal('list<func(list<number>): any>', typename(l))
+
+ # Check that circular list/dict references don't cause infinite recursion.
+ # Use legacy script where lv_type is not set so the copyID mechanism is used.
+ v9.CheckSourceLegacySuccess([
+ 'let circ_l = []',
+ 'call add(circ_l, circ_l)',
+ "call assert_equal('list<list<any>>', typename(circ_l))",
+ 'let circ_d = {}',
+ "let circ_d['self'] = circ_d",
+ "call assert_equal('dict<dict<any>>', typename(circ_d))",
+ ])
+
+ # Shared (non-circular) references must not be treated as circular.
+ # repeat() makes all elements point to the same inner list/dict object.
+ assert_equal('list<list<string>>', [[" "]]->repeat(3)->typename())
+ assert_equal('list<dict<number>>', [{'a': 1}]->repeat(3)->typename())
enddef
def Test_undofile()
diff --git a/src/version.c b/src/version.c
index 478b9f1bc..620e1a5bd 100644
--- a/src/version.c
+++ b/src/version.c
@@ -734,6 +734,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 49,
/**/
48,
/**/
diff --git a/src/vim9type.c b/src/vim9type.c
index f6bf1b255..be578f26b 100644
--- a/src/vim9type.c
+++ b/src/vim9type.c
@@ -612,6 +612,10 @@ list_typval2type(typval_T *tv, int copyID, garray_T *type_gap, int flags)
common_type(typval2type(&li->li_tv, copyID, type_gap, TVTT_DO_MEMBER),
member_type, &member_type, type_gap);
+ // Reset copyID so that a shared reference to this list (not a circular
+ // reference) can be processed again to get the correct type.
+ l->lv_copyID = 0;
+
return get_list_type(member_type, type_gap);
}
@@ -661,6 +665,9 @@ tuple_typval2type(typval_T *tv, int copyID, garray_T *type_gap, int flags)
if (ga_grow(&tuple_types_ga, 1) == FAIL)
{
ga_clear(&tuple_types_ga);
+ // Reset copyID so that a shared reference to this tuple can be
+ // processed again.
+ tuple->tv_copyID = 0;
return NULL;
}
((type_T **)tuple_types_ga.ga_data)[tuple_types_ga.ga_len] = type;
@@ -670,6 +677,10 @@ tuple_typval2type(typval_T *tv, int copyID, garray_T *type_gap, int flags)
type_T *tuple_type = get_tuple_type(&tuple_types_ga, type_gap);
ga_clear(&tuple_types_ga);
+ // Reset copyID so that a shared reference to this tuple (not a circular
+ // reference) can be processed again to get the correct type.
+ tuple->tv_copyID = 0;
+
return tuple_type;
}
@@ -716,6 +727,10 @@ dict_typval2type(typval_T *tv, int copyID, garray_T *type_gap, int flags)
common_type(typval2type(value, copyID, type_gap, TVTT_DO_MEMBER),
member_type, &member_type, type_gap);
+ // Reset copyID so that a shared reference to this dict (not a circular
+ // reference) can be processed again to get the correct type.
+ d->dv_copyID = 0;
+
return get_dict_type(member_type, type_gap);
}