patch 9.1.1232: Vim script is missing the tuple data type

Problem:  Vim script is missing the tuple data type
Solution: Add support for the tuple data type
          (Yegappan Lakshmanan)

closes: #16776

Signed-off-by: Yegappan Lakshmanan <yegappan@yahoo.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/src/Make_ami.mak b/src/Make_ami.mak
index c779914..4dc86dd 100644
--- a/src/Make_ami.mak
+++ b/src/Make_ami.mak
@@ -169,6 +169,7 @@
 	textobject.c \
 	textprop.c \
 	time.c \
+	tuple.c \
 	typval.c \
 	ui.c \
 	undo.c \
diff --git a/src/Make_cyg_ming.mak b/src/Make_cyg_ming.mak
index 3f2a6da..991feb9 100644
--- a/src/Make_cyg_ming.mak
+++ b/src/Make_cyg_ming.mak
@@ -865,6 +865,7 @@
 	$(OUTDIR)/textobject.o \
 	$(OUTDIR)/textprop.o \
 	$(OUTDIR)/time.o \
+	$(OUTDIR)/tuple.o \
 	$(OUTDIR)/typval.o \
 	$(OUTDIR)/ui.o \
 	$(OUTDIR)/undo.o \
diff --git a/src/Make_mvc.mak b/src/Make_mvc.mak
index c24f0af..2825865 100644
--- a/src/Make_mvc.mak
+++ b/src/Make_mvc.mak
@@ -786,6 +786,7 @@
 	$(OUTDIR)\textobject.obj \
 	$(OUTDIR)\textprop.obj \
 	$(OUTDIR)\time.obj \
+	$(OUTDIR)\tuple.obj \
 	$(OUTDIR)\typval.obj \
 	$(OUTDIR)\ui.obj \
 	$(OUTDIR)\undo.obj \
@@ -1791,6 +1792,8 @@
 
 $(OUTDIR)/time.obj:	$(OUTDIR) time.c  $(INCL)
 
+$(OUTDIR)/tuple.obj:	$(OUTDIR) tuple.c  $(INCL)
+
 $(OUTDIR)/typval.obj:	$(OUTDIR) typval.c  $(INCL)
 
 $(OUTDIR)/ui.obj:	$(OUTDIR) ui.c  $(INCL)
@@ -2005,6 +2008,7 @@
 	proto/textobject.pro \
 	proto/textprop.pro \
 	proto/time.pro \
+	proto/tuple.pro \
 	proto/typval.pro \
 	proto/ui.pro \
 	proto/undo.pro \
diff --git a/src/Make_vms.mms b/src/Make_vms.mms
index 2063023..a30ed65 100644
--- a/src/Make_vms.mms
+++ b/src/Make_vms.mms
@@ -433,6 +433,7 @@
 	textobject.c \
 	textprop.c \
 	time.c \
+	tuple.c \
 	typval.c \
 	ui.c \
 	undo.c \
@@ -567,6 +568,7 @@
 	textobject.obj \
 	textprop.obj \
 	time.obj \
+	tuple.obj \
 	typval.obj \
 	ui.obj \
 	undo.obj \
@@ -1169,6 +1171,9 @@
 time.obj : time.c vim.h [.auto]config.h feature.h os_unix.h   \
  ascii.h keymap.h termdefs.h macros.h structs.h regexp.h gui.h beval.h \
  [.proto]gui_beval.pro option.h ex_cmds.h proto.h errors.h globals.h
+tuple.obj : tuple.c vim.h [.auto]config.h feature.h os_unix.h   \
+ ascii.h keymap.h termdefs.h macros.h structs.h regexp.h gui.h beval.h \
+ [.proto]gui_beval.pro option.h ex_cmds.h proto.h errors.h globals.h
 typval.obj : typval.c vim.h [.auto]config.h feature.h os_unix.h   \
  ascii.h keymap.h termdefs.h macros.h structs.h regexp.h gui.h beval.h \
  [.proto]gui_beval.pro option.h ex_cmds.h proto.h errors.h globals.h
diff --git a/src/Makefile b/src/Makefile
index d6c76b1..cde2e55 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -1584,6 +1584,7 @@
 	textobject.c \
 	textprop.c \
 	time.c \
+	tuple.c \
 	typval.c \
 	ui.c \
 	undo.c \
@@ -1744,6 +1745,7 @@
 	objects/textobject.o \
 	objects/textprop.o \
 	objects/time.o \
+	objects/tuple.o \
 	objects/typval.o \
 	objects/ui.o \
 	objects/undo.o \
@@ -1937,6 +1939,7 @@
 	textobject.pro \
 	textprop.pro \
 	time.pro \
+	tuple.pro \
 	typval.pro \
 	ui.pro \
 	undo.pro \
@@ -3568,6 +3571,9 @@
 objects/time.o: time.c
 	$(CCC) -o $@ time.c
 
+objects/tuple.o: tuple.c
+	$(CCC) -o $@ tuple.c
+
 objects/typval.o: typval.c
 	$(CCC) -o $@ typval.c
 
@@ -4248,6 +4254,11 @@
  proto/gui_beval.pro structs.h regexp.h gui.h libvterm/include/vterm.h \
  libvterm/include/vterm_keycodes.h alloc.h ex_cmds.h spell.h proto.h \
  globals.h errors.h
+objects/tuple.o: tuple.c vim.h protodef.h auto/config.h feature.h os_unix.h \
+ auto/osdef.h ascii.h keymap.h termdefs.h macros.h option.h beval.h \
+ proto/gui_beval.pro structs.h regexp.h gui.h libvterm/include/vterm.h \
+ libvterm/include/vterm_keycodes.h alloc.h ex_cmds.h spell.h proto.h \
+ globals.h errors.h
 objects/typval.o: typval.c vim.h protodef.h auto/config.h feature.h os_unix.h \
  auto/osdef.h ascii.h keymap.h termdefs.h macros.h option.h beval.h \
  proto/gui_beval.pro structs.h regexp.h gui.h libvterm/include/vterm.h \
diff --git a/src/channel.c b/src/channel.c
index 69bbff2..a369c71 100644
--- a/src/channel.c
+++ b/src/channel.c
@@ -5004,7 +5004,7 @@
 	{
 	    tv.v_type = VAR_CHANNEL;
 	    tv.vval.v_channel = channel;
-	    abort = abort || set_ref_in_item(&tv, copyID, NULL, NULL);
+	    abort = abort || set_ref_in_item(&tv, copyID, NULL, NULL, NULL);
 	}
     return abort;
 }
diff --git a/src/dict.c b/src/dict.c
index 7975904..d43ca86 100644
--- a/src/dict.c
+++ b/src/dict.c
@@ -1554,7 +1554,7 @@
 	return;
 
     if ((what == DICT2LIST_ITEMS
-		? check_for_string_or_list_or_dict_arg(argvars, 0)
+		? check_for_string_list_tuple_or_dict_arg(argvars, 0)
 		: check_for_dict_arg(argvars, 0)) == FAIL)
 	return;
 
@@ -1617,6 +1617,8 @@
 	string2items(argvars, rettv);
     else if (argvars[0].v_type == VAR_LIST)
 	list2items(argvars, rettv);
+    else if (argvars[0].v_type == VAR_TUPLE)
+	tuple2items(argvars, rettv);
     else
 	dict2list(argvars, rettv, DICT2LIST_ITEMS);
 }
diff --git a/src/errors.h b/src/errors.h
index ca5ec85..9331484 100644
--- a/src/errors.h
+++ b/src/errors.h
@@ -2932,8 +2932,8 @@
 	INIT(= N_("E1138: Using a Bool as a Number"));
 EXTERN char e_missing_matching_bracket_after_dict_key[]
 	INIT(= N_("E1139: Missing matching bracket after dict key"));
-EXTERN char e_for_argument_must_be_sequence_of_lists[]
-	INIT(= N_("E1140: :for argument must be a sequence of lists"));
+EXTERN char e_for_argument_must_be_sequence_of_lists_or_tuples[]
+	INIT(= N_("E1140: :for argument must be a sequence of lists or tuples"));
 EXTERN char e_indexable_type_required[]
 	INIT(= N_("E1141: Indexable type required"));
 EXTERN char e_calling_test_garbagecollect_now_while_v_testing_is_not_set[]
@@ -3146,8 +3146,8 @@
 	INIT(= N_("E1223: String or Dictionary required for argument %d"));
 EXTERN char e_string_number_or_list_required_for_argument_nr[]
 	INIT(= N_("E1224: String, Number or List required for argument %d"));
-EXTERN char e_string_list_or_dict_required_for_argument_nr[]
-	INIT(= N_("E1225: String, List or Dictionary required for argument %d"));
+EXTERN char e_string_list_tuple_or_dict_required_for_argument_nr[]
+	INIT(= N_("E1225: String, List, Tuple or Dictionary required for argument %d"));
 EXTERN char e_list_or_blob_required_for_argument_nr[]
 	INIT(= N_("E1226: List or Blob required for argument %d"));
 EXTERN char e_list_or_dict_required_for_argument_nr[]
@@ -3218,10 +3218,12 @@
 #ifdef FEAT_EVAL
 EXTERN char e_argument_of_str_must_be_list_string_dictionary_or_blob[]
 	INIT(= N_("E1250: Argument of %s must be a List, String, Dictionary or Blob"));
-EXTERN char e_list_dict_blob_or_string_required_for_argument_nr[]
-	INIT(= N_("E1251: List, Dictionary, Blob or String required for argument %d"));
+EXTERN char e_list_tuple_dict_blob_or_string_required_for_argument_nr[]
+	INIT(= N_("E1251: List, Tuple, Dictionary, Blob or String required for argument %d"));
 EXTERN char e_string_list_or_blob_required_for_argument_nr[]
 	INIT(= N_("E1252: String, List or Blob required for argument %d"));
+EXTERN char e_string_list_tuple_or_blob_required_for_argument_nr[]
+	INIT(= N_("E1253: String, List, Tuple or Blob required for argument %d"));
 // E1253 unused
 EXTERN char e_cannot_use_script_variable_in_for_loop[]
 	INIT(= N_("E1254: Cannot use script variable in for loop"));
@@ -3351,8 +3353,8 @@
 #ifdef FEAT_EVAL
 EXTERN char e_cannot_use_partial_with_dictionary_for_defer[]
 	INIT(= N_("E1300: Cannot use a partial with dictionary for :defer"));
-EXTERN char e_string_number_list_or_blob_required_for_argument_nr[]
-	INIT(= N_("E1301: String, Number, List or Blob required for argument %d"));
+EXTERN char e_repeatable_type_required_for_argument_nr[]
+	INIT(= N_("E1301: String, Number, List, Tuple or Blob required for argument %d"));
 EXTERN char e_script_variable_was_deleted[]
 	INIT(= N_("E1302: Script variable was deleted"));
 EXTERN char e_custom_list_completion_function_does_not_return_list_but_str[]
@@ -3664,3 +3666,53 @@
 	INIT(= N_("E1515: Unable to convert from '%s' encoding"));
 EXTERN char e_str_encoding_to_failed[]
 	INIT(= N_("E1516: Unable to convert to '%s' encoding"));
+#ifdef FEAT_EVAL
+EXTERN char e_can_only_compare_tuple_with_tuple[]
+	INIT(= N_("E1517: Can only compare Tuple with Tuple"));
+EXTERN char e_invalid_operation_for_tuple[]
+	INIT(= N_("E1518: Invalid operation for Tuple"));
+EXTERN char e_tuple_index_out_of_range_nr[]
+	INIT(= N_("E1519: Tuple index out of range: %ld"));
+EXTERN char e_using_tuple_as_number[]
+	INIT(= N_("E1520: Using a Tuple as a Number"));
+EXTERN char e_using_tuple_as_float[]
+	INIT(= N_("E1521: Using a Tuple as a Float"));
+EXTERN char e_using_tuple_as_string[]
+	INIT(= N_("E1522: Using a Tuple as a String"));
+EXTERN char e_string_list_tuple_or_blob_required[]
+	INIT(= N_("E1523: String, List, Tuple or Blob required"));
+EXTERN char e_cannot_use_tuple_with_function_str[]
+	INIT(= N_("E1524: Cannot use a tuple with function %s"));
+EXTERN char e_argument_of_str_must_be_list_tuple_string_dictionary_or_blob[]
+	INIT(= N_("E1525: Argument of %s must be a List, Tuple, String, Dictionary or Blob"));
+EXTERN char e_missing_end_of_tuple_rsp_str[]
+	INIT(= N_("E1526: Missing end of Tuple ')': %s"));
+EXTERN char e_missing_comma_in_tuple_str[]
+	INIT(= N_("E1527: Missing comma in Tuple: %s"));
+EXTERN char e_list_or_tuple_or_blob_required_for_argument_nr[]
+	INIT(= N_("E1528: List or Tuple or Blob required for argument %d"));
+EXTERN char e_list_or_tuple_required_for_argument_nr[]
+	INIT(= N_("E1529: List or Tuple required for argument %d"));
+EXTERN char e_list_or_tuple_or_dict_required_for_argument_nr[]
+	INIT(= N_("E1530: List or Tuple or Dictionary required for argument %d"));
+EXTERN char e_argument_of_str_must_be_list_tuple_dictionary_or_blob[]
+	INIT(= N_("E1531: Argument of %s must be a List, Tuple, Dictionary or Blob"));
+EXTERN char e_tuple_is_immutable[]
+	INIT(= N_("E1532: Cannot modify a tuple"));
+EXTERN char e_cannot_slice_tuple[]
+	INIT(= N_("E1533: Cannot slice a tuple"));
+EXTERN char e_tuple_required_for_argument_nr[]
+	INIT(= N_("E1534: Tuple required for argument %d"));
+EXTERN char e_list_or_tuple_required[]
+	INIT(= N_("E1535: List or Tuple required"));
+EXTERN char e_tuple_required[]
+	INIT(= N_("E1536: Tuple required"));
+EXTERN char e_less_targets_than_tuple_items[]
+	INIT(= N_("E1537: Less targets than Tuple items"));
+EXTERN char e_more_targets_than_tuple_items[]
+	INIT(= N_("E1538: More targets than Tuple items"));
+EXTERN char e_variadic_tuple_must_end_with_list_type_str[]
+	INIT(= N_("E1539: Variadic tuple must end with a list type: %s"));
+EXTERN char e_cannot_use_variadic_tuple_in_concatenation[]
+	INIT(= N_("E1540: Cannot use a variadic tuple in concatenation"));
+#endif
diff --git a/src/eval.c b/src/eval.c
index 9a140c1..bd8e7cf 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -107,7 +107,7 @@
     // autoloaded script names
     free_autoload_scriptnames();
 
-    // unreferenced lists and dicts
+    // unreferenced lists, tuples and dicts
     (void)garbage_collect(FALSE);
 
     // functions not garbage collected
@@ -620,28 +620,48 @@
 
 /*
  * Convert "tv" to a string.
- * When "join_list" is TRUE convert a List into a sequence of lines.
+ * When "join_list" is TRUE convert a List or a Tuple into a sequence of lines.
  * Returns an allocated string (NULL when out of memory).
  */
     char_u *
 typval2string(typval_T *tv, int join_list)
 {
     garray_T	ga;
-    char_u	*retval;
+    char_u	*retval = NULL;
 
-    if (join_list && tv->v_type == VAR_LIST)
+    if (join_list && (tv->v_type == VAR_LIST || tv->v_type == VAR_TUPLE))
     {
-	ga_init2(&ga, sizeof(char), 80);
-	if (tv->vval.v_list != NULL)
+	if (tv->v_type == VAR_LIST)
 	{
-	    list_join(&ga, tv->vval.v_list, (char_u *)"\n", TRUE, FALSE, 0);
-	    if (tv->vval.v_list->lv_len > 0)
-		ga_append(&ga, NL);
+	    ga_init2(&ga, sizeof(char), 80);
+	    if (tv->vval.v_list != NULL)
+	    {
+		list_join(&ga, tv->vval.v_list, (char_u *)"\n", TRUE, FALSE,
+									0);
+		if (tv->vval.v_list->lv_len > 0)
+		    ga_append(&ga, NL);
+	    }
+	    ga_append(&ga, NUL);
+	    retval = (char_u *)ga.ga_data;
 	}
-	ga_append(&ga, NUL);
-	retval = (char_u *)ga.ga_data;
+	else
+	{
+	    // tuple
+	    ga_init2(&ga, sizeof(char), 80);
+	    if (tv->vval.v_tuple != NULL)
+	    {
+		tuple_join(&ga, tv->vval.v_tuple, (char_u *)"\n", TRUE, FALSE,
+									0);
+		if (TUPLE_LEN(tv->vval.v_tuple) > 0)
+		    ga_append(&ga, NL);
+	    }
+	    ga_append(&ga, NUL);
+	    retval = (char_u *)ga.ga_data;
+	}
     }
-    else if (tv->v_type == VAR_LIST || tv->v_type == VAR_DICT)
+    else if (tv->v_type == VAR_LIST
+	    || tv->v_type == VAR_TUPLE
+	    || tv->v_type == VAR_DICT)
     {
 	char_u	*tofree;
 	char_u	numbuf[NUMBUFLEN];
@@ -659,7 +679,8 @@
 /*
  * Top level evaluation function, returning a string.  Does not handle line
  * breaks.
- * When "join_list" is TRUE convert a List into a sequence of lines.
+ * When "join_list" is TRUE convert a List and a Tuple into a sequence of
+ * lines.
  * Return pointer to allocated memory, or NULL for failure.
  */
     char_u *
@@ -1095,7 +1116,7 @@
  *
  * This is typically called with "lval_root" as "root". For a class, find
  * the name from lp in the class from root, fill in lval_T if found. For a
- * complex type, list/dict use it as the result; just put the root into
+ * complex type, list/tuple/dict use it as the result; just put the root into
  * ll_tv.
  *
  * "lval_root" is a hack used during run-time/instr-execution to provide the
@@ -1322,8 +1343,11 @@
 	    return GLV_FAIL;
     }
     lp->ll_list = NULL;
+    lp->ll_list = NULL;
+    lp->ll_blob = NULL;
     lp->ll_object = NULL;
     lp->ll_class = NULL;
+    lp->ll_tuple = NULL;
 
     // a NULL dict is equivalent with an empty dict
     if (lp->ll_tv->vval.v_dict == NULL)
@@ -1425,7 +1449,13 @@
 {
     long	bloblen = blob_len(lp->ll_tv->vval.v_blob);
 
-    // Get the number and item for the only or first index of the List.
+    lp->ll_list = NULL;
+    lp->ll_dict = NULL;
+    lp->ll_object = NULL;
+    lp->ll_class = NULL;
+    lp->ll_tuple = NULL;
+
+    // Get the number and item for the only or first index of a List or Tuple.
     if (empty1)
 	lp->ll_n1 = 0;
     else
@@ -1484,6 +1514,7 @@
     lp->ll_dict = NULL;
     lp->ll_object = NULL;
     lp->ll_class = NULL;
+    lp->ll_tuple = NULL;
     lp->ll_list = lp->ll_tv->vval.v_list;
     lp->ll_li = check_range_index_one(lp->ll_list, &lp->ll_n1,
 				(flags & GLV_ASSIGN_WITH_OP) == 0, quiet);
@@ -1524,6 +1555,64 @@
 }
 
 /*
+ * Get a tuple lval variable that can be assigned a value to: "name",
+ * "na{me}", "name[expr]", "name[expr][expr]", etc.
+ *
+ * 'idx' specifies the tuple index.
+ * If 'quiet' is TRUE, then error messages are not displayed for an invalid
+ * index.
+ *
+ * The typval is returned in 'lp'.  Returns GLV_OK on success and GLV_FAIL on
+ * failure.
+ */
+    static int
+get_lval_tuple(
+    lval_T	*lp,
+    typval_T	*idx,
+    int		quiet)
+{
+    // is number or string
+    lp->ll_n1 = (long)tv_get_number(idx);
+
+    lp->ll_list = NULL;
+    lp->ll_dict = NULL;
+    lp->ll_blob = NULL;
+    lp->ll_object = NULL;
+    lp->ll_class = NULL;
+
+    lp->ll_tuple = lp->ll_tv->vval.v_tuple;
+    lp->ll_tv = tuple_find(lp->ll_tuple, lp->ll_n1);
+    if (lp->ll_tv == NULL)
+    {
+	if (!quiet)
+	    semsg(_(e_tuple_index_out_of_range_nr), lp->ll_n1);
+	return GLV_FAIL;
+    }
+
+    // use the type of the member
+    if (lp->ll_valtype != NULL)
+    {
+	if (lp->ll_valtype != NULL
+		&& lp->ll_valtype->tt_type == VAR_TUPLE
+		&& lp->ll_valtype->tt_argcount == 1)
+	{
+	    // a variadic tuple or a single item tuple
+	    if (lp->ll_valtype->tt_flags & TTFLAG_VARARGS)
+		lp->ll_valtype = lp->ll_valtype->tt_args[0]->tt_member;
+	    else
+		lp->ll_valtype = lp->ll_valtype->tt_args[0];
+	}
+	else
+	    // If the LHS member type is not known (VAR_ANY), then get it from
+	    // the tuple item (after indexing)
+	    lp->ll_valtype = typval2type(lp->ll_tv, get_copyID(),
+					&lp->ll_type_list, TVTT_DO_MEMBER);
+    }
+
+    return GLV_OK;
+}
+
+/*
  * Get a class or object lval method in class "cl".  The 'key' argument points
  * to the method name and 'key_end' points to the character after 'key'.
  * 'v_type' is VAR_CLASS or VAR_OBJECT.
@@ -1630,6 +1719,7 @@
 {
     lp->ll_dict = NULL;
     lp->ll_list = NULL;
+    lp->ll_tuple = NULL;
 
     class_T *cl;
     if (v_type == VAR_OBJECT)
@@ -1697,8 +1787,8 @@
 
 /*
  * Check whether left bracket ("[") is allowed after the variable "name" with
- * type "v_type".  Only Dict, List and Blob types support a bracket after the
- * variable name.  Returns TRUE if bracket is allowed after the name.
+ * type "v_type".  Only Dict, List, Tuple and Blob types support a bracket
+ * after the variable name.  Returns TRUE if bracket is allowed after the name.
  */
     static int
 bracket_allowed_after_type(char_u *name, vartype_T v_type, int quiet)
@@ -1716,14 +1806,18 @@
 
 /*
  * Check whether the variable "name" with type "v_type" can be followed by an
- * index.  Only Dict, List, Blob, Object and Class types support indexing.
- * Returns TRUE if indexing is allowed after the name.
+ * index.  Only Dict, List, Tuple, Blob, Object and Class types support
+ * indexing.  Returns TRUE if indexing is allowed after the name.
  */
     static int
 index_allowed_after_type(char_u *name, vartype_T v_type, int quiet)
 {
-    if (v_type != VAR_LIST && v_type != VAR_DICT && v_type != VAR_BLOB &&
-	    v_type != VAR_OBJECT && v_type != VAR_CLASS)
+    if (v_type != VAR_LIST
+	    && v_type != VAR_TUPLE
+	    && v_type != VAR_DICT
+	    && v_type != VAR_BLOB
+	    && v_type != VAR_OBJECT
+	    && v_type != VAR_CLASS)
     {
 	if (!quiet)
 	    semsg(_(e_index_not_allowed_after_str_str),
@@ -1735,8 +1829,8 @@
 }
 
 /*
- * Get the lval of a list/dict/blob/object/class subitem starting at "p". Loop
- * until no more [idx] or .key is following.
+ * Get the lval of a list/tuple/dict/blob/object/class subitem starting at "p".
+ * Loop until no more [idx] or .key is following.
  *
  * If "rettv" is not NULL it points to the value to be assigned.
  * "unlet" is TRUE for ":unlet".
@@ -1863,6 +1957,12 @@
 			emsg(_(e_cannot_slice_dictionary));
 		    goto done;
 		}
+		if (v_type == VAR_TUPLE)
+		{
+		    if (!quiet)
+			emsg(_(e_cannot_slice_tuple));
+		    goto done;
+		}
 		if (rettv != NULL
 			&& !(rettv->v_type == VAR_LIST
 			    && rettv->vval.v_list != NULL)
@@ -1932,6 +2032,11 @@
 	    if (get_lval_list(lp, &var1, &var2, empty1, flags, quiet) == FAIL)
 		goto done;
 	}
+	else if (v_type == VAR_TUPLE)
+	{
+	    if (get_lval_tuple(lp, &var1, quiet) == FAIL)
+		goto done;
+	}
 	else  // v_type == VAR_CLASS || v_type == VAR_OBJECT
 	{
 	    if (get_lval_class_or_obj(lp, key, p, v_type, cl_exec, flags,
@@ -1945,6 +2050,13 @@
 	var2.v_type = VAR_UNKNOWN;
     }
 
+    if (lp->ll_tuple != NULL)
+    {
+	if (!quiet)
+	    emsg(_(e_tuple_is_immutable));
+	goto done;
+    }
+
     rc = OK;
 
 done:
@@ -2575,6 +2687,7 @@
 	case VAR_OBJECT:
 	case VAR_CLASS:
 	case VAR_TYPEALIAS:
+	case VAR_TUPLE:
 	    break;
 
 	case VAR_BLOB:
@@ -2619,6 +2732,7 @@
     char_u	*expr;
     typval_T	tv;
     list_T	*l;
+    tuple_T	*tuple;
     int		skip = !(evalarg->eval_flags & EVAL_EVALUATE);
 
     *errp = TRUE;	// default: there is an error
@@ -2671,6 +2785,22 @@
 		    fi->fi_lw.lw_item = l->lv_first;
 		}
 	    }
+	    else if (tv.v_type == VAR_TUPLE)
+	    {
+		tuple = tv.vval.v_tuple;
+		if (tuple == NULL)
+		{
+		    // a null tuple is like an empty tuple: do nothing
+		    clear_tv(&tv);
+		}
+		else
+		{
+		    // No need to increment the refcount, it's already set for
+		    // the tuple being used in "tv".
+		    fi->fi_tuple = tuple;
+		    fi->fi_tuple_idx = 0;
+		}
+	    }
 	    else if (tv.v_type == VAR_BLOB)
 	    {
 		fi->fi_bi = 0;
@@ -2695,7 +2825,7 @@
 	    }
 	    else
 	    {
-		emsg(_(e_string_list_or_blob_required));
+		emsg(_(e_string_list_tuple_or_blob_required));
 		clear_tv(&tv);
 	    }
 	}
@@ -2780,6 +2910,22 @@
 	return result;
     }
 
+    if (fi->fi_tuple != NULL)
+    {
+	typval_T	tv;
+
+	if (fi->fi_tuple_idx >= TUPLE_LEN(fi->fi_tuple))
+	    return FALSE;
+
+	copy_tv(TUPLE_ITEM(fi->fi_tuple, fi->fi_tuple_idx), &tv);
+	++fi->fi_tuple_idx;
+	++fi->fi_bi;
+	if (skip_assign)
+	    return TRUE;
+	return ex_let_vars(arg, &tv, TRUE, fi->fi_semicolon,
+					    fi->fi_varcount, flag, NULL) == OK;
+    }
+
     item = fi->fi_lw.lw_item;
     if (item == NULL)
 	result = FALSE;
@@ -2813,6 +2959,8 @@
     }
     else if (fi->fi_blob != NULL)
 	blob_unref(fi->fi_blob);
+    else if (fi->fi_tuple != NULL)
+	tuple_unref(fi->fi_tuple);
     else
 	vim_free(fi->fi_string);
     vim_free(fi);
@@ -3960,6 +4108,36 @@
 }
 
 /*
+ * Make a copy of tuple "tv1" and append tuple "tv2".
+ */
+    int
+eval_addtuple(typval_T *tv1, typval_T *tv2)
+{
+    int		vim9script = in_vim9script();
+    typval_T	var3;
+
+    if (vim9script && tv1->vval.v_tuple != NULL && tv2->vval.v_tuple != NULL
+	    && tv1->vval.v_tuple->tv_type != NULL
+	    && tv2->vval.v_tuple->tv_type != NULL)
+    {
+	if (!check_tuples_addable(tv1->vval.v_tuple->tv_type,
+						tv2->vval.v_tuple->tv_type))
+	    return FAIL;
+    }
+
+    // concatenate tuples
+    if (tuple_concat(tv1->vval.v_tuple, tv2->vval.v_tuple, &var3) == FAIL)
+    {
+	clear_tv(tv1);
+	clear_tv(tv2);
+	return FAIL;
+    }
+    clear_tv(tv1);
+    *tv1 = var3;
+    return OK;
+}
+
+/*
  * Left or right shift the number "tv1" by the number "tv2" and store the
  * result in "tv1".
  *
@@ -4231,6 +4409,7 @@
 	int	    concat;
 	typval_T    var2;
 	int	    vim9script = in_vim9script();
+	long	    op_lnum = SOURCING_LNUM;
 
 	// "." is only string concatenation when scriptversion is 1
 	// "+=", "-=" and "..=" are assignments
@@ -4259,7 +4438,8 @@
 	    *arg = p;
 	}
 	if ((op != '+' || (rettv->v_type != VAR_LIST
-						 && rettv->v_type != VAR_BLOB))
+						&& rettv->v_type != VAR_TUPLE
+						&& rettv->v_type != VAR_BLOB))
 		&& (op == '.' || rettv->v_type != VAR_FLOAT)
 		&& evaluate)
 	{
@@ -4302,6 +4482,8 @@
 	    /*
 	     * Compute the result.
 	     */
+	    // use the line of the operation for messages
+	    SOURCING_LNUM = op_lnum;
 	    if (op == '.')
 	    {
 		if (eval_concat_str(rettv, &var2) == FAIL)
@@ -4316,6 +4498,12 @@
 		if (eval_addlist(rettv, &var2) == FAIL)
 		    return FAIL;
 	    }
+	    else if (op == '+' && rettv->v_type == VAR_TUPLE
+					   && var2.v_type == VAR_TUPLE)
+	    {
+		if (eval_addtuple(rettv, &var2) == FAIL)
+		    return FAIL;
+	    }
 	    else
 	    {
 		if (eval_addsub_number(rettv, &var2, op) == FAIL)
@@ -4681,13 +4869,23 @@
 		    return OK;
 		}
 		break;
-	case 10: if (STRNCMP(s, "null_class", 10) == 0)
+	case 10:
+		if (STRNCMP(s, "null_", 5) != 0)
+		    break;
+		// null_class
+		if (STRNCMP(s + 5, "class", 5) == 0)
 		{
 		    rettv->v_type = VAR_CLASS;
 		    rettv->vval.v_class = NULL;
 		    return OK;
 		}
-		 break;
+		if (STRNCMP(s + 5, "tuple", 5) == 0)
+		{
+		    rettv->v_type = VAR_TUPLE;
+		    rettv->vval.v_tuple = NULL;
+		    return OK;
+		}
+		break;
 	case 11: if (STRNCMP(s, "null_string", 11) == 0)
 		{
 		    rettv->v_type = VAR_STRING;
@@ -4796,16 +4994,26 @@
     if (ret == NOTDONE)
     {
 	*arg = skipwhite_and_linebreak(*arg + 1, evalarg);
-	ret = eval1(arg, rettv, evalarg);	// recursive!
-
-	*arg = skipwhite_and_linebreak(*arg, evalarg);
 	if (**arg == ')')
-	    ++*arg;
-	else if (ret == OK)
+	    // empty tuple
+	    ret = eval_tuple(arg, rettv, evalarg, TRUE);
+	else
 	{
-	    emsg(_(e_missing_closing_paren));
-	    clear_tv(rettv);
-	    ret = FAIL;
+	    ret = eval1(arg, rettv, evalarg);	// recursive!
+
+	    *arg = skipwhite_and_linebreak(*arg, evalarg);
+
+	    if (**arg == ',')
+		// tuple
+		ret = eval_tuple(arg, rettv, evalarg, TRUE);
+	    else if (**arg == ')')
+		++*arg;
+	    else if (ret == OK)
+	    {
+		emsg(_(e_missing_closing_paren));
+		clear_tv(rettv);
+		ret = FAIL;
+	    }
 	}
     }
 
@@ -4896,6 +5104,7 @@
  *  $VAR		environment variable
  *  (expression)	nested expression
  *  [expr, expr]	List
+ *  (expr, expr)	Tuple
  *  {arg, arg -> expr}	Lambda
  *  {key: val, key: val}   Dictionary
  *  #{key: val, key: val}  Dictionary with literal keys
@@ -4904,7 +5113,7 @@
  *  ! in front		logical NOT
  *  - in front		unary minus
  *  + in front		unary plus (ignored)
- *  trailing []		subscript in String or List
+ *  trailing []		subscript in String or List or Tuple
  *  trailing .name	entry in Dictionary
  *  trailing ->name()	method call
  *
@@ -5049,6 +5258,7 @@
     /*
      * nested expression: (expression).
      * or lambda: (arg) => expr
+     * or tuple
      */
     case '(':	ret = eval9_nested_expr(arg, rettv, evalarg, evaluate);
 		break;
@@ -5484,7 +5694,8 @@
 		var1.v_type = VAR_STRING;
 	    }
 
-	    if (vim9script && rettv->v_type == VAR_LIST)
+	    if (vim9script && (rettv->v_type == VAR_LIST
+						|| rettv->v_type == VAR_TUPLE))
 		tv_get_number_chk(&var1, &error);
 	    else
 		error = tv_get_string_chk(&var1) == NULL;
@@ -5603,6 +5814,7 @@
 
 	case VAR_STRING:
 	case VAR_LIST:
+	case VAR_TUPLE:
 	case VAR_DICT:
 	case VAR_BLOB:
 	    break;
@@ -5735,6 +5947,16 @@
 		return FAIL;
 	    break;
 
+	case VAR_TUPLE:
+	    if (var1 == NULL)
+		n1 = 0;
+	    if (var2 == NULL)
+		n2 = VARNUM_MAX;
+	    if (tuple_slice_or_index(rettv->vval.v_tuple,
+			  is_range, n1, n2, exclusive, rettv, verbose) == FAIL)
+		return FAIL;
+	    break;
+
 	case VAR_DICT:
 	    {
 		dictitem_T	*item;
@@ -6080,6 +6302,51 @@
 }
 
 /*
+ * Return a textual representation of a Tuple in "tv".
+ * If the memory is allocated "tofree" is set to it, otherwise NULL.
+ * When "copyID" is not zero replace recursive lists with "...".  When
+ * "restore_copyID" is FALSE, repeated items in tuples are replaced with "...".
+ * May return NULL.
+ */
+    static char_u *
+tuple_tv2string(
+    typval_T	*tv,
+    char_u	**tofree,
+    int		copyID,
+    int		restore_copyID)
+{
+    tuple_T	*tuple = tv->vval.v_tuple;
+    char_u	*r = NULL;
+
+    if (tuple == NULL)
+    {
+	// NULL tuple is equivalent to an empty tuple.
+	*tofree = NULL;
+	r = (char_u *)"()";
+    }
+    else if (copyID != 0 && tuple->tv_copyID == copyID
+					&& tuple->tv_items.ga_len > 0)
+    {
+	*tofree = NULL;
+	r = (char_u *)"(...)";
+    }
+    else
+    {
+	int old_copyID;
+	if (restore_copyID)
+	    old_copyID = tuple->tv_copyID;
+
+	tuple->tv_copyID = copyID;
+	*tofree = tuple2string(tv, copyID, restore_copyID);
+	if (restore_copyID)
+	    tuple->tv_copyID = old_copyID;
+	r = *tofree;
+    }
+
+    return r;
+}
+
+/*
  * Return a textual representation of a Dict in "tv".
  * If the memory is allocated "tofree" is set to it, otherwise NULL.
  * When "copyID" is not zero replace recursive dicts with "...".
@@ -6316,6 +6583,10 @@
 	    r = list_tv2string(tv, tofree, copyID, restore_copyID);
 	    break;
 
+	case VAR_TUPLE:
+	    r = tuple_tv2string(tv, tofree, copyID, restore_copyID);
+	    break;
+
 	case VAR_DICT:
 	    r = dict_tv2string(tv, tofree, copyID, restore_copyID);
 	    break;
@@ -7257,6 +7528,23 @@
 	    if (to->vval.v_list == NULL)
 		ret = FAIL;
 	    break;
+	case VAR_TUPLE:
+	    to->v_type = VAR_TUPLE;
+	    to->v_lock = 0;
+	    if (from->vval.v_tuple == NULL)
+		to->vval.v_tuple = NULL;
+	    else if (copyID != 0 && from->vval.v_tuple->tv_copyID == copyID)
+	    {
+		// use the copy made earlier
+		to->vval.v_tuple = from->vval.v_tuple->tv_copytuple;
+		++to->vval.v_tuple->tv_refcount;
+	    }
+	    else
+		to->vval.v_tuple = tuple_copy(from->vval.v_tuple,
+							    deep, top, copyID);
+	    if (to->vval.v_tuple == NULL)
+		ret = FAIL;
+	    break;
 	case VAR_BLOB:
 	    ret = blob_copy(from->vval.v_blob, to);
 	    break;
diff --git a/src/evalfunc.c b/src/evalfunc.c
index 5592471..21ed15e 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -361,6 +361,15 @@
 }
 
 /*
+ * Check "type" is a tuple of 'any'.
+ */
+    static int
+arg_tuple_any(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
+{
+    return check_arg_type(&t_tuple_any, type, context);
+}
+
+/*
  * Check "type" is a string.
  */
     static int
@@ -430,6 +439,42 @@
 }
 
 /*
+ * Check "type" is a list of 'any' or a tuple.
+ */
+    static int
+arg_list_or_tuple(
+    type_T		*type,
+    type_T		*decl_type UNUSED,
+    argcontext_T	*context)
+{
+    if (type->tt_type == VAR_LIST
+	    || type->tt_type == VAR_TUPLE
+	    || type_any_or_unknown(type))
+	return OK;
+    arg_type_mismatch(&t_list_any, type, context->arg_idx + 1);
+    return FAIL;
+}
+
+
+/*
+ * Check "type" is a list of 'any', a tuple or a blob.
+ */
+    static int
+arg_list_or_tuple_or_blob(
+    type_T		*type,
+    type_T		*decl_type UNUSED,
+    argcontext_T	*context)
+{
+    if (type->tt_type == VAR_LIST
+	    || type->tt_type == VAR_TUPLE
+	    || type->tt_type == VAR_BLOB
+	    || type_any_or_unknown(type))
+	return OK;
+    arg_type_mismatch(&t_list_any, type, context->arg_idx + 1);
+    return FAIL;
+}
+
+/*
  * Check "type" is a string or a number
  */
     static int
@@ -461,7 +506,10 @@
  * Check "type" is a buffer or a dict of any
  */
     static int
-arg_buffer_or_dict_any(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
+arg_buffer_or_dict_any(
+    type_T		*type,
+    type_T		*decl_type UNUSED,
+    argcontext_T	*context)
 {
     if (type->tt_type == VAR_STRING
 	    || type->tt_type == VAR_NUMBER
@@ -490,7 +538,10 @@
  * Check "type" is a string or a list of strings.
  */
     static int
-arg_string_or_list_string(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
+arg_string_or_list_string(
+    type_T		*type,
+    type_T		*decl_type UNUSED,
+    argcontext_T	*context)
 {
     if (type->tt_type == VAR_STRING
 	    || type_any_or_unknown(type))
@@ -512,7 +563,10 @@
  * Check "type" is a string or a list of 'any'
  */
     static int
-arg_string_or_list_any(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
+arg_string_or_list_any(
+    type_T		*type,
+    type_T		*decl_type UNUSED,
+    argcontext_T	*context)
 {
     if (type->tt_type == VAR_STRING
 	    || type->tt_type == VAR_LIST
@@ -526,7 +580,10 @@
  * Check "type" is a string or a dict of 'any'
  */
     static int
-arg_string_or_dict_any(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
+arg_string_or_dict_any(
+    type_T		*type,
+    type_T		*decl_type UNUSED,
+    argcontext_T	*context)
 {
     if (type->tt_type == VAR_STRING
 	    || type->tt_type == VAR_DICT
@@ -540,7 +597,10 @@
  * Check "type" is a string or a blob
  */
     static int
-arg_string_or_blob(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
+arg_string_or_blob(
+    type_T		*type,
+    type_T		*decl_type UNUSED,
+    argcontext_T	*context)
 {
     if (type->tt_type == VAR_STRING
 	    || type->tt_type == VAR_BLOB
@@ -579,7 +639,25 @@
 }
 
 /*
- * Check "type" is a list of 'any' or a dict of 'any' or a blob.
+ * Check "type" is a list of 'any', a tuple of 'any' or dict of 'any'.
+ */
+    static int
+arg_list_or_tuple_or_dict(
+    type_T		*type,
+    type_T		*decl_type UNUSED,
+    argcontext_T	*context)
+{
+    if (type->tt_type == VAR_LIST
+	    || type->tt_type == VAR_TUPLE
+	    || type->tt_type == VAR_DICT
+	    || type_any_or_unknown(type))
+	return OK;
+    arg_type_mismatch(&t_list_any, type, context->arg_idx + 1);
+    return FAIL;
+}
+
+/*
+ * Check "type" is a list of 'any', a dict of 'any' or a blob.
  * Also check if "type" is modifiable.
  */
     static int
@@ -601,7 +679,10 @@
  * Check "type" is a list of 'any' or a dict of 'any' or a blob or a string.
  */
     static int
-arg_list_or_dict_or_blob_or_string(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
+arg_list_or_dict_or_blob_or_string(
+    type_T		*type,
+    type_T		*decl_type UNUSED,
+    argcontext_T	*context)
 {
     if (type->tt_type == VAR_LIST
 	    || type->tt_type == VAR_DICT
@@ -629,11 +710,35 @@
 }
 
 /*
+ * Check "type" is a list of 'any', a tuple of 'any', a dict of 'any', a blob
+ * or a string.
+ */
+    static int
+arg_list_tuple_dict_blob_or_string(
+    type_T		*type,
+    type_T		*decl_type UNUSED,
+    argcontext_T	*context)
+{
+    if (type->tt_type == VAR_LIST
+	    || type->tt_type == VAR_TUPLE
+	    || type->tt_type == VAR_DICT
+	    || type->tt_type == VAR_BLOB
+	    || type->tt_type == VAR_STRING
+	    || type_any_or_unknown(type))
+	return OK;
+    arg_type_mismatch(&t_list_any, type, context->arg_idx + 1);
+    return FAIL;
+}
+
+
+/*
  * Check second argument of map(), filter(), foreach().
  */
     static int
-check_map_filter_arg2(type_T *type, argcontext_T *context,
-							filtermap_T filtermap)
+check_map_filter_arg2(
+    type_T		*type,
+    argcontext_T	*context,
+    filtermap_T		filtermap)
 {
     type_T *expected_member = NULL;
     type_T *(args[2]);
@@ -801,7 +906,10 @@
  * Also accept a number, one and zero are accepted.
  */
     static int
-arg_string_or_func(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
+arg_string_or_func(
+    type_T		*type,
+    type_T		*decl_type UNUSED,
+    argcontext_T	*context)
 {
     if (type->tt_type == VAR_STRING
 	    || type->tt_type == VAR_PARTIAL
@@ -835,12 +943,16 @@
 }
 
 /*
- * Check "type" is a list of 'any' or a blob or a string.
+ * Check "type" is a list of 'any', a tuple, a blob or a string.
  */
     static int
-arg_string_list_or_blob(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
+arg_string_list_tuple_or_blob(
+    type_T		*type,
+    type_T		*decl_type UNUSED,
+    argcontext_T	*context)
 {
     if (type->tt_type == VAR_LIST
+	    || type->tt_type == VAR_TUPLE
 	    || type->tt_type == VAR_BLOB
 	    || type->tt_type == VAR_STRING
 	    || type_any_or_unknown(type))
@@ -850,12 +962,12 @@
 }
 
 /*
- * Check "type" is a modifiable list of 'any' or a blob or a string.
+ * Check "type" is a tuple or a modifiable list of 'any' or a blob or a string.
  */
     static int
-arg_string_list_or_blob_mod(type_T *type, type_T *decl_type, argcontext_T *context)
+arg_reverse(type_T *type, type_T *decl_type, argcontext_T *context)
 {
-    if (arg_string_list_or_blob(type, decl_type, context) == FAIL)
+    if (arg_string_list_tuple_or_blob(type, decl_type, context) == FAIL)
 	return FAIL;
     return arg_type_modifiable(type, context->arg_idx + 1);
 }
@@ -901,7 +1013,10 @@
  * Must not be used for the first argcheck_T entry.
  */
     static int
-arg_same_struct_as_prev(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
+arg_same_struct_as_prev(
+    type_T		*type,
+    type_T		*decl_type UNUSED,
+    argcontext_T	*context)
 {
     type_T *prev_type = context->arg_types[context->arg_idx - 1].type_curr;
 
@@ -935,7 +1050,10 @@
  * Check "type" is a string or a number or a list
  */
     static int
-arg_str_or_nr_or_list(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
+arg_str_or_nr_or_list(
+    type_T		*type,
+    type_T		*decl_type UNUSED,
+    argcontext_T	*context)
 {
     if (type->tt_type == VAR_STRING
 	    || type->tt_type == VAR_NUMBER
@@ -950,7 +1068,10 @@
  * Check "type" is a dict of 'any' or a string
  */
     static int
-arg_dict_any_or_string(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
+arg_dict_any_or_string(
+    type_T		*type,
+    type_T		*decl_type UNUSED,
+    argcontext_T	*context)
 {
     if (type->tt_type == VAR_DICT
 	    || type->tt_type == VAR_STRING
@@ -977,14 +1098,15 @@
 }
 
 /*
- * Check "type" which is the first argument of get() (blob or list or dict or
- * funcref)
+ * Check "type" which is the first argument of get() (a blob, a list, a tuple,
+ * a dict or a funcref)
  */
     static int
 arg_get1(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
 {
     if (type->tt_type == VAR_BLOB
 	    || type->tt_type == VAR_LIST
+	    || type->tt_type == VAR_TUPLE
 	    || type->tt_type == VAR_DICT
 	    || type->tt_type == VAR_FUNC
 	    || type->tt_type == VAR_PARTIAL
@@ -996,8 +1118,8 @@
 }
 
 /*
- * Check "type" which is the first argument of len() (number or string or
- * blob or list or dict)
+ * Check "type" which is the first argument of len() (a string, a number, a
+ * blob, a list, a tuple, a dict or an object)
  */
     static int
 arg_len1(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
@@ -1006,6 +1128,7 @@
 	    || type->tt_type == VAR_NUMBER
 	    || type->tt_type == VAR_BLOB
 	    || type->tt_type == VAR_LIST
+	    || type->tt_type == VAR_TUPLE
 	    || type->tt_type == VAR_DICT
 	    || type->tt_type == VAR_OBJECT
 	    || type_any_or_unknown(type))
@@ -1032,8 +1155,8 @@
 }
 
 /*
- * Check "type" which is the first argument of repeat() (string or number or
- * list or any)
+ * Check "type" which is the first argument of repeat() (a string, a number, a
+ * blob, a list, a tuple or any)
  */
     static int
 arg_repeat1(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
@@ -1042,6 +1165,7 @@
 	    || type->tt_type == VAR_NUMBER
 	    || type->tt_type == VAR_BLOB
 	    || type->tt_type == VAR_LIST
+	    || type->tt_type == VAR_TUPLE
 	    || type_any_or_unknown(type))
 	return OK;
 
@@ -1050,13 +1174,14 @@
 }
 
 /*
- * Check "type" which is the first argument of slice() (list or blob or string
- * or any)
+ * Check "type" which is the first argument of slice() (a list, a tuple, a
+ * blob, a string or any)
  */
     static int
 arg_slice1(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
 {
     if (type->tt_type == VAR_LIST
+	    || type->tt_type == VAR_TUPLE
 	    || type->tt_type == VAR_BLOB
 	    || type->tt_type == VAR_STRING
 	    || type_any_or_unknown(type))
@@ -1067,19 +1192,23 @@
 }
 
 /*
- * Check "type" which is the first argument of count() (string or list or dict
- * or any)
+ * Check "type" which is the first argument of count() (a string, a list, a
+ * tuple, a dict or any)
  */
     static int
-arg_string_or_list_or_dict(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
+arg_string_list_tuple_or_dict(
+    type_T		*type,
+    type_T		*decl_type UNUSED,
+    argcontext_T	*context)
 {
     if (type->tt_type == VAR_STRING
 	    || type->tt_type == VAR_LIST
+	    || type->tt_type == VAR_TUPLE
 	    || type->tt_type == VAR_DICT
 	    || type_any_or_unknown(type))
 	return OK;
 
-    semsg(_(e_string_list_or_dict_required_for_argument_nr),
+    semsg(_(e_string_list_tuple_or_dict_required_for_argument_nr),
 							 context->arg_idx + 1);
     return FAIL;
 }
@@ -1114,11 +1243,12 @@
 static argcheck_T arg1_float_or_nr[] = {arg_float_or_nr};
 static argcheck_T arg1_job[] = {arg_job};
 static argcheck_T arg1_list_any[] = {arg_list_any};
+static argcheck_T arg1_tuple_any[] = {arg_tuple_any};
 static argcheck_T arg1_list_number[] = {arg_list_number};
-static argcheck_T arg1_string_or_list_or_blob_mod[] = {arg_string_list_or_blob_mod};
-static argcheck_T arg1_list_or_dict[] = {arg_list_or_dict};
+static argcheck_T arg1_reverse[] = {arg_reverse};
+static argcheck_T arg1_list_or_tuple_or_dict[] = {arg_list_or_tuple_or_dict};
 static argcheck_T arg1_list_string[] = {arg_list_string};
-static argcheck_T arg1_string_or_list_or_dict[] = {arg_string_or_list_or_dict};
+static argcheck_T arg1_string_list_tuple_or_dict[] = {arg_string_list_tuple_or_dict};
 static argcheck_T arg1_lnum[] = {arg_lnum};
 static argcheck_T arg1_number[] = {arg_number};
 static argcheck_T arg1_string[] = {arg_string};
@@ -1141,7 +1271,6 @@
 static argcheck_T arg2_job_dict[] = {arg_job, arg_dict_any};
 static argcheck_T arg2_job_string_or_number[] = {arg_job, arg_string_or_nr};
 static argcheck_T arg2_list_any_number[] = {arg_list_any, arg_number};
-static argcheck_T arg2_list_any_string[] = {arg_list_any, arg_string};
 static argcheck_T arg2_list_number[] = {arg_list_number, arg_list_number};
 static argcheck_T arg2_list_number_bool[] = {arg_list_number, arg_bool};
 static argcheck_T arg2_list_string_dict[] = {arg_list_string, arg_dict_any};
@@ -1168,6 +1297,7 @@
 static argcheck_T arg2_string_or_list_number[] = {arg_string_or_list_any, arg_number};
 static argcheck_T arg2_string_string_or_number[] = {arg_string, arg_string_or_nr};
 static argcheck_T arg2_blob_dict[] = {arg_blob, arg_dict_any};
+static argcheck_T arg2_list_or_tuple_string[] = {arg_list_or_tuple, arg_string};
 static argcheck_T arg3_any_list_dict[] = {arg_any, arg_list_any, arg_dict_any};
 static argcheck_T arg3_buffer_lnum_lnum[] = {arg_buffer, arg_lnum, arg_lnum};
 static argcheck_T arg3_buffer_number_number[] = {arg_buffer, arg_number, arg_number};
@@ -1205,7 +1335,7 @@
 static argcheck_T arg4_browse[] = {arg_bool, arg_string, arg_string, arg_string};
 static argcheck_T arg23_chanexpr[] = {arg_chan_or_job, arg_any, arg_dict_any};
 static argcheck_T arg23_chanraw[] = {arg_chan_or_job, arg_string_or_blob, arg_dict_any};
-static argcheck_T arg24_count[] = {arg_string_or_list_or_dict, arg_any, arg_bool, arg_number};
+static argcheck_T arg24_count[] = {arg_string_list_tuple_or_dict, arg_any, arg_bool, arg_number};
 static argcheck_T arg13_cursor[] = {arg_cursor1, arg_number, arg_number};
 static argcheck_T arg12_deepcopy[] = {arg_any, arg_bool};
 static argcheck_T arg12_execute[] = {arg_string_or_list_string, arg_string};
@@ -1215,14 +1345,14 @@
 static argcheck_T arg23_get[] = {arg_get1, arg_string_or_nr, arg_any};
 static argcheck_T arg14_glob[] = {arg_string, arg_bool, arg_bool, arg_bool};
 static argcheck_T arg25_globpath[] = {arg_string, arg_string, arg_bool, arg_bool, arg_bool};
-static argcheck_T arg24_index[] = {arg_list_or_blob, arg_item_of_prev, arg_number, arg_bool};
-static argcheck_T arg23_index[] = {arg_list_or_blob, arg_filter_func, arg_dict_any};
+static argcheck_T arg24_index[] = {arg_list_or_tuple_or_blob, arg_item_of_prev, arg_number, arg_bool};
+static argcheck_T arg23_index[] = {arg_list_or_tuple_or_blob, arg_filter_func, arg_dict_any};
 static argcheck_T arg23_insert[] = {arg_list_or_blob, arg_item_of_prev, arg_number};
 static argcheck_T arg1_len[] = {arg_len1};
 static argcheck_T arg3_libcall[] = {arg_string, arg_string, arg_string_or_nr};
 static argcheck_T arg14_maparg[] = {arg_string, arg_string, arg_bool, arg_bool};
 static argcheck_T arg2_filter[] = {arg_list_or_dict_or_blob_or_string_mod, arg_filter_func};
-static argcheck_T arg2_foreach[] = {arg_list_or_dict_or_blob_or_string, arg_foreach_func};
+static argcheck_T arg2_foreach[] = {arg_list_tuple_dict_blob_or_string, arg_foreach_func};
 static argcheck_T arg2_instanceof[] = {arg_object, varargs_class, NULL };
 static argcheck_T arg2_map[] = {arg_list_or_dict_or_blob_or_string_mod, arg_map_func};
 static argcheck_T arg2_mapnew[] = {arg_list_or_dict_or_blob_or_string, arg_any};
@@ -1231,7 +1361,7 @@
 static argcheck_T arg23_matchstrlist[] = {arg_list_string, arg_string, arg_dict_any};
 static argcheck_T arg45_matchbufline[] = {arg_buffer, arg_string, arg_lnum, arg_lnum, arg_dict_any};
 static argcheck_T arg119_printf[] = {arg_string_or_nr, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any};
-static argcheck_T arg23_reduce[] = {arg_string_list_or_blob, arg_any, arg_any};
+static argcheck_T arg23_reduce[] = {arg_string_list_tuple_or_blob, arg_any, arg_any};
 static argcheck_T arg24_remote_expr[] = {arg_string, arg_string, arg_string, arg_number};
 static argcheck_T arg23_remove[] = {arg_list_or_dict_or_blob_mod, arg_remove2, arg_number};
 static argcheck_T arg2_repeat[] = {arg_repeat1, arg_number};
@@ -1364,6 +1494,13 @@
     return &t_list_list_list_number;
 }
     static type_T *
+ret_tuple_any(int argcount UNUSED,
+	type2_T *argtypes UNUSED,
+	type_T	**decl_type UNUSED)
+{
+    return &t_tuple_any;
+}
+    static type_T *
 ret_dict_any(int argcount UNUSED,
 	type2_T *argtypes UNUSED,
 	type_T	**decl_type UNUSED)
@@ -1457,6 +1594,7 @@
 		case VAR_STRING: *decl_type = &t_string; break;
 		case VAR_BLOB: *decl_type = &t_blob; break;
 		case VAR_LIST: *decl_type = &t_list_any; break;
+		case VAR_TUPLE: *decl_type = &t_tuple_any; break;
 		default: break;
 	    }
 	}
@@ -2288,7 +2426,7 @@
 			ret_number_bool,    f_islocked},
     {"isnan",		1, 1, FEARG_1,	    arg1_float_or_nr,
 			ret_number_bool,    MATH_FUNC(f_isnan)},
-    {"items",		1, 1, FEARG_1,	    arg1_string_or_list_or_dict,
+    {"items",		1, 1, FEARG_1,	    arg1_string_list_tuple_or_dict,
 			ret_list_items,	    f_items},
     {"job_getchannel",	1, 1, FEARG_1,	    arg1_job,
 			ret_channel,	    JOB_FUNC(f_job_getchannel)},
@@ -2302,7 +2440,7 @@
 			ret_string,	    JOB_FUNC(f_job_status)},
     {"job_stop",	1, 2, FEARG_1,	    arg2_job_string_or_number,
 			ret_number_bool,    JOB_FUNC(f_job_stop)},
-    {"join",		1, 2, FEARG_1,	    arg2_list_any_string,
+    {"join",		1, 2, FEARG_1,	    arg2_list_or_tuple_string,
 			ret_string,	    f_join},
     {"js_decode",	1, 1, FEARG_1,	    arg1_string,
 			ret_any,	    f_js_decode},
@@ -2334,6 +2472,8 @@
 			ret_blob,	    f_list2blob},
     {"list2str",	1, 2, FEARG_1,	    arg2_list_number_bool,
 			ret_string,	    f_list2str},
+    {"list2tuple",	1, 1, FEARG_1,	    arg1_list_any,
+			ret_tuple_any,	    f_list2tuple},
     {"listener_add",	1, 2, FEARG_2,	    arg2_any_buffer,
 			ret_number,	    f_listener_add},
     {"listener_flush",	0, 1, FEARG_1,	    arg1_buffer,
@@ -2392,7 +2532,7 @@
 			ret_list_any,	    f_matchstrlist},
     {"matchstrpos",	2, 4, FEARG_1,	    arg24_match_func,
 			ret_list_any,	    f_matchstrpos},
-    {"max",		1, 1, FEARG_1,	    arg1_list_or_dict,
+    {"max",		1, 1, FEARG_1,	    arg1_list_or_tuple_or_dict,
 			ret_number,	    f_max},
     {"menu_info",	1, 2, FEARG_1,	    arg2_string,
 			ret_dict_any,
@@ -2402,7 +2542,7 @@
 	    NULL
 #endif
 			},
-    {"min",		1, 1, FEARG_1,	    arg1_list_or_dict,
+    {"min",		1, 1, FEARG_1,	    arg1_list_or_tuple_or_dict,
 			ret_number,	    f_min},
     {"mkdir",		1, 3, FEARG_1,	    arg3_string_string_number,
 			ret_number_bool,    f_mkdir},
@@ -2588,7 +2728,7 @@
 			ret_repeat,	    f_repeat},
     {"resolve",		1, 1, FEARG_1,	    arg1_string,
 			ret_string,	    f_resolve},
-    {"reverse",		1, 1, FEARG_1,	    arg1_string_or_list_or_blob_mod,
+    {"reverse",		1, 1, FEARG_1,	    arg1_reverse,
 			ret_first_arg,	    f_reverse},
     {"round",		1, 1, FEARG_1,	    arg1_float_or_nr,
 			ret_float,	    f_round},
@@ -2918,6 +3058,8 @@
 			ret_func_any,	    f_test_null_partial},
     {"test_null_string", 0, 0, 0,	    NULL,
 			ret_string,	    f_test_null_string},
+    {"test_null_tuple",	0, 0, 0,	    NULL,
+			ret_tuple_any,	    f_test_null_tuple},
     {"test_option_not_set", 1, 1, FEARG_1,  arg1_string,
 			ret_void,	    f_test_option_not_set},
     {"test_override",	2, 2, FEARG_2,	    arg2_string_number,
@@ -2954,6 +3096,8 @@
 			ret_string,	    f_trim},
     {"trunc",		1, 1, FEARG_1,	    arg1_float_or_nr,
 			ret_float,	    f_trunc},
+    {"tuple2list",	1, 1, FEARG_1,	    arg1_tuple_any,
+			ret_list_any,	    f_tuple2list},
     {"type",		1, 1, FEARG_1|FE_X, NULL,
 			ret_number,	    f_type},
     {"typename",	1, 1, FEARG_1|FE_X, NULL,
@@ -4226,6 +4370,9 @@
 	    n = argvars[0].vval.v_list == NULL
 					|| argvars[0].vval.v_list->lv_len == 0;
 	    break;
+	case VAR_TUPLE:
+	    n = tuple_len(argvars[0].vval.v_tuple) == 0;
+	    break;
 	case VAR_DICT:
 	    n = argvars[0].vval.v_dict == NULL
 			|| argvars[0].vval.v_dict->dv_hashtab.ht_used == 0;
@@ -5263,6 +5410,7 @@
 {
     listitem_T	*li;
     list_T	*l;
+    tuple_T	*tuple;
     dictitem_T	*di;
     dict_T	*d;
     typval_T	*tv = NULL;
@@ -5298,6 +5446,18 @@
 		tv = &li->li_tv;
 	}
     }
+    else if (argvars[0].v_type == VAR_TUPLE)
+    {
+	if ((tuple = argvars[0].vval.v_tuple) != NULL)
+	{
+	    int		error = FALSE;
+	    long	idx;
+
+	    idx = (long)tv_get_number_chk(&argvars[1], &error);
+	    if (!error)
+		tv = tuple_find(tuple, idx);
+	}
+    }
     else if (argvars[0].v_type == VAR_DICT)
     {
 	if ((d = argvars[0].vval.v_dict) != NULL)
@@ -5400,7 +5560,7 @@
 	}
     }
     else
-	semsg(_(e_argument_of_str_must_be_list_dictionary_or_blob), "get()");
+	semsg(_(e_argument_of_str_must_be_list_tuple_dictionary_or_blob), "get()");
 
     if (tv == NULL)
     {
@@ -7811,6 +7971,7 @@
     switch (argvars[0].v_type)
     {
 	case VAR_LIST:
+	case VAR_TUPLE:
 	case VAR_DICT:
 	case VAR_OBJECT:
 	case VAR_JOB:
@@ -7837,67 +7998,84 @@
 }
 
 /*
- * "index()" function
+ * index() function for a blob
  */
     static void
-f_index(typval_T *argvars, typval_T *rettv)
+index_func_blob(typval_T *argvars, typval_T *rettv)
 {
-    list_T	*l;
-    listitem_T	*item;
+    typval_T	tv;
     blob_T	*b;
-    long	idx = 0;
+    int		start = 0;
+    int		error = FALSE;
+    int		ic = FALSE;
+
+    b = argvars[0].vval.v_blob;
+    if (b == NULL)
+	return;
+
+    if (argvars[2].v_type != VAR_UNKNOWN)
+    {
+	start = tv_get_number_chk(&argvars[2], &error);
+	if (error)
+	    return;
+    }
+
+    if (start < 0)
+    {
+	start = blob_len(b) + start;
+	if (start < 0)
+	    start = 0;
+    }
+
+    for (int idx = start; idx < blob_len(b); ++idx)
+    {
+	tv.v_type = VAR_NUMBER;
+	tv.vval.v_number = blob_get(b, idx);
+	if (tv_equal(&tv, &argvars[1], ic))
+	{
+	    rettv->vval.v_number = idx;
+	    return;
+	}
+    }
+}
+
+/*
+ * index() function for a tuple
+ */
+    static void
+index_func_tuple(typval_T *argvars, typval_T *rettv)
+{
+    tuple_T	*tuple = argvars[0].vval.v_tuple;
     int		ic = FALSE;
     int		error = FALSE;
 
-    rettv->vval.v_number = -1;
-
-    if (in_vim9script()
-	    && (check_for_list_or_blob_arg(argvars, 0) == FAIL
-		|| (argvars[0].v_type == VAR_BLOB
-		    && check_for_number_arg(argvars, 1) == FAIL)
-		|| check_for_opt_number_arg(argvars, 2) == FAIL
-		|| (argvars[2].v_type != VAR_UNKNOWN
-		    && check_for_opt_bool_arg(argvars, 3) == FAIL)))
+    if (tuple == NULL)
 	return;
 
-    if (argvars[0].v_type == VAR_BLOB)
+    int	start_idx = 0;
+    if (argvars[2].v_type != VAR_UNKNOWN)
     {
-	typval_T	tv;
-	int		start = 0;
-
-	if (argvars[2].v_type != VAR_UNKNOWN)
-	{
-	    start = tv_get_number_chk(&argvars[2], &error);
-	    if (error)
-		return;
-	}
-	b = argvars[0].vval.v_blob;
-	if (b == NULL)
+	start_idx = tv_get_number_chk(&argvars[2], &error);
+	if (!error && argvars[3].v_type != VAR_UNKNOWN)
+	    ic = (int)tv_get_bool_chk(&argvars[3], &error);
+	if (error)
 	    return;
-	if (start < 0)
-	{
-	    start = blob_len(b) + start;
-	    if (start < 0)
-		start = 0;
-	}
+    }
 
-	for (idx = start; idx < blob_len(b); ++idx)
-	{
-	    tv.v_type = VAR_NUMBER;
-	    tv.vval.v_number = blob_get(b, idx);
-	    if (tv_equal(&tv, &argvars[1], ic))
-	    {
-		rettv->vval.v_number = idx;
-		return;
-	    }
-	}
-	return;
-    }
-    else if (argvars[0].v_type != VAR_LIST)
-    {
-	emsg(_(e_list_or_blob_required));
-	return;
-    }
+    rettv->vval.v_number = index_tuple(tuple, &argvars[1], start_idx, ic);
+}
+
+/*
+ * index() function for a list
+ */
+    static void
+index_func_list(typval_T *argvars, typval_T *rettv)
+{
+    list_T	*l;
+    listitem_T	*item;
+    long	idx = 0;
+    int		ic = FALSE;
+    int		error = FALSE;
 
     l = argvars[0].vval.v_list;
     if (l == NULL)
@@ -7926,11 +8104,38 @@
 }
 
 /*
+ * "index()" function
+ */
+    static void
+f_index(typval_T *argvars, typval_T *rettv)
+{
+    rettv->vval.v_number = -1;
+
+    if (in_vim9script()
+	    && (check_for_list_or_tuple_or_blob_arg(argvars, 0) == FAIL
+		|| (argvars[0].v_type == VAR_BLOB
+		    && check_for_number_arg(argvars, 1) == FAIL)
+		|| check_for_opt_number_arg(argvars, 2) == FAIL
+		|| (argvars[2].v_type != VAR_UNKNOWN
+		    && check_for_opt_bool_arg(argvars, 3) == FAIL)))
+	return;
+
+    if (argvars[0].v_type == VAR_BLOB)
+	index_func_blob(argvars, rettv);
+    else if (argvars[0].v_type == VAR_TUPLE)
+	index_func_tuple(argvars, rettv);
+    else if (argvars[0].v_type == VAR_LIST)
+	index_func_list(argvars, rettv);
+    else
+	emsg(_(e_list_or_blob_required));
+}
+
+/*
  * Evaluate 'expr' with the v:key and v:val arguments and return the result.
  * The expression is expected to return a boolean value.  The caller should set
  * the VV_KEY and VV_VAL vim variables before calling this function.
  */
-    static int
+    int
 indexof_eval_expr(typval_T *expr)
 {
     typval_T	argv[3];
@@ -8053,7 +8258,7 @@
 
     rettv->vval.v_number = -1;
 
-    if (check_for_list_or_blob_arg(argvars, 0) == FAIL
+    if (check_for_list_or_tuple_or_blob_arg(argvars, 0) == FAIL
 	    || check_for_string_or_func_arg(argvars, 1) == FAIL
 	    || check_for_opt_dict_arg(argvars, 2) == FAIL)
 	return;
@@ -8079,6 +8284,9 @@
     if (argvars[0].v_type == VAR_BLOB)
 	rettv->vval.v_number = indexof_blob(argvars[0].vval.v_blob, startidx,
 								&argvars[1]);
+    else if (argvars[0].v_type == VAR_TUPLE)
+	rettv->vval.v_number = indexof_tuple(argvars[0].vval.v_tuple, startidx,
+								&argvars[1]);
     else
 	rettv->vval.v_number = indexof_list(argvars[0].vval.v_list, startidx,
 								&argvars[1]);
@@ -8488,6 +8696,9 @@
 	case VAR_LIST:
 	    rettv->vval.v_number = list_len(argvars[0].vval.v_list);
 	    break;
+	case VAR_TUPLE:
+	    rettv->vval.v_number = tuple_len(argvars[0].vval.v_tuple);
+	    break;
 	case VAR_DICT:
 	    rettv->vval.v_number = dict_len(argvars[0].vval.v_dict);
 	    break;
@@ -9229,7 +9440,8 @@
     varnumber_T	i;
     int		error = FALSE;
 
-    if (in_vim9script() && check_for_list_or_dict_arg(argvars, 0) == FAIL)
+    if (in_vim9script() &&
+	    check_for_list_or_tuple_or_dict_arg(argvars, 0) == FAIL)
 	return;
 
     if (argvars[0].v_type == VAR_LIST)
@@ -9271,6 +9483,12 @@
 	    }
 	}
     }
+    else if (argvars[0].v_type == VAR_TUPLE)
+    {
+	n = tuple_max_min(argvars[0].vval.v_tuple, domax, &error);
+	if (error)
+	    return;
+    }
     else if (argvars[0].v_type == VAR_DICT)
     {
 	dict_T		*d;
@@ -10076,83 +10294,114 @@
 }
 
 /*
- * "repeat()" function
+ * Repeat the list "l" "n" times and set "rettv" to the new list.
  */
     static void
-f_repeat(typval_T *argvars, typval_T *rettv)
+repeat_list(list_T *l, int n, typval_T *rettv)
+{
+    if (rettv_list_alloc(rettv) == FAIL
+	    || l == NULL
+	    || n <= 0)
+	return;
+
+    while (n-- > 0)
+	if (list_extend(rettv->vval.v_list, l, NULL) == FAIL)
+	    break;
+}
+
+/*
+ * Repeat the blob "b" "n" times and set "rettv" to the new blob.
+ */
+    static void
+repeat_blob(typval_T *blob_tv, int n, typval_T *rettv)
+{
+    int		slen;
+    int		len;
+    int		i;
+    blob_T	*blob = blob_tv->vval.v_blob;
+
+    if (rettv_blob_alloc(rettv) == FAIL
+	    || blob == NULL
+	    || n <= 0)
+	return;
+
+    slen = blob->bv_ga.ga_len;
+    len = (int)slen * n;
+    if (len <= 0)
+	return;
+
+    if (ga_grow(&rettv->vval.v_blob->bv_ga, len) == FAIL)
+	return;
+
+    rettv->vval.v_blob->bv_ga.ga_len = len;
+
+    for (i = 0; i < slen; ++i)
+	if (blob_get(blob, i) != 0)
+	    break;
+
+    if (i == slen)
+	// No need to copy since all bytes are already zero
+	return;
+
+    for (i = 0; i < n; ++i)
+	blob_set_range(rettv->vval.v_blob,
+		(long)i * slen, ((long)i + 1) * slen - 1, blob_tv);
+}
+
+/*
+ * Repeat the string "str" "n" times and set "rettv" to the new string.
+ */
+    static void
+repeat_string(typval_T *str_tv, int n, typval_T *rettv)
 {
     char_u	*p;
-    varnumber_T	n;
     int		slen;
     int		len;
     char_u	*r;
     int		i;
 
+    p = tv_get_string(str_tv);
+    rettv->v_type = VAR_STRING;
+    rettv->vval.v_string = NULL;
+
+    slen = (int)STRLEN(p);
+    len = slen * n;
+    if (len <= 0)
+	return;
+
+    r = alloc(len + 1);
+    if (r == NULL)
+	return;
+
+    for (i = 0; i < n; i++)
+	mch_memmove(r + i * slen, p, (size_t)slen);
+    r[len] = NUL;
+
+    rettv->vval.v_string = r;
+}
+
+/*
+ * "repeat()" function
+ */
+    static void
+f_repeat(typval_T *argvars, typval_T *rettv)
+{
+    varnumber_T	n;
+
     if (in_vim9script()
-	    && (check_for_string_or_number_or_list_or_blob_arg(argvars, 0)
-		    == FAIL
+	    && (check_for_repeat_func_arg(argvars, 0) == FAIL
 		|| check_for_number_arg(argvars, 1) == FAIL))
 	return;
 
     n = tv_get_number(&argvars[1]);
     if (argvars[0].v_type == VAR_LIST)
-    {
-	if (rettv_list_alloc(rettv) == OK && argvars[0].vval.v_list != NULL)
-	    while (n-- > 0)
-		if (list_extend(rettv->vval.v_list,
-					argvars[0].vval.v_list, NULL) == FAIL)
-		    break;
-    }
+	repeat_list(argvars[0].vval.v_list, n, rettv);
+    else if (argvars[0].v_type == VAR_TUPLE)
+	tuple_repeat(argvars[0].vval.v_tuple, n, rettv);
     else if (argvars[0].v_type == VAR_BLOB)
-    {
-	if (rettv_blob_alloc(rettv) == FAIL
-		|| argvars[0].vval.v_blob == NULL
-		|| n <= 0)
-	    return;
-
-	slen = argvars[0].vval.v_blob->bv_ga.ga_len;
-	len = (int)slen * n;
-	if (len <= 0)
-	    return;
-
-	if (ga_grow(&rettv->vval.v_blob->bv_ga, len) == FAIL)
-	    return;
-
-	rettv->vval.v_blob->bv_ga.ga_len = len;
-
-	for (i = 0; i < slen; ++i)
-	    if (blob_get(argvars[0].vval.v_blob, i) != 0)
-		break;
-
-	if (i == slen)
-	    // No need to copy since all bytes are already zero
-	    return;
-
-	for (i = 0; i < n; ++i)
-	    blob_set_range(rettv->vval.v_blob,
-		    (long)i * slen, ((long)i + 1) * slen - 1, argvars);
-    }
+	repeat_blob(&argvars[0], n, rettv);
     else
-    {
-	p = tv_get_string(&argvars[0]);
-	rettv->v_type = VAR_STRING;
-	rettv->vval.v_string = NULL;
-
-	slen = (int)STRLEN(p);
-	len = slen * n;
-	if (len <= 0)
-	    return;
-
-	r = alloc(len + 1);
-	if (r != NULL)
-	{
-	    for (i = 0; i < n; i++)
-		mch_memmove(r + i * slen, p, (size_t)slen);
-	    r[len] = NUL;
-	}
-
-	rettv->vval.v_string = r;
-    }
+	repeat_string(&argvars[0], n, rettv);
 }
 
 #define SP_NOMOVE	0x01	    // don't move cursor
@@ -12191,6 +12440,7 @@
 	case VAR_PARTIAL:
 	case VAR_FUNC:    n = VAR_TYPE_FUNC; break;
 	case VAR_LIST:    n = VAR_TYPE_LIST; break;
+	case VAR_TUPLE:   n = VAR_TYPE_TUPLE; break;
 	case VAR_DICT:    n = VAR_TYPE_DICT; break;
 	case VAR_FLOAT:   n = VAR_TYPE_FLOAT; break;
 	case VAR_BOOL:	  n = VAR_TYPE_BOOL; break;
diff --git a/src/evalvars.c b/src/evalvars.c
index 2745ac2..9382842 100644
--- a/src/evalvars.c
+++ b/src/evalvars.c
@@ -162,6 +162,7 @@
     {VV_NAME("t_enum",		 VAR_NUMBER), NULL, VV_RO},
     {VV_NAME("t_enumvalue",	 VAR_NUMBER), NULL, VV_RO},
     {VV_NAME("stacktrace",	 VAR_LIST), &t_list_dict_any, VV_RO},
+    {VV_NAME("t_tuple",		 VAR_NUMBER), NULL, VV_RO},
 };
 
 // shorthand
@@ -265,8 +266,9 @@
     set_vim_var_nr(VV_TYPE_CLASS,   VAR_TYPE_CLASS);
     set_vim_var_nr(VV_TYPE_OBJECT,  VAR_TYPE_OBJECT);
     set_vim_var_nr(VV_TYPE_TYPEALIAS,  VAR_TYPE_TYPEALIAS);
-    set_vim_var_nr(VV_TYPE_ENUM,  VAR_TYPE_ENUM);
+    set_vim_var_nr(VV_TYPE_ENUM,    VAR_TYPE_ENUM);
     set_vim_var_nr(VV_TYPE_ENUMVALUE,  VAR_TYPE_ENUMVALUE);
+    set_vim_var_nr(VV_TYPE_TUPLE,   VAR_TYPE_TUPLE);
 
     set_vim_var_nr(VV_ECHOSPACE,    sc_col - 1);
 
@@ -321,13 +323,13 @@
     int
 garbage_collect_globvars(int copyID)
 {
-    return set_ref_in_ht(&globvarht, copyID, NULL);
+    return set_ref_in_ht(&globvarht, copyID, NULL, NULL);
 }
 
     int
 garbage_collect_vimvars(int copyID)
 {
-    return set_ref_in_ht(&vimvarht, copyID, NULL);
+    return set_ref_in_ht(&vimvarht, copyID, NULL, NULL);
 }
 
     int
@@ -340,7 +342,7 @@
 
     for (i = 1; i <= script_items.ga_len; ++i)
     {
-	abort = abort || set_ref_in_ht(&SCRIPT_VARS(i), copyID, NULL);
+	abort = abort || set_ref_in_ht(&SCRIPT_VARS(i), copyID, NULL, NULL);
 
 	si = SCRIPT_ITEM(i);
 	for (idx = 0; idx < si->sn_var_vals.ga_len; ++idx)
@@ -348,7 +350,7 @@
 	    svar_T    *sv = ((svar_T *)si->sn_var_vals.ga_data) + idx;
 
 	    if (sv->sv_name != NULL)
-		abort = abort || set_ref_in_item(sv->sv_tv, copyID, NULL, NULL);
+		abort = abort || set_ref_in_item(sv->sv_tv, copyID, NULL, NULL, NULL);
 	}
     }
 
@@ -1234,10 +1236,13 @@
 {
     char_u	*arg = arg_start;
     list_T	*l;
+    tuple_T	*tuple = NULL;
     int		i;
     int		var_idx = 0;
-    listitem_T	*item;
+    listitem_T	*item = NULL;
     typval_T	ltv;
+    int		is_list = tv->v_type == VAR_LIST;
+    int		idx;
 
     if (tv->v_type == VAR_VOID)
     {
@@ -1253,58 +1258,121 @@
     }
 
     // ":let [v1, v2] = list" or ":for [v1, v2] in listlist"
-    if (tv->v_type != VAR_LIST || (l = tv->vval.v_list) == NULL)
+    // or
+    // ":let [v1, v2] = tuple" or ":for [v1, v2] in tupletuple"
+    if (tv->v_type != VAR_LIST && tv->v_type != VAR_TUPLE)
     {
-	emsg(_(e_list_required));
+	emsg(_(e_list_or_tuple_required));
 	return FAIL;
     }
+    if (is_list)
+    {
+	l = tv->vval.v_list;
+	if (l == NULL)
+	{
+	    emsg(_(e_list_required));
+	    return FAIL;
+	}
+	i = list_len(l);
+    }
+    else
+    {
+	tuple = tv->vval.v_tuple;
+	if (tuple == NULL)
+	{
+	    emsg(_(e_tuple_required));
+	    return FAIL;
+	}
+	i = tuple_len(tuple);
+    }
 
-    i = list_len(l);
     if (semicolon == 0 && var_count < i)
     {
-	emsg(_(e_less_targets_than_list_items));
+	emsg(_(is_list ? e_less_targets_than_list_items
+					: e_less_targets_than_tuple_items));
 	return FAIL;
     }
     if (var_count - semicolon > i)
     {
-	emsg(_(e_more_targets_than_list_items));
+	emsg(_(is_list ? e_more_targets_than_list_items
+					: e_more_targets_than_tuple_items));
 	return FAIL;
     }
 
-    CHECK_LIST_MATERIALIZE(l);
-    item = l->lv_first;
+    if (is_list)
+    {
+	CHECK_LIST_MATERIALIZE(l);
+	item = l->lv_first;
+    }
+    else
+	idx = 0;
+
     while (*arg != ']')
     {
 	arg = skipwhite(arg + 1);
 	++var_idx;
-	arg = ex_let_one(arg, &item->li_tv, TRUE,
-			  flags | ASSIGN_UNPACK, (char_u *)",;]", op, var_idx);
-	item = item->li_next;
+	arg = ex_let_one(arg, is_list ? &item->li_tv : TUPLE_ITEM(tuple, idx),
+			 TRUE, flags | ASSIGN_UNPACK, (char_u *)",;]", op,
+			 var_idx);
+	if (is_list)
+	    item = item->li_next;
+	else
+	    idx++;
 	if (arg == NULL)
 	    return FAIL;
 
 	arg = skipwhite(arg);
 	if (*arg == ';')
 	{
-	    // Put the rest of the list (may be empty) in the var after ';'.
-	    // Create a new list for this.
-	    l = list_alloc();
-	    if (l == NULL)
-		return FAIL;
-	    while (item != NULL)
+	    // Put the rest of the list or tuple (may be empty) in the var
+	    // after ';'.  Create a new list or tuple for this.
+	    if (is_list)
 	    {
-		list_append_tv(l, &item->li_tv);
-		item = item->li_next;
+		// Put the rest of the list (may be empty) in the var
+		// after ';'.  Create a new list for this.
+		l = list_alloc();
+		if (l == NULL)
+		    return FAIL;
+
+		// list
+		while (item != NULL)
+		{
+		    list_append_tv(l, &item->li_tv);
+		    item = item->li_next;
+		}
+
+		ltv.v_type = VAR_LIST;
+		ltv.v_lock = 0;
+		ltv.vval.v_list = l;
+		l->lv_refcount = 1;
+	    }
+	    else
+	    {
+		tuple_T	*new_tuple = tuple_alloc();
+		if (new_tuple == NULL)
+		    return FAIL;
+
+		// Put the rest of the tuple (may be empty) in the var
+		// after ';'.  Create a new tuple for this.
+		while (idx < TUPLE_LEN(tuple))
+		{
+		    typval_T    new_tv;
+
+		    copy_tv(TUPLE_ITEM(tuple, idx), &new_tv);
+		    if (tuple_append_tv(new_tuple, &new_tv) == FAIL)
+			return FAIL;
+		    idx++;
+		}
+
+		ltv.v_type = VAR_TUPLE;
+		ltv.v_lock = 0;
+		ltv.vval.v_tuple = new_tuple;
+		new_tuple->tv_refcount = 1;
 	    }
 
-	    ltv.v_type = VAR_LIST;
-	    ltv.v_lock = 0;
-	    ltv.vval.v_list = l;
-	    l->lv_refcount = 1;
 	    ++var_idx;
-
 	    arg = ex_let_one(skipwhite(arg + 1), &ltv, FALSE,
-			    flags | ASSIGN_UNPACK, (char_u *)"]", op, var_idx);
+			flags | ASSIGN_UNPACK, (char_u *)"]", op, var_idx);
 	    clear_tv(&ltv);
 	    if (arg == NULL)
 		return FAIL;
@@ -2418,6 +2486,9 @@
 		}
 	    }
 	    break;
+	case VAR_TUPLE:
+	    tuple_lock(tv->vval.v_tuple, deep, lock, check_refcount);
+	    break;
 	case VAR_DICT:
 	    if ((d = tv->vval.v_dict) != NULL
 				    && !(check_refcount && d->dv_refcount > 1))
@@ -3189,9 +3260,9 @@
 		}
 	    }
 
-	    // If a list or dict variable wasn't initialized and has meaningful
-	    // type, do it now.  Not for global variables, they are not
-	    // declared.
+	    // If a list or tuple or dict variable wasn't initialized and has
+	    // meaningful type, do it now.  Not for global variables, they are
+	    // not declared.
 	    if (ht != &globvarht)
 	    {
 		if (tv->v_type == VAR_DICT && tv->vval.v_dict == NULL
@@ -3220,6 +3291,19 @@
 			    sv->sv_flags |= SVFLAG_ASSIGNED;
 		    }
 		}
+		else if (tv->v_type == VAR_TUPLE && tv->vval.v_tuple == NULL
+					    && ((type != NULL && !was_assigned)
+							  || !in_vim9script()))
+		{
+		    tv->vval.v_tuple = tuple_alloc();
+		    if (tv->vval.v_tuple != NULL)
+		    {
+			++tv->vval.v_tuple->tv_refcount;
+			tv->vval.v_tuple->tv_type = alloc_type(type);
+			if (sv != NULL)
+			    sv->sv_flags |= SVFLAG_ASSIGNED;
+		    }
+		}
 		else if (tv->v_type == VAR_BLOB && tv->vval.v_blob == NULL
 					    && ((type != NULL && !was_assigned)
 							  || !in_vim9script()))
diff --git a/src/ex_docmd.c b/src/ex_docmd.c
index bda20f5..f341fd4 100644
--- a/src/ex_docmd.c
+++ b/src/ex_docmd.c
@@ -3799,8 +3799,12 @@
 		if (eq != NULL)
 		{
 		    eq = skipwhite(eq);
-		    if (vim_strchr((char_u *)"+-*/%", *eq) != NULL)
+		    if (vim_strchr((char_u *)"+-*/%.", *eq) != NULL)
+		    {
+			if (eq[0] == '.' && eq[1] == '.')
+			    ++eq;
 			++eq;
+		    }
 		}
 		if (p == NULL || p == eap->cmd || *eq != '=')
 		{
diff --git a/src/gc.c b/src/gc.c
index 54c7444..b95b2ca 100644
--- a/src/gc.c
+++ b/src/gc.c
@@ -122,31 +122,31 @@
     // buffer-local variables
     FOR_ALL_BUFFERS(buf)
 	abort = abort || set_ref_in_item(&buf->b_bufvar.di_tv, copyID,
-								  NULL, NULL);
+							NULL, NULL, NULL);
 
     // window-local variables
     FOR_ALL_TAB_WINDOWS(tp, wp)
 	abort = abort || set_ref_in_item(&wp->w_winvar.di_tv, copyID,
-								  NULL, NULL);
+							NULL, NULL, NULL);
     // window-local variables in autocmd windows
     for (int i = 0; i < AUCMD_WIN_COUNT; ++i)
 	if (aucmd_win[i].auc_win != NULL)
 	    abort = abort || set_ref_in_item(
-		    &aucmd_win[i].auc_win->w_winvar.di_tv, copyID, NULL, NULL);
+		    &aucmd_win[i].auc_win->w_winvar.di_tv, copyID, NULL, NULL, NULL);
 #ifdef FEAT_PROP_POPUP
     FOR_ALL_POPUPWINS(wp)
 	abort = abort || set_ref_in_item(&wp->w_winvar.di_tv, copyID,
-								  NULL, NULL);
+								  NULL, NULL, NULL);
     FOR_ALL_TABPAGES(tp)
 	FOR_ALL_POPUPWINS_IN_TAB(tp, wp)
 		abort = abort || set_ref_in_item(&wp->w_winvar.di_tv, copyID,
-								  NULL, NULL);
+								  NULL, NULL, NULL);
 #endif
 
     // tabpage-local variables
     FOR_ALL_TABPAGES(tp)
 	abort = abort || set_ref_in_item(&tp->tp_winvar.di_tv, copyID,
-								  NULL, NULL);
+								  NULL, NULL, NULL);
     // global variables
     abort = abort || garbage_collect_globvars(copyID);
 
@@ -269,6 +269,9 @@
     // Go through the list of lists and free items without this copyID.
     did_free |= list_free_nonref(copyID);
 
+    // Go through the list of tuples and free items without this copyID.
+    did_free |= tuple_free_nonref(copyID);
+
     // Go through the list of objects and free items without this copyID.
     did_free |= object_free_nonref(copyID);
 
@@ -291,6 +294,7 @@
     object_free_items(copyID);
     dict_free_items(copyID);
     list_free_items(copyID);
+    tuple_free_items(copyID);
 
 #ifdef FEAT_JOB_CHANNEL
     // Go through the list of jobs and free items without the copyID. This
@@ -314,7 +318,11 @@
  * Returns TRUE if setting references failed somehow.
  */
     int
-set_ref_in_ht(hashtab_T *ht, int copyID, list_stack_T **list_stack)
+set_ref_in_ht(
+    hashtab_T		*ht,
+    int			copyID,
+    list_stack_T	**list_stack,
+    tuple_stack_T	**tuple_stack)
 {
     int		todo;
     int		abort = FALSE;
@@ -336,8 +344,9 @@
 		if (!HASHITEM_EMPTY(hi))
 		{
 		    --todo;
-		    abort = abort || set_ref_in_item(&HI2DI(hi)->di_tv, copyID,
-						       &ht_stack, list_stack);
+		    abort = abort
+			|| set_ref_in_item(&HI2DI(hi)->di_tv, copyID,
+				       &ht_stack, list_stack, tuple_stack);
 		}
 	}
 
@@ -366,7 +375,7 @@
     if (d != NULL && d->dv_copyID != copyID)
     {
 	d->dv_copyID = copyID;
-	return set_ref_in_ht(&d->dv_hashtab, copyID, NULL);
+	return set_ref_in_ht(&d->dv_hashtab, copyID, NULL, NULL);
     }
     return FALSE;
 }
@@ -382,7 +391,7 @@
     if (ll != NULL && ll->lv_copyID != copyID)
     {
 	ll->lv_copyID = copyID;
-	return set_ref_in_list_items(ll, copyID, NULL);
+	return set_ref_in_list_items(ll, copyID, NULL, NULL);
     }
     return FALSE;
 }
@@ -394,7 +403,11 @@
  * Returns TRUE if setting references failed somehow.
  */
     int
-set_ref_in_list_items(list_T *l, int copyID, ht_stack_T **ht_stack)
+set_ref_in_list_items(
+    list_T		*l,
+    int			copyID,
+    ht_stack_T		**ht_stack,
+    tuple_stack_T	**tuple_stack)
 {
     listitem_T	 *li;
     int		 abort = FALSE;
@@ -411,7 +424,7 @@
 	    // list_stack.
 	    for (li = cur_l->lv_first; !abort && li != NULL; li = li->li_next)
 		abort = abort || set_ref_in_item(&li->li_tv, copyID,
-						       ht_stack, &list_stack);
+				       ht_stack, &list_stack, tuple_stack);
 	if (list_stack == NULL)
 	    break;
 
@@ -426,6 +439,50 @@
 }
 
 /*
+ * Mark all lists and dicts referenced through tuple "t" with "copyID".
+ * "ht_stack" is used to add hashtabs to be marked.  Can be NULL.
+ *
+ * Returns TRUE if setting references failed somehow.
+ */
+    int
+set_ref_in_tuple_items(
+    tuple_T		*tuple,
+    int			copyID,
+    ht_stack_T		**ht_stack,
+    list_stack_T	**list_stack)
+{
+    int			abort = FALSE;
+    tuple_T		*cur_t;
+    tuple_stack_T	*tuple_stack = NULL;
+    tuple_stack_T	*tempitem;
+
+    cur_t = tuple;
+    for (;;)
+    {
+	// Mark each item in the tuple.  If the item contains a hashtab
+	// it is added to ht_stack, if it contains a list it is added to
+	// list_stack.
+	for (int i = 0; i < cur_t->tv_items.ga_len; i++)
+	{
+	    typval_T *tv = ((typval_T *)cur_t->tv_items.ga_data) + i;
+	    abort = abort
+		|| set_ref_in_item(tv, copyID,
+			ht_stack, list_stack, &tuple_stack);
+	}
+	if (tuple_stack == NULL)
+	    break;
+
+	// take an item from the stack
+	cur_t = tuple_stack->tuple;
+	tempitem = tuple_stack;
+	tuple_stack = tuple_stack->prev;
+	free(tempitem);
+    }
+
+    return abort;
+}
+
+/*
  * Mark the partial in callback 'cb' with "copyID".
  */
     int
@@ -438,7 +495,7 @@
 
     tv.v_type = VAR_PARTIAL;
     tv.vval.v_partial = cb->cb_partial;
-    return set_ref_in_item(&tv, copyID, NULL, NULL);
+    return set_ref_in_item(&tv, copyID, NULL, NULL, NULL);
 }
 
 /*
@@ -450,7 +507,8 @@
     dict_T		*dd,
     int			copyID,
     ht_stack_T		**ht_stack,
-    list_stack_T	**list_stack)
+    list_stack_T	**list_stack,
+    tuple_stack_T	**tuple_stack)
 {
     if (dd == NULL || dd->dv_copyID == copyID)
 	return FALSE;
@@ -458,7 +516,7 @@
     // Didn't see this dict yet.
     dd->dv_copyID = copyID;
     if (ht_stack == NULL)
-	return set_ref_in_ht(&dd->dv_hashtab, copyID, list_stack);
+	return set_ref_in_ht(&dd->dv_hashtab, copyID, list_stack, tuple_stack);
 
     ht_stack_T *newitem = ALLOC_ONE(ht_stack_T);
     if (newitem == NULL)
@@ -480,7 +538,8 @@
     list_T		*ll,
     int			copyID,
     ht_stack_T		**ht_stack,
-    list_stack_T	**list_stack)
+    list_stack_T	**list_stack,
+    tuple_stack_T	**tuple_stack)
 {
     if (ll == NULL || ll->lv_copyID == copyID)
 	return FALSE;
@@ -488,7 +547,7 @@
     // Didn't see this list yet.
     ll->lv_copyID = copyID;
     if (list_stack == NULL)
-	return set_ref_in_list_items(ll, copyID, ht_stack);
+	return set_ref_in_list_items(ll, copyID, ht_stack, tuple_stack);
 
     list_stack_T *newitem = ALLOC_ONE(list_stack_T);
     if (newitem == NULL)
@@ -502,6 +561,37 @@
 }
 
 /*
+ * Mark the tuple "tt" with "copyID".
+ * Also see set_ref_in_item().
+ */
+    static int
+set_ref_in_item_tuple(
+    tuple_T		*tt,
+    int			copyID,
+    ht_stack_T		**ht_stack,
+    list_stack_T	**list_stack,
+    tuple_stack_T	**tuple_stack)
+{
+    if (tt == NULL || tt->tv_copyID == copyID)
+	return FALSE;
+
+    // Didn't see this tuple yet.
+    tt->tv_copyID = copyID;
+    if (tuple_stack == NULL)
+	return set_ref_in_tuple_items(tt, copyID, ht_stack, list_stack);
+
+    tuple_stack_T *newitem = ALLOC_ONE(tuple_stack_T);
+    if (newitem == NULL)
+	return TRUE;
+
+    newitem->tuple = tt;
+    newitem->prev = *tuple_stack;
+    *tuple_stack = newitem;
+
+    return FALSE;
+}
+
+/*
  * Mark the partial "pt" with "copyID".
  * Also see set_ref_in_item().
  */
@@ -510,7 +600,8 @@
     partial_T		*pt,
     int			copyID,
     ht_stack_T		**ht_stack,
-    list_stack_T	**list_stack)
+    list_stack_T	**list_stack,
+    tuple_stack_T	**tuple_stack)
 {
     if (pt == NULL || pt->pt_copyID == copyID)
 	return FALSE;
@@ -526,7 +617,7 @@
 
 	dtv.v_type = VAR_DICT;
 	dtv.vval.v_dict = pt->pt_dict;
-	set_ref_in_item(&dtv, copyID, ht_stack, list_stack);
+	set_ref_in_item(&dtv, copyID, ht_stack, list_stack, tuple_stack);
     }
 
     if (pt->pt_obj != NULL)
@@ -535,12 +626,12 @@
 
 	objtv.v_type = VAR_OBJECT;
 	objtv.vval.v_object = pt->pt_obj;
-	set_ref_in_item(&objtv, copyID, ht_stack, list_stack);
+	set_ref_in_item(&objtv, copyID, ht_stack, list_stack, tuple_stack);
     }
 
     for (int i = 0; i < pt->pt_argc; ++i)
 	abort = abort || set_ref_in_item(&pt->pt_argv[i], copyID,
-		ht_stack, list_stack);
+		ht_stack, list_stack, tuple_stack);
     // pt_funcstack is handled in set_ref_in_funcstacks()
     // pt_loopvars is handled in set_ref_in_loopvars()
 
@@ -557,7 +648,8 @@
     job_T		*job,
     int			copyID,
     ht_stack_T		**ht_stack,
-    list_stack_T	**list_stack)
+    list_stack_T	**list_stack,
+    tuple_stack_T	**tuple_stack)
 {
     typval_T    dtv;
 
@@ -569,13 +661,13 @@
     {
 	dtv.v_type = VAR_CHANNEL;
 	dtv.vval.v_channel = job->jv_channel;
-	set_ref_in_item(&dtv, copyID, ht_stack, list_stack);
+	set_ref_in_item(&dtv, copyID, ht_stack, list_stack, tuple_stack);
     }
     if (job->jv_exit_cb.cb_partial != NULL)
     {
 	dtv.v_type = VAR_PARTIAL;
 	dtv.vval.v_partial = job->jv_exit_cb.cb_partial;
-	set_ref_in_item(&dtv, copyID, ht_stack, list_stack);
+	set_ref_in_item(&dtv, copyID, ht_stack, list_stack, tuple_stack);
     }
 
     return FALSE;
@@ -590,7 +682,8 @@
     channel_T		*ch,
     int			copyID,
     ht_stack_T		**ht_stack,
-    list_stack_T	**list_stack)
+    list_stack_T	**list_stack,
+    tuple_stack_T	**tuple_stack)
 {
     typval_T    dtv;
 
@@ -602,33 +695,33 @@
     {
 	for (jsonq_T *jq = ch->ch_part[part].ch_json_head.jq_next;
 		jq != NULL; jq = jq->jq_next)
-	    set_ref_in_item(jq->jq_value, copyID, ht_stack, list_stack);
+	    set_ref_in_item(jq->jq_value, copyID, ht_stack, list_stack, tuple_stack);
 	for (cbq_T *cq = ch->ch_part[part].ch_cb_head.cq_next; cq != NULL;
 		cq = cq->cq_next)
 	    if (cq->cq_callback.cb_partial != NULL)
 	    {
 		dtv.v_type = VAR_PARTIAL;
 		dtv.vval.v_partial = cq->cq_callback.cb_partial;
-		set_ref_in_item(&dtv, copyID, ht_stack, list_stack);
+		set_ref_in_item(&dtv, copyID, ht_stack, list_stack, tuple_stack);
 	    }
 	if (ch->ch_part[part].ch_callback.cb_partial != NULL)
 	{
 	    dtv.v_type = VAR_PARTIAL;
 	    dtv.vval.v_partial = ch->ch_part[part].ch_callback.cb_partial;
-	    set_ref_in_item(&dtv, copyID, ht_stack, list_stack);
+	    set_ref_in_item(&dtv, copyID, ht_stack, list_stack, tuple_stack);
 	}
     }
     if (ch->ch_callback.cb_partial != NULL)
     {
 	dtv.v_type = VAR_PARTIAL;
 	dtv.vval.v_partial = ch->ch_callback.cb_partial;
-	set_ref_in_item(&dtv, copyID, ht_stack, list_stack);
+	set_ref_in_item(&dtv, copyID, ht_stack, list_stack, tuple_stack);
     }
     if (ch->ch_close_cb.cb_partial != NULL)
     {
 	dtv.v_type = VAR_PARTIAL;
 	dtv.vval.v_partial = ch->ch_close_cb.cb_partial;
-	set_ref_in_item(&dtv, copyID, ht_stack, list_stack);
+	set_ref_in_item(&dtv, copyID, ht_stack, list_stack, tuple_stack);
     }
 
     return FALSE;
@@ -644,7 +737,8 @@
     class_T		*cl,
     int			copyID,
     ht_stack_T		**ht_stack,
-    list_stack_T	**list_stack)
+    list_stack_T	**list_stack,
+    tuple_stack_T	**tuple_stack)
 {
     int abort = FALSE;
 
@@ -659,7 +753,7 @@
 	for (int i = 0; !abort && i < cl->class_class_member_count; ++i)
 	    abort = abort || set_ref_in_item(
 		    &cl->class_members_tv[i],
-		    copyID, ht_stack, list_stack);
+		    copyID, ht_stack, list_stack, tuple_stack);
     }
 
     for (int i = 0; !abort && i < cl->class_class_function_count; ++i)
@@ -682,7 +776,8 @@
     object_T		*obj,
     int			copyID,
     ht_stack_T		**ht_stack,
-    list_stack_T	**list_stack)
+    list_stack_T	**list_stack,
+    tuple_stack_T	**tuple_stack)
 {
     int abort = FALSE;
 
@@ -696,7 +791,7 @@
     for (int i = 0; !abort
 	    && i < obj->obj_class->class_obj_member_count; ++i)
 	abort = abort || set_ref_in_item(mtv + i, copyID,
-		ht_stack, list_stack);
+		ht_stack, list_stack, tuple_stack);
 
     return abort;
 }
@@ -714,7 +809,8 @@
     typval_T	    *tv,
     int		    copyID,
     ht_stack_T	    **ht_stack,
-    list_stack_T    **list_stack)
+    list_stack_T    **list_stack,
+    tuple_stack_T   **tuple_stack)
 {
     int		abort = FALSE;
 
@@ -722,12 +818,15 @@
     {
 	case VAR_DICT:
 	    return set_ref_in_item_dict(tv->vval.v_dict, copyID,
-							 ht_stack, list_stack);
+					 ht_stack, list_stack, tuple_stack);
 
 	case VAR_LIST:
 	    return set_ref_in_item_list(tv->vval.v_list, copyID,
-							 ht_stack, list_stack);
+					 ht_stack, list_stack, tuple_stack);
 
+	case VAR_TUPLE:
+	    return set_ref_in_item_tuple(tv->vval.v_tuple, copyID,
+					 ht_stack, list_stack, tuple_stack);
 	case VAR_FUNC:
 	{
 	    abort = set_ref_in_func(tv->vval.v_string, NULL, copyID);
@@ -736,12 +835,12 @@
 
 	case VAR_PARTIAL:
 	    return set_ref_in_item_partial(tv->vval.v_partial, copyID,
-							ht_stack, list_stack);
+					ht_stack, list_stack, tuple_stack);
 
 	case VAR_JOB:
 #ifdef FEAT_JOB_CHANNEL
 	    return set_ref_in_item_job(tv->vval.v_job, copyID,
-							 ht_stack, list_stack);
+					ht_stack, list_stack, tuple_stack);
 #else
 	    break;
 #endif
@@ -749,18 +848,18 @@
 	case VAR_CHANNEL:
 #ifdef FEAT_JOB_CHANNEL
 	    return set_ref_in_item_channel(tv->vval.v_channel, copyID,
-							 ht_stack, list_stack);
+					ht_stack, list_stack, tuple_stack);
 #else
 	    break;
 #endif
 
 	case VAR_CLASS:
 	    return set_ref_in_item_class(tv->vval.v_class, copyID,
-							 ht_stack, list_stack);
+					ht_stack, list_stack, tuple_stack);
 
 	case VAR_OBJECT:
 	    return set_ref_in_item_object(tv->vval.v_object, copyID,
-							 ht_stack, list_stack);
+					ht_stack, list_stack, tuple_stack);
 
 	case VAR_UNKNOWN:
 	case VAR_ANY:
diff --git a/src/globals.h b/src/globals.h
index ef78b44..7e65af3 100644
--- a/src/globals.h
+++ b/src/globals.h
@@ -549,7 +549,14 @@
 #define t_typealias		(static_types[90])
 #define t_const_typealias	(static_types[91])
 
-EXTERN type_T static_types[92]
+#define t_tuple_any		(static_types[92])
+#define t_const_tuple_any	(static_types[93])
+
+#define t_tuple_empty		(static_types[94])
+#define t_const_tuple_empty	(static_types[95])
+
+
+EXTERN type_T static_types[96]
 #ifdef DO_INIT
 = {
     // 0: t_unknown
@@ -735,6 +742,14 @@
     // 90: t_typealias
     {VAR_TYPEALIAS, 0, 0, TTFLAG_STATIC, NULL, NULL, NULL},
     {VAR_TYPEALIAS, 0, 0, TTFLAG_STATIC|TTFLAG_CONST, NULL, NULL, NULL},
+
+    // 92: t_tuple_any
+    {VAR_TUPLE, -1, 0, TTFLAG_STATIC, NULL, NULL, NULL},
+    {VAR_TUPLE, -1, 0, TTFLAG_STATIC|TTFLAG_CONST, NULL, NULL, NULL},
+
+    // 94: t_tuple_empty
+    {VAR_TUPLE, 0, 0, TTFLAG_STATIC, NULL, NULL, NULL},
+    {VAR_TUPLE, 0, 0, TTFLAG_STATIC|TTFLAG_CONST, NULL, NULL, NULL},
 }
 #endif
 ;
diff --git a/src/if_py_both.h b/src/if_py_both.h
index a679be5..9f2f582 100644
--- a/src/if_py_both.h
+++ b/src/if_py_both.h
@@ -6248,7 +6248,7 @@
 	    if (func->argc)
 		for (i = 0; !abort && i < func->argc; ++i)
 		    abort = abort
-			|| set_ref_in_item(&func->argv[i], copyID, NULL, NULL);
+			|| set_ref_in_item(&func->argv[i], copyID, NULL, NULL, NULL);
 	}
     }
 
@@ -6777,6 +6777,7 @@
 	case VAR_CLASS:
 	case VAR_OBJECT:
 	case VAR_TYPEALIAS:
+	case VAR_TUPLE:		// FIXME: Need to add support for tuple
 	    Py_INCREF(Py_None);
 	    return Py_None;
 	case VAR_BOOL:
diff --git a/src/job.c b/src/job.c
index 2a2e531..9d6c862 100644
--- a/src/job.c
+++ b/src/job.c
@@ -1087,7 +1087,7 @@
 	{
 	    tv.v_type = VAR_JOB;
 	    tv.vval.v_job = job;
-	    abort = abort || set_ref_in_item(&tv, copyID, NULL, NULL);
+	    abort = abort || set_ref_in_item(&tv, copyID, NULL, NULL, NULL);
 	}
     return abort;
 }
diff --git a/src/json.c b/src/json.c
index 66b8bf9..faa35e5 100644
--- a/src/json.c
+++ b/src/json.c
@@ -267,6 +267,7 @@
     char_u	*res;
     blob_T	*b;
     list_T	*l;
+    tuple_T	*tuple;
     dict_T	*d;
     int		i;
 
@@ -369,6 +370,42 @@
 	    }
 	    break;
 
+	case VAR_TUPLE:
+	    tuple = val->vval.v_tuple;
+	    if (tuple == NULL)
+		ga_concat(gap, (char_u *)"[]");
+	    else
+	    {
+		if (tuple->tv_copyID == copyID)
+		    ga_concat(gap, (char_u *)"[]");
+		else
+		{
+		    int		len = TUPLE_LEN(tuple);
+
+		    tuple->tv_copyID = copyID;
+		    ga_append(gap, '[');
+		    for (i = 0; i < len && !got_int; i++)
+		    {
+			typval_T	*t_item = TUPLE_ITEM(tuple, i);
+			if (json_encode_item(gap, t_item, copyID,
+						   options & JSON_JS) == FAIL)
+			    return FAIL;
+
+			if ((options & JSON_JS)
+				&& i == len - 1
+				&& t_item->v_type == VAR_SPECIAL
+				&& t_item->vval.v_number == VVAL_NONE)
+			    // add an extra comma if the last item is v:none
+			    ga_append(gap, ',');
+			if (i <= len - 2)
+			    ga_append(gap, ',');
+		    }
+		    ga_append(gap, ']');
+		    tuple->tv_copyID = 0;
+		}
+	    }
+	    break;
+
 	case VAR_DICT:
 	    d = val->vval.v_dict;
 	    if (d == NULL)
diff --git a/src/list.c b/src/list.c
index 36ce494..c48c751 100644
--- a/src/list.c
+++ b/src/list.c
@@ -1520,15 +1520,13 @@
 
     rettv->v_type = VAR_STRING;
 
-    if (in_vim9script()
-	    && (check_for_list_arg(argvars, 0) == FAIL
-		|| check_for_opt_string_arg(argvars, 1) == FAIL))
+    if (check_for_list_or_tuple_arg(argvars, 0) == FAIL
+	    || check_for_opt_string_arg(argvars, 1) == FAIL)
 	return;
 
-    if (check_for_list_arg(argvars, 0) == FAIL)
-	return;
-
-    if (argvars[0].vval.v_list == NULL)
+    if ((argvars[0].v_type == VAR_LIST && argvars[0].vval.v_list == NULL)
+	    || (argvars[0].v_type == VAR_TUPLE
+		&& argvars[0].vval.v_tuple == NULL))
 	return;
 
     if (argvars[1].v_type == VAR_UNKNOWN)
@@ -1539,7 +1537,10 @@
     if (sep != NULL)
     {
 	ga_init2(&ga, sizeof(char), 80);
-	list_join(&ga, argvars[0].vval.v_list, sep, TRUE, FALSE, 0);
+	if (argvars[0].v_type == VAR_LIST)
+	    list_join(&ga, argvars[0].vval.v_list, sep, TRUE, FALSE, 0);
+	else
+	    tuple_join(&ga, argvars[0].vval.v_tuple, sep, TRUE, FALSE, 0);
 	ga_append(&ga, NUL);
 	rettv->vval.v_string = (char_u *)ga.ga_data;
     }
@@ -1778,6 +1779,63 @@
 }
 
 /*
+ * "list2tuple()" function
+ */
+    void
+f_list2tuple(typval_T *argvars, typval_T *rettv)
+{
+    list_T	*l;
+    listitem_T	*li;
+    tuple_T	*tuple;
+
+    rettv->v_type = VAR_TUPLE;
+    rettv->vval.v_tuple = NULL;
+
+    if (check_for_list_arg(argvars, 0) == FAIL)
+	return;
+
+    l = argvars[0].vval.v_list;
+    if (l == NULL)
+	return;  // empty list results in empty tuple
+
+    CHECK_LIST_MATERIALIZE(l);
+
+    if (rettv_tuple_set_with_items(rettv, list_len(l)) == FAIL)
+	return;
+
+    tuple = rettv->vval.v_tuple;
+    FOR_ALL_LIST_ITEMS(l, li)
+    {
+	copy_tv(&li->li_tv, TUPLE_ITEM(tuple, TUPLE_LEN(tuple)));
+	tuple->tv_items.ga_len++;
+    }
+}
+
+/*
+ * "tuple2list()" function
+ */
+    void
+f_tuple2list(typval_T *argvars, typval_T *rettv)
+{
+    list_T	*l;
+    tuple_T	*tuple;
+
+    if (rettv_list_alloc(rettv) == FAIL)
+	return;
+
+    if (check_for_tuple_arg(argvars, 0) == FAIL)
+	return;
+
+    tuple = argvars[0].vval.v_tuple;
+    if (tuple == NULL)
+	return;  // empty tuple results in empty list
+
+    l = rettv->vval.v_list;
+    for (int i = 0; i < tuple_len(tuple); i++)
+	list_append_tv(l, TUPLE_ITEM(tuple, i));
+}
+
+/*
  * Remove item argvars[1] from List argvars[0]. If argvars[2] is supplied, then
  * remove the range of items from argvars[1] to argvars[2] (inclusive).
  */
@@ -2560,10 +2618,16 @@
 	copy_tv(&argvars[0], rettv);
 
     if (in_vim9script()
-	    && (check_for_list_or_dict_or_blob_or_string_arg(argvars, 0)
+	    && (check_for_list_tuple_dict_blob_or_string_arg(argvars, 0)
 								== FAIL))
 	return;
 
+    if (argvars[0].v_type == VAR_TUPLE && filtermap != FILTERMAP_FOREACH)
+    {
+	semsg(_(e_cannot_use_tuple_with_function_str), func_name);
+	return;
+    }
+
     if (filtermap == FILTERMAP_MAP && in_vim9script())
     {
 	// Check that map() does not change the declared type of the list or
@@ -2577,11 +2641,17 @@
 
     if (argvars[0].v_type != VAR_BLOB
 	    && argvars[0].v_type != VAR_LIST
+	    && argvars[0].v_type != VAR_TUPLE
 	    && argvars[0].v_type != VAR_DICT
 	    && argvars[0].v_type != VAR_STRING)
     {
-	semsg(_(e_argument_of_str_must_be_list_string_dictionary_or_blob),
-								    func_name);
+	char *msg;
+
+	if (filtermap == FILTERMAP_FOREACH)
+	    msg = e_argument_of_str_must_be_list_tuple_string_dictionary_or_blob;
+	else
+	    msg = e_argument_of_str_must_be_list_string_dictionary_or_blob;
+	semsg(_(msg), func_name);
 	return;
     }
 
@@ -2611,6 +2681,8 @@
 							    arg_errmsg, rettv);
     else if (argvars[0].v_type == VAR_STRING)
 	string_filter_map(tv_get_string(&argvars[0]), filtermap, expr, rettv);
+    else if (argvars[0].v_type == VAR_TUPLE)
+	tuple_foreach(argvars[0].vval.v_tuple, filtermap, expr);
     else // argvars[0].v_type == VAR_LIST
 	list_filter_map(argvars[0].vval.v_list, filtermap, type, func_name,
 						      arg_errmsg, expr, rettv);
@@ -2743,7 +2815,7 @@
     int		error = FALSE;
 
     if (in_vim9script()
-	    && (check_for_string_or_list_or_dict_arg(argvars, 0) == FAIL
+	    && (check_for_string_list_tuple_or_dict_arg(argvars, 0) == FAIL
 		|| check_for_opt_bool_arg(argvars, 2) == FAIL
 		|| (argvars[2].v_type != VAR_UNKNOWN
 		    && check_for_opt_number_arg(argvars, 3) == FAIL)))
@@ -2765,6 +2837,16 @@
 	if (!error)
 	    n = list_count(argvars[0].vval.v_list, &argvars[1], idx, ic);
     }
+    else if (!error && argvars[0].v_type == VAR_TUPLE)
+    {
+	long idx = 0;
+
+	if (argvars[2].v_type != VAR_UNKNOWN
+		&& argvars[3].v_type != VAR_UNKNOWN)
+	    idx = (long)tv_get_number_chk(&argvars[3], &error);
+	if (!error)
+	    n = tuple_count(argvars[0].vval.v_tuple, &argvars[1], idx, ic);
+    }
     else if (!error && argvars[0].v_type == VAR_DICT)
     {
 	if (argvars[2].v_type != VAR_UNKNOWN
@@ -3033,7 +3115,7 @@
     void
 f_reverse(typval_T *argvars, typval_T *rettv)
 {
-    if (check_for_string_or_list_or_blob_arg(argvars, 0) == FAIL)
+    if (check_for_string_or_list_or_tuple_or_blob_arg(argvars, 0) == FAIL)
 	return;
 
     if (argvars[0].v_type == VAR_BLOB)
@@ -3048,6 +3130,8 @@
     }
     else if (argvars[0].v_type == VAR_LIST)
 	list_reverse(argvars[0].vval.v_list, rettv);
+    else if (argvars[0].v_type == VAR_TUPLE)
+	tuple_reverse(argvars[0].vval.v_tuple, rettv);
 }
 
 /*
@@ -3153,6 +3237,7 @@
 
 /*
  * "reduce(list, { accumulator, element -> value } [, initial])" function
+ * "reduce(tuple, { accumulator, element -> value } [, initial])" function
  * "reduce(blob, { accumulator, element -> value } [, initial])"
  * "reduce(string, { accumulator, element -> value } [, initial])"
  */
@@ -3161,18 +3246,9 @@
 {
     char_u	*func_name;
 
-    if (in_vim9script()
-		   && check_for_string_or_list_or_blob_arg(argvars, 0) == FAIL)
+    if (check_for_string_or_list_or_tuple_or_blob_arg(argvars, 0) == FAIL)
 	return;
 
-    if (argvars[0].v_type != VAR_STRING
-	    && argvars[0].v_type != VAR_LIST
-	    && argvars[0].v_type != VAR_BLOB)
-    {
-	emsg(_(e_string_list_or_blob_required));
-	return;
-    }
-
     if (argvars[1].v_type == VAR_FUNC)
 	func_name = argvars[1].vval.v_string;
     else if (argvars[1].v_type == VAR_PARTIAL)
@@ -3187,6 +3263,8 @@
 
     if (argvars[0].v_type == VAR_LIST)
 	list_reduce(argvars, &argvars[1], rettv);
+    else if (argvars[0].v_type == VAR_TUPLE)
+	tuple_reduce(argvars, &argvars[1], rettv);
     else if (argvars[0].v_type == VAR_STRING)
 	string_reduce(argvars, &argvars[1], rettv);
     else
@@ -3202,6 +3280,7 @@
     if (in_vim9script()
 	    && ((argvars[0].v_type != VAR_STRING
 		    && argvars[0].v_type != VAR_LIST
+		    && argvars[0].v_type != VAR_TUPLE
 		    && argvars[0].v_type != VAR_BLOB
 		    && check_for_list_arg(argvars, 0) == FAIL)
 		|| check_for_number_arg(argvars, 1) == FAIL
diff --git a/src/macros.h b/src/macros.h
index 6c2931a..c11bfa5 100644
--- a/src/macros.h
+++ b/src/macros.h
@@ -487,3 +487,7 @@
 // Iterate over all the items in a hash table
 #define FOR_ALL_HASHTAB_ITEMS(ht, hi, todo) \
     for ((hi) = (ht)->ht_array; (todo) > 0; ++(hi))
+
+#define TUPLE_LEN(t)	    (t->tv_items.ga_len)
+#define TUPLE_ITEM(t, i) \
+	    (((typval_T *)t->tv_items.ga_data) + i)
diff --git a/src/netbeans.c b/src/netbeans.c
index 781caa8..5cbbab7 100644
--- a/src/netbeans.c
+++ b/src/netbeans.c
@@ -2561,7 +2561,7 @@
 
     tv.v_type = VAR_CHANNEL;
     tv.vval.v_channel = nb_channel;
-    abort = set_ref_in_item(&tv, copyID, NULL, NULL);
+    abort = set_ref_in_item(&tv, copyID, NULL, NULL, NULL);
     return abort;
 }
 #endif
diff --git a/src/popupwin.c b/src/popupwin.c
index 76ebf38..46e5483 100644
--- a/src/popupwin.c
+++ b/src/popupwin.c
@@ -4416,13 +4416,13 @@
     {
 	tv.v_type = VAR_PARTIAL;
 	tv.vval.v_partial = wp->w_close_cb.cb_partial;
-	abort = abort || set_ref_in_item(&tv, copyID, NULL, NULL);
+	abort = abort || set_ref_in_item(&tv, copyID, NULL, NULL, NULL);
     }
     if (wp->w_filter_cb.cb_partial != NULL)
     {
 	tv.v_type = VAR_PARTIAL;
 	tv.vval.v_partial = wp->w_filter_cb.cb_partial;
-	abort = abort || set_ref_in_item(&tv, copyID, NULL, NULL);
+	abort = abort || set_ref_in_item(&tv, copyID, NULL, NULL, NULL);
     }
     abort = abort || set_ref_in_list(wp->w_popup_mask, copyID);
     return abort;
diff --git a/src/proto.h b/src/proto.h
index 091e093..f04ba05 100644
--- a/src/proto.h
+++ b/src/proto.h
@@ -207,6 +207,7 @@
 # include "textobject.pro"
 # include "textformat.pro"
 # include "time.pro"
+# include "tuple.pro"
 # include "typval.pro"
 # include "ui.pro"
 # include "undo.pro"
diff --git a/src/proto/eval.pro b/src/proto/eval.pro
index e945a28..df59383 100644
--- a/src/proto/eval.pro
+++ b/src/proto/eval.pro
@@ -45,6 +45,7 @@
 int eval1(char_u **arg, typval_T *rettv, evalarg_T *evalarg);
 void eval_addblob(typval_T *tv1, typval_T *tv2);
 int eval_addlist(typval_T *tv1, typval_T *tv2);
+int eval_addtuple(typval_T *tv1, typval_T *tv2);
 int eval_leader(char_u **arg, int vim9);
 int handle_predefined(char_u *s, int len, typval_T *rettv);
 int check_can_index(typval_T *rettv, int evaluate, int verbose);
diff --git a/src/proto/evalfunc.pro b/src/proto/evalfunc.pro
index a720b64..627af17 100644
--- a/src/proto/evalfunc.pro
+++ b/src/proto/evalfunc.pro
@@ -23,6 +23,7 @@
 void f_exists(typval_T *argvars, typval_T *rettv);
 void f_has(typval_T *argvars, typval_T *rettv);
 int dynamic_feature(char_u *feature);
+int indexof_eval_expr(typval_T *expr);
 void f_len(typval_T *argvars, typval_T *rettv);
 void mzscheme_call_vim(char_u *name, typval_T *args, typval_T *rettv);
 void range_list_materialize(list_T *list);
diff --git a/src/proto/gc.pro b/src/proto/gc.pro
index e13dbda..8b55030 100644
--- a/src/proto/gc.pro
+++ b/src/proto/gc.pro
@@ -1,12 +1,12 @@
 /* gc.c */
 int get_copyID(void);
 int garbage_collect(int testing);
-int set_ref_in_ht(hashtab_T *ht, int copyID, list_stack_T **list_stack);
+int set_ref_in_ht(hashtab_T *ht, int copyID, list_stack_T **list_stack, tuple_stack_T **tuple_stack);
 int set_ref_in_dict(dict_T *d, int copyID);
 int set_ref_in_list(list_T *ll, int copyID);
-int set_ref_in_list_items(list_T *l, int copyID, ht_stack_T **ht_stack);
+int set_ref_in_list_items(list_T *l, int copyID, ht_stack_T **ht_stack, tuple_stack_T **tuple_stack);
+int set_ref_in_tuple_items(tuple_T *tuple, int copyID, ht_stack_T **ht_stack, list_stack_T **list_stack);
 int set_ref_in_callback(callback_T *cb, int copyID);
-int set_ref_in_item_class(class_T *cl, int copyID, ht_stack_T **ht_stack, list_stack_T **list_stack);
-int set_ref_in_item(typval_T *tv, int copyID, ht_stack_T **ht_stack, list_stack_T **list_stack);
+int set_ref_in_item_class(class_T *cl, int copyID, ht_stack_T **ht_stack, list_stack_T **list_stack, tuple_stack_T **tuple_stack);
+int set_ref_in_item(typval_T *tv, int copyID, ht_stack_T **ht_stack, list_stack_T **list_stack, tuple_stack_T **tuple_stack);
 /* vim: set ft=c : */
-
diff --git a/src/proto/list.pro b/src/proto/list.pro
index 27bea5e..cb05203 100644
--- a/src/proto/list.pro
+++ b/src/proto/list.pro
@@ -50,6 +50,8 @@
 int write_list(FILE *fd, list_T *list, int binary);
 void init_static_list(staticList10_T *sl);
 void f_list2str(typval_T *argvars, typval_T *rettv);
+void f_list2tuple(typval_T *argvars, typval_T *rettv);
+void f_tuple2list(typval_T *argvars, typval_T *rettv);
 void f_sort(typval_T *argvars, typval_T *rettv);
 void f_uniq(typval_T *argvars, typval_T *rettv);
 int filter_map_one(typval_T *tv, typval_T *expr, filtermap_T filtermap, funccall_T *fc, typval_T *newtv, int *remp);
diff --git a/src/proto/testing.pro b/src/proto/testing.pro
index dea4f75..1c93f8c 100644
--- a/src/proto/testing.pro
+++ b/src/proto/testing.pro
@@ -30,6 +30,7 @@
 void f_test_null_function(typval_T *argvars, typval_T *rettv);
 void f_test_null_partial(typval_T *argvars, typval_T *rettv);
 void f_test_null_string(typval_T *argvars, typval_T *rettv);
+void f_test_null_tuple(typval_T *argvars, typval_T *rettv);
 void f_test_unknown(typval_T *argvars, typval_T *rettv);
 void f_test_void(typval_T *argvars, typval_T *rettv);
 void f_test_setmouse(typval_T *argvars, typval_T *rettv);
diff --git a/src/proto/tuple.pro b/src/proto/tuple.pro
new file mode 100644
index 0000000..138648d
--- /dev/null
+++ b/src/proto/tuple.pro
@@ -0,0 +1,34 @@
+/* tuple.c */
+tuple_T *tuple_alloc(void);
+tuple_T *tuple_alloc_with_items(int count);
+void tuple_set_item(tuple_T *tuple, int idx, typval_T *tv);
+int rettv_tuple_alloc(typval_T *rettv);
+void rettv_tuple_set(typval_T *rettv, tuple_T *tuple);
+int rettv_tuple_set_with_items(typval_T *rettv, int count);
+void tuple_unref(tuple_T *tuple);
+int tuple_free_nonref(int copyID);
+void tuple_free_items(int copyID);
+void tuple_free(tuple_T *tuple);
+long tuple_len(tuple_T *tuple);
+int tuple_equal(tuple_T *t1, tuple_T *t2, int ic);
+typval_T *tuple_find(tuple_T *tuple, long n);
+int tuple_append_tv(tuple_T *tuple, typval_T *tv);
+int tuple_concat(tuple_T *t1, tuple_T *t2, typval_T *tv);
+tuple_T *tuple_slice(tuple_T *tuple, long n1, long n2);
+int tuple_slice_or_index(tuple_T *tuple, int range, varnumber_T n1_arg, varnumber_T n2_arg, int exclusive, typval_T *rettv, int verbose);
+tuple_T *tuple_copy(tuple_T *orig, int deep, int top, int copyID);
+int eval_tuple(char_u **arg, typval_T *rettv, evalarg_T *evalarg, int do_error);
+void tuple_lock(tuple_T *tuple, int deep, int lock, int check_refcount);
+int tuple_join(garray_T *gap, tuple_T *tuple, char_u *sep, int echo_style, int restore_copyID, int copyID);
+char_u *tuple2string(typval_T *tv, int copyID, int restore_copyID);
+void tuple_foreach(tuple_T *tuple, filtermap_T filtermap, typval_T *expr);
+long tuple_count(tuple_T *tuple, typval_T *needle, long idx, int ic);
+void tuple2items(typval_T *argvars, typval_T *rettv);
+int index_tuple(tuple_T *tuple, typval_T *tv, int start_idx, int ic);
+int indexof_tuple(tuple_T *tuple, long startidx, typval_T *expr);
+varnumber_T tuple_max_min(tuple_T *tuple, int domax, int *error);
+void tuple_repeat(tuple_T *tuple, int n, typval_T *rettv);
+void tuple_reverse(tuple_T *tuple, typval_T *rettv);
+void tuple_reduce(typval_T *argvars, typval_T *expr, typval_T *rettv);
+int check_tuples_addable(type_T *type1, type_T *type2);
+/* vim: set ft=c : */
diff --git a/src/proto/typval.pro b/src/proto/typval.pro
index 90dcc54..d30cded 100644
--- a/src/proto/typval.pro
+++ b/src/proto/typval.pro
@@ -18,13 +18,13 @@
 int check_for_opt_number_arg(typval_T *args, int idx);
 int check_for_float_or_nr_arg(typval_T *args, int idx);
 int check_for_bool_arg(typval_T *args, int idx);
-int check_for_bool_or_number_arg(typval_T *args, int idx);
 int check_for_opt_bool_arg(typval_T *args, int idx);
 int check_for_opt_bool_or_number_arg(typval_T *args, int idx);
 int check_for_blob_arg(typval_T *args, int idx);
 int check_for_list_arg(typval_T *args, int idx);
 int check_for_nonnull_list_arg(typval_T *args, int idx);
 int check_for_opt_list_arg(typval_T *args, int idx);
+int check_for_tuple_arg(typval_T *args, int idx);
 int check_for_dict_arg(typval_T *args, int idx);
 int check_for_nonnull_dict_arg(typval_T *args, int idx);
 int check_for_opt_dict_arg(typval_T *args, int idx);
@@ -41,18 +41,20 @@
 int check_for_opt_lnum_arg(typval_T *args, int idx);
 int check_for_string_or_blob_arg(typval_T *args, int idx);
 int check_for_string_or_list_arg(typval_T *args, int idx);
-int check_for_string_or_list_or_blob_arg(typval_T *args, int idx);
+int check_for_string_or_list_or_tuple_or_blob_arg(typval_T *args, int idx);
 int check_for_opt_string_or_list_arg(typval_T *args, int idx);
 int check_for_string_or_dict_arg(typval_T *args, int idx);
 int check_for_string_or_number_or_list_arg(typval_T *args, int idx);
 int check_for_opt_string_or_number_or_list_arg(typval_T *args, int idx);
-int check_for_string_or_number_or_list_or_blob_arg(typval_T *args, int idx);
-int check_for_string_or_list_or_dict_arg(typval_T *args, int idx);
+int check_for_repeat_func_arg(typval_T *args, int idx);
+int check_for_string_list_tuple_or_dict_arg(typval_T *args, int idx);
 int check_for_string_or_func_arg(typval_T *args, int idx);
 int check_for_list_or_blob_arg(typval_T *args, int idx);
-int check_for_list_or_dict_arg(typval_T *args, int idx);
+int check_for_list_or_tuple_arg(typval_T *args, int idx);
+int check_for_list_or_tuple_or_blob_arg(typval_T *args, int idx);
+int check_for_list_or_tuple_or_dict_arg(typval_T *args, int idx);
 int check_for_list_or_dict_or_blob_arg(typval_T *args, int idx);
-int check_for_list_or_dict_or_blob_or_string_arg(typval_T *args, int idx);
+int check_for_list_tuple_dict_blob_or_string_arg(typval_T *args, int idx);
 int check_for_opt_buffer_or_dict_arg(typval_T *args, int idx);
 int check_for_object_arg(typval_T *args, int idx);
 int check_for_class_or_typealias_args(typval_T *args, int idx);
@@ -67,6 +69,7 @@
 void copy_tv(typval_T *from, typval_T *to);
 int typval_compare(typval_T *tv1, typval_T *tv2, exprtype_T type, int ic);
 int typval_compare_list(typval_T *tv1, typval_T *tv2, exprtype_T type, int ic, int *res);
+int typval_compare_tuple(typval_T *tv1, typval_T *tv2, exprtype_T type, int ic, int *res);
 int typval_compare_null(typval_T *tv1, typval_T *tv2);
 int typval_compare_blob(typval_T *tv1, typval_T *tv2, exprtype_T type, int *res);
 int typval_compare_object(typval_T *tv1, typval_T *tv2, exprtype_T type, int ic, int *res);
diff --git a/src/proto/vim9instr.pro b/src/proto/vim9instr.pro
index 641648f..a4504c5 100644
--- a/src/proto/vim9instr.pro
+++ b/src/proto/vim9instr.pro
@@ -44,6 +44,7 @@
 int generate_OLDSCRIPT(cctx_T *cctx, isntype_T isn_type, char_u *name, int sid, type_T *type);
 int generate_VIM9SCRIPT(cctx_T *cctx, isntype_T isn_type, int sid, int idx, type_T *type);
 int generate_NEWLIST(cctx_T *cctx, int count, int use_null);
+int generate_NEWTUPLE(cctx_T *cctx, int count, int use_null);
 int generate_NEWDICT(cctx_T *cctx, int count, int use_null);
 int generate_FUNCREF(cctx_T *cctx, ufunc_T *ufunc, class_T *cl, int object_method, int fi, int *isn_idx);
 int generate_NEWFUNC(cctx_T *cctx, char_u *lambda_name, char_u *func_name);
diff --git a/src/proto/vim9type.pro b/src/proto/vim9type.pro
index 093a5ec..865a93c 100644
--- a/src/proto/vim9type.pro
+++ b/src/proto/vim9type.pro
@@ -7,6 +7,7 @@
 void free_type(type_T *type);
 void set_tv_type(typval_T *tv, type_T *type);
 type_T *get_list_type(type_T *member_type, garray_T *type_gap);
+type_T *get_tuple_type(garray_T *tuple_types_gap, garray_T *type_gap);
 type_T *get_dict_type(type_T *member_type, garray_T *type_gap);
 type_T *alloc_func_type(type_T *ret_type, int argcount, garray_T *type_gap);
 type_T *get_func_type(type_T *ret_type, int argcount, garray_T *type_gap);
@@ -27,12 +28,14 @@
 type_T *parse_type(char_u **arg, garray_T *type_gap, int give_error);
 int equal_type(type_T *type1, type_T *type2, int flags);
 void common_type(type_T *type1, type_T *type2, type_T **dest, garray_T *type_gap);
+type_T *get_item_type(type_T *type);
 int push_type_stack(cctx_T *cctx, type_T *type);
 int push_type_stack2(cctx_T *cctx, type_T *type, type_T *decl_type);
 void set_type_on_stack(cctx_T *cctx, type_T *type, int offset);
 type_T *get_type_on_stack(cctx_T *cctx, int offset);
 type_T *get_decl_type_on_stack(cctx_T *cctx, int offset);
 type_T *get_member_type_from_stack(int count, int skip, cctx_T *cctx);
+int get_tuple_type_from_stack(int count, garray_T *tuple_types_gap, cctx_T *cctx);
 char *vartype_name(vartype_T type);
 char *type_name(type_T *type, char **tofree);
 void f_typename(typval_T *argvars, typval_T *rettv);
diff --git a/src/quickfix.c b/src/quickfix.c
index e498d5e..30353c5 100644
--- a/src/quickfix.c
+++ b/src/quickfix.c
@@ -8065,7 +8065,7 @@
 	    typval_T* user_data = &qfp->qf_user_data;
 	    if (user_data != NULL && user_data->v_type != VAR_NUMBER
 		&& user_data->v_type != VAR_STRING && user_data->v_type != VAR_FLOAT)
-		abort = abort || set_ref_in_item(user_data, copyID, NULL, NULL);
+		abort = abort || set_ref_in_item(user_data, copyID, NULL, NULL, NULL);
 	}
     }
     return abort;
@@ -8088,7 +8088,7 @@
 	ctx = qi->qf_lists[i].qf_ctx;
 	if (ctx != NULL && ctx->v_type != VAR_NUMBER
 		&& ctx->v_type != VAR_STRING && ctx->v_type != VAR_FLOAT)
-	    abort = abort || set_ref_in_item(ctx, copyID, NULL, NULL);
+	    abort = abort || set_ref_in_item(ctx, copyID, NULL, NULL, NULL);
 
 	cb = &qi->qf_lists[i].qf_qftf_cb;
 	abort = abort || set_ref_in_callback(cb, copyID);
diff --git a/src/structs.h b/src/structs.h
index ce98bce..1a3abcb 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -73,6 +73,7 @@
 typedef struct dictvar_S	dict_T;
 typedef struct partial_S	partial_T;
 typedef struct blobvar_S	blob_T;
+typedef struct tuplevar_S	tuple_T;
 
 typedef struct window_S		win_T;
 typedef struct wininfo_S	wininfo_T;
@@ -1511,7 +1512,8 @@
     VAR_INSTR,		// "v_instr" is used
     VAR_CLASS,		// "v_class" is used (also used for interface)
     VAR_OBJECT,		// "v_object" is used
-    VAR_TYPEALIAS	// "v_typealias" is used
+    VAR_TYPEALIAS,	// "v_typealias" is used
+    VAR_TUPLE		// "v_tuple" is used
 } vartype_T;
 
 // A type specification.
@@ -1679,6 +1681,7 @@
 	class_T		*v_class;	// class value (can be NULL)
 	object_T	*v_object;	// object value (can be NULL)
 	typealias_T	*v_typealias;	// user-defined type name
+	tuple_T		*v_tuple;	// tuple
     }		vval;
 };
 
@@ -1818,6 +1821,21 @@
     char	bv_lock;	// zero, VAR_LOCKED, VAR_FIXED
 };
 
+/*
+ * Structure to hold info about a tuple.
+ */
+struct tuplevar_S
+{
+    garray_T	tv_items;	// tuple items
+    type_T	*tv_type;	// current type, allocated by alloc_type()
+    tuple_T	*tv_copytuple;	// copied tuple used by deepcopy()
+    tuple_T	*tv_used_next;	// next tuple in used tuples list
+    tuple_T	*tv_used_prev;	// previous tuple in used tuples list
+    int		tv_refcount;	// reference count
+    int		tv_copyID;	// ID used by deepcopy()
+    char	tv_lock;	// zero, VAR_LOCKED, VAR_FIXED
+};
+
 typedef int (*cfunc_T)(int argcount, typval_T *argvars, typval_T *rettv, void *state);
 typedef void (*cfunc_free_T)(void *state);
 
@@ -1846,6 +1864,8 @@
     blob_T	*fi_blob;	// blob being used
     char_u	*fi_string;	// copy of string being used
     int		fi_byte_idx;	// byte index in fi_string
+    tuple_T	*fi_tuple;	// tuple being used
+    int		fi_tuple_idx;	// tuple index in fi_tuple
     int		fi_cs_flags;	// cs_flags or'ed together
 } forinfo_T;
 
@@ -2821,6 +2841,15 @@
 } list_stack_T;
 
 /*
+ * structure used for explicit stack while garbage collecting tuples
+ */
+typedef struct tuple_stack_S
+{
+    tuple_T			*tuple;
+    struct tuple_stack_S	*prev;
+} tuple_stack_T;
+
+/*
  * Structure used for iterating over dictionary items.
  * Initialize with dict_iterate_start().
  */
@@ -4692,6 +4721,7 @@
     char_u	*ll_newkey;	// New key for Dict in alloc. mem or NULL.
     type_T	*ll_valtype;	// type expected for the value or NULL
     blob_T	*ll_blob;	// The Blob or NULL
+    tuple_T	*ll_tuple;	// tuple or NULL
     ufunc_T	*ll_ufunc;	// The function or NULL
     object_T	*ll_object;	// The object or NULL, class is not NULL
     class_T	*ll_class;	// The class or NULL, object may be NULL
diff --git a/src/terminal.c b/src/terminal.c
index 6edc21a..eb27fa5 100644
--- a/src/terminal.c
+++ b/src/terminal.c
@@ -5081,7 +5081,7 @@
 	{
 	    tv.v_type = VAR_JOB;
 	    tv.vval.v_job = term->tl_job;
-	    abort = abort || set_ref_in_item(&tv, copyID, NULL, NULL);
+	    abort = abort || set_ref_in_item(&tv, copyID, NULL, NULL, NULL);
 	}
     return abort;
 }
diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak
index 7d50a7e..55434b0 100644
--- a/src/testdir/Make_all.mak
+++ b/src/testdir/Make_all.mak
@@ -327,6 +327,7 @@
 	test_timers \
 	test_true_false \
 	test_trycatch \
+	test_tuple \
 	test_undo \
 	test_unlet \
 	test_user_func \
@@ -578,6 +579,7 @@
 	test_timers.res \
 	test_true_false.res \
 	test_trycatch.res \
+	test_tuple.res \
 	test_undo.res \
 	test_user_func.res \
 	test_usercommands.res \
diff --git a/src/testdir/test_blob.vim b/src/testdir/test_blob.vim
index f0e8209..a4a567d 100644
--- a/src/testdir/test_blob.vim
+++ b/src/testdir/test_blob.vim
@@ -35,7 +35,7 @@
       call assert_fails('VAR b = 0z1.1')
       call assert_fails('VAR b = 0z.')
       call assert_fails('VAR b = 0z001122.')
-      call assert_fails('call get("", 1)', 'E896:')
+      call assert_fails('call get("", 1)', 'E1531:')
       call assert_equal(0, len(test_null_blob()))
       call assert_equal(0z, copy(test_null_blob()))
   END
@@ -786,6 +786,7 @@
 
 func Test_blob_repeat()
   call assert_equal(0z, repeat(0z00, 0))
+  call assert_equal(0z, repeat(0z, 1))
   call assert_equal(0z00, repeat(0z00, 1))
   call assert_equal(0z0000, repeat(0z00, 2))
   call assert_equal(0z00000000, repeat(0z0000, 2))
diff --git a/src/testdir/test_eval_stuff.vim b/src/testdir/test_eval_stuff.vim
index 1b17108..d2f949e 100644
--- a/src/testdir/test_eval_stuff.vim
+++ b/src/testdir/test_eval_stuff.vim
Binary files differ
diff --git a/src/testdir/test_expr.vim b/src/testdir/test_expr.vim
index 5dedce5..41b54aa 100644
--- a/src/testdir/test_expr.vim
+++ b/src/testdir/test_expr.vim
@@ -658,10 +658,10 @@
 endfunc
 
 func Test_max_min_errors()
-  call v9.CheckLegacyAndVim9Failure(['call max(v:true)'], ['E712:', 'E1013:', 'E1227:'])
-  call v9.CheckLegacyAndVim9Failure(['call max(v:true)'], ['max()', 'E1013:', 'E1227:'])
-  call v9.CheckLegacyAndVim9Failure(['call min(v:true)'], ['E712:', 'E1013:', 'E1227:'])
-  call v9.CheckLegacyAndVim9Failure(['call min(v:true)'], ['min()', 'E1013:', 'E1227:'])
+  call v9.CheckLegacyAndVim9Failure(['call max(v:true)'], ['E712:', 'E1013:', 'E1530:'])
+  call v9.CheckLegacyAndVim9Failure(['call max(v:true)'], ['max()', 'E1013:', 'E1530:'])
+  call v9.CheckLegacyAndVim9Failure(['call min(v:true)'], ['E712:', 'E1013:', 'E1530:'])
+  call v9.CheckLegacyAndVim9Failure(['call min(v:true)'], ['min()', 'E1013:', 'E1530:'])
 endfunc
 
 func Test_function_with_funcref()
diff --git a/src/testdir/test_filter_map.vim b/src/testdir/test_filter_map.vim
index 37ebe84..39da767 100644
--- a/src/testdir/test_filter_map.vim
+++ b/src/testdir/test_filter_map.vim
@@ -173,6 +173,7 @@
   call assert_fails("let l = filter([1, 2], {a, b, c -> 1})", 'E119:')
   call assert_fails('call foreach([1], "xyzzy")', 'E492:')
   call assert_fails('call foreach([1], "let a = foo")', 'E121:')
+  call assert_fails('call foreach(test_null_function(), "")', 'E1525:')
 endfunc
 
 func Test_map_and_modify()
diff --git a/src/testdir/test_lambda.vim b/src/testdir/test_lambda.vim
index 250ce5c..f05080e 100644
--- a/src/testdir/test_lambda.vim
+++ b/src/testdir/test_lambda.vim
@@ -362,7 +362,7 @@
 
 func Test_lambda_error()
   " This was causing a crash
-  call assert_fails('ec{@{->{d->()()', 'E15:')
+  call assert_fails('ec{@{->{d->()()', 'E451:')
 endfunc
 
 func Test_closure_error()
diff --git a/src/testdir/test_let.vim b/src/testdir/test_let.vim
index e31b514..dffc5c6 100644
--- a/src/testdir/test_let.vim
+++ b/src/testdir/test_let.vim
@@ -310,7 +310,7 @@
   call assert_fails('let [a]', 'E474:')
   call assert_fails('let [a, b] = [', 'E697:')
   call assert_fails('let [a, b] = [10, 20', 'E696:')
-  call assert_fails('let [a, b] = 10', 'E714:')
+  call assert_fails('let [a, b] = 10', 'E1535:')
   call assert_fails('let [a, , b] = [10, 20]', 'E475:')
   call assert_fails('let [a, b&] = [10, 20]', 'E475:')
   call assert_fails('let $ = 10', 'E475:')
diff --git a/src/testdir/test_listdict.vim b/src/testdir/test_listdict.vim
index f3bdcd4..fb350a8 100644
--- a/src/testdir/test_listdict.vim
+++ b/src/testdir/test_listdict.vim
@@ -192,6 +192,14 @@
   END
   call v9.CheckLegacyAndVim9Success(lines)
 
+  let lines =<< trim END
+    VAR [x, y] = test_null_list()
+  END
+  call v9.CheckLegacyAndVim9Failure(lines, [
+        \ 'E714: List required',
+        \ 'E1093: Expected 2 items but got 0',
+        \ 'E714: List required'])
+
   let d = {'abc': [1, 2, 3]}
   call assert_fails('let d.abc[0:0z10] = [10, 20]', 'E976: Using a Blob as a String')
 endfunc
@@ -1000,7 +1008,7 @@
   END
   call v9.CheckLegacyAndVim9Success(lines)
 
-  call assert_fails('call reverse({})', 'E1252:')
+  call assert_fails('call reverse({})', 'E1253:')
   call assert_fails('call uniq([1, 2], {x, y -> []})', 'E745:')
   call assert_fails("call sort([1, 2], function('min'), 1)", "E1206:")
   call assert_fails("call sort([1, 2], function('invalid_func'))", "E700:")
@@ -1073,15 +1081,15 @@
   call assert_fails("call reduce('', { acc, val -> acc + val })", 'E998: Reduce of an empty String with no initial value')
   call assert_fails("call reduce(test_null_string(), { acc, val -> acc + val })", 'E998: Reduce of an empty String with no initial value')
 
-  call assert_fails("call reduce({}, { acc, val -> acc + val }, 1)", 'E1098:')
-  call assert_fails("call reduce(0, { acc, val -> acc + val }, 1)", 'E1098:')
+  call assert_fails("call reduce({}, { acc, val -> acc + val }, 1)", 'E1253:')
+  call assert_fails("call reduce(0, { acc, val -> acc + val }, 1)", 'E1253:')
   call assert_fails("call reduce([1, 2], 'Xdoes_not_exist')", 'E117:')
   call assert_fails("echo reduce(0z01, { acc, val -> 2 * acc + val }, '')", 'E1210:')
 
-  call assert_fails("vim9 reduce(0, (acc, val) => (acc .. val), '')", 'E1252:')
-  call assert_fails("vim9 reduce({}, (acc, val) => (acc .. val), '')", 'E1252:')
-  call assert_fails("vim9 reduce(0.1, (acc, val) => (acc .. val), '')", 'E1252:')
-  call assert_fails("vim9 reduce(function('tr'), (acc, val) => (acc .. val), '')", 'E1252:')
+  call assert_fails("vim9 reduce(0, (acc, val) => (acc .. val), '')", 'E1253:')
+  call assert_fails("vim9 reduce({}, (acc, val) => (acc .. val), '')", 'E1253:')
+  call assert_fails("vim9 reduce(0.1, (acc, val) => (acc .. val), '')", 'E1253:')
+  call assert_fails("vim9 reduce(function('tr'), (acc, val) => (acc .. val), '')", 'E1253:')
   call assert_fails("call reduce('', { acc, val -> acc + val }, 1)", 'E1174:')
   call assert_fails("call reduce('', { acc, val -> acc + val }, {})", 'E1174:')
   call assert_fails("call reduce('', { acc, val -> acc + val }, 0.1)", 'E1174:')
@@ -1463,7 +1471,7 @@
   let l = test_null_list()
   call assert_equal([], extend(l, l, 0))
   call assert_equal(0, insert(test_null_list(), 2, -1))
-  call assert_fails('let s = join([1, 2], [])', 'E730:')
+  call assert_fails('let s = join([1, 2], [])', 'E1174:')
   call assert_fails('call remove(l, 0, 2)', 'E684:')
   call assert_fails('call insert(l, 2, -1)', 'E684:')
   call assert_fails('call extend(test_null_list(), test_null_list())', 'E1134:')
@@ -1544,7 +1552,7 @@
   call assert_fails('let i = indexof(l, "v:val == ''cyan''")', 'E735:')
   call assert_fails('let i = indexof(l, "color == ''cyan''")', 'E121:')
   call assert_fails('let i = indexof(l, {})', 'E1256:')
-  call assert_fails('let i = indexof({}, "v:val == 2")', 'E1226:')
+  call assert_fails('let i = indexof({}, "v:val == 2")', 'E1528:')
   call assert_fails('let i = indexof([], "v:val == 2", [])', 'E1206:')
 
   func TestIdx(k, v)
diff --git a/src/testdir/test_method.vim b/src/testdir/test_method.vim
index f18ac14..99e4917 100644
--- a/src/testdir/test_method.vim
+++ b/src/testdir/test_method.vim
@@ -52,7 +52,7 @@
   call assert_fails("let x = d->insert(0)", 'E899:')
   call assert_true(d->has_key('two'))
   call assert_equal([['one', 1], ['two', 2], ['three', 3]], d->items())
-  call assert_fails("let x = d->join()", 'E1211:')
+  call assert_fails("let x = d->join()", 'E1529:')
   call assert_equal(['one', 'two', 'three'], d->keys())
   call assert_equal(3, d->len())
   call assert_equal(#{one: 2, two: 3, three: 4}, d->map('v:val + 1'))
@@ -62,7 +62,7 @@
   call assert_equal(2, d->remove("two"))
   let d.two = 2
   call assert_fails('let x = d->repeat(2)', 'E731:')
-  call assert_fails('let x = d->reverse()', 'E1252:')
+  call assert_fails('let x = d->reverse()', 'E1253:')
   call assert_fails('let x = d->sort()', 'E686:')
   call assert_equal("{'one': 1, 'two': 2, 'three': 3}", d->string())
   call assert_equal(v:t_dict, d->type())
diff --git a/src/testdir/test_put.vim b/src/testdir/test_put.vim
index 26eb7f0..1e123d1 100644
--- a/src/testdir/test_put.vim
+++ b/src/testdir/test_put.vim
Binary files differ
diff --git a/src/testdir/test_tuple.vim b/src/testdir/test_tuple.vim
new file mode 100644
index 0000000..fce5292
--- /dev/null
+++ b/src/testdir/test_tuple.vim
@@ -0,0 +1,2306 @@
+" Tests for the Tuple types
+
+import './vim9.vim' as v9
+
+func TearDown()
+  " Run garbage collection after every test
+  call test_garbagecollect_now()
+endfunc
+
+" Tuple declaration
+func Test_tuple_declaration()
+  let lines =<< trim END
+    var Fn = function('min')
+    var t = (1, 'a', true, 3.1, 0z10, ['x'], {'a': []}, Fn)
+    assert_equal((1, 'a', true, 3.1, 0z10, ['x'], {'a': []}, Fn), t)
+  END
+  call v9.CheckSourceDefAndScriptSuccess(lines)
+
+  " Multiline tuple declaration
+  let lines =<< trim END
+    var t = (
+        'a',
+        'b',
+      )
+    assert_equal(('a', 'b'), t)
+  END
+  call v9.CheckSourceDefAndScriptSuccess(lines)
+
+  " Tuple declaration with comments
+  let lines =<< trim END
+    var t = (   # xxx
+        # xxx
+        'a',  # xxx
+        # xxx
+        'b',  # xxx
+      )  # xxx
+    assert_equal(('a', 'b'), t)
+  END
+  call v9.CheckSourceDefAndScriptSuccess(lines)
+
+  " Tuple declaration separated by '|'
+  let lines =<< trim END
+    VAR t1 = ('a', 'b') | VAR t2 = ('c', 'd')
+    call assert_equal(('a', 'b'), t1)
+    call assert_equal(('c', 'd'), t2)
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  " Space after and before parens
+  let lines =<< trim END
+    var t = ( 1, 2 )
+    assert_equal((1, 2), t)
+  END
+  call v9.CheckSourceDefAndScriptSuccess(lines)
+endfunc
+
+" Tuple declaration error
+func Test_tuple_declaration_error()
+  let lines =<< trim END
+    var t: tuple<> = ('a', 'b')
+  END
+  call v9.CheckSourceDefAndScriptFailure(lines, "E1008: Missing <type> after > = ('a', 'b')")
+
+  let lines =<< trim END
+    var t: tuple = ('a', 'b')
+  END
+  call v9.CheckSourceDefAndScriptFailure(lines, "E1008: Missing <type> after tuple")
+
+  let lines =<< trim END
+    var t: tuple<number> = ('a','b')
+  END
+  call v9.CheckSourceDefAndScriptFailure(lines, "E1069: White space required after ',': ,'b')")
+
+  let lines =<< trim END
+    var t: tuple<number> = ('a', 'b','c')
+  END
+  call v9.CheckSourceDefAndScriptFailure(lines, "E1069: White space required after ',': ,'c')")
+
+  let lines =<< trim END
+    var t: tuple <number> = ()
+  END
+  call v9.CheckSourceDefAndScriptFailure(lines, "E1068: No white space allowed before '<'")
+
+  let lines =<< trim END
+    var t: tuple<number,string>
+  END
+  call v9.CheckSourceDefAndScriptFailure(lines, "E1069: White space required after ','")
+
+  let lines =<< trim END
+    var t: tuple<number , string>
+  END
+  call v9.CheckSourceDefFailure(lines, "E1068: No white space allowed before ','")
+
+  let lines =<< trim END
+    var t = ('a', 'b' , 'c')
+  END
+  call v9.CheckSourceDefAndScriptFailure(lines, [
+        \ "E1068: No white space allowed before ','",
+        \ "E1068: No white space allowed before ','"])
+
+  let lines =<< trim END
+    VAR t = ('a', 'b' 'c')
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ "E1527: Missing comma in Tuple: 'c')",
+        \ "E1527: Missing comma in Tuple: 'c')",
+        \ "E1527: Missing comma in Tuple: 'c')"])
+
+  let lines =<< trim END
+    VAR t = ('a', 'b',
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ "E1526: Missing end of Tuple ')'",
+        \ "E1526: Missing end of Tuple ')'",
+        \ "E1526: Missing end of Tuple ')'"])
+
+  let lines =<< trim END
+    var t: tuple<number, ...> = (1, 2, 3)
+  END
+  call v9.CheckSourceDefAndScriptFailure(lines, [
+        \ 'E1010: Type not recognized: ',
+        \ 'E1010: Type not recognized: '])
+
+  let lines =<< trim END
+    var t: tuple<number, ...number> = (1, 2, 3)
+  END
+  call v9.CheckSourceDefAndScriptFailure(lines, [
+        \ 'E1539: Variadic tuple must end with a list type: number',
+        \ 'E1539: Variadic tuple must end with a list type: number'])
+
+  " Invalid expression in the tuple
+  let lines =<< trim END
+    def Foo()
+      var t = (1, 1*2, 2)
+    enddef
+    defcompile
+  END
+  call v9.CheckSourceDefFailure(lines, 'E1004: White space required before and after ''*'' at "*2, 2)"')
+
+  let lines =<< trim END
+    VAR t = ('a', , 'b',)
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E15: Invalid expression: ", ''b'',)"',
+        \ "E1068: No white space allowed before ',': , 'b',)",
+        \ 'E15: Invalid expression: ", ''b'',)"'])
+
+  let lines =<< trim END
+    VAR t = ('a', 'b', ,)
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E15: Invalid expression: ",)"',
+        \ "E1068: No white space allowed before ',': ,)",
+        \ 'E15: Invalid expression: ",)"'])
+
+  let lines =<< trim END
+    VAR t = (, 'a', 'b')
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E15: Invalid expression: ", ''a'', ''b'')"',
+        \ "E1015: Name expected: , 'a', 'b')",
+        \ 'E15: Invalid expression: ", ''a'', ''b'')"'])
+
+  let lines =<< trim END
+    var t: tupel<number> = (1,)
+  END
+  call v9.CheckSourceDefAndScriptFailure(lines, 'E1010: Type not recognized: tupel<number>')
+
+  let lines =<< trim END
+    var t: tuple<number> = [1, 2]
+  END
+  call v9.CheckSourceDefAndScriptFailure(lines, 'E1012: Type mismatch; expected tuple<number> but got list<number>')
+endfunc
+
+" Test for indexing a tuple
+func Test_tuple_indexing()
+  let lines =<< trim END
+    VAR t = ('a', 'b', 'c')
+    call assert_equal(['a', 'b', 'c'], [t[0], t[1], t[2]])
+    call assert_equal(['c', 'b', 'a'], [t[-1], t[-2], t[-3]])
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  " Indexing a tuple passed as a function argument
+  let lines =<< trim END
+    vim9script
+    def Fn(t: any)
+      call assert_equal(['a', 'b', 'c'], [t[0], t[1], t[2]])
+      call assert_equal(['c', 'b', 'a'], [t[-1], t[-2], t[-3]])
+    enddef
+    Fn(('a', 'b', 'c'))
+  END
+  call v9.CheckSourceSuccess(lines)
+
+  let lines =<< trim END
+    var t: tuple<...list<number>> = (10, 20)
+    var x: number = t[0]
+    assert_equal(10, x)
+  END
+  call v9.CheckSourceDefAndScriptSuccess(lines)
+
+  let lines =<< trim END
+    var t: tuple<...list<list<number>>> = ([1, 2], [3, 4])
+    t[0][1] = 5
+    assert_equal(([1, 5], [3, 4]), t)
+  END
+  call v9.CheckSourceDefAndScriptSuccess(lines)
+
+  let lines =<< trim END
+    var t: tuple<list<number>> = ([2, 4],)
+    t[0][1] = 6
+    assert_equal(([2, 6],), t)
+  END
+  call v9.CheckSourceDefAndScriptSuccess(lines)
+endfunc
+
+" Indexing a tuple in a Dict
+func Test_tuple_in_a_dict_index()
+  let lines =<< trim END
+    vim9script
+    def Fn()
+      var d = {a: (1, 2)}
+      var x = d.a[0]
+      assert_equal('number', typename(x))
+    enddef
+    Fn()
+  END
+  call v9.CheckSourceSuccess(lines)
+endfunc
+
+func Test_tuple_index_error()
+  let lines =<< trim END
+    echo ('a', 'b', 'c')[3]
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1519: Tuple index out of range: 3',
+        \ 'E1519: Tuple index out of range: 3',
+        \ 'E1519: Tuple index out of range: 3'])
+
+  let lines =<< trim END
+    echo ('a', 'b', 'c')[-4]
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1519: Tuple index out of range: -4',
+        \ 'E1519: Tuple index out of range: -4',
+        \ 'E1519: Tuple index out of range: -4'])
+
+  let lines =<< trim END
+    vim9script
+    def Fn(t: any)
+      echo t[3]
+    enddef
+    Fn(('a', 'b', 'c'))
+  END
+  call v9.CheckSourceFailure(lines, 'E1519: Tuple index out of range: 3')
+
+  let lines =<< trim END
+    vim9script
+    def Fn(t: any)
+      echo t[-4]
+    enddef
+    Fn(('a', 'b', 'c'))
+  END
+  call v9.CheckSourceFailure(lines, 'E1519: Tuple index out of range: -4')
+
+  let lines =<< trim END
+    vim9script
+    def Fn(t: any)
+      var x = t[0]
+    enddef
+    Fn(())
+  END
+  call v9.CheckSourceFailure(lines, 'E1519: Tuple index out of range: 0')
+
+  " Index a null tuple
+  let lines =<< trim END
+    VAR t = test_null_tuple()
+    LET t[0][0] = 10
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1519: Tuple index out of range: 0',
+        \ 'E1519: Tuple index out of range: 0',
+        \ 'E1519: Tuple index out of range: 0'])
+
+  let lines =<< trim END
+    var x = null_tuple
+    x[0][0] = 10
+  END
+  call v9.CheckSourceDefExecAndScriptFailure(lines, [
+        \ 'E1519: Tuple index out of range: 0',
+        \ 'E1519: Tuple index out of range: 0'])
+
+  " Use a float as the index
+  let lines =<< trim END
+    VAR t = (1, 2)
+    VAR x = t[0.1]
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E805: Using a Float as a Number',
+        \ 'E1012: Type mismatch; expected number but got float',
+        \ 'E805: Using a Float as a Number'])
+endfunc
+
+" Test for slicing a tuple
+func Test_tuple_slice()
+  let lines =<< trim END
+    VAR t = (1, 3, 5, 7, 9)
+    call assert_equal((3, 5), t[1 : 2])
+    call assert_equal((9,), t[4 : 4])
+    call assert_equal((7, 9), t[3 : 6])
+    call assert_equal((1, 3, 5), t[: 2])
+    call assert_equal((5, 7, 9), t[2 :])
+    call assert_equal((1, 3, 5, 7, 9), t[:])
+    call assert_equal((), test_null_tuple()[:])
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  let lines =<< trim END
+    call assert_equal(('b', 'c'), ('a', 'b', 'c')[1 : 5])
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+endfunc
+
+" Test for concatenating tuples
+func Test_tuple_concatenate()
+  let lines =<< trim END
+    VAR t1 = ('a', 'b') + ('c', 'd')
+    call assert_equal(('a', 'b', 'c', 'd'), t1)
+
+    VAR t2 = ('a',) + ('b',)
+    call assert_equal(('a', 'b'), t2)
+
+    VAR t3 = ('a',) + ()
+    call assert_equal(('a',), t3)
+
+    VAR t4 = () + ('b',)
+    call assert_equal(('b',), t4)
+
+    VAR t5 = ('a', 'b') + test_null_tuple()
+    call assert_equal(('a', 'b'), t5)
+    call assert_equal('tuple<string, string>', typename(t5))
+
+    VAR t6 = test_null_tuple() + ('c', 'd')
+    call assert_equal(('c', 'd'), t6)
+    call assert_equal('tuple<string, string>', typename(t6))
+
+    VAR t7 = ('a', 'b') + (8, 9)
+    call assert_equal(('a', 'b', 8, 9), t7)
+    call assert_equal('tuple<string, string, number, number>', typename(t7))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  let lines =<< trim END
+    var t1: tuple<...list<tuple<number, number>>> = ()
+    var t2: tuple<...list<tuple<number, number>>> = ()
+    var t: tuple<...list<tuple<number, number>>> = t1 + t2
+    assert_equal((), t)
+  END
+  call v9.CheckSourceDefAndScriptSuccess(lines)
+
+  let lines =<< trim END
+    var t: tuple<...list<number>> = (1, 2) + ('a', 'b')
+  END
+  call v9.CheckSourceDefExecAndScriptFailure(lines, [
+        \ 'E1012: Type mismatch; expected tuple<...list<number>> but got tuple<number, number, string, string>',
+        \ 'E1012: Type mismatch; expected tuple<...list<number>> but got tuple<number, number, string, string>'])
+
+  let lines =<< trim END
+    var a: tuple<...list<number>> = (1, 2)
+    var b: tuple<...list<string>> = ('a', 'b')
+    var t = a + b
+  END
+  call v9.CheckSourceDefExecAndScriptFailure(lines, [
+        \ 'E1540: Cannot use a variadic tuple in concatenation',
+        \ 'E1540: Cannot use a variadic tuple in concatenation'])
+
+  let lines =<< trim END
+    var a: tuple<...list<number>> = (1, 2)
+    var b: tuple<string, string> = ('a', 'b')
+    var t = a + b
+  END
+  call v9.CheckSourceDefExecAndScriptFailure(lines, [
+        \ 'E1540: Cannot use a variadic tuple in concatenation',
+        \ 'E1540: Cannot use a variadic tuple in concatenation'])
+
+  let lines =<< trim END
+    var a: tuple<number, ...list<string>> = (1, 'a', 'b')
+    var b: tuple<number, ...list<string>> = (2, 'c', 'd')
+    var t = a + b
+  END
+  call v9.CheckSourceDefExecAndScriptFailure(lines, [
+        \ 'E1540: Cannot use a variadic tuple in concatenation',
+        \ 'E1540: Cannot use a variadic tuple in concatenation'])
+
+  let lines =<< trim END
+    var a: tuple<number, ...list<string>> = (1, 'a', 'b')
+    var b: tuple<...list<string>> = ('c', 'd')
+    var t = a + b
+  END
+  call v9.CheckSourceDefExecAndScriptFailure(lines, [
+        \ 'E1540: Cannot use a variadic tuple in concatenation',
+        \ 'E1540: Cannot use a variadic tuple in concatenation'])
+
+  let lines =<< trim END
+    var a: tuple<...list<string>> = ('a', 'b')
+    var b: tuple<number, ...list<string>> = (2, 'c', 'd')
+    var t = a + b
+  END
+  call v9.CheckSourceDefExecAndScriptFailure(lines, [
+        \ 'E1540: Cannot use a variadic tuple in concatenation',
+        \ 'E1540: Cannot use a variadic tuple in concatenation'])
+
+  let lines =<< trim END
+    var t1: tuple<...list<tuple<number, number>>> = ()
+    var t2: tuple<...list<tuple<number, string>>> = ()
+    var t = t1 + t2
+  END
+  call v9.CheckSourceDefExecAndScriptFailure(lines, [
+        \ 'E1540: Cannot use a variadic tuple in concatenation',
+        \ 'E1540: Cannot use a variadic tuple in concatenation'])
+
+  " Make sure the correct line number is used in the error message
+  let lines =<< trim END
+    vim9script
+    var t1: tuple<...list<tuple<number, number>>> = ()
+    var t2: tuple<...list<tuple<number, string>>> = ()
+    var t = t1 + t2
+
+  END
+  call v9.CheckSourceFailure(lines, 'E1540: Cannot use a variadic tuple in concatenation', 4)
+
+  let lines =<< trim END
+    vim9script
+
+    def Fn()
+      var t1: tuple<...list<tuple<number, number>>> = ()
+      var t2: tuple<...list<tuple<number, string>>> = ()
+      var t = t1 + t2
+
+    enddef
+    Fn()
+  END
+  call v9.CheckSourceFailure(lines, 'E1540: Cannot use a variadic tuple in concatenation', 3)
+
+  " One or both the operands are variadic tuples
+  let lines =<< trim END
+    var a1: tuple<number, number> = (1, 2)
+    var b1: tuple<...list<string>> = ('a', 'b')
+    var t1 = a1 + b1
+    assert_equal((1, 2, 'a', 'b'), t1)
+
+    var a2: tuple<string, string> = ('a', 'b')
+    var b2: tuple<number, ...list<string>> = (1, 'c', 'd')
+    var t2 = a2 + b2
+    assert_equal(('a', 'b', 1, 'c', 'd'), t2)
+
+    var a3: tuple<...list<string>> = ('a', 'b')
+    var b3: tuple<...list<string>> = ('c', 'd')
+    var t3 = a3 + b3
+    assert_equal(('a', 'b', 'c', 'd'), t3)
+
+    var a4: tuple<...list<number>> = (1, 2)
+    var t4 = a4 + ()
+    assert_equal((1, 2), t4)
+
+    var b5: tuple<...list<number>> = (1, 2)
+    var t5 = () + b5
+    assert_equal((1, 2), t5)
+
+    var a6: tuple<...list<number>> = (1, 2)
+    var t6 = a6 + null_tuple
+    assert_equal((1, 2), t6)
+
+    var b7: tuple<...list<string>> = ('a', 'b')
+    var t7 = null_tuple + b7
+    assert_equal(('a', 'b'), t7)
+  END
+  call v9.CheckSourceDefAndScriptSuccess(lines)
+
+  let lines =<< trim END
+    VAR t = test_null_tuple() + test_null_tuple()
+    call assert_equal(test_null_tuple(), t)
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  let lines =<< trim END
+    vim9script
+    def Fn(x: any, y: any): any
+      return x + y
+    enddef
+    assert_equal((1, 2), Fn((1,), (2,)))
+    assert_equal((1, 'a'), Fn((1,), ('a',)))
+    assert_equal((1,), Fn((1,), null_tuple))
+    assert_equal(('a',), Fn(null_tuple, ('a',)))
+    assert_equal((), Fn(null_tuple, null_tuple))
+  END
+  call v9.CheckSourceScriptSuccess(lines)
+
+  " Test for concatenating to lists containing tuples
+  let lines =<< trim END
+    var x = [test_null_tuple()] + [test_null_tuple()]
+    assert_equal([(), ()], x)
+    var y = [()] + [()]
+    assert_equal([(), ()], y)
+  END
+  call v9.CheckSourceDefAndScriptSuccess(lines)
+endfunc
+
+" Test for comparing tuples
+func Test_tuple_compare()
+  let lines =<< trim END
+    call assert_false((1, 2) == (1, 3))
+    call assert_true((1, 2) == (1, 2))
+    call assert_true((1,) == (1,))
+    call assert_true(() == ())
+    call assert_false((1, 2) == (1, 2, 3))
+    call assert_false((1, 2) == test_null_tuple())
+    VAR t1 = (1, 2)
+    VAR t2 = t1
+    call assert_true(t1 == t2)
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  let lines =<< trim END
+    echo (1.0, ) == 1.0
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1517: Can only compare Tuple with Tuple',
+        \ 'E1072: Cannot compare tuple with float',
+        \ 'E1072: Cannot compare tuple with float'])
+
+  let lines =<< trim END
+    echo 1.0 == (1.0,)
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1517: Can only compare Tuple with Tuple',
+        \ 'E1072: Cannot compare float with tuple',
+        \ 'E1072: Cannot compare float with tuple'])
+
+  let lines =<< trim END
+    echo (1, 2) =~ []
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E691: Can only compare List with List',
+        \ 'E1072: Cannot compare tuple with list',
+        \ 'E1072: Cannot compare tuple with list'])
+
+  let lines =<< trim END
+    echo (1, 2) =~ (1, 2)
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1518: Invalid operation for Tuple',
+        \ 'E1518: Invalid operation for Tuple',
+        \ 'E1518: Invalid operation for Tuple'])
+endfunc
+
+" Test for assigning multiple items from a tuple
+func Test_multi_assign_from_tuple()
+  let lines =<< trim END
+    VAR [v1, v2] = ('a', 'b')
+    call assert_equal(['a', 'b'], [v1, v2])
+
+    VAR [v3] = ('c',)
+    call assert_equal('c', v3)
+
+    VAR [v4; v5] = ('a', 'b', 'c')
+    call assert_equal('a', v4)
+    call assert_equal(('b', 'c'), v5)
+
+    VAR [v6; v7] = ('a',)
+    call assert_equal('a', v6)
+    call assert_equal((), v7)
+
+    VAR sum = 0
+    for [v8, v9] in ((2, 2), (2, 3))
+      LET sum += v8 * v9
+    endfor
+    call assert_equal(10, sum)
+
+    #" for: rest of the items in a List
+    LET sum = 0
+    for [v10; v11] in ((2, 1, 2, 5), (2, 1, 2, 10))
+      LET sum += v10 * max(v11)
+    endfor
+    call assert_equal(30, sum)
+
+    #" for: one item in the list
+    LET sum = 0
+    for [v12; v13] in ((2, 6), (2, 7))
+      LET sum += v12 * max(v13)
+    endfor
+    call assert_equal(26, sum)
+
+    #" for: zero items in the list
+    LET sum = 0
+    for [v14; v15] in ((4,), (5,))
+      LET sum += v14 + max(v15)
+    endfor
+    call assert_equal(9, sum)
+
+    #" A null tuple should be treated like an empty tuple
+    for [v16, v17] in test_null_tuple()
+    endfor
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  let lines =<< trim END
+    var t: tuple<...list<number>> = (4, 8)
+    var [x: number, y: number] = t
+    assert_equal([4, 8], [x, y])
+  END
+  call v9.CheckSourceDefAndScriptSuccess(lines)
+
+  " Test a mix lists and tuples with "any" type
+  let lines =<< trim END
+    vim9script
+    def Fn(x: any): string
+      var str = ''
+      for [a, b] in x
+        str ..= a .. b
+      endfor
+      return str
+    enddef
+    # List of lists
+    assert_equal('abcd', Fn([['a', 'b'], ['c', 'd']]))
+    # List of tuples
+    assert_equal('abcd', Fn([('a', 'b'), ('c', 'd')]))
+    # Tuple of lists
+    assert_equal('abcd', Fn((['a', 'b'], ['c', 'd'])))
+    # Tuple of tuples
+    assert_equal('abcd', Fn((('a', 'b'), ('c', 'd'))))
+  END
+  call v9.CheckSourceSuccess(lines)
+
+  let lines =<< trim END
+    VAR [v1, v2] = ('a', 'b', 'c')
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1537: Less targets than Tuple items',
+        \ 'E1093: Expected 2 items but got 3',
+        \ 'E1537: Less targets than Tuple items'])
+
+  let lines =<< trim END
+    VAR [v1, v2, v3] = ('a', 'b')
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1538: More targets than Tuple items',
+        \ 'E1093: Expected 3 items but got 2',
+        \ 'E1538: More targets than Tuple items'])
+
+  let lines =<< trim END
+    VAR [v1; v2] = test_null_tuple()
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1536: Tuple required',
+        \ 'E1093: Expected 1 items but got 0',
+        \ 'E1536: Tuple required'])
+
+  let lines =<< trim END
+    for [v1, v2] in (('a', 'b', 'c'),)
+    endfor
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1537: Less targets than Tuple items',
+        \ 'E1537: Less targets than Tuple items',
+        \ 'E1537: Less targets than Tuple items'])
+
+  let lines =<< trim END
+    for [v1, v2] in (('a',),)
+    endfor
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1538: More targets than Tuple items',
+        \ 'E1538: More targets than Tuple items',
+        \ 'E1538: More targets than Tuple items'])
+
+  let lines =<< trim END
+    for [v1, v2] in (test_null_tuple(),)
+    endfor
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1536: Tuple required',
+        \ 'E1538: More targets than Tuple items',
+        \ 'E1536: Tuple required'])
+
+  let lines =<< trim END
+    for [v1; v2] in (test_null_tuple(),)
+    endfor
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1536: Tuple required',
+        \ 'E1538: More targets than Tuple items',
+        \ 'E1536: Tuple required'])
+
+  " List assignment errors using a function tuple argument
+  let lines =<< trim END
+    vim9script
+    def Fn(x: tuple<...list<number>>)
+      var [a, b] = x
+    enddef
+    Fn((1, 2, 3))
+  END
+  call v9.CheckSourceFailure(lines, 'E1093: Expected 2 items but got 3')
+
+  let lines =<< trim END
+    vim9script
+    def Fn(x: tuple<number>)
+      var [a, b] = x
+    enddef
+    Fn((1,))
+  END
+  call v9.CheckSourceFailure(lines, 'E1093: Expected 2 items but got 1')
+
+  let lines =<< trim END
+    vim9script
+    def Fn(x: tuple<number>)
+      var [a, b] = x
+    enddef
+    Fn(null_tuple)
+  END
+  call v9.CheckSourceFailure(lines, 'E1093: Expected 2 items but got 0')
+endfunc
+
+" Test for performing an arithmetic operation on multiple variables using
+" items from a tuple
+func Test_multi_arithmetic_op_from_tuple()
+  let lines =<< trim END
+    VAR x = 10
+    VAR y = 10
+    LET [x, y] += (2, 4)
+    call assert_equal([12, 14], [x, y])
+    LET [x, y] -= (4, 2)
+    call assert_equal([8, 12], [x, y])
+    LET [x, y] *= (2, 3)
+    call assert_equal([16, 36], [x, y])
+    LET [x, y] /= (4, 2)
+    call assert_equal([4, 18], [x, y])
+    LET [x, y] %= (3, 5)
+    call assert_equal([1, 3], [x, y])
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  " The "." operator is supported only in Vim script
+  let lines =<< trim END
+    let x = 'a'
+    let y = 'b'
+    let [x, y] .= ('a', 'b')
+    call assert_equal(['aa', 'bb'], [x, y])
+  END
+  call v9.CheckSourceSuccess(lines)
+
+  let lines =<< trim END
+    VAR x = 'a'
+    VAR y = 'b'
+    LET [x, y] ..= ('a', 'b')
+    call assert_equal(('aa', 'bb'), (x, y))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+endfunc
+
+" Test for using a tuple in a for statement
+func Test_tuple_for()
+  let lines =<< trim END
+    VAR sum = 0
+    for v1 in (1, 3, 5)
+      LET sum += v1
+    endfor
+    call assert_equal(9, sum)
+
+    LET sum = 0
+    for v2 in ()
+      LET sum += v2
+    endfor
+    call assert_equal(0, sum)
+
+    LET sum = 0
+    for v2 in test_null_tuple()
+      LET sum += v2
+    endfor
+    call assert_equal(0, sum)
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  " ignoring the for loop assignment using '_'
+  let lines =<< trim END
+    vim9script
+    var count = 0
+    for _ in (1, 2, 3)
+      count += 1
+    endfor
+    assert_equal(3, count)
+  END
+  call v9.CheckSourceSuccess(lines)
+
+  let lines =<< trim END
+    var sum = 0
+    for v in null_tuple
+      sum += v
+    endfor
+    assert_equal(0, sum)
+  END
+  call v9.CheckSourceDefAndScriptSuccess(lines)
+
+  let lines =<< trim END
+    vim9script
+    def Foo()
+      for x in ((1, 2), (3, 4))
+      endfor
+    enddef
+    Foo()
+  END
+  call v9.CheckSourceSuccess(lines)
+
+  " Test for assigning multiple items from a tuple in a for loop
+  let lines =<< trim END
+    vim9script
+    def Fn()
+      for [x, y] in ([1, 2],)
+        assert_equal([1, 2], [x, y])
+      endfor
+    enddef
+    defcompile
+    Fn()
+  END
+  call v9.CheckSourceSuccess(lines)
+
+  " iterate over tuple<...list<number>
+  let lines =<< trim END
+    vim9script
+    def Fn()
+      var t: tuple<...list<number>> = (1, 2)
+      var sum = 0
+      for i: number in t
+        sum += i
+      endfor
+      assert_equal(3, sum)
+    enddef
+    Fn()
+  END
+  call v9.CheckSourceSuccess(lines)
+
+  " iterate over tuple<...list<list<number>>>
+  let lines =<< trim END
+    vim9script
+    def Fn()
+      var t: tuple<...list<list<number>>> = ([1, 2], [3, 4])
+      var sum = 0
+      for [x: number, y: number] in t
+        sum += x + y
+      endfor
+      assert_equal(10, sum)
+    enddef
+    Fn()
+  END
+  call v9.CheckSourceSuccess(lines)
+
+  " iterate over tuple<...list<tuple<...list<number>>>>
+  let lines =<< trim END
+    vim9script
+    def Fn()
+      var t: tuple<...list<tuple<...list<number>>>> = ((1, 2), (3, 4))
+      var sum = 0
+      for [x: number, y: number] in t
+        sum += x + y
+      endfor
+      assert_equal(10, sum)
+    enddef
+    Fn()
+  END
+  call v9.CheckSourceSuccess(lines)
+
+  " iterate over tuple<...list<list<number>>>
+  let lines =<< trim END
+    vim9script
+    def Fn()
+      var t: tuple<...list<list<number>>> = ([1, 2], [3, 4])
+      var sum = 0
+      for [x: number, y: number] in t
+        sum += x + y
+      endfor
+      assert_equal(10, sum)
+    enddef
+    Fn()
+  END
+  call v9.CheckSourceSuccess(lines)
+
+  " iterate over a tuple<...list<any>>
+  let lines =<< trim END
+    vim9script
+    def Fn()
+      var t: tuple<...list<any>> = (1, 'x', true, [], {}, ())
+      var str = ''
+      for v in t
+        str ..= string(v)
+      endfor
+      assert_equal("1'x'true[]{}()", str)
+    enddef
+    Fn()
+  END
+  call v9.CheckSourceSuccess(lines)
+
+  " use multiple variable assignment syntax with a tuple<...list<number>>
+  let lines =<< trim END
+    vim9script
+    def Fn()
+      var t: tuple<...list<number>> = (1, 2, 3)
+      for [i] in t
+      endfor
+    enddef
+    Fn()
+  END
+  call v9.CheckSourceFailure(lines, 'E1140: :for argument must be a sequence of lists or tuples', 2)
+endfunc
+
+" Test for checking the tuple type in assignment and return value
+func Test_tuple_type_check()
+  let lines =<< trim END
+    var t: tuple<...list<number>> = ('a', 'b')
+  END
+  call v9.CheckSourceDefFailure(lines, 'E1012: Type mismatch; expected tuple<...list<number>> but got tuple<string, string>', 1)
+
+  let lines =<< trim END
+    var t1: tuple<...list<string>> = ('a', 'b')
+    assert_equal(('a', 'b'), t1)
+    var t2 = (1, 2)
+    assert_equal((1, 2), t2)
+    var t = null_tuple
+    assert_equal(null_tuple, t)
+    t = test_null_tuple()
+    assert_equal(test_null_tuple(), t)
+  END
+  call v9.CheckSourceDefAndScriptSuccess(lines)
+
+  let lines =<< trim END
+    var t = ('a', 'b')
+    t = (1, 2)
+  END
+  call v9.CheckSourceDefFailure(lines, 'E1012: Type mismatch; expected tuple<string, string> but got tuple<number, number>', 2)
+
+  let lines =<< trim END
+    var t: tuple<number> = []
+  END
+  call v9.CheckSourceDefFailure(lines, 'E1012: Type mismatch; expected tuple<number> but got list<any>', 1)
+
+  let lines =<< trim END
+    var t: tuple<number> = {}
+  END
+  call v9.CheckSourceDefFailure(lines, 'E1012: Type mismatch; expected tuple<number> but got dict<any>', 1)
+
+  let lines =<< trim END
+    var l: list<number> = (1, 2)
+  END
+  call v9.CheckSourceDefFailure(lines, 'E1012: Type mismatch; expected list<number> but got tuple<number, number>', 1)
+
+  let lines =<< trim END
+    vim9script
+    def Fn(): tuple<...list<tuple<...list<string>>>>
+      return ((1, 2), (3, 4))
+    enddef
+    defcompile
+  END
+  call v9.CheckSourceFailure(lines, 'E1012: Type mismatch; expected tuple<...list<tuple<...list<string>>>> but got tuple<tuple<number, number>, tuple<number, number>>', 1)
+
+  let lines =<< trim END
+    var t: tuple<number> = ()
+  END
+  call v9.CheckSourceDefSuccess(lines)
+
+  let lines =<< trim END
+    vim9script
+    def Fn(): tuple<tuple<string>>
+      return ()
+    enddef
+    defcompile
+  END
+  call v9.CheckSourceSuccess(lines)
+
+  let lines =<< trim END
+    vim9script
+    def Fn(t: tuple<...list<number>>)
+    enddef
+    Fn(('a', 'b'))
+  END
+  call v9.CheckSourceFailure(lines, 'E1013: Argument 1: type mismatch, expected tuple<...list<number>> but got tuple<string, string>')
+
+  let lines =<< trim END
+    var t: any = (1, 2)
+    t = ('a', 'b')
+  END
+  call v9.CheckSourceDefSuccess(lines)
+
+  let lines =<< trim END
+    var t: tuple<...list<any>> = (1, 2)
+    t = ('a', 'b')
+  END
+  call v9.CheckSourceDefSuccess(lines)
+
+  let lines =<< trim END
+    var nll: tuple<list<number>> = ([1, 2],)
+    nll->copy()[0]->extend(['x'])
+  END
+  call v9.CheckSourceDefAndScriptFailure(lines, [
+        \ 'E1013: Argument 2: type mismatch, expected list<number> but got list<string>',
+        \ 'E1013: Argument 2: type mismatch, expected list<number> but got list<string> in extend()'])
+
+  let lines =<< trim END
+    vim9script
+    def Fn(y: tuple<number, ...list<bool>>)
+      var x: tuple<number, ...list<string>>
+      x = y
+    enddef
+
+    var t: tuple<number, ...list<bool>> = (1, true, false)
+    Fn(t)
+  END
+  call v9.CheckSourceFailure(lines, 'E1012: Type mismatch; expected tuple<number, ...list<string>> but got tuple<number, ...list<bool>>')
+endfunc
+
+" Test for setting the type of a script variable to tuple
+func Test_tuple_scriptvar_type()
+  " Uninitialized script variable should retain the type
+  let lines =<< trim END
+    vim9script
+    var foobar: tuple<list<string>>
+    def Foo()
+      var x = foobar
+      assert_equal('tuple<list<string>>', typename(x))
+    enddef
+    Foo()
+  END
+  call v9.CheckSourceScriptSuccess(lines)
+
+  " Initialized script variable should retain the type
+  let lines =<< trim END
+    vim9script
+    var foobar: tuple<...list<string>> = ('a', 'b')
+    def Foo()
+      var x = foobar
+      assert_equal('tuple<...list<string>>', typename(x))
+    enddef
+    Foo()
+  END
+  call v9.CheckSourceScriptSuccess(lines)
+endfunc
+
+" Test for modifying a tuple
+func Test_tuple_modify()
+  let lines =<< trim END
+    var t = (1, 2)
+    t[0] = 3
+  END
+  call v9.CheckSourceDefAndScriptFailure(lines, ['E1532: Cannot modify a tuple', 'E1532: Cannot modify a tuple'])
+endfunc
+
+def Test_using_null_tuple()
+  var lines =<< trim END
+    var x = null_tuple
+    assert_true(x is null_tuple)
+    var y = copy(x)
+    assert_true(y is null_tuple)
+    call assert_true((1, 2) != null_tuple)
+    call assert_true(null_tuple != (1, 2))
+    assert_equal(0, count(null_tuple, 'xx'))
+    var z = deepcopy(x)
+    assert_true(z is null_tuple)
+    assert_equal(1, empty(x))
+    assert_equal('xx', get(x, 0, 'xx'))
+    assert_equal(-1, index(null_tuple, 10))
+    assert_equal(-1, indexof(null_tuple, 'v:val == 2'))
+    assert_equal('', join(null_tuple))
+    assert_equal(0, len(x))
+    assert_equal(0, min(null_tuple))
+    assert_equal(0, max(null_tuple))
+    assert_equal((), repeat(null_tuple, 3))
+    assert_equal((), reverse(null_tuple))
+    assert_equal((), slice(null_tuple, 0, 0))
+    assert_equal('()', string(x))
+    assert_equal('tuple<any>', typename(x))
+    assert_equal(17, type(x))
+  END
+  v9.CheckSourceDefAndScriptSuccess(lines)
+
+  lines =<< trim END
+    # An uninitialized tuple is not equal to null
+    var t1: tuple<any>
+    assert_true(t1 != null)
+
+    # An empty tuple is equal to null_tuple but not equal to null
+    var t2: tuple<any> = ()
+    assert_true(t2 == null_tuple)
+    assert_true(t2 != null)
+
+    # null_tuple is equal to null
+    assert_true(null_tuple == null)
+  END
+  v9.CheckSourceDefAndScriptSuccess(lines)
+
+  lines =<< trim END
+    var x = null_tupel
+  END
+  v9.CheckSourceDefAndScriptFailure(lines, [
+        \ 'E1001: Variable not found: null_tupel',
+        \ 'E121: Undefined variable: null_tupel'])
+enddef
+
+" Test for modifying a mutable item in a tuple
+func Test_tuple_modify_mutable_item()
+  let lines =<< trim END
+    VAR t = ('a', ['b', 'c'], {'a': 10, 'b': 20})
+    LET t[1][1] = 'x'
+    LET t[2].a = 30
+    call assert_equal(('a', ['b', 'x'], {'a': 30, 'b': 20}), t)
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  let lines =<< trim END
+    VAR t = ('a', (['b'], 'c'))
+    LET t[1][0][0] = 'x'
+    call assert_equal(('a', (['x'], 'c')), t)
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  " Use a negative index
+  let lines =<< trim END
+    VAR t = ([1, 2], [3])
+    LET t[-2][-2] = 5
+    call assert_equal(([5, 2], [3]), t)
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  let lines =<< trim END
+    VAR t = ('a', ('b', 'c'))
+    LET t[1][0] = 'x'
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1532: Cannot modify a tuple',
+        \ 'E1532: Cannot modify a tuple',
+        \ 'E1532: Cannot modify a tuple'])
+
+  let lines =<< trim END
+    VAR t = ['a', ('b', 'c')]
+    LET t[1][0] = 'x'
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1532: Cannot modify a tuple',
+        \ 'E1532: Cannot modify a tuple',
+        \ 'E1532: Cannot modify a tuple'])
+
+  let lines =<< trim END
+    VAR t = {'a': ('b', 'c')}
+    LET t['a'][0] = 'x'
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1532: Cannot modify a tuple',
+        \ 'E1532: Cannot modify a tuple',
+        \ 'E1532: Cannot modify a tuple'])
+
+  let lines =<< trim END
+    VAR t = {'a': ['b', ('c',)]}
+    LET t['a'][1][0] = 'x'
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1532: Cannot modify a tuple',
+        \ 'E1532: Cannot modify a tuple',
+        \ 'E1532: Cannot modify a tuple'])
+
+  let lines =<< trim END
+    let t = ('a', 'b', 'c', 'd')
+    let t[1 : 2] = ('x', 'y')
+  END
+  call v9.CheckSourceFailure(lines, 'E1533: Cannot slice a tuple')
+
+  let lines =<< trim END
+    var t: tuple<...list<string>> = ('a', 'b', 'c', 'd')
+    t[1 : 2] = ('x', 'y')
+  END
+  call v9.CheckSourceDefAndScriptFailure(lines, [
+        \ 'E1533: Cannot slice a tuple',
+        \ 'E1533: Cannot slice a tuple'])
+
+  let lines =<< trim END
+    var t: tuple<...list<string>> = ('a', 'b', 'c', 'd')
+    t[ : 2] = ('x', 'y')
+  END
+  call v9.CheckSourceDefAndScriptFailure(lines, [
+        \ 'E1533: Cannot slice a tuple',
+        \ 'E1533: Cannot slice a tuple'])
+
+  let lines =<< trim END
+    let t = ('a', 'b', 'c', 'd')
+    let t[ : ] = ('x', 'y')
+  END
+  call v9.CheckSourceFailure(lines, 'E1533: Cannot slice a tuple')
+
+  let lines =<< trim END
+    var t: tuple<...list<string>> = ('a', 'b', 'c', 'd')
+    t[ : ] = ('x', 'y')
+  END
+  call v9.CheckSourceDefAndScriptFailure(lines, [
+        \ 'E1533: Cannot slice a tuple',
+        \ 'E1533: Cannot slice a tuple'])
+
+  let lines =<< trim END
+    VAR t = ('abc',)
+    LET t[0][1] = 'x'
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ "E689: Index not allowed after a string: t[0][1] = 'x'",
+        \ 'E1148: Cannot index a string',
+        \ "E689: Index not allowed after a string: t[0][1] = 'x'"])
+
+  " Out of range indexing
+  let lines =<< trim END
+    VAR t = ([1, 2], [3])
+    LET t[2][0] = 5
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1519: Tuple index out of range: 2',
+        \ 'E1519: Tuple index out of range: 2',
+        \ 'E1519: Tuple index out of range: 2'])
+
+  let lines =<< trim END
+    VAR t = ([1, 2], [3])
+    LET t[-3][0] = 5
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1519: Tuple index out of range: -3',
+        \ 'E1519: Tuple index out of range: -3',
+        \ 'E1519: Tuple index out of range: -3'])
+
+  " Use a null tuple
+  let lines =<< trim END
+    VAR t = test_null_tuple()
+    LET t[0][0] = 5
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1519: Tuple index out of range: 0',
+        \ 'E1519: Tuple index out of range: 0',
+        \ 'E1519: Tuple index out of range: 0'])
+endfunc
+
+" Test for locking and unlocking a tuple variable
+func Test_tuple_lock()
+  let lines =<< trim END
+    VAR t = ([0, 1],)
+    call add(t[0], 2)
+    call assert_equal(([0, 1, 2], ), t)
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  let lines =<< trim END
+    VAR t = ([0, 1],)
+    lockvar 2 t
+    call add(t[0], 2)
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E741: Value is locked: add() argument',
+        \ 'E1178: Cannot lock or unlock a local variable',
+        \ 'E741: Value is locked: add() argument'])
+
+  let lines =<< trim END
+    LET g:t = ([0, 1],)
+    lockvar 2 g:t
+    unlockvar 2 g:t
+    call add(g:t[0], 3)
+    call assert_equal(([0, 1, 3], ), g:t)
+    unlet g:t
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  let lines =<< trim END
+    VAR t1 = (1, 2)
+    const t2 = t1
+    LET t2 = ()
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E741: Value is locked: t2',
+        \ 'E1018: Cannot assign to a constant: t2',
+        \ 'E46: Cannot change read-only variable "t2"'])
+endfunc
+
+" Test for using a class as a tuple item
+func Test_tuple_use_class_item()
+  let lines =<< trim END
+    vim9script
+    class A
+    endclass
+    var t = (A,)
+  END
+  call v9.CheckSourceScriptFailure(lines, 'E1405: Class "A" cannot be used as a value', 4)
+
+  let lines =<< trim END
+    vim9script
+    class A
+    endclass
+    var t = ('a', A)
+  END
+  call v9.CheckSourceScriptFailure(lines, 'E1405: Class "A" cannot be used as a value', 4)
+
+  let lines =<< trim END
+    vim9script
+    class A
+    endclass
+    def Fn()
+      var t = (A,)
+    enddef
+    defcompile
+  END
+  call v9.CheckSourceScriptFailure(lines, 'E1405: Class "A" cannot be used as a value', 1)
+
+  let lines =<< trim END
+    vim9script
+    class A
+    endclass
+    def Fn()
+      var t = ('a', A)
+    enddef
+    defcompile
+  END
+  call v9.CheckSourceScriptFailure(lines, 'E1405: Class "A" cannot be used as a value', 1)
+endfunc
+
+" Test for using a user-defined type as a tuple item
+func Test_tuple_user_defined_type_as_item()
+  let lines =<< trim END
+    vim9script
+    type N = number
+    var t = (N,)
+  END
+  call v9.CheckSourceScriptFailure(lines, 'E1403: Type alias "N" cannot be used as a value', 3)
+
+  let lines =<< trim END
+    vim9script
+    type N = number
+    var t = ('a', N)
+  END
+  call v9.CheckSourceScriptFailure(lines, 'E1403: Type alias "N" cannot be used as a value', 3)
+
+  let lines =<< trim END
+    vim9script
+    type N = number
+    def Fn()
+      var t = (N,)
+    enddef
+    defcompile
+  END
+  call v9.CheckSourceScriptFailure(lines, 'E1407: Cannot use a Typealias as a variable or value', 1)
+
+  let lines =<< trim END
+    vim9script
+    type N = number
+    def Fn()
+      var t = ('a', N)
+    enddef
+    defcompile
+  END
+  call v9.CheckSourceScriptFailure(lines, 'E1407: Cannot use a Typealias as a variable or value', 1)
+endfunc
+
+" Test for using a tuple as a function argument
+func Test_tuple_func_arg()
+  let lines =<< trim END
+    vim9script
+    def Fn(t: tuple<...list<string>>): tuple<...list<string>>
+      return t[:]
+    enddef
+    var r1 = Fn(('a', 'b'))
+    assert_equal(('a', 'b'), r1)
+    var r2 = Fn(('a',))
+    assert_equal(('a',), r2)
+    var r3 = Fn(())
+    assert_equal((), r3)
+    var r4 = Fn(null_tuple)
+    assert_equal((), r4)
+  END
+  call v9.CheckSourceScriptSuccess(lines)
+
+  func TupleArgFunc(t)
+    return a:t[:]
+  endfunc
+  let r = TupleArgFunc(('a', 'b'))
+  call assert_equal(('a', 'b'), r)
+  let r = TupleArgFunc(('a',))
+  call assert_equal(('a',), r)
+  let r = TupleArgFunc(())
+  call assert_equal((), r)
+  let r = TupleArgFunc(test_null_tuple())
+  call assert_equal((), r)
+  delfunc TupleArgFunc
+endfunc
+
+" Test for tuple identity
+func Test_tuple_identity()
+  let lines =<< trim END
+    call assert_false((1, 2) is (1, 2))
+    call assert_true((1, 2) isnot (1, 2))
+    call assert_true((1, 2) isnot test_null_tuple())
+    VAR t1 = ('abc', 'def')
+    VAR t2 = t1
+    call assert_true(t2 is t1)
+    VAR t3 = (1, 2)
+    call assert_false(t3 is t1)
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+endfunc
+
+" Test for using a compound op with a tuple
+func Test_tuple_compound_op()
+  let lines =<< trim END
+    VAR t = (1, 2)
+    LET t += (3,)
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E734: Wrong variable type for +=',
+        \ 'E734: Wrong variable type for +=',
+        \ 'E734: Wrong variable type for +='])
+
+  for op in ['-', '*', '/', '%']
+    let lines =<< trim eval END
+      VAR t = (1, 2)
+      LET t {op}= (3,)
+    END
+    call v9.CheckSourceLegacyAndVim9Failure(lines, [
+          \ $'E734: Wrong variable type for {op}=',
+          \ $'E734: Wrong variable type for {op}=',
+          \ $'E734: Wrong variable type for {op}='])
+  endfor
+
+  let lines =<< trim END
+    VAR t = (1, 2)
+    LET t ..= (3,)
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E734: Wrong variable type for .=',
+        \ 'E1019: Can only concatenate to string',
+        \ 'E734: Wrong variable type for .='])
+endfunc
+
+" Test for using the falsy operator with tuple
+func Test_tuple_falsy_op()
+  let lines =<< trim END
+    VAR t = test_null_tuple()
+    call assert_equal('null tuple', t ?? 'null tuple')
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+endfunc
+
+" Test for tuple typecasting
+def Test_tuple_typecast()
+  var lines =<< trim END
+    var x = <tuple<number>>('a', 'b')
+  END
+  v9.CheckSourceDefAndScriptFailure(lines, [
+        \ 'E1012: Type mismatch; expected tuple<number> but got tuple<string, string>',
+        \ 'E1012: Type mismatch; expected tuple<number> but got tuple<string, string>'])
+enddef
+
+" Test for using a tuple in string interpolation
+def Test_tuple_string_interop()
+  var lines =<< trim END
+    VAR emptytuple = ()
+    call assert_equal("a()b", $'a{emptytuple}b')
+    VAR nulltuple = test_null_tuple()
+    call assert_equal("a()b", $'a{nulltuple}b')
+
+    #" Tuple interpolation
+    VAR t = ('a', 'b', 'c')
+    call assert_equal("x('a', 'b', 'c')x", $'x{t}x')
+  END
+  v9.CheckSourceLegacyAndVim9Success(lines)
+
+  lines =<< trim END
+    call assert_equal("a()b", $'a{null_tuple}b')
+  END
+  v9.CheckSourceDefAndScriptSuccess(lines)
+
+  #" Tuple evaluation in heredoc
+  lines =<< trim END
+    VAR t1 = ('a', 'b', 'c')
+    VAR data =<< eval trim DATA
+      let x = {t1}
+    DATA
+    call assert_equal(["let x = ('a', 'b', 'c')"], data)
+  END
+  v9.CheckSourceLegacyAndVim9Success(lines)
+
+  #" Empty tuple evaluation in heredoc
+  lines =<< trim END
+    VAR t1 = ()
+    VAR data =<< eval trim DATA
+      let x = {t1}
+    DATA
+    call assert_equal(["let x = ()"], data)
+  END
+  v9.CheckSourceLegacyAndVim9Success(lines)
+
+  #" Null tuple evaluation in heredoc
+  lines =<< trim END
+    VAR t1 = test_null_tuple()
+    VAR data =<< eval trim DATA
+      let x = {t1}
+    DATA
+    call assert_equal(["let x = ()"], data)
+  END
+  v9.CheckSourceLegacyAndVim9Success(lines)
+
+  lines =<< trim END
+    var t1 = null_tuple
+    var data =<< eval trim DATA
+      let x = {t1}
+    DATA
+    call assert_equal(["let x = ()"], data)
+  END
+  v9.CheckSourceDefAndScriptSuccess(lines)
+enddef
+
+" Test for a return in "finally" block overriding the tuple return value in a
+" try block.
+func Test_try_finally_with_tuple_return()
+  let lines =<< trim END
+    func s:Fn()
+      try
+        return (1, 2)
+      finally
+        return (3, 4)
+      endtry
+    endfunc
+    call assert_equal((3, 4), s:Fn())
+    delfunc s:Fn
+  END
+  call v9.CheckSourceSuccess(lines)
+
+  let lines =<< trim END
+    vim9script
+    def Fn(): tuple<...list<number>>
+      try
+        return (1, 2)
+      finally
+        return (3, 4)
+      endtry
+    enddef
+    assert_equal((3, 4), Fn())
+  END
+  call v9.CheckSourceSuccess(lines)
+endfunc
+
+" Test for add() with a tuple
+func Test_tuple_add()
+  let lines =<< trim END
+    VAR t = (1, 2)
+    call add(t, 3)
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E897: List or Blob required',
+        \ 'E1013: Argument 1: type mismatch, expected list<any> but got tuple<number, number>',
+        \ 'E1226: List or Blob required for argument 1'])
+endfunc
+
+" Test for copy()
+func Test_tuple_copy()
+  let lines =<< trim END
+    VAR t1 = (['a', 'b'], ['c', 'd'], ['e', 'f'])
+    VAR t2 = copy(t1)
+    VAR t3 = t1
+    call assert_false(t2 is t1)
+    call assert_true(t3 is t1)
+    call assert_true(t2[1] is t1[1])
+    call assert_equal((), copy(()))
+    call assert_equal((), copy(test_null_tuple()))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+endfunc
+
+" Test for count()
+func Test_tuple_count()
+  let lines =<< trim END
+    VAR t = ('ab', 'cd', 'ab')
+    call assert_equal(2, count(t, 'ab'))
+    call assert_equal(0, count(t, 'xx'))
+    call assert_equal(0, count((), 'xx'))
+    call assert_equal(0, count(test_null_tuple(), 'xx'))
+    call assert_fails("call count((1, 2), 1, v:true, 2)", 'E1519: Tuple index out of range: 2')
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+endfunc
+
+" Test for deepcopy()
+func Test_tuple_deepcopy()
+  let lines =<< trim END
+    VAR t1 = (['a', 'b'], ['c', 'd'], ['e', 'f'])
+    VAR t2 = deepcopy(t1)
+    VAR t3 = t1
+    call assert_false(t2 is t1)
+    call assert_true(t3 is t1)
+    call assert_false(t2[1] is t1[1])
+    call assert_equal((), deepcopy(()))
+    call assert_equal((), deepcopy(test_null_tuple()))
+
+    #" copy a recursive tuple
+    VAR l = []
+    VAR tuple = (l,)
+    call add(l, tuple)
+    call assert_equal('([(...)], )', string(deepcopy(tuple)))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+endfunc
+
+" Test for empty()
+func Test_tuple_empty()
+  let lines =<< trim END
+    call assert_true(empty(()))
+    call assert_true(empty(test_null_tuple()))
+    call assert_false(empty((1, 2)))
+    VAR t = ('abc', 'def')
+    call assert_false(empty(t))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+endfunc
+
+" Test for eval()
+func Test_tuple_eval()
+  let lines =<< trim END
+    call assert_equal((), eval('()'))
+    call assert_equal(([],), eval('([],)'))
+    call assert_equal((1, 2, 3), eval('(1, 2, 3)'))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+endfunc
+
+" Test for extend() with a tuple
+func Test_tuple_extend()
+  let lines =<< trim END
+    VAR t = (1, 2, 3)
+    call extend(t, (4, 5))
+    call extendnew(t, (4, 5))
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E712: Argument of extend() must be a List or Dictionary',
+        \ 'E1013: Argument 1: type mismatch, expected list<any> but got tuple<number, number, number>',
+        \ 'E712: Argument of extend() must be a List or Dictionary'])
+endfunc
+
+" Test for filter() with a tuple
+func Test_tuple_filter()
+  let lines =<< trim END
+    VAR t = (1, 2, 3)
+    call filter(t, 'v:val == 2')
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1524: Cannot use a tuple with function filter()',
+        \ 'E1013: Argument 1: type mismatch, expected list<any> but got tuple<number, number, number>',
+        \ 'E1524: Cannot use a tuple with function filter()'])
+endfunc
+
+" Test for flatten() with a tuple
+func Test_tuple_flatten()
+  let t = ([1, 2], [3, 4], [5, 6])
+  call assert_fails("call flatten(t, 2)", 'E686: Argument of flatten() must be a List')
+endfunc
+
+" Test for flattennew() with a tuple
+func Test_tuple_flattennew()
+  let lines =<< trim END
+    var t = ([1, 2], [3, 4], [5, 6])
+    flattennew(t, 2)
+  END
+  call v9.CheckSourceDefFailure(lines, 'E1013: Argument 1: type mismatch, expected list<any> but got tuple<list<number>, list<number>, list<number>>')
+endfunc
+
+" Test for foreach() with a tuple
+func Test_tuple_foreach()
+  let t = ('a', 'b', 'c')
+  let str = ''
+  call foreach(t, 'let str ..= v:val')
+  call assert_equal('abc', str)
+
+  let sum = 0
+  call foreach(test_null_tuple(), 'let sum += v:val')
+  call assert_equal(0, sum)
+
+  let lines =<< trim END
+    def Concatenate(k: number, v: string)
+      g:str ..= v
+    enddef
+    var t = ('a', 'b', 'c')
+    var str = 0
+    g:str = ''
+    call foreach(t, Concatenate)
+    call assert_equal('abc', g:str)
+
+    g:str = ''
+    call foreach(test_null_tuple(), Concatenate)
+    call assert_equal('', g:str)
+  END
+  call v9.CheckSourceDefAndScriptSuccess(lines)
+
+  let lines =<< trim END
+    LET g:sum = 0
+    call foreach((1, 2, 3), 'LET g:sum += x')
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E121: Undefined variable: x',
+        \ 'E121: Undefined variable: x',
+        \ 'E121: Undefined variable: x'])
+endfunc
+
+" Test for get()
+func Test_tuple_get()
+  let lines =<< trim END
+    VAR t = (10, 20, 30)
+    for [i, v] in [[0, 10], [1, 20], [2, 30], [3, 0]]
+      call assert_equal(v, get(t, i))
+    endfor
+
+    for [i, v] in [[-1, 30], [-2, 20], [-3, 10], [-4, 0]]
+      call assert_equal(v, get(t, i))
+    endfor
+    call assert_equal(0, get((), 5))
+    call assert_equal('c', get(('a', 'b'), 2, 'c'))
+    call assert_equal('x', get(test_null_tuple(), 0, 'x'))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+endfunc
+
+" Test for id()
+func Test_tuple_id()
+  let lines =<< trim END
+    VAR t1 = (['a'], ['b'], ['c'])
+    VAR t2 = (['a'], ['b'], ['c'])
+    VAR t3 = t1
+    call assert_true(id(t1) != id(t2))
+    call assert_true(id(t1) == id(t3))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+endfunc
+
+" Test for index() function
+func Test_tuple_index_func()
+  let lines =<< trim END
+    VAR t = (88, 33, 99, 77)
+    call assert_equal(3, index(t, 77))
+    call assert_equal(2, index(t, 99, 1))
+    call assert_equal(2, index(t, 99, -4))
+    call assert_equal(2, index(t, 99, -5))
+    call assert_equal(-1, index(t, 66))
+    call assert_equal(-1, index(t, 77, 4))
+    call assert_equal(-1, index((), 8))
+    call assert_equal(-1, index(test_null_tuple(), 9))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  let lines =<< trim END
+    VAR t = (88, 33, 99, 77)
+    call assert_equal(-1, index(t, 77, []))
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E745: Using a List as a Number',
+        \ 'E1013: Argument 3: type mismatch, expected number but got list<any>',
+        \ 'E1210: Number required for argument 3'])
+
+  let lines =<< trim END
+    VAR t = (88,)
+    call assert_equal(-1, index(t, 77, 1, ()))
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1520: Using a Tuple as a Number',
+        \ 'E1013: Argument 4: type mismatch, expected bool but got tuple<any>',
+        \ 'E1212: Bool required for argument 4'])
+endfunc
+
+" Test for indexof()
+func Test_tuple_indexof()
+  let lines =<< trim END
+    VAR t = ('a', 'b', 'c', 'd')
+    call assert_equal(2, indexof(t, 'v:val =~ "c"'))
+    call assert_equal(2, indexof(t, 'v:val =~ "c"', {'startidx': 2}))
+    call assert_equal(-1, indexof(t, 'v:val =~ "c"', {'startidx': 3}))
+    call assert_equal(2, indexof(t, 'v:val =~ "c"', {'startidx': -3}))
+    call assert_equal(2, indexof(t, 'v:val =~ "c"', {'startidx': -6}))
+    call assert_equal(-1, indexof(t, 'v:val =~ "e"'))
+    call assert_equal(-1, indexof((), 'v:val == 1'))
+    call assert_equal(-1, indexof(test_null_tuple(), 'v:val == 2'))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  func g:MyIndexOf(k, v)
+    echoerr 'MyIndexOf failed'
+  endfunc
+  let lines =<< trim END
+    VAR t = (1, 2, 3)
+    echo indexof(t, function('g:MyIndexOf'))
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'MyIndexOf failed',
+        \ 'MyIndexOf failed',
+        \ 'MyIndexOf failed'])
+  delfunc g:MyIndexOf
+endfunc
+
+" Test for insert()
+func Test_tuple_insert()
+  let lines =<< trim END
+    VAR t = (1, 2, 3)
+    call insert(t, 4)
+    call insert(t, 4, 2)
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E899: Argument of insert() must be a List or Blob',
+        \ 'E1013: Argument 1: type mismatch, expected list<any> but got tuple<number, number, number>',
+        \ 'E1226: List or Blob required for argument 1'])
+endfunc
+
+" Test for items()
+func Test_tuple_items()
+  let lines =<< trim END
+    VAR t = ([], {}, ())
+    call assert_equal([[0, []], [1, {}], [2, ()]], items(t))
+    call assert_equal([[0, 1]], items((1, )))
+    call assert_equal([], items(()))
+    call assert_equal([], items(test_null_tuple()))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+endfunc
+
+" Test for join()
+func Test_tuple_join()
+  let lines =<< trim END
+    VAR t = ('a', 'b', 'c')
+    call assert_equal('a b c', join(t))
+    call assert_equal('f o o', ('f', 'o', 'o')->join())
+    call assert_equal('a-b-c', join(t, '-'))
+    call assert_equal('', join(()))
+    call assert_equal('', join(test_null_tuple()))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+endfunc
+
+" Test for js_encode()
+func Test_tuple_js_encode()
+  let lines =<< trim END
+    call assert_equal('["a","b","c"]', js_encode(('a', 'b', 'c')))
+    call assert_equal('["a","b"]', js_encode(('a', 'b')))
+    call assert_equal('["a"]', js_encode(('a',)))
+    call assert_equal("[]", js_encode(()))
+    call assert_equal("[]", js_encode(test_null_tuple()))
+    call assert_equal('["a",,]', js_encode(('a', v:none)))
+
+    #" encode a recursive tuple
+    VAR l = []
+    VAR tuple = (l,)
+    call add(l, tuple)
+    call assert_equal("[[[]]]", js_encode(tuple))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+endfunc
+
+" Test for json_encode()
+func Test_tuple_json_encode()
+  let lines =<< trim END
+    call assert_equal('["a","b","c"]', json_encode(('a', 'b', 'c')))
+    call assert_equal('["a","b"]', json_encode(('a', 'b')))
+    call assert_equal('["a"]', json_encode(('a',)))
+    call assert_equal("[]", json_encode(()))
+    call assert_equal("[]", json_encode(test_null_tuple()))
+
+    #" encode a recursive tuple
+    VAR l = []
+    VAR tuple = (l,)
+    call add(l, tuple)
+    call assert_equal("[[[]]]", json_encode(tuple))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  let lines =<< trim END
+    VAR t = (function('min'), function('max'))
+    VAR s = json_encode(t)
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1161: Cannot json encode a func',
+        \ 'E1161: Cannot json encode a func',
+        \ 'E1161: Cannot json encode a func'])
+endfunc
+
+" Test for len()
+func Test_tuple_len()
+  let lines =<< trim END
+    call assert_equal(0, len(()))
+    call assert_equal(0, len(test_null_tuple()))
+    call assert_equal(1, len(("abc",)))
+    call assert_equal(3, len(("abc", "def", "ghi")))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+endfunc
+
+" Test for map() with a tuple
+func Test_tuple_map()
+  let t = (1, 3, 5)
+  call assert_fails("call map(t, 'v:val + 1')", 'E1524: Cannot use a tuple with function map()')
+endfunc
+
+" Test for max()
+func Test_tuple_max()
+  let lines =<< trim END
+    VAR t1 = (1, 3, 5)
+    call assert_equal(5, max(t1))
+    VAR t2 = (6,)
+    call assert_equal(6, max(t2))
+    call assert_equal(0, max(()))
+    call assert_equal(0, max(test_null_tuple()))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  let lines =<< trim END
+    vim9script
+    var x = max(('a', 2))
+  END
+  call v9.CheckSourceFailure(lines, 'E1030: Using a String as a Number: "a"')
+
+  let lines =<< trim END
+    vim9script
+    var x = max((1, 'b'))
+  END
+  call v9.CheckSourceFailure(lines, 'E1030: Using a String as a Number: "b"')
+
+  let lines =<< trim END
+    vim9script
+    def Fn()
+      var x = max(('a', 'b'))
+    enddef
+    Fn()
+  END
+  call v9.CheckSourceFailure(lines, 'E1030: Using a String as a Number: "a"')
+
+  let lines =<< trim END
+    echo max([('a', 'b'), 20])
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1520: Using a Tuple as a Number',
+        \ 'E1520: Using a Tuple as a Number',
+        \ 'E1520: Using a Tuple as a Number'])
+endfunc
+
+" Test for min()
+func Test_tuple_min()
+  let lines =<< trim END
+    VAR t1 = (5, 3, 1)
+    call assert_equal(1, min(t1))
+    VAR t2 = (6,)
+    call assert_equal(6, min(t2))
+    call assert_equal(0, min(()))
+    call assert_equal(0, min(test_null_tuple()))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  let lines =<< trim END
+    vim9script
+    var x = min(('a', 2))
+  END
+  call v9.CheckSourceFailure(lines, 'E1030: Using a String as a Number: "a"')
+
+  let lines =<< trim END
+    vim9script
+    var x = min((1, 'b'))
+  END
+  call v9.CheckSourceFailure(lines, 'E1030: Using a String as a Number: "b"')
+
+
+  let lines =<< trim END
+    vim9script
+    def Fn()
+      var x = min(('a', 'b'))
+    enddef
+    Fn()
+  END
+  call v9.CheckSourceFailure(lines, 'E1030: Using a String as a Number: "a"')
+endfunc
+
+" Test for reduce()
+func Test_tuple_reduce()
+  let lines =<< trim END
+    call assert_equal(1, reduce((), LSTART acc, val LMIDDLE acc + val LEND, 1))
+    call assert_equal(10, reduce((1, 3, 5), LSTART acc, val LMIDDLE acc + val LEND, 1))
+    call assert_equal(2 * (2 * ((2 * 1) + 2) + 3) + 4, reduce((2, 3, 4), LSTART acc, val LMIDDLE 2 * acc + val LEND, 1))
+    call assert_equal('a x y z', ('x', 'y', 'z')->reduce(LSTART acc, val LMIDDLE acc .. ' ' .. val LEND, 'a'))
+
+    VAR t = ('x', 'y', 'z')
+    call assert_equal(42, reduce(t, function('get'), {'x': {'y': {'z': 42 } } }))
+    call assert_equal(('x', 'y', 'z'), t)
+    call assert_equal(1, reduce((1,), LSTART acc, val LMIDDLE acc + val LEND))
+    call assert_equal('x y z', reduce(('x', 'y', 'z'), LSTART acc, val LMIDDLE acc .. ' ' .. val LEND))
+    call assert_equal(5, reduce(test_null_tuple(), LSTART acc, val LMIDDLE acc + val LEND, 5))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  call assert_equal({'x': 1, 'y': 1, 'z': 1 }, ('x', 'y', 'z')->reduce({ acc, val -> extend(acc, { val: 1 }) }, {}))
+
+  call assert_fails("call reduce((), { acc, val -> acc + val })", 'E998: Reduce of an empty Tuple with no initial value')
+  call assert_fails("call reduce(test_null_tuple(), { acc, val -> acc + val })", 'E998: Reduce of an empty Tuple with no initial value')
+
+  let lines =<< trim END
+    echo reduce((1, 2, 3), LSTART acc, val LMIDDLE acc + foo LEND)
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E121: Undefined variable: foo',
+        \ 'E1001: Variable not found: foo',
+        \ 'E1001: Variable not found: foo'])
+endfunc
+
+" Test for remove()
+func Test_tuple_remove()
+  let lines =<< trim END
+    VAR t = (1, 3, 5)
+    call remove(t, 1)
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E896: Argument of remove() must be a List, Dictionary or Blob',
+        \ 'E1013: Argument 1: type mismatch, expected list<any> but got tuple<number, number, number>',
+        \ 'E1228: List, Dictionary or Blob required for argument 1'])
+endfunc
+
+" Test for test_refcount()
+func Test_tuple_refcount()
+  let lines =<< trim END
+    VAR t = (1, 2, 3)
+    call assert_equal(1, test_refcount(t))
+    VAR x = t
+    call assert_equal(2, test_refcount(t))
+    LET x = (4, 5, 6)
+    call assert_equal(1, test_refcount(t))
+    for n in t
+      call assert_equal(2, test_refcount(t))
+    endfor
+    call assert_equal(1, test_refcount(t))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+endfunc
+
+" Test for repeat()
+func Test_tuple_repeat()
+  let lines =<< trim END
+    VAR t = ('a', 'b')
+    call assert_equal(('a', 'b', 'a', 'b', 'a', 'b'), repeat(('a', 'b'), 3))
+    call assert_equal(('x', 'x', 'x'), repeat(('x',), 3))
+    call assert_equal((), repeat((), 3))
+    call assert_equal((), repeat((), 0))
+    call assert_equal((), repeat((), -1))
+    call assert_equal((), repeat(test_null_tuple(), 3))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+endfunc
+
+" Test for reverse()
+func Test_tuple_reverse()
+  let lines =<< trim END
+    VAR t = (['a'], ['b'], ['c'])
+    call assert_equal((['c'], ['b'], ['a']), reverse(t))
+    call assert_equal(('a',), reverse(('a',)))
+    call assert_equal((), reverse(()))
+    call assert_equal((), reverse(test_null_tuple()))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+endfunc
+
+" Test for slicing a tuple
+func Test_tuple_slice_func()
+  let lines =<< trim END
+    VAR t = (1, 3, 5, 7, 9)
+    call assert_equal((9,), slice(t, 4))
+    call assert_equal((5, 7, 9), slice(t, 2))
+    call assert_equal((), slice(t, 5))
+    call assert_equal((), slice((), 1, 2))
+    call assert_equal((), slice(test_null_tuple(), 1, 2))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  " return value of slice() should be the correct tuple type
+  let lines =<< trim END
+    var t: tuple<...list<number>> = (1, 3, 5)
+    var x: tuple<...list<number>> = slice(t, 1, 2)
+    assert_equal((3,), x)
+  END
+  call v9.CheckSourceDefAndScriptSuccess(lines)
+endfunc
+
+" Test for sort()
+func Test_tuple_sort()
+  let lines =<< trim END
+    call sort([1.1, (1.2,)], 'f')
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1521: Using a Tuple as a Float',
+        \ 'E1521: Using a Tuple as a Float',
+        \ 'E1521: Using a Tuple as a Float'])
+endfunc
+
+" Test for stridx()
+func Test_tuple_stridx()
+  let lines =<< trim END
+    call stridx(('abc', ), 'a')
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1522: Using a Tuple as a String',
+        \ 'E1013: Argument 1: type mismatch, expected string but got tuple<string>',
+        \ 'E1174: String required for argument 1'])
+endfunc
+
+" Test for string()
+func Test_tuple_string()
+  let lines =<< trim END
+    VAR t1 = (1, 'as''d', [1, 2, function("strlen")], {'a': 1}, )
+    call assert_equal("(1, 'as''d', [1, 2, function('strlen')], {'a': 1})", string(t1))
+
+    #" empty tuple
+    VAR t2 = ()
+    call assert_equal("()", string(t2))
+
+    #" one item tuple
+    VAR t3 = ("a", )
+    call assert_equal("('a', )", string(t3))
+
+    call assert_equal("()", string(test_null_tuple()))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  " recursive tuple
+  let lines =<< trim END
+    VAR l = []
+    VAR t = (l,)
+    call add(l, t)
+    call assert_equal('([(...)], )', string(t))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+endfunc
+
+" Test for type()
+func Test_tuple_type()
+  let lines =<< trim END
+    VAR t = (1, 2)
+    call assert_equal(17, type(t))
+    call assert_equal(v:t_tuple, type(t))
+    call assert_equal(v:t_tuple, type(()))
+    call assert_equal(v:t_tuple, type(test_null_tuple()))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+endfunc
+
+" Test for typename()
+func Test_tuple_typename()
+  let lines =<< trim END
+    call assert_equal('tuple<number, number>', typename((1, 2)))
+    call assert_equal('tuple<string, string>', typename(('a', 'b')))
+    call assert_equal('tuple<bool, bool>', typename((v:true, v:true)))
+    call assert_equal('tuple<number, string>', typename((1, 'b')))
+    call assert_equal('tuple<any>', typename(()))
+    call assert_equal('tuple<dict<any>>', typename(({}, )))
+    call assert_equal('tuple<list<any>>', typename(([], )))
+    call assert_equal('tuple<list<number>>', typename(([1, 2], )))
+    call assert_equal('tuple<list<string>>', typename((['a', 'b'], )))
+    call assert_equal('tuple<list<list<number>>>', typename(([[1], [2]], )))
+    call assert_equal('tuple<tuple<number, number>>', typename(((1, 2), )))
+    VAR t1 = (([1, 2],), (['a', 'b'],))
+    call assert_equal('tuple<tuple<list<number>>, tuple<list<string>>>', typename(t1))
+    call assert_equal('list<tuple<number>>', typename([(1,)]))
+    call assert_equal('list<tuple<any>>', typename([()]))
+    call assert_equal('tuple<any>', typename(test_null_tuple()))
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  let lines =<< trim END
+    var d: dict<any> = {a: 0}
+    var t2 = (d,)
+    t2[0].e = {b: t2}
+    call assert_equal('tuple<dict<any>>', typename(t2))
+  END
+  call v9.CheckSourceDefAndScriptSuccess(lines)
+
+  " check the type of a circular reference tuple
+  let lines =<< trim END
+    # circular reference tuple
+    var l: list<tuple<any>> = []
+    var t = (l,)
+    add(l, t)
+    assert_equal('tuple<list<tuple<any>>>', typename(t))
+    assert_equal('list<tuple<any>>', typename(l))
+  END
+  call v9.CheckSourceDefAndScriptSuccess(lines)
+
+  " When a tuple item is used in a "for" loop, the type is tuple<any>
+  let lines =<< trim END
+    vim9script
+    var l = [(1, 2)]
+    for t in l
+      assert_equal('tuple<any>', typename(t))
+    endfor
+  END
+  call v9.CheckSourceScriptSuccess(lines)
+
+  " type of a tuple copy should be the same
+  let lines =<< trim END
+    var t: tuple<...list<number>> =  (1, 2)
+    var x: tuple<...list<number>> =  t
+    assert_equal('tuple<...list<number>>', typename(x))
+  END
+  call v9.CheckSourceDefAndScriptSuccess(lines)
+endfunc
+
+" Test for saving and restoring tuples from a viminfo file
+func Test_tuple_viminfo()
+  let viminfo_save = &viminfo
+  set viminfo^=!
+
+  let g:MYTUPLE = ([1, 2], [3, 4], 'a', 'b', 1, 2)
+
+  " create a tuple with circular reference
+  " This should not be saved in the viminfo file
+  let l = []
+  let g:CIRCTUPLE = (l,)
+  call add(l, g:CIRCTUPLE)
+
+  wviminfo! Xviminfo
+  unlet g:MYTUPLE
+  unlet g:CIRCTUPLE
+  rviminfo! Xviminfo
+  call assert_equal(([1, 2], [3, 4], 'a', 'b', 1, 2), g:MYTUPLE)
+  call assert_false(exists('g:CIRCTUPLE'))
+  let &viminfo = viminfo_save
+  call delete('Xviminfo')
+endfunc
+
+" Test for list2tuple()
+func Test_list2tuple()
+  let lines =<< trim END
+    call assert_equal((), list2tuple([]))
+    call assert_equal((), list2tuple(test_null_list()))
+    call assert_equal(('a', ['b'], {'n': 20}), list2tuple(['a', ['b'], {'n': 20}]))
+
+    VAR l = ['a', 'b']
+    VAR t = list2tuple(l)
+    LET l[0] = 'x'
+    call assert_equal(('a', 'b'), t)
+
+    call assert_equal((0, 1, 2), list2tuple(range(3)))
+
+    call assert_equal(((),), [()]->list2tuple())
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  call assert_fails('call list2tuple(())', 'E1211: List required for argument 1')
+
+  " Check the returned type
+  let lines =<< trim END
+    var l1 = [1, 2]
+    var t1: tuple<...list<number>> = list2tuple(l1)
+    assert_equal('tuple<...list<number>>', typename(t1))
+    var l2 = ['a', 'b']
+    var t2: tuple<...list<string>> = list2tuple(l2)
+    assert_equal('tuple<...list<string>>', typename(t2))
+    var l3 = []
+    var t3 = list2tuple(l3)
+    assert_equal('tuple<any>', typename(t3))
+    var l4 = [([{}])]
+    var t4: tuple<list<dict<any>>> = list2tuple(l4)
+    assert_equal('tuple<list<dict<any>>>', typename(t4))
+  END
+  call v9.CheckSourceDefAndScriptSuccess(lines)
+endfunc
+
+" Test for tuple2list()
+func Test_tuple2list()
+  let lines =<< trim END
+    call assert_equal([], tuple2list(()))
+    call assert_equal([], tuple2list(test_null_tuple()))
+
+    VAR t1 = ('a', ['b'], {'n': 20}, ('a',))
+    call assert_equal(['a', ['b'], {'n': 20}, ('a',)], tuple2list(t1))
+
+    VAR t = ('a', 'b')
+    VAR l = tuple2list(t)
+    LET l[0] = 'x'
+    call assert_equal(('a', 'b'), t)
+
+    call assert_equal([[]], ([],)->tuple2list())
+  END
+  call v9.CheckSourceLegacyAndVim9Success(lines)
+
+  call assert_fails('call tuple2list([])', 'E1534: Tuple required for argument 1')
+
+  " Check the returned type
+  let lines =<< trim END
+    var t1 = (1, 2)
+    var l1 = tuple2list(t1)
+    assert_equal('list<number>', typename(l1))
+    var t2 = ('a', 'b')
+    var l2 = tuple2list(t2)
+    assert_equal('list<string>', typename(l2))
+    var t3 = ()
+    var l3 = tuple2list(t3)
+    assert_equal('list<any>', typename(l3))
+    var t4 = ([({},)],)
+    var l4 = tuple2list(t4)
+    assert_equal('list<list<tuple<dict<any>>>>', typename(l4))
+  END
+  call v9.CheckSourceDefAndScriptSuccess(lines)
+endfunc
+
+" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/testdir/test_vim9_assign.vim b/src/testdir/test_vim9_assign.vim
index 616e224..97c11a7 100644
--- a/src/testdir/test_vim9_assign.vim
+++ b/src/testdir/test_vim9_assign.vim
@@ -538,7 +538,7 @@
       var v2: number
       [v1, v2] = ''
   END
-  v9.CheckDefFailure(lines, 'E1012: Type mismatch; expected list<any> but got string', 3)
+  v9.CheckDefFailure(lines, 'E1535: List or Tuple required', 3)
 
   lines =<< trim END
     g:values = [false, 0]
diff --git a/src/testdir/test_vim9_builtin.vim b/src/testdir/test_vim9_builtin.vim
index 80ed2b2..f8e1306 100644
--- a/src/testdir/test_vim9_builtin.vim
+++ b/src/testdir/test_vim9_builtin.vim
@@ -882,7 +882,7 @@
 def Test_count()
   count('ABC ABC ABC', 'b', true)->assert_equal(3)
   count('ABC ABC ABC', 'b', false)->assert_equal(0)
-  v9.CheckSourceDefAndScriptFailure(['count(10, 1)'], 'E1225: String, List or Dictionary required for argument 1')
+  v9.CheckSourceDefAndScriptFailure(['count(10, 1)'], 'E1225: String, List, Tuple or Dictionary required for argument 1')
   v9.CheckSourceDefAndScriptFailure(['count("a", [1], 2)'], ['E1013: Argument 3: type mismatch, expected bool but got number', 'E1212: Bool required for argument 3'])
   v9.CheckSourceDefAndScriptFailure(['count("a", [1], 0, "b")'], ['E1013: Argument 4: type mismatch, expected number but got string', 'E1210: Number required for argument 4'])
   count([1, 2, 2, 3], 2)->assert_equal(2)
@@ -1530,7 +1530,7 @@
   END
   v9.CheckSourceScriptSuccess(lines)
 
-  v9.CheckSourceDefAndScriptFailure(['filter(1.1, "1")'], ['E1013: Argument 1: type mismatch, expected list<any> but got float', 'E1251: List, Dictionary, Blob or String required for argument 1'])
+  v9.CheckSourceDefAndScriptFailure(['filter(1.1, "1")'], ['E1013: Argument 1: type mismatch, expected list<any> but got float', 'E1251: List, Tuple, Dictionary, Blob or String required for argument 1'])
   v9.CheckSourceDefAndScriptFailure(['filter([1, 2], 4)'], ['E1256: String or function required for argument 2', 'E1024: Using a Number as a String'])
 
   lines =<< trim END
@@ -1647,6 +1647,10 @@
   assert_equal('', foldtextresult('.'))
 enddef
 
+def Test_foreach()
+  v9.CheckSourceDefAndScriptFailure(['foreach(test_null_job(), "")'], ['E1013: Argument 1: type mismatch, expected list<any> but got job', 'E1251: List, Tuple, Dictionary, Blob or String required for argument 1'])
+enddef
+
 def Test_fullcommand()
   assert_equal('next', fullcommand('n'))
   assert_equal('noremap', fullcommand('no'))
@@ -1755,7 +1759,7 @@
 enddef
 
 def Test_get()
-  v9.CheckSourceDefAndScriptFailure(['get("a", 1)'], ['E1013: Argument 1: type mismatch, expected list<any> but got string', 'E896: Argument of get() must be a List, Dictionary or Blob'])
+  v9.CheckSourceDefAndScriptFailure(['get("a", 1)'], ['E1013: Argument 1: type mismatch, expected list<any> but got string', 'E1531: Argument of get() must be a List, Tuple, Dictionary or Blob'])
   [3, 5, 2]->get(1)->assert_equal(5)
   [3, 5, 2]->get(3)->assert_equal(0)
   [3, 5, 2]->get(3, 9)->assert_equal(9)
@@ -2276,7 +2280,7 @@
 
 def Test_index()
   index(['a', 'b', 'a', 'B'], 'b', 2, true)->assert_equal(3)
-  v9.CheckSourceDefAndScriptFailure(['index("a", "a")'], ['E1013: Argument 1: type mismatch, expected list<any> but got string', 'E1226: List or Blob required for argument 1'])
+  v9.CheckSourceDefAndScriptFailure(['index("a", "a")'], ['E1013: Argument 1: type mismatch, expected list<any> but got string', 'E1528: List or Tuple or Blob required for argument 1'])
   v9.CheckSourceDefFailure(['index(["1"], 1)'], 'E1013: Argument 2: type mismatch, expected string but got number')
   v9.CheckSourceDefAndScriptFailure(['index(0z10, "b")'], ['E1013: Argument 2: type mismatch, expected number but got string', 'E1210: Number required for argument 2'])
   v9.CheckSourceDefAndScriptFailure(['index([1], 1, "c")'], ['E1013: Argument 3: type mismatch, expected number but got string', 'E1210: Number required for argument 3'])
@@ -2539,7 +2543,7 @@
 enddef
 
 def Test_join()
-  v9.CheckSourceDefAndScriptFailure(['join("abc")'], ['E1013: Argument 1: type mismatch, expected list<any> but got string', 'E1211: List required for argument 1'])
+  v9.CheckSourceDefAndScriptFailure(['join("abc")'], ['E1013: Argument 1: type mismatch, expected list<any> but got string', 'E1529: List or Tuple required for argument 1'])
   v9.CheckSourceDefAndScriptFailure(['join([], 2)'], ['E1013: Argument 2: type mismatch, expected string but got number', 'E1174: String required for argument 2'])
   join([''], '')->assert_equal('')
 enddef
@@ -2660,9 +2664,9 @@
 
 def Test_map()
   if has('channel')
-    v9.CheckSourceDefAndScriptFailure(['map(test_null_channel(), "1")'], ['E1013: Argument 1: type mismatch, expected list<any> but got channel', 'E1251: List, Dictionary, Blob or String required for argument 1'])
+    v9.CheckSourceDefAndScriptFailure(['map(test_null_channel(), "1")'], ['E1013: Argument 1: type mismatch, expected list<any> but got channel', 'E1251: List, Tuple, Dictionary, Blob or String required for argument 1'])
   endif
-  v9.CheckSourceDefAndScriptFailure(['map(1, "1")'], ['E1013: Argument 1: type mismatch, expected list<any> but got number', 'E1251: List, Dictionary, Blob or String required for argument 1'])
+  v9.CheckSourceDefAndScriptFailure(['map(1, "1")'], ['E1013: Argument 1: type mismatch, expected list<any> but got number', 'E1251: List, Tuple, Dictionary, Blob or String required for argument 1'])
   v9.CheckSourceDefAndScriptFailure(['map([1, 2], 4)'], ['E1256: String or function required for argument 2', 'E1024: Using a Number as a String'])
 
   # type of dict remains dict<any> even when type of values changes
@@ -2893,9 +2897,9 @@
 
 def Test_mapnew()
   if has('channel')
-    v9.CheckSourceDefAndScriptFailure(['mapnew(test_null_job(), "1")'], ['E1013: Argument 1: type mismatch, expected list<any> but got job', 'E1251: List, Dictionary, Blob or String required for argument 1'])
+    v9.CheckSourceDefAndScriptFailure(['mapnew(test_null_job(), "1")'], ['E1013: Argument 1: type mismatch, expected list<any> but got job', 'E1251: List, Tuple, Dictionary, Blob or String required for argument 1'])
   endif
-  v9.CheckSourceDefAndScriptFailure(['mapnew(1, "1")'], ['E1013: Argument 1: type mismatch, expected list<any> but got number', 'E1251: List, Dictionary, Blob or String required for argument 1'])
+  v9.CheckSourceDefAndScriptFailure(['mapnew(1, "1")'], ['E1013: Argument 1: type mismatch, expected list<any> but got number', 'E1251: List, Tuple, Dictionary, Blob or String required for argument 1'])
 enddef
 
 def Test_mapset()
@@ -3072,7 +3076,7 @@
           ? [1, max([2, 3])]
           : [4, 5]
   assert_equal([4, 5], l2)
-  v9.CheckSourceDefAndScriptFailure(['max(5)'], ['E1013: Argument 1: type mismatch, expected list<any> but got number', 'E1227: List or Dictionary required for argument 1'])
+  v9.CheckSourceDefAndScriptFailure(['max(5)'], ['E1013: Argument 1: type mismatch, expected list<any> but got number', 'E1530: List or Tuple or Dictionary required for argument 1'])
 enddef
 
 def Test_menu_info()
@@ -3094,7 +3098,7 @@
           ? [1, min([2, 3])]
           : [4, 5]
   assert_equal([4, 5], l2)
-  v9.CheckSourceDefAndScriptFailure(['min(5)'], ['E1013: Argument 1: type mismatch, expected list<any> but got number', 'E1227: List or Dictionary required for argument 1'])
+  v9.CheckSourceDefAndScriptFailure(['min(5)'], ['E1013: Argument 1: type mismatch, expected list<any> but got number', 'E1530: List or Tuple or Dictionary required for argument 1'])
 enddef
 
 def Test_mkdir()
@@ -3453,7 +3457,7 @@
 enddef
 
 def Test_reduce()
-  v9.CheckSourceDefAndScriptFailure(['reduce({a: 10}, "1")'], ['E1013: Argument 1: type mismatch, expected list<any> but got dict<number>', 'E1252: String, List or Blob required for argument 1'])
+  v9.CheckSourceDefAndScriptFailure(['reduce({a: 10}, "1")'], ['E1013: Argument 1: type mismatch, expected list<any> but got dict<number>', 'E1253: String, List, Tuple or Blob required for argument 1'])
   assert_equal(6, [1, 2, 3]->reduce((r, c) => r + c, 0))
   assert_equal(11, 0z0506->reduce((r, c) => r + c, 0))
 enddef
@@ -3616,8 +3620,8 @@
 enddef
 
 def Test_repeat()
-  v9.CheckSourceDefAndScriptFailure(['repeat(1.1, 2)'], ['E1013: Argument 1: type mismatch, expected string but got float', 'E1301: String, Number, List or Blob required for argument 1'])
-  v9.CheckSourceDefAndScriptFailure(['repeat({a: 10}, 2)'], ['E1013: Argument 1: type mismatch, expected string but got dict<', 'E1301: String, Number, List or Blob required for argument 1'])
+  v9.CheckSourceDefAndScriptFailure(['repeat(1.1, 2)'], ['E1013: Argument 1: type mismatch, expected string but got float', 'E1301: String, Number, List, Tuple or Blob required for argument 1'])
+  v9.CheckSourceDefAndScriptFailure(['repeat({a: 10}, 2)'], ['E1013: Argument 1: type mismatch, expected string but got dict<', 'E1301: String, Number, List, Tuple or Blob required for argument 1'])
   var lines =<< trim END
       assert_equal('aaa', repeat('a', 3))
       assert_equal('111', repeat(1, 3))
@@ -3638,7 +3642,7 @@
 enddef
 
 def Test_reverse()
-  v9.CheckSourceDefAndScriptFailure(['reverse(10)'], ['E1013: Argument 1: type mismatch, expected list<any> but got number', 'E1252: String, List or Blob required for argument 1'])
+  v9.CheckSourceDefAndScriptFailure(['reverse(10)'], ['E1013: Argument 1: type mismatch, expected list<any> but got number', 'E1253: String, List, Tuple or Blob required for argument 1'])
 enddef
 
 def Test_reverse_return_type()
diff --git a/src/testdir/test_vim9_disassemble.vim b/src/testdir/test_vim9_disassemble.vim
index 27b71fe..4877933 100644
--- a/src/testdir/test_vim9_disassemble.vim
+++ b/src/testdir/test_vim9_disassemble.vim
@@ -3695,4 +3695,184 @@
   unlet g:instr
 enddef
 
+" Disassemble the code generated for indexing a tuple
+def Test_disassemble_tuple_indexing()
+  var lines =<< trim END
+    vim9script
+    def Fn(): tuple<...list<number>>
+      var t = (5, 6, 7)
+      var i = t[2]
+      var j = t[1 : 2]
+      return t
+    enddef
+    g:instr = execute('disassemble Fn')
+  END
+  v9.CheckScriptSuccess(lines)
+  assert_match('<SNR>\d\+_Fn\_s*' ..
+    'var t = (5, 6, 7)\_s*' ..
+    '0 PUSHNR 5\_s*' ..
+    '1 PUSHNR 6\_s*' ..
+    '2 PUSHNR 7\_s*' ..
+    '3 NEWTUPLE size 3\_s*' ..
+    '4 SETTYPE tuple<number, number, number>\_s*' ..
+    '5 STORE $0\_s*' ..
+    'var i = t\[2\]\_s*' ..
+    '6 LOAD $0\_s*' ..
+    '7 PUSHNR 2\_s*' ..
+    '8 TUPLEINDEX\_s*' ..
+    '9 STORE $1\_s*' ..
+    'var j = t\[1 : 2\]\_s*' ..
+    '10 LOAD $0\_s*' ..
+    '11 PUSHNR 1\_s*' ..
+    '12 PUSHNR 2\_s*' ..
+    '13 TUPLESLICE\_s*' ..
+    '14 SETTYPE tuple<number, number, number>\_s*' ..
+    '15 STORE $2\_s*' ..
+    'return t\_s*' ..
+    '16 LOAD $0\_s*' ..
+    '17 RETURN', g:instr)
+  unlet g:instr
+enddef
+
+" Disassemble the code generated for assigning a tuple to a default value
+def Test_disassemble_tuple_default_value()
+  var lines =<< trim END
+    vim9script
+    def Fn()
+      var t: tuple<number>
+    enddef
+    g:instr = execute('disassemble Fn')
+  END
+  v9.CheckScriptSuccess(lines)
+  assert_match('<SNR>\d\+_Fn\_s*' ..
+    'var t: tuple<number>\_s*' ..
+    '0 NEWTUPLE size 0\_s*' ..
+    '1 SETTYPE tuple<number>\_s*' ..
+    '2 STORE $0\_s*' ..
+    '3 RETURN void', g:instr)
+  unlet g:instr
+enddef
+
+" Disassemble the code generated for comparing tuples
+def Test_disassemble_tuple_compare()
+  var lines =<< trim END
+    vim9script
+    def Fn()
+      var t1 = (1, 2)
+      var t2 = t1
+      var x = t1 == t2
+    enddef
+    g:instr = execute('disassemble Fn')
+  END
+  v9.CheckScriptSuccess(lines)
+  assert_match('<SNR>\d\+_Fn\_s*' ..
+    'var t1 = (1, 2)\_s*' ..
+    '0 PUSHNR 1\_s*' ..
+    '1 PUSHNR 2\_s*' ..
+    '2 NEWTUPLE size 2\_s*' ..
+    '3 SETTYPE tuple<number, number>\_s*' ..
+    '4 STORE $0\_s*' ..
+    'var t2 = t1\_s*' ..
+    '5 LOAD $0\_s*' ..
+    '6 SETTYPE tuple<number, number>\_s*' ..
+    '7 STORE $1\_s*' ..
+    'var x = t1 == t2\_s*' ..
+    '8 LOAD $0\_s*' ..
+    '9 LOAD $1\_s*' ..
+    '10 COMPARETUPLE ==\_s*' ..
+    '11 STORE $2\_s*' ..
+    '12 RETURN void', g:instr)
+  unlet g:instr
+enddef
+
+" Disassemble the code generated for concatenating tuples
+def Test_disassemble_tuple_concatenate()
+  var lines =<< trim END
+    vim9script
+    def Fn()
+      var t1 = (1,) + (2,)
+    enddef
+    g:instr = execute('disassemble Fn')
+  END
+  v9.CheckScriptSuccess(lines)
+  assert_match('<SNR>\d\+_Fn\_s*' ..
+    'var t1 = (1,) + (2,)\_s*' ..
+    '0 PUSHNR 1\_s*' ..
+    '1 NEWTUPLE size 1\_s*' ..
+    '2 PUSHNR 2\_s*' ..
+    '3 NEWTUPLE size 1\_s*' ..
+    '4 ADDTUPLE\_s*' ..
+    '5 SETTYPE tuple<number, number>\_s*' ..
+    '6 STORE $0\_s*' ..
+    '7 RETURN void', g:instr)
+  unlet g:instr
+enddef
+
+" Disassemble the code generated for a constant tupe
+def Test_disassemble_tuple_const()
+  var lines =<< trim END
+    vim9script
+    def Fn()
+      const t = (1, 2, 3)
+      var x = t[1 : 2]
+    enddef
+    g:instr = execute('disassemble Fn')
+  END
+  v9.CheckScriptSuccess(lines)
+  assert_match('<SNR>\d\+_Fn\_s*' ..
+    'const t = (1, 2, 3)\_s*' ..
+    '0 PUSHNR 1\_s*' ..
+    '1 PUSHNR 2\_s*' ..
+    '2 PUSHNR 3\_s*' ..
+    '3 NEWTUPLE size 3\_s*' ..
+    '4 LOCKCONST\_s*' ..
+    '5 SETTYPE tuple<number, number, number>\_s*' ..
+    '6 STORE $0\_s*' ..
+    'var x = t\[1 : 2\]\_s*' ..
+    '7 LOAD $0\_s*' ..
+    '8 PUSHNR 1\_s*' ..
+    '9 PUSHNR 2\_s*' ..
+    '10 TUPLESLICE\_s*' ..
+    '11 SETTYPE tuple<number, number, number>\_s*' ..
+    '12 STORE $1\_s*' ..
+    '13 RETURN void', g:instr)
+  unlet g:instr
+enddef
+
+" Disassemble the code generated for setting the type when using a tuple in an
+" assignment
+def Test_disassemble_assign_tuple_set_type()
+  var lines =<< trim END
+    vim9script
+    def Fn()
+      var x = (1,)
+    enddef
+    g:instr = execute('disassemble Fn')
+  END
+  v9.CheckScriptSuccess(lines)
+  assert_match('<SNR>\d\+_Fn\_s*' ..
+    'var x = (1,)\_s*' ..
+    '0 PUSHNR 1\_s*' ..
+    '1 NEWTUPLE size 1\_s*' ..
+    '2 SETTYPE tuple<number>\_s*' ..
+    '3 STORE $0\_s*' ..
+    '4 RETURN void', g:instr)
+
+  lines =<< trim END
+    vim9script
+    def Fn()
+      var x = ()
+    enddef
+    g:instr = execute('disassemble Fn')
+  END
+  v9.CheckScriptSuccess(lines)
+  assert_match('<SNR>\d\+_Fn\_s*' ..
+    'var x = ()\_s*' ..
+    '0 NEWTUPLE size 0\_s*' ..
+    '1 STORE $0\_s*' ..
+    '2 RETURN void', g:instr)
+
+  unlet g:instr
+enddef
+
 " vim: ts=8 sw=2 sts=2 expandtab tw=80 fdm=marker
diff --git a/src/testdir/test_vimscript.vim b/src/testdir/test_vimscript.vim
index 21f894e..5c88ec6 100644
--- a/src/testdir/test_vimscript.vim
+++ b/src/testdir/test_vimscript.vim
@@ -7519,6 +7519,16 @@
   endfor
 endfunc
 
+" Test for 'for' loop failures
+func Test_for_loop_failure()
+  func ForFn()
+    for x in test_null_job()
+    endfor
+  endfunc
+  call assert_fails('call ForFn()', 'E1523: String, List, Tuple or Blob required')
+  delfunc ForFn
+endfunc
+
 " Test for deeply nested :source command  {{{1
 func Test_deeply_nested_source()
   let lines =<< trim END
diff --git a/src/testdir/vim9.vim b/src/testdir/vim9.vim
index ff4db7b..64922b7 100644
--- a/src/testdir/vim9.vim
+++ b/src/testdir/vim9.vim
@@ -191,19 +191,23 @@
   endtry
 endfunc
 
+# Translate "lines" to legacy Vim script
+def LegacyTrans(lines: list<string>): list<string>
+  return lines->mapnew((_, v) =>
+		v->substitute('\<VAR\>', 'let', 'g')
+		->substitute('\<LET\>', 'let', 'g')
+		->substitute('\<LSTART\>', '{', 'g')
+		->substitute('\<LMIDDLE\>', '->', 'g')
+		->substitute('\<LEND\>', '}', 'g')
+		->substitute('\<TRUE\>', '1', 'g')
+		->substitute('\<FALSE\>', '0', 'g')
+		->substitute('#"', ' "', 'g'))
+enddef
+
 # Execute "lines" in a legacy function, translated as in
 # CheckLegacyAndVim9Success()
 export def CheckTransLegacySuccess(lines: list<string>)
-  var legacylines = lines->mapnew((_, v) =>
-				v->substitute('\<VAR\>', 'let', 'g')
-				 ->substitute('\<LET\>', 'let', 'g')
-				 ->substitute('\<LSTART\>', '{', 'g')
-				 ->substitute('\<LMIDDLE\>', '->', 'g')
-				 ->substitute('\<LEND\>', '}', 'g')
-				 ->substitute('\<TRUE\>', '1', 'g')
-				 ->substitute('\<FALSE\>', '0', 'g')
-				 ->substitute('#"', ' "', 'g'))
-  CheckLegacySuccess(legacylines)
+  CheckLegacySuccess(LegacyTrans(lines))
 enddef
 
 export def Vim9Trans(lines: list<string>): list<string>
@@ -264,16 +268,87 @@
   var legacylines = lines->mapnew((_, v) =>
 				v->substitute('\<VAR\>', 'let', 'g')
 				 ->substitute('\<LET\>', 'let', 'g')
+				 ->substitute('\<LSTART\>', '{', 'g')
+				 ->substitute('\<LMIDDLE\>', '->', 'g')
+				 ->substitute('\<LEND\>', '}', 'g')
+				 ->substitute('\<TRUE\>', '1', 'g')
+				 ->substitute('\<FALSE\>', '0', 'g')
 				 ->substitute('#"', ' "', 'g'))
   CheckLegacyFailure(legacylines, legacyError)
 
   var vim9lines = lines->mapnew((_, v) =>
 				v->substitute('\<VAR\>', 'var', 'g')
-				 ->substitute('\<LET ', '', 'g'))
+				 ->substitute('\<LET ', '', 'g')
+				 ->substitute('\<LSTART\>', '(', 'g')
+				 ->substitute('\<LMIDDLE\>', ') =>', 'g')
+				 ->substitute(' *\<LEND\> *', '', 'g')
+				 ->substitute('\<TRUE\>', 'true', 'g')
+				 ->substitute('\<FALSE\>', 'false', 'g'))
   CheckDefExecFailure(vim9lines, defError)
   CheckScriptFailure(['vim9script'] + vim9lines, scriptError)
 enddef
 
+# Check that "lines" inside a legacy function has no error.
+export func CheckSourceLegacySuccess(lines)
+  let cwd = getcwd()
+  new
+  call setline(1, ['func Func()'] + a:lines + ['endfunc', 'call Func()'])
+  let bnr = bufnr()
+  try
+    :source
+  finally
+    delfunc! Func
+    call chdir(cwd)
+    exe $':bw! {bnr}'
+  endtry
+endfunc
+
+# Check that "lines" inside a legacy function results in the expected error
+export func CheckSourceLegacyFailure(lines, error)
+  let cwd = getcwd()
+  new
+  call setline(1, ['func Func()'] + a:lines + ['endfunc', 'call Func()'])
+  let bnr = bufnr()
+  try
+    call assert_fails('source', a:error)
+  finally
+    delfunc! Func
+    call chdir(cwd)
+    exe $':bw! {bnr}'
+  endtry
+endfunc
+
+# Execute "lines" in a legacy function, translated as in
+# CheckSourceLegacyAndVim9Success()
+export def CheckSourceTransLegacySuccess(lines: list<string>)
+  CheckSourceLegacySuccess(LegacyTrans(lines))
+enddef
+
+# Execute "lines" in a :def function, translated as in
+# CheckLegacyAndVim9Success()
+export def CheckSourceTransDefSuccess(lines: list<string>)
+  CheckSourceDefSuccess(Vim9Trans(lines))
+enddef
+
+# Execute "lines" in a Vim9 script, translated as in
+# CheckLegacyAndVim9Success()
+export def CheckSourceTransVim9Success(lines: list<string>)
+  CheckSourceScriptSuccess(['vim9script'] + Vim9Trans(lines))
+enddef
+
+# Execute "lines" in a legacy function, :def function and Vim9 script.
+# Use 'VAR' for a declaration.
+# Use 'LET' for an assignment
+# Use ' #"' for a comment
+# Use LSTART arg LMIDDLE expr LEND for lambda
+# Use 'TRUE' for 1 in legacy, true in Vim9
+# Use 'FALSE' for 0 in legacy, false in Vim9
+export def CheckSourceLegacyAndVim9Success(lines: list<string>)
+  CheckSourceTransLegacySuccess(lines)
+  CheckSourceTransDefSuccess(lines)
+  CheckSourceTransVim9Success(lines)
+enddef
+
 # :source a list of "lines" and check whether it fails with "error"
 export def CheckSourceScriptFailure(lines: list<string>, error: string, lnum = -3)
   var cwd = getcwd()
@@ -317,18 +392,6 @@
   endtry
 enddef
 
-export def CheckSourceSuccess(lines: list<string>)
-  CheckSourceScriptSuccess(lines)
-enddef
-
-export def CheckSourceFailure(lines: list<string>, error: string, lnum = -3)
-  CheckSourceScriptFailure(lines, error, lnum)
-enddef
-
-export def CheckSourceFailureList(lines: list<string>, errors: list<string>, lnum = -3)
-  CheckSourceScriptFailureList(lines, errors, lnum)
-enddef
-
 # :source a List of "lines" inside a ":def" function and check that no error
 # occurs when called.
 export func CheckSourceDefSuccess(lines)
@@ -346,11 +409,6 @@
   endtry
 endfunc
 
-export def CheckSourceDefAndScriptSuccess(lines: list<string>)
-  CheckSourceDefSuccess(lines)
-  CheckSourceScriptSuccess(['vim9script'] + lines)
-enddef
-
 # Check that "lines" inside a ":def" function has no error when compiled.
 export func CheckSourceDefCompileSuccess(lines)
   let cwd = getcwd()
@@ -447,3 +505,45 @@
   CheckSourceScriptFailure(['vim9script'] + lines, errorScript, lnum + 1)
 enddef
 
+export def CheckSourceSuccess(lines: list<string>)
+  CheckSourceScriptSuccess(lines)
+enddef
+
+export def CheckSourceFailure(lines: list<string>, error: string, lnum = -3)
+  CheckSourceScriptFailure(lines, error, lnum)
+enddef
+
+export def CheckSourceFailureList(lines: list<string>, errors: list<string>, lnum = -3)
+  CheckSourceScriptFailureList(lines, errors, lnum)
+enddef
+
+export def CheckSourceDefAndScriptSuccess(lines: list<string>)
+  CheckSourceDefSuccess(lines)
+  CheckSourceScriptSuccess(['vim9script'] + lines)
+enddef
+
+# Execute "lines" in a legacy function, :def function and Vim9 script.
+# Use 'VAR' for a declaration.
+# Use 'LET' for an assignment
+# Use ' #"' for a comment
+export def CheckSourceLegacyAndVim9Failure(lines: list<string>, error: any)
+  var legacyError: string
+  var defError: string
+  var scriptError: string
+
+  if type(error) == type('string')
+    legacyError = error
+    defError = error
+    scriptError = error
+  else
+    legacyError = error[0]
+    defError = error[1]
+    scriptError = error[2]
+  endif
+
+  CheckSourceLegacyFailure(LegacyTrans(lines), legacyError)
+  var vim9lines = Vim9Trans(lines)
+  CheckSourceDefExecFailure(vim9lines, defError)
+  CheckSourceScriptFailure(['vim9script'] + vim9lines, scriptError)
+enddef
+
diff --git a/src/testing.c b/src/testing.c
index 7ab109c..b316b64 100644
--- a/src/testing.c
+++ b/src/testing.c
@@ -1132,6 +1132,10 @@
 	    if (argvars[0].vval.v_list != NULL)
 		retval = argvars[0].vval.v_list->lv_refcount - 1;
 	    break;
+	case VAR_TUPLE:
+	    if (argvars[0].vval.v_tuple != NULL)
+		retval = argvars[0].vval.v_tuple->tv_refcount - 1;
+	    break;
 	case VAR_DICT:
 	    if (argvars[0].vval.v_dict != NULL)
 		retval = argvars[0].vval.v_dict->dv_refcount - 1;
@@ -1249,6 +1253,12 @@
 }
 
     void
+f_test_null_tuple(typval_T *argvars UNUSED, typval_T *rettv)
+{
+    rettv_tuple_set(rettv, NULL);
+}
+
+    void
 f_test_unknown(typval_T *argvars UNUSED, typval_T *rettv)
 {
     rettv->v_type = VAR_UNKNOWN;
diff --git a/src/time.c b/src/time.c
index 8725a88..2fb0c39 100644
--- a/src/time.c
+++ b/src/time.c
@@ -778,7 +778,7 @@
 	    tv.v_type = VAR_FUNC;
 	    tv.vval.v_string = timer->tr_callback.cb_name;
 	}
-	abort = abort || set_ref_in_item(&tv, copyID, NULL, NULL);
+	abort = abort || set_ref_in_item(&tv, copyID, NULL, NULL, NULL);
     }
     return abort;
 }
diff --git a/src/tuple.c b/src/tuple.c
new file mode 100644
index 0000000..eff4bdc
--- /dev/null
+++ b/src/tuple.c
@@ -0,0 +1,1122 @@
+/* vi:set ts=8 sts=4 sw=4 noet:
+ *
+ * VIM - Vi IMproved	by Bram Moolenaar
+ *
+ * Do ":help uganda"  in Vim to read copying and usage conditions.
+ * Do ":help credits" in Vim to see a list of people who contributed.
+ * See README.txt for an overview of the Vim source code.
+ */
+
+/*
+ * tuple.c: Tuple support functions.
+ */
+
+#include "vim.h"
+
+#if defined(FEAT_EVAL) || defined(PROTO)
+
+// Tuple heads for garbage collection.
+static tuple_T		*first_tuple = NULL;	// list of all tuples
+
+    static void
+tuple_init(tuple_T *tuple)
+{
+    // Prepend the tuple to the list of tuples for garbage collection.
+    if (first_tuple != NULL)
+	first_tuple->tv_used_prev = tuple;
+    tuple->tv_used_prev = NULL;
+    tuple->tv_used_next = first_tuple;
+    first_tuple = tuple;
+
+    ga_init2(&tuple->tv_items, sizeof(typval_T), 20);
+}
+
+/*
+ * Allocate an empty header for a tuple.
+ * Caller should take care of the reference count.
+ */
+    tuple_T *
+tuple_alloc(void)
+{
+    tuple_T  *tuple;
+
+    tuple = ALLOC_CLEAR_ONE(tuple_T);
+    if (tuple != NULL)
+	tuple_init(tuple);
+    return tuple;
+}
+
+/*
+ * Allocate space for a tuple with "count" items.
+ * This uses one allocation for efficiency.
+ * The reference count is not set.
+ * Next tuple_set_item() must be called for each item.
+ */
+    tuple_T *
+tuple_alloc_with_items(int count)
+{
+    tuple_T	*tuple;
+
+    tuple = tuple_alloc();
+    if (tuple == NULL)
+	return NULL;
+
+    if (count <= 0)
+	return tuple;
+
+    if (ga_grow(&tuple->tv_items, count) == FAIL)
+    {
+	tuple_free(tuple);
+	return NULL;
+    }
+
+    return tuple;
+}
+
+/*
+ * Set item "idx" for a tuple previously allocated with
+ * tuple_alloc_with_items().
+ * The contents of "tv" is copied into the tuple item.
+ * Each item must be set exactly once.
+ */
+    void
+tuple_set_item(tuple_T *tuple, int idx, typval_T *tv)
+{
+    *TUPLE_ITEM(tuple, idx) = *tv;
+    tuple->tv_items.ga_len++;
+}
+
+/*
+ * Allocate an empty tuple for a return value, with reference count set.
+ * Returns OK or FAIL.
+ */
+    int
+rettv_tuple_alloc(typval_T *rettv)
+{
+    tuple_T	*tuple = tuple_alloc();
+
+    if (tuple == NULL)
+	return FAIL;
+
+    rettv->v_lock = 0;
+    rettv_tuple_set(rettv, tuple);
+    return OK;
+}
+
+/*
+ * Set a tuple as the return value.  Increments the reference count.
+ */
+    void
+rettv_tuple_set(typval_T *rettv, tuple_T *tuple)
+{
+    rettv->v_type = VAR_TUPLE;
+    rettv->vval.v_tuple = tuple;
+    if (tuple != NULL)
+	++tuple->tv_refcount;
+}
+
+/*
+ * Set a new tuple with "count" items as the return value.
+ * Returns OK on success and FAIL on allocation failure.
+ */
+    int
+rettv_tuple_set_with_items(typval_T *rettv, int count)
+{
+    tuple_T *new_tuple;
+
+    new_tuple = tuple_alloc_with_items(count);
+    if (new_tuple == NULL)
+	return FAIL;
+
+    rettv_tuple_set(rettv, new_tuple);
+
+    return OK;
+}
+
+/*
+ * Unreference a tuple: decrement the reference count and free it when it
+ * becomes zero.
+ */
+    void
+tuple_unref(tuple_T *tuple)
+{
+    if (tuple != NULL && --tuple->tv_refcount <= 0)
+	tuple_free(tuple);
+}
+
+/*
+ * Free a tuple, including all non-container items it points to.
+ * Ignores the reference count.
+ */
+    static void
+tuple_free_contents(tuple_T *tuple)
+{
+    for (int i = 0; i < TUPLE_LEN(tuple); i++)
+	clear_tv(TUPLE_ITEM(tuple, i));
+
+    ga_clear(&tuple->tv_items);
+}
+
+/*
+ * Go through the list of tuples and free items without the copyID.
+ * But don't free a tuple that has a watcher (used in a for loop), these
+ * are not referenced anywhere.
+ */
+    int
+tuple_free_nonref(int copyID)
+{
+    tuple_T	*tt;
+    int		did_free = FALSE;
+
+    for (tt = first_tuple; tt != NULL; tt = tt->tv_used_next)
+	if ((tt->tv_copyID & COPYID_MASK) != (copyID & COPYID_MASK))
+	{
+	    // Free the Tuple and ordinary items it contains, but don't recurse
+	    // into Lists and Dictionaries, they will be in the list of dicts
+	    // or list of lists.
+	    tuple_free_contents(tt);
+	    did_free = TRUE;
+	}
+    return did_free;
+}
+
+    static void
+tuple_free_list(tuple_T  *tuple)
+{
+    // Remove the tuple from the list of tuples for garbage collection.
+    if (tuple->tv_used_prev == NULL)
+	first_tuple = tuple->tv_used_next;
+    else
+	tuple->tv_used_prev->tv_used_next = tuple->tv_used_next;
+    if (tuple->tv_used_next != NULL)
+	tuple->tv_used_next->tv_used_prev = tuple->tv_used_prev;
+
+    free_type(tuple->tv_type);
+    vim_free(tuple);
+}
+
+    void
+tuple_free_items(int copyID)
+{
+    tuple_T	*tt, *tt_next;
+
+    for (tt = first_tuple; tt != NULL; tt = tt_next)
+    {
+	tt_next = tt->tv_used_next;
+	if ((tt->tv_copyID & COPYID_MASK) != (copyID & COPYID_MASK))
+	{
+	    // Free the tuple and ordinary items it contains, but don't recurse
+	    // into Lists and Dictionaries, they will be in the list of dicts
+	    // or list of lists.
+	    tuple_free_list(tt);
+	}
+    }
+}
+
+    void
+tuple_free(tuple_T *tuple)
+{
+    if (in_free_unref_items)
+	return;
+
+    tuple_free_contents(tuple);
+    tuple_free_list(tuple);
+}
+
+/*
+ * Get the number of items in a tuple.
+ */
+    long
+tuple_len(tuple_T *tuple)
+{
+    if (tuple == NULL)
+	return 0L;
+    return tuple->tv_items.ga_len;
+}
+
+/*
+ * Return TRUE when two tuples have exactly the same values.
+ */
+    int
+tuple_equal(
+    tuple_T	*t1,
+    tuple_T	*t2,
+    int		ic)	// ignore case for strings
+{
+    if (t1 == t2)
+	return TRUE;
+
+    int t1_len = tuple_len(t1);
+    int t2_len = tuple_len(t2);
+
+    if (t1_len != t2_len)
+	return FALSE;
+
+    if (t1_len == 0)
+	// empty and NULL tuples are considered equal
+	return TRUE;
+
+    // If the tuples "t1" or "t2" is NULL, then it is handled by the length
+    // checks above.
+
+    for (int i = 0, j = 0; i < t1_len && j < t2_len; i++, j++)
+	if (!tv_equal(TUPLE_ITEM(t1, i), TUPLE_ITEM(t2, j), ic))
+	    return FALSE;
+
+    return TRUE;
+}
+
+/*
+ * Locate item with index "n" in tuple "tuple" and return it.
+ * A negative index is counted from the end; -1 is the last item.
+ * Returns NULL when "n" is out of range.
+ */
+    typval_T *
+tuple_find(tuple_T *tuple, long n)
+{
+    if (tuple == NULL)
+	return NULL;
+
+    // Negative index is relative to the end.
+    if (n < 0)
+	n = TUPLE_LEN(tuple) + n;
+
+    // Check for index out of range.
+    if (n < 0 || n >= TUPLE_LEN(tuple))
+	return NULL;
+
+    return TUPLE_ITEM(tuple, n);
+}
+
+    int
+tuple_append_tv(tuple_T *tuple, typval_T *tv)
+{
+    if (ga_grow(&tuple->tv_items, 1) == FAIL)
+	return FAIL;
+
+    tuple_set_item(tuple, TUPLE_LEN(tuple), tv);
+
+    return OK;
+}
+
+/*
+ * Concatenate tuples "t1" and "t2" into a new tuple, stored in "tv".
+ * Return FAIL when out of memory.
+ */
+    int
+tuple_concat(tuple_T *t1, tuple_T *t2, typval_T *tv)
+{
+    tuple_T	*tuple;
+
+    // make a copy of the first tuple.
+    if (t1 == NULL)
+	tuple = tuple_alloc();
+    else
+	tuple = tuple_copy(t1, FALSE, TRUE, 0);
+    if (tuple == NULL)
+	return FAIL;
+
+    tv->v_type = VAR_TUPLE;
+    tv->v_lock = 0;
+    tv->vval.v_tuple = tuple;
+    if (t1 == NULL)
+	++tuple->tv_refcount;
+
+    // append all the items from the second tuple
+    for (int i = 0; i < tuple_len(t2); i++)
+    {
+	typval_T    new_tv;
+
+	copy_tv(TUPLE_ITEM(t2, i), &new_tv);
+
+	if (tuple_append_tv(tuple, &new_tv) == FAIL)
+	{
+	    tuple_free(tuple);
+	    return FAIL;
+	}
+    }
+
+    return OK;
+}
+
+/*
+ * Return a slice of tuple starting at index n1 and ending at index n2,
+ * inclusive (tuple[n1 : n2])
+ */
+    tuple_T *
+tuple_slice(tuple_T *tuple, long n1, long n2)
+{
+    tuple_T	*new_tuple;
+
+    new_tuple = tuple_alloc_with_items(n2 - n1 + 1);
+    if (new_tuple == NULL)
+	return NULL;
+
+    for (int i = n1; i <= n2; i++)
+    {
+	typval_T    new_tv;
+
+	copy_tv(TUPLE_ITEM(tuple, i), &new_tv);
+
+	if (tuple_append_tv(new_tuple, &new_tv) == FAIL)
+	{
+	    tuple_free(new_tuple);
+	    return NULL;
+	}
+    }
+
+    return new_tuple;
+}
+
+    int
+tuple_slice_or_index(
+    tuple_T	*tuple,
+    int		range,
+    varnumber_T	n1_arg,
+    varnumber_T	n2_arg,
+    int		exclusive,
+    typval_T	*rettv,
+    int		verbose)
+{
+    long	len = tuple_len(tuple);
+    varnumber_T	n1 = n1_arg;
+    varnumber_T	n2 = n2_arg;
+    typval_T	var1;
+
+    if (n1 < 0)
+	n1 = len + n1;
+    if (n1 < 0 || n1 >= len)
+    {
+	// For a range we allow invalid values and for legacy script return an
+	// empty tuple, for Vim9 script start at the first item.
+	// A tuple index out of range is an error.
+	if (!range)
+	{
+	    if (verbose)
+		semsg(_(e_tuple_index_out_of_range_nr), (long)n1_arg);
+	    return FAIL;
+	}
+	if (in_vim9script())
+	    n1 = n1 < 0 ? 0 : len;
+	else
+	    n1 = len;
+    }
+    if (range)
+    {
+	tuple_T	*new_tuple;
+
+	if (n2 < 0)
+	    n2 = len + n2;
+	else if (n2 >= len)
+	    n2 = len - (exclusive ? 0 : 1);
+	if (exclusive)
+	    --n2;
+	if (n2 < 0 || n2 + 1 < n1)
+	    n2 = -1;
+	new_tuple = tuple_slice(tuple, n1, n2);
+	if (new_tuple == NULL)
+	    return FAIL;
+	clear_tv(rettv);
+	rettv_tuple_set(rettv, new_tuple);
+    }
+    else
+    {
+	// copy the item to "var1" to avoid that freeing the tuple makes it
+	// invalid.
+	copy_tv(tuple_find(tuple, n1), &var1);
+	clear_tv(rettv);
+	*rettv = var1;
+    }
+    return OK;
+}
+
+/*
+ * Make a copy of tuple "orig".  Shallow if "deep" is FALSE.
+ * The refcount of the new tuple is set to 1.
+ * See item_copy() for "top" and "copyID".
+ * Returns NULL when out of memory.
+ */
+    tuple_T *
+tuple_copy(tuple_T *orig, int deep, int top, int copyID)
+{
+    tuple_T	*copy;
+    int		idx;
+
+    if (orig == NULL)
+	return NULL;
+
+    copy = tuple_alloc_with_items(TUPLE_LEN(orig));
+    if (copy == NULL)
+	return NULL;
+
+    if (orig->tv_type == NULL || top || deep)
+	copy->tv_type = NULL;
+    else
+	copy->tv_type = alloc_type(orig->tv_type);
+    if (copyID != 0)
+    {
+	// Do this before adding the items, because one of the items may
+	// refer back to this tuple.
+	orig->tv_copyID = copyID;
+	orig->tv_copytuple = copy;
+    }
+
+    for (idx = 0; idx < TUPLE_LEN(orig) && !got_int; idx++)
+    {
+	copy->tv_items.ga_len++;
+	if (deep)
+	{
+	    if (item_copy(TUPLE_ITEM(orig, idx), TUPLE_ITEM(copy, idx),
+						deep, FALSE, copyID) == FAIL)
+		break;
+	}
+	else
+	    copy_tv(TUPLE_ITEM(orig, idx), TUPLE_ITEM(copy, idx));
+    }
+
+    ++copy->tv_refcount;
+    if (idx != TUPLE_LEN(orig))
+    {
+	tuple_unref(copy);
+	copy = NULL;
+    }
+
+    return copy;
+}
+
+/*
+ * Allocate a variable for a tuple and fill it from "*arg".
+ * "*arg" points to the "," after the first element.
+ * "rettv" contains the first element.
+ * Returns OK or FAIL.
+ */
+    int
+eval_tuple(char_u **arg, typval_T *rettv, evalarg_T *evalarg, int do_error)
+{
+    int		evaluate = evalarg == NULL ? FALSE
+					 : evalarg->eval_flags & EVAL_EVALUATE;
+    tuple_T	*tuple = NULL;
+    typval_T	tv;
+    int		vim9script = in_vim9script();
+    int		had_comma;
+
+    if (check_typval_is_value(rettv) == FAIL)
+    {
+	// the first item is not a valid value type
+	clear_tv(rettv);
+	return FAIL;
+    }
+
+    if (evaluate)
+    {
+	tuple = tuple_alloc();
+	if (tuple == NULL)
+	    return FAIL;
+
+	if (rettv->v_type != VAR_UNKNOWN)
+	{
+	    // Add the first item to the tuple from "rettv"
+	    if (tuple_append_tv(tuple, rettv) == FAIL)
+		return FAIL;
+	}
+    }
+
+    if (**arg == ')')
+	// empty tuple
+	goto done;
+
+    if (vim9script && !IS_WHITE_NL_OR_NUL((*arg)[1]) && (*arg)[1] != ')')
+    {
+	semsg(_(e_white_space_required_after_str_str), ",", *arg);
+	goto failret;
+    }
+
+    *arg = skipwhite_and_linebreak(*arg + 1, evalarg);
+    while (**arg != ')' && **arg != NUL)
+    {
+	if (eval1(arg, &tv, evalarg) == FAIL)	// recursive!
+	    goto failret;
+	if (check_typval_is_value(&tv) == FAIL)
+	{
+	    if (evaluate)
+		clear_tv(&tv);
+	    goto failret;
+	}
+
+	if (evaluate)
+	{
+	    if (tuple_append_tv(tuple, &tv) == FAIL)
+	    {
+		clear_tv(&tv);
+		goto failret;
+	    }
+	}
+
+	if (!vim9script)
+	    *arg = skipwhite(*arg);
+
+	// the comma must come after the value
+	had_comma = **arg == ',';
+	if (had_comma)
+	{
+	    if (vim9script && !IS_WHITE_NL_OR_NUL((*arg)[1]) && (*arg)[1] != ')')
+	    {
+		semsg(_(e_white_space_required_after_str_str), ",", *arg);
+		goto failret;
+	    }
+	    *arg = skipwhite(*arg + 1);
+	}
+
+	// The ")" can be on the next line.  But a double quoted string may
+	// follow, not a comment.
+	*arg = skipwhite_and_linebreak(*arg, evalarg);
+	if (**arg == ')')
+	    break;
+
+	if (!had_comma)
+	{
+	    if (do_error)
+	    {
+		if (**arg == ',')
+		    semsg(_(e_no_white_space_allowed_before_str_str),
+								    ",", *arg);
+		else
+		    semsg(_(e_missing_comma_in_tuple_str), *arg);
+	    }
+	    goto failret;
+	}
+    }
+
+    if (**arg != ')')
+    {
+	if (do_error)
+	    semsg(_(e_missing_end_of_tuple_rsp_str), *arg);
+failret:
+	if (evaluate)
+	    tuple_free(tuple);
+	return FAIL;
+    }
+
+done:
+    *arg += 1;
+    if (evaluate)
+	rettv_tuple_set(rettv, tuple);
+
+    return OK;
+}
+
+/*
+ * Lock or unlock a tuple.  "deep" is number of levels to go.
+ * When "check_refcount" is TRUE do not lock a tuple with a reference
+ * count larger than 1.
+ */
+    void
+tuple_lock(tuple_T *tuple, int deep, int lock, int check_refcount)
+{
+    if (tuple == NULL || (check_refcount && tuple->tv_refcount > 1))
+	return;
+
+    if (lock)
+	tuple->tv_lock |= VAR_LOCKED;
+    else
+	tuple->tv_lock &= ~VAR_LOCKED;
+
+    if (deep < 0 || deep > 1)
+    {
+	// recursive: lock/unlock the items the Tuple contains
+	for (int i = 0; i < TUPLE_LEN(tuple); i++)
+	    item_lock(TUPLE_ITEM(tuple, i), deep - 1, lock, check_refcount);
+    }
+}
+
+typedef struct join_S {
+    char_u	*s;
+    char_u	*tofree;
+} join_T;
+
+    static int
+tuple_join_inner(
+    garray_T	*gap,		// to store the result in
+    tuple_T	*tuple,
+    char_u	*sep,
+    int		echo_style,
+    int		restore_copyID,
+    int		copyID,
+    garray_T	*join_gap)	// to keep each tuple item string
+{
+    int		i;
+    join_T	*p;
+    int		len;
+    int		sumlen = 0;
+    int		first = TRUE;
+    char_u	*tofree;
+    char_u	numbuf[NUMBUFLEN];
+    char_u	*s;
+    typval_T	*tv;
+
+    // Stringify each item in the tuple.
+    for (i = 0; i < TUPLE_LEN(tuple) && !got_int; i++)
+    {
+	tv = TUPLE_ITEM(tuple, i);
+	s = echo_string_core(tv, &tofree, numbuf, copyID,
+				      echo_style, restore_copyID, !echo_style);
+	if (s == NULL)
+	    return FAIL;
+
+	len = (int)STRLEN(s);
+	sumlen += len;
+
+	(void)ga_grow(join_gap, 1);
+	p = ((join_T *)join_gap->ga_data) + (join_gap->ga_len++);
+	if (tofree != NULL || s != numbuf)
+	{
+	    p->s = s;
+	    p->tofree = tofree;
+	}
+	else
+	{
+	    p->s = vim_strnsave(s, len);
+	    p->tofree = p->s;
+	}
+
+	line_breakcheck();
+	if (did_echo_string_emsg)  // recursion error, bail out
+	    break;
+    }
+
+    // Allocate result buffer with its total size, avoid re-allocation and
+    // multiple copy operations.  Add 2 for a tailing ')' and NUL.
+    if (join_gap->ga_len >= 2)
+	sumlen += (int)STRLEN(sep) * (join_gap->ga_len - 1);
+    if (ga_grow(gap, sumlen + 2) == FAIL)
+	return FAIL;
+
+    for (i = 0; i < join_gap->ga_len && !got_int; ++i)
+    {
+	if (first)
+	    first = FALSE;
+	else
+	    ga_concat(gap, sep);
+	p = ((join_T *)join_gap->ga_data) + i;
+
+	if (p->s != NULL)
+	    ga_concat(gap, p->s);
+	line_breakcheck();
+    }
+
+    // If there is only one item in the tuple, then add the separator after
+    // that.
+    if (join_gap->ga_len == 1)
+	ga_concat(gap, sep);
+
+    return OK;
+}
+
+/*
+ * Join tuple "tuple" into a string in "*gap", using separator "sep".
+ * When "echo_style" is TRUE use String as echoed, otherwise as inside a Tuple.
+ * Return FAIL or OK.
+ */
+    int
+tuple_join(
+    garray_T	*gap,
+    tuple_T	*tuple,
+    char_u	*sep,
+    int		echo_style,
+    int		restore_copyID,
+    int		copyID)
+{
+    garray_T	join_ga;
+    int		retval;
+    join_T	*p;
+    int		i;
+
+    if (TUPLE_LEN(tuple) < 1)
+	return OK; // nothing to do
+    ga_init2(&join_ga, sizeof(join_T), TUPLE_LEN(tuple));
+    retval = tuple_join_inner(gap, tuple, sep, echo_style, restore_copyID,
+							    copyID, &join_ga);
+
+    if (join_ga.ga_data == NULL)
+	return retval;
+
+    // Dispose each item in join_ga.
+    p = (join_T *)join_ga.ga_data;
+    for (i = 0; i < join_ga.ga_len; ++i)
+    {
+	vim_free(p->tofree);
+	++p;
+    }
+    ga_clear(&join_ga);
+
+    return retval;
+}
+
+/*
+ * Return an allocated string with the string representation of a tuple.
+ * May return NULL.
+ */
+    char_u *
+tuple2string(typval_T *tv, int copyID, int restore_copyID)
+{
+    garray_T	ga;
+
+    if (tv->vval.v_tuple == NULL)
+	return NULL;
+    ga_init2(&ga, sizeof(char), 80);
+    ga_append(&ga, '(');
+    if (tuple_join(&ga, tv->vval.v_tuple, (char_u *)", ",
+				       FALSE, restore_copyID, copyID) == FAIL)
+    {
+	vim_free(ga.ga_data);
+	return NULL;
+    }
+    ga_append(&ga, ')');
+    ga_append(&ga, NUL);
+    return (char_u *)ga.ga_data;
+}
+
+/*
+ * Implementation of foreach() for a Tuple.  Apply "expr" to
+ * every item in Tuple "tuple" and return the result in "rettv".
+ */
+    void
+tuple_foreach(
+    tuple_T	*tuple,
+    filtermap_T	filtermap,
+    typval_T	*expr)
+{
+    int		len = tuple_len(tuple);
+    int		rem;
+    typval_T	newtv;
+    funccall_T	*fc;
+
+    // set_vim_var_nr() doesn't set the type
+    set_vim_var_type(VV_KEY, VAR_NUMBER);
+
+    // Create one funccall_T for all eval_expr_typval() calls.
+    fc = eval_expr_get_funccal(expr, &newtv);
+
+    for (int idx = 0; idx < len; idx++)
+    {
+	set_vim_var_nr(VV_KEY, idx);
+	if (filter_map_one(TUPLE_ITEM(tuple, idx), expr, filtermap, fc,
+						     &newtv, &rem) == FAIL)
+	    break;
+    }
+
+    if (fc != NULL)
+	remove_funccal();
+}
+
+/*
+ * Count the number of times item "needle" occurs in Tuple "l" starting at index
+ * "idx". Case is ignored if "ic" is TRUE.
+ */
+    long
+tuple_count(tuple_T *tuple, typval_T *needle, long idx, int ic)
+{
+    long	n = 0;
+
+    if (tuple == NULL)
+	return 0;
+
+    int	len = TUPLE_LEN(tuple);
+    if (len == 0)
+	return 0;
+
+    if (idx < 0 || idx >= len)
+    {
+	semsg(_(e_tuple_index_out_of_range_nr), idx);
+	return 0;
+    }
+
+    for (int i = idx; i < len; i++)
+    {
+	if (tv_equal(TUPLE_ITEM(tuple, i), needle, ic))
+	    ++n;
+    }
+
+    return n;
+}
+
+/*
+ * "items(tuple)" function
+ * Caller must have already checked that argvars[0] is a tuple.
+ */
+    void
+tuple2items(typval_T *argvars, typval_T *rettv)
+{
+    tuple_T	*tuple = argvars[0].vval.v_tuple;
+    varnumber_T	idx;
+
+    if (rettv_list_alloc(rettv) == FAIL)
+	return;
+
+    if (tuple == NULL)
+	return;  // null tuple behaves like an empty list
+
+    for (idx = 0; idx < TUPLE_LEN(tuple); idx++)
+    {
+	list_T	*l = list_alloc();
+
+	if (l == NULL)
+	    break;
+
+	if (list_append_list(rettv->vval.v_list, l) == FAIL)
+	{
+	    vim_free(l);
+	    break;
+	}
+	if (list_append_number(l, idx) == FAIL
+		|| list_append_tv(l, TUPLE_ITEM(tuple, idx)) == FAIL)
+	    break;
+    }
+}
+
+/*
+ * Search for item "tv" in tuple "tuple" starting from index "start_idx".
+ * If "ic" is set to TRUE, then case is ignored.
+ *
+ * Returns the index where "tv" is present or -1 if it is not found.
+ */
+    int
+index_tuple(tuple_T *tuple, typval_T *tv, int start_idx, int ic)
+{
+    if (start_idx < 0)
+    {
+	start_idx = TUPLE_LEN(tuple) + start_idx;
+	if (start_idx < 0)
+	    start_idx = 0;
+    }
+
+    for (int idx = start_idx; idx < TUPLE_LEN(tuple); idx++)
+    {
+	if (tv_equal(TUPLE_ITEM(tuple, idx), tv, ic))
+	    return idx;
+    }
+
+    return -1;		// "tv" not found
+}
+
+/*
+ * Evaluate 'expr' for each item in the Tuple 'tuple' starting with the item at
+ * 'startidx' and return the index of the item where 'expr' is TRUE.  Returns
+ * -1 if 'expr' doesn't evaluate to TRUE for any of the items.
+ */
+    int
+indexof_tuple(tuple_T *tuple, long startidx, typval_T *expr)
+{
+    long	idx = 0;
+    int		len;
+    int		found;
+
+    if (tuple == NULL)
+	return -1;
+
+    len = TUPLE_LEN(tuple);
+
+    if (startidx < 0)
+    {
+	// negative index: index from the end
+	startidx = len + startidx;
+	if (startidx < 0)
+	    startidx = 0;
+    }
+
+    set_vim_var_type(VV_KEY, VAR_NUMBER);
+
+    int		called_emsg_start = called_emsg;
+
+    for (idx = startidx; idx < len; idx++)
+    {
+	set_vim_var_nr(VV_KEY, idx);
+	copy_tv(TUPLE_ITEM(tuple, idx), get_vim_var_tv(VV_VAL));
+
+	found = indexof_eval_expr(expr);
+	clear_tv(get_vim_var_tv(VV_VAL));
+
+	if (found)
+	    return idx;
+
+	if (called_emsg != called_emsg_start)
+	    return -1;
+    }
+
+    return -1;
+}
+
+/*
+ * Return the max or min of the items in tuple "tuple".
+ * If a tuple item is not a number, then "error" is set to TRUE.
+ */
+    varnumber_T
+tuple_max_min(tuple_T *tuple, int domax, int *error)
+{
+    varnumber_T	n = 0;
+    varnumber_T	v;
+
+    if (tuple == NULL || TUPLE_LEN(tuple) == 0)
+	return 0;
+
+    n = tv_get_number_chk(TUPLE_ITEM(tuple, 0), error);
+    if (*error)
+	return n; // type error; errmsg already given
+
+    for (int idx = 1; idx < TUPLE_LEN(tuple); idx++)
+    {
+	v = tv_get_number_chk(TUPLE_ITEM(tuple, idx), error);
+	if (*error)
+	    return n; // type error; errmsg already given
+	if (domax ? v > n : v < n)
+	    n = v;
+    }
+
+    return n;
+}
+
+/*
+ * Repeat the tuple "tuple" "n" times and set "rettv" to the new tuple.
+ */
+    void
+tuple_repeat(tuple_T *tuple, int n, typval_T *rettv)
+{
+    rettv->v_type = VAR_TUPLE;
+    rettv->vval.v_tuple = NULL;
+
+    if (tuple == NULL || TUPLE_LEN(tuple) == 0 || n <= 0)
+	return;
+
+    if (rettv_tuple_set_with_items(rettv, TUPLE_LEN(tuple) * n) == FAIL)
+	return;
+
+    tuple_T	*new_tuple = rettv->vval.v_tuple;
+    for (int count = 0; count < n; count++)
+    {
+	for (int idx = 0; idx < TUPLE_LEN(tuple); idx++)
+	{
+	    copy_tv(TUPLE_ITEM(tuple, idx),
+		    TUPLE_ITEM(new_tuple, TUPLE_LEN(new_tuple)));
+	    new_tuple->tv_items.ga_len++;
+	}
+    }
+}
+
+/*
+ * Reverse "tuple" and return the new tuple in "rettv"
+ */
+    void
+tuple_reverse(tuple_T *tuple, typval_T *rettv)
+{
+    rettv->v_type = VAR_TUPLE;
+    rettv->vval.v_tuple = NULL;
+
+    int	len = tuple_len(tuple);
+
+    if (len == 0)
+	return;
+
+    if (rettv_tuple_set_with_items(rettv, len) == FAIL)
+	return;
+
+    tuple_T	*new_tuple = rettv->vval.v_tuple;
+    for (int i = 0; i < len; i++)
+	copy_tv(TUPLE_ITEM(tuple, i), TUPLE_ITEM(new_tuple, len - i - 1));
+    new_tuple->tv_items.ga_len = tuple->tv_items.ga_len;
+}
+
+/*
+ * Tuple reduce() function
+ */
+    void
+tuple_reduce(typval_T *argvars, typval_T *expr, typval_T *rettv)
+{
+    tuple_T	*tuple = argvars[0].vval.v_tuple;
+    int		called_emsg_start = called_emsg;
+    typval_T	initial;
+    int		idx = 0;
+    funccall_T	*fc;
+    typval_T	argv[3];
+    int		r;
+
+    if (argvars[2].v_type == VAR_UNKNOWN)
+    {
+	if (tuple == NULL || TUPLE_LEN(tuple) == 0)
+	{
+	    semsg(_(e_reduce_of_an_empty_str_with_no_initial_value), "Tuple");
+	    return;
+	}
+	initial = *TUPLE_ITEM(tuple, 0);
+	idx = 1;
+    }
+    else
+    {
+	initial = argvars[2];
+	idx = 0;
+    }
+
+    copy_tv(&initial, rettv);
+
+    if (tuple == NULL)
+	return;
+
+    // Create one funccall_T for all eval_expr_typval() calls.
+    fc = eval_expr_get_funccal(expr, rettv);
+
+    for ( ; idx < TUPLE_LEN(tuple); idx++)
+    {
+	argv[0] = *rettv;
+	rettv->v_type = VAR_UNKNOWN;
+	argv[1] = *TUPLE_ITEM(tuple, idx);
+
+	r = eval_expr_typval(expr, TRUE, argv, 2, fc, rettv);
+
+	clear_tv(&argv[0]);
+
+	if (r == FAIL || called_emsg != called_emsg_start)
+	    break;
+    }
+
+    if (fc != NULL)
+	remove_funccal();
+}
+
+/*
+ * Returns TRUE if two tuples with types "type1" and "type2" are addable.
+ * Otherwise returns FALSE.
+ */
+    int
+check_tuples_addable(type_T *type1, type_T *type2)
+{
+    int	addable = TRUE;
+
+    // If the first operand is a variadic tuple and the second argument is
+    // non-variadic, then concatenation is not possible.
+    if ((type1->tt_flags & TTFLAG_VARARGS)
+	    && !(type2->tt_flags & TTFLAG_VARARGS)
+	    && (type2->tt_argcount > 0))
+	addable = FALSE;
+
+    if ((type1->tt_flags & TTFLAG_VARARGS)
+	    && (type2->tt_flags & TTFLAG_VARARGS))
+    {
+	// two variadic tuples
+	if (type1->tt_argcount > 1 || type2->tt_argcount > 1)
+	    // one of the variadic tuple has fixed number of items
+	    addable = FALSE;
+	else if ((type1->tt_argcount == 1 && type2->tt_argcount == 1)
+		&& !equal_type(type1->tt_args[0], type2->tt_args[0], 0))
+	    // the tuples have different item types
+	    addable = FALSE;
+    }
+
+    if (!addable)
+    {
+	emsg(_(e_cannot_use_variadic_tuple_in_concatenation));
+	return FAIL;
+    }
+
+    return OK;
+}
+
+#endif // defined(FEAT_EVAL)
diff --git a/src/typval.c b/src/typval.c
index cd39a0d..59ac611 100644
--- a/src/typval.c
+++ b/src/typval.c
@@ -72,6 +72,9 @@
 	case VAR_LIST:
 	    list_unref(varp->vval.v_list);
 	    break;
+	case VAR_TUPLE:
+	    tuple_unref(varp->vval.v_tuple);
+	    break;
 	case VAR_DICT:
 	    dict_unref(varp->vval.v_dict);
 	    break;
@@ -138,6 +141,10 @@
 	    list_unref(varp->vval.v_list);
 	    varp->vval.v_list = NULL;
 	    break;
+	case VAR_TUPLE:
+	    tuple_unref(varp->vval.v_tuple);
+	    varp->vval.v_tuple = NULL;
+	    break;
 	case VAR_DICT:
 	    dict_unref(varp->vval.v_dict);
 	    varp->vval.v_dict = NULL;
@@ -234,6 +241,9 @@
 	case VAR_LIST:
 	    emsg(_(e_using_list_as_number));
 	    break;
+	case VAR_TUPLE:
+	    emsg(_(e_using_tuple_as_number));
+	    break;
 	case VAR_DICT:
 	    emsg(_(e_using_dictionary_as_number));
 	    break;
@@ -368,6 +378,9 @@
 	case VAR_LIST:
 	    emsg(_(e_using_list_as_float));
 	    break;
+	case VAR_TUPLE:
+	    emsg(_(e_using_tuple_as_float));
+	    break;
 	case VAR_DICT:
 	    emsg(_(e_using_dictionary_as_float));
 	    break;
@@ -529,7 +542,7 @@
 /*
  * Give an error and return FAIL unless "args[idx]" is a bool or a number.
  */
-    int
+    static int
 check_for_bool_or_number_arg(typval_T *args, int idx)
 {
     if (args[idx].v_type != VAR_BOOL && args[idx].v_type != VAR_NUMBER)
@@ -620,6 +633,20 @@
 }
 
 /*
+ * Give an error and return FAIL unless "args[idx]" is a tuple.
+ */
+    int
+check_for_tuple_arg(typval_T *args, int idx)
+{
+    if (args[idx].v_type != VAR_TUPLE)
+    {
+	semsg(_(e_tuple_required_for_argument_nr), idx + 1);
+	return FAIL;
+    }
+    return OK;
+}
+
+/*
  * Give an error and return FAIL unless "args[idx]" is a dict.
  */
     int
@@ -827,17 +854,18 @@
 }
 
 /*
- * Give an error and return FAIL unless "args[idx]" is a string, a list or a
- * blob.
+ * Give an error and return FAIL unless "args[idx]" is a string, a list, a
+ * tuple or a blob.
  */
     int
-check_for_string_or_list_or_blob_arg(typval_T *args, int idx)
+check_for_string_or_list_or_tuple_or_blob_arg(typval_T *args, int idx)
 {
     if (args[idx].v_type != VAR_STRING
 	    && args[idx].v_type != VAR_LIST
+	    && args[idx].v_type != VAR_TUPLE
 	    && args[idx].v_type != VAR_BLOB)
     {
-	semsg(_(e_string_list_or_blob_required_for_argument_nr), idx + 1);
+	semsg(_(e_string_list_tuple_or_blob_required_for_argument_nr), idx + 1);
 	return FAIL;
     }
     return OK;
@@ -897,35 +925,37 @@
 }
 
 /*
- * Give an error and return FAIL unless "args[idx]" is a string or a number
- * or a list or a blob.
+ * Give an error and return FAIL unless "args[idx]" is a string, a number, a
+ * list, a tuple or a blob.
  */
     int
-check_for_string_or_number_or_list_or_blob_arg(typval_T *args, int idx)
+check_for_repeat_func_arg(typval_T *args, int idx)
 {
     if (args[idx].v_type != VAR_STRING
 	    && args[idx].v_type != VAR_NUMBER
 	    && args[idx].v_type != VAR_LIST
+	    && args[idx].v_type != VAR_TUPLE
 	    && args[idx].v_type != VAR_BLOB)
     {
-	semsg(_(e_string_number_list_or_blob_required_for_argument_nr), idx + 1);
+	semsg(_(e_repeatable_type_required_for_argument_nr), idx + 1);
 	return FAIL;
     }
     return OK;
 }
 
 /*
- * Give an error and return FAIL unless "args[idx]" is a string or a list
- * or a dict.
+ * Give an error and return FAIL unless "args[idx]" is a string, a list, a
+ * tuple or a dict.
  */
     int
-check_for_string_or_list_or_dict_arg(typval_T *args, int idx)
+check_for_string_list_tuple_or_dict_arg(typval_T *args, int idx)
 {
     if (args[idx].v_type != VAR_STRING
 	    && args[idx].v_type != VAR_LIST
+	    && args[idx].v_type != VAR_TUPLE
 	    && args[idx].v_type != VAR_DICT)
     {
-	semsg(_(e_string_list_or_dict_required_for_argument_nr), idx + 1);
+	semsg(_(e_string_list_tuple_or_dict_required_for_argument_nr), idx + 1);
 	return FAIL;
     }
     return OK;
@@ -963,15 +993,48 @@
 }
 
 /*
- * Give an error and return FAIL unless "args[idx]" is a list or dict
+ * Give an error and return FAIL unless "args[idx]" is a list or a tuple.
  */
     int
-check_for_list_or_dict_arg(typval_T *args, int idx)
+check_for_list_or_tuple_arg(typval_T *args, int idx)
+{
+    if (args[idx].v_type != VAR_LIST && args[idx].v_type != VAR_TUPLE)
+    {
+	semsg(_(e_list_or_tuple_required_for_argument_nr), idx + 1);
+	return FAIL;
+    }
+    return OK;
+}
+
+/*
+ * Give an error and return FAIL unless "args[idx]" is a list, a tuple or a
+ * blob.
+ */
+    int
+check_for_list_or_tuple_or_blob_arg(typval_T *args, int idx)
 {
     if (args[idx].v_type != VAR_LIST
+	    && args[idx].v_type != VAR_TUPLE
+	    && args[idx].v_type != VAR_BLOB)
+    {
+	semsg(_(e_list_or_tuple_or_blob_required_for_argument_nr), idx + 1);
+	return FAIL;
+    }
+    return OK;
+}
+
+/*
+ * Give an error and return FAIL unless "args[idx]" is a list, a tuple or a
+ * dict
+ */
+    int
+check_for_list_or_tuple_or_dict_arg(typval_T *args, int idx)
+{
+    if (args[idx].v_type != VAR_LIST
+	    && args[idx].v_type != VAR_TUPLE
 	    && args[idx].v_type != VAR_DICT)
     {
-	semsg(_(e_list_or_dict_required_for_argument_nr), idx + 1);
+	semsg(_(e_list_or_tuple_or_dict_required_for_argument_nr), idx + 1);
 	return FAIL;
     }
     return OK;
@@ -995,18 +1058,19 @@
 }
 
 /*
- * Give an error and return FAIL unless "args[idx]" is a list or dict or a
- * blob or a string.
+ * Give an error and return FAIL unless "args[idx]" is a list, a tuple, a dict,
+ * a blob or a string.
  */
     int
-check_for_list_or_dict_or_blob_or_string_arg(typval_T *args, int idx)
+check_for_list_tuple_dict_blob_or_string_arg(typval_T *args, int idx)
 {
     if (args[idx].v_type != VAR_LIST
+	    && args[idx].v_type != VAR_TUPLE
 	    && args[idx].v_type != VAR_DICT
 	    && args[idx].v_type != VAR_BLOB
 	    && args[idx].v_type != VAR_STRING)
     {
-	semsg(_(e_list_dict_blob_or_string_required_for_argument_nr), idx + 1);
+	semsg(_(e_list_tuple_dict_blob_or_string_required_for_argument_nr), idx + 1);
 	return FAIL;
     }
     return OK;
@@ -1149,6 +1213,9 @@
 	case VAR_LIST:
 	    emsg(_(e_using_list_as_string));
 	    break;
+	case VAR_TUPLE:
+	    emsg(_(e_using_tuple_as_string));
+	    break;
 	case VAR_DICT:
 	    emsg(_(e_using_dictionary_as_string));
 	    break;
@@ -1267,6 +1334,10 @@
 	    if (tv->vval.v_list != NULL)
 		lock = tv->vval.v_list->lv_lock;
 	    break;
+	case VAR_TUPLE:
+	    if (tv->vval.v_tuple != NULL)
+		lock = tv->vval.v_tuple->tv_lock;
+	    break;
 	case VAR_DICT:
 	    if (tv->vval.v_dict != NULL)
 		lock = tv->vval.v_dict->dv_lock;
@@ -1364,6 +1435,15 @@
 		++to->vval.v_list->lv_refcount;
 	    }
 	    break;
+	case VAR_TUPLE:
+	    if (from->vval.v_tuple == NULL)
+		to->vval.v_tuple = NULL;
+	    else
+	    {
+		to->vval.v_tuple = from->vval.v_tuple;
+		++to->vval.v_tuple->tv_refcount;
+	    }
+	    break;
 	case VAR_DICT:
 	    if (from->vval.v_dict == NULL)
 		to->vval.v_dict = NULL;
@@ -1452,6 +1532,15 @@
 	}
 	n1 = res;
     }
+    else if (tv1->v_type == VAR_TUPLE || tv2->v_type == VAR_TUPLE)
+    {
+	if (typval_compare_tuple(tv1, tv2, type, ic, &res) == FAIL)
+	{
+	    clear_tv(tv1);
+	    return FAIL;
+	}
+	n1 = res;
+    }
     else if (tv1->v_type == VAR_OBJECT || tv2->v_type == VAR_OBJECT)
     {
 	if (typval_compare_object(tv1, tv2, type, ic, &res) == FAIL)
@@ -1650,6 +1739,47 @@
 }
 
 /*
+ * Compare "tv1" to "tv2" as tuples according to "type" and "ic".
+ * Put the result, false or true, in "res".
+ * Return FAIL and give an error message when the comparison can't be done.
+ */
+    int
+typval_compare_tuple(
+	typval_T    *tv1,
+	typval_T    *tv2,
+	exprtype_T  type,
+	int	    ic,
+	int	    *res)
+{
+    int	    val = 0;
+
+    if (type == EXPR_IS || type == EXPR_ISNOT)
+    {
+	val = (tv1->v_type == tv2->v_type
+			      && tv1->vval.v_tuple == tv2->vval.v_tuple);
+	if (type == EXPR_ISNOT)
+	    val = !val;
+    }
+    else if (tv1->v_type != tv2->v_type
+	    || (type != EXPR_EQUAL && type != EXPR_NEQUAL))
+    {
+	if (tv1->v_type != tv2->v_type)
+	    emsg(_(e_can_only_compare_tuple_with_tuple));
+	else
+	    emsg(_(e_invalid_operation_for_tuple));
+	return FAIL;
+    }
+    else
+    {
+	val = tuple_equal(tv1->vval.v_tuple, tv2->vval.v_tuple, ic);
+	if (type == EXPR_NEQUAL)
+	    val = !val;
+    }
+    *res = val;
+    return OK;
+}
+
+/*
  * Compare v:null with another type.  Return TRUE if the value is NULL.
  */
     int
@@ -1674,6 +1804,7 @@
 	    case VAR_JOB: return tv->vval.v_job == NULL;
 #endif
 	    case VAR_LIST: return tv->vval.v_list == NULL;
+	    case VAR_TUPLE: return tv->vval.v_tuple == NULL;
 	    case VAR_OBJECT: return tv->vval.v_object == NULL;
 	    case VAR_PARTIAL: return tv->vval.v_partial == NULL;
 	    case VAR_STRING: return tv->vval.v_string == NULL;
@@ -2078,6 +2209,12 @@
 	    --recursive_cnt;
 	    return r;
 
+	case VAR_TUPLE:
+	    ++recursive_cnt;
+	    r = tuple_equal(tv1->vval.v_tuple, tv2->vval.v_tuple, ic);
+	    --recursive_cnt;
+	    return r;
+
 	case VAR_DICT:
 	    ++recursive_cnt;
 	    r = dict_equal(tv1->vval.v_dict, tv2->vval.v_dict, ic);
diff --git a/src/userfunc.c b/src/userfunc.c
index d8d7014..b328cf5 100644
--- a/src/userfunc.c
+++ b/src/userfunc.c
@@ -7286,9 +7286,9 @@
     for (fc = previous_funccal; fc != NULL; fc = fc->fc_caller)
     {
 	fc->fc_copyID = copyID + 1;
-	if (set_ref_in_ht(&fc->fc_l_vars.dv_hashtab, copyID + 1, NULL)
-		|| set_ref_in_ht(&fc->fc_l_avars.dv_hashtab, copyID + 1, NULL)
-		|| set_ref_in_list_items(&fc->fc_l_varlist, copyID + 1, NULL))
+	if (set_ref_in_ht(&fc->fc_l_vars.dv_hashtab, copyID + 1, NULL, NULL)
+		|| set_ref_in_ht(&fc->fc_l_avars.dv_hashtab, copyID + 1, NULL, NULL)
+		|| set_ref_in_list_items(&fc->fc_l_varlist, copyID + 1, NULL, NULL))
 	    return TRUE;
     }
     return FALSE;
@@ -7300,9 +7300,9 @@
     if (fc->fc_copyID != copyID)
     {
 	fc->fc_copyID = copyID;
-	if (set_ref_in_ht(&fc->fc_l_vars.dv_hashtab, copyID, NULL)
-		|| set_ref_in_ht(&fc->fc_l_avars.dv_hashtab, copyID, NULL)
-		|| set_ref_in_list_items(&fc->fc_l_varlist, copyID, NULL)
+	if (set_ref_in_ht(&fc->fc_l_vars.dv_hashtab, copyID, NULL, NULL)
+		|| set_ref_in_ht(&fc->fc_l_avars.dv_hashtab, copyID, NULL, NULL)
+		|| set_ref_in_list_items(&fc->fc_l_varlist, copyID, NULL, NULL)
 		|| set_ref_in_func(NULL, fc->fc_func, copyID))
 	    return TRUE;
     }
@@ -7365,7 +7365,7 @@
 
     for (i = 0; i < funcargs.ga_len; ++i)
 	if (set_ref_in_item(((typval_T **)funcargs.ga_data)[i],
-							  copyID, NULL, NULL))
+						copyID, NULL, NULL, NULL))
 	    return TRUE;
     return FALSE;
 }
diff --git a/src/version.c b/src/version.c
index dedfd0a..7ee6d0a 100644
--- a/src/version.c
+++ b/src/version.c
@@ -705,6 +705,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    1232,
+/**/
     1231,
 /**/
     1230,
diff --git a/src/vim.h b/src/vim.h
index 85fad6c..dd04f65 100644
--- a/src/vim.h
+++ b/src/vim.h
@@ -2201,7 +2201,8 @@
 #define VV_TYPE_ENUM	  108
 #define VV_TYPE_ENUMVALUE 109
 #define VV_STACKTRACE	110
-#define VV_LEN		111	// number of v: vars
+#define VV_TYPE_TUPLE	111
+#define VV_LEN		112	// number of v: vars
 
 // used for v_number in VAR_BOOL and VAR_SPECIAL
 #define VVAL_FALSE	0L	// VAR_BOOL
@@ -2227,6 +2228,7 @@
 #define VAR_TYPE_TYPEALIAS  14
 #define VAR_TYPE_ENUM	    15
 #define VAR_TYPE_ENUMVALUE  16
+#define VAR_TYPE_TUPLE	    17
 
 #define DICT_MAXNEST 100	// maximum nesting of lists and dicts
 
diff --git a/src/vim9.h b/src/vim9.h
index 7c731fa..c10d435 100644
--- a/src/vim9.h
+++ b/src/vim9.h
@@ -105,6 +105,8 @@
     ISN_PUSHCLASS,	// push class, uses isn_arg.classarg
     ISN_NEWLIST,	// push list from stack items, size is isn_arg.number
 			// -1 for null_list
+    ISN_NEWTUPLE,	// push tuple from stack items, size is isn_arg.number
+			// -1 for null_list
     ISN_NEWDICT,	// push dict from stack items, size is isn_arg.number
 			// -1 for null_dict
     ISN_NEWPARTIAL,	// push NULL partial
@@ -149,6 +151,7 @@
 
     // more expression operations
     ISN_ADDLIST,    // add two lists
+    ISN_ADDTUPLE,   // add two tuples
     ISN_ADDBLOB,    // add two blobs
 
     // operation with two arguments; isn_arg.op.op_type is exprtype_T
@@ -165,6 +168,7 @@
     ISN_COMPARESTRING,
     ISN_COMPAREBLOB,
     ISN_COMPARELIST,
+    ISN_COMPARETUPLE,
     ISN_COMPAREDICT,
     ISN_COMPAREFUNC,
     ISN_COMPAREANY,
@@ -177,6 +181,8 @@
     ISN_LISTAPPEND, // append to a list, like add()
     ISN_LISTINDEX,  // [expr] list index
     ISN_LISTSLICE,  // [expr:expr] list slice
+    ISN_TUPLEINDEX,  // [expr] tuple index
+    ISN_TUPLESLICE,  // [expr:expr] tuple slice
     ISN_BLOBINDEX,  // [expr] blob index
     ISN_BLOBSLICE,  // [expr:expr] blob slice
     ISN_ANYINDEX,   // [expr] runtime index
diff --git a/src/vim9class.c b/src/vim9class.c
index 1c54474..5249f40 100644
--- a/src/vim9class.c
+++ b/src/vim9class.c
@@ -3650,7 +3650,7 @@
 set_ref_in_classes(int copyID)
 {
     for (class_T *cl = first_class; cl != NULL; cl = cl->class_next_used)
-	set_ref_in_item_class(cl, copyID, NULL, NULL);
+	set_ref_in_item_class(cl, copyID, NULL, NULL, NULL);
 
     return FALSE;
 }
diff --git a/src/vim9cmds.c b/src/vim9cmds.c
index aeb742e..6295090 100644
--- a/src/vim9cmds.c
+++ b/src/vim9cmds.c
@@ -871,6 +871,40 @@
 }
 
 /*
+ * When compiling a for loop to iterate over a tuple, get the type of the loop
+ * variable to use.
+ */
+    static type_T *
+compile_for_tuple_get_vartype(type_T *vartype, int var_list)
+{
+    // If this is not a variadic tuple, or all the tuple items don't have
+    // the same type, then use t_any
+    if (!(vartype->tt_flags & TTFLAG_VARARGS) || vartype->tt_argcount != 1)
+	return &t_any;
+
+    // variadic tuple
+    type_T *member_type = vartype->tt_args[0]->tt_member;
+    if (member_type->tt_type == VAR_ANY)
+	return &t_any;
+
+    if (!var_list)
+	// for x in tuple<...list<xxx>>
+	return member_type;
+
+    if (member_type->tt_type == VAR_LIST
+	    && member_type->tt_member->tt_type != VAR_ANY)
+	// for [x, y] in tuple<...list<list<xxx>>>
+	return member_type->tt_member;
+    else if (member_type->tt_type == VAR_TUPLE
+				&& member_type->tt_flags & TTFLAG_VARARGS
+				&& member_type->tt_argcount == 1)
+	// for [x, y] in tuple<...list<tuple<...list<xxx>>>>
+	return member_type->tt_args[0]->tt_member;
+
+    return &t_any;
+}
+
+/*
  * Compile "for var in expr":
  *
  * Produces instructions:
@@ -1000,6 +1034,7 @@
 	// give an error now.
 	vartype = get_type_on_stack(cctx, 0);
 	if (vartype->tt_type != VAR_LIST
+		&& vartype->tt_type != VAR_TUPLE
 		&& vartype->tt_type != VAR_STRING
 		&& vartype->tt_type != VAR_BLOB
 		&& vartype->tt_type != VAR_ANY
@@ -1024,6 +1059,8 @@
 			  && vartype->tt_member->tt_member->tt_type != VAR_ANY)
 		item_type = vartype->tt_member->tt_member;
 	}
+	else if (vartype->tt_type == VAR_TUPLE)
+	    item_type = compile_for_tuple_get_vartype(vartype, var_list);
 
 	// CMDMOD_REV must come before the FOR instruction.
 	generate_undo_cmdmods(cctx);
diff --git a/src/vim9compile.c b/src/vim9compile.c
index cb7b948..34a78da 100644
--- a/src/vim9compile.c
+++ b/src/vim9compile.c
@@ -524,8 +524,10 @@
 	return TRUE;
     if (actual->tt_type == VAR_OBJECT && expected->tt_type == VAR_OBJECT)
 	return TRUE;
-    if ((actual->tt_type == VAR_LIST || actual->tt_type == VAR_DICT)
-				       && actual->tt_type == expected->tt_type)
+    if ((actual->tt_type == VAR_LIST
+		|| actual->tt_type == VAR_TUPLE
+		|| actual->tt_type == VAR_DICT)
+	    && actual->tt_type == expected->tt_type)
 	// This takes care of a nested list or dict.
 	return use_typecheck(actual->tt_member, expected->tt_member);
     return FALSE;
@@ -2642,7 +2644,10 @@
 	    && lhs->lhs_type != &t_blob
 	    && lhs->lhs_type != &t_any)
     {
-	semsg(_(e_cannot_use_range_with_assignment_str), var_start);
+	if (lhs->lhs_type->tt_type == VAR_TUPLE)
+	    emsg(_(e_cannot_slice_tuple));
+	else
+	    semsg(_(e_cannot_use_range_with_assignment_str), var_start);
 	return FAIL;
     }
 
@@ -2743,7 +2748,10 @@
     }
     else
     {
-	emsg(_(e_indexable_type_required));
+	if (dest_type == VAR_TUPLE)
+	    emsg(_(e_tuple_is_immutable));
+	else
+	    emsg(_(e_indexable_type_required));
 	return FAIL;
     }
 
@@ -2785,6 +2793,9 @@
 	case VAR_LIST:
 	    r = generate_NEWLIST(cctx, 0, FALSE);
 	    break;
+	case VAR_TUPLE:
+	    r = generate_NEWTUPLE(cctx, 0, FALSE);
+	    break;
 	case VAR_DICT:
 	    r = generate_NEWDICT(cctx, 0, FALSE);
 	    break;
@@ -3015,11 +3026,31 @@
 	return FAIL;
     }
 
-    if (need_type(stacktype, &t_list_any, FALSE, -1, 0, cctx,
-						FALSE, FALSE) == FAIL)
+    if (stacktype->tt_type != VAR_LIST && stacktype->tt_type != VAR_TUPLE
+					&& stacktype->tt_type != VAR_ANY)
+    {
+	emsg(_(e_list_or_tuple_required));
+	return FAIL;
+    }
+
+    if (need_type(stacktype,
+		  stacktype->tt_type == VAR_TUPLE ? &t_tuple_any : &t_list_any,
+		  FALSE, -1, 0, cctx, FALSE, FALSE) == FAIL)
 	return FAIL;
 
-    if (stacktype->tt_member != NULL)
+    if (stacktype->tt_type == VAR_TUPLE)
+    {
+	if (stacktype->tt_argcount != 1)
+	    cac->cac_rhs_type = &t_any;
+	else
+	{
+	    if (stacktype->tt_flags & TTFLAG_VARARGS)
+		cac->cac_rhs_type = stacktype->tt_args[0]->tt_member;
+	    else
+		cac->cac_rhs_type = stacktype->tt_args[0];
+	}
+    }
+    else if (stacktype->tt_member != NULL)
 	cac->cac_rhs_type = stacktype->tt_member;
 
     return OK;
@@ -3043,7 +3074,7 @@
 	isn_T	*isn = ((isn_T *)cac->cac_instr->ga_data) +
 						cac->cac_instr->ga_len - 1;
 
-	if (isn->isn_type == ISN_NEWLIST)
+	if (isn->isn_type == ISN_NEWLIST || isn->isn_type == ISN_NEWTUPLE)
 	{
 	    did_check = TRUE;
 	    if (cac->cac_semicolon ?
@@ -3408,6 +3439,17 @@
     type_T	    *expected;
     type_T	    *stacktype = NULL;
 
+    if (cac->cac_lhs.lhs_type->tt_type == VAR_TUPLE)
+    {
+	// compound operators are not supported with a tuple
+	char_u	op[2];
+
+	op[0] = *cac->cac_op;
+	op[1] = NUL;
+	semsg(_(e_wrong_variable_type_for_str_equal), op);
+	return FAIL;
+    }
+
     if (*cac->cac_op == '.')
     {
 	if (may_generate_2STRING(-1, TOSTRING_NONE, cctx) == FAIL)
@@ -3486,8 +3528,11 @@
 		&& lhs->lhs_type->tt_member != NULL
 		&& lhs->lhs_type->tt_member != &t_any
 		&& lhs->lhs_type->tt_member != &t_unknown)
-	    // Set the type in the list or dict, so that it can be checked,
-	    // also in legacy script.
+	    // Set the type in the list or dict, so that it can be
+	    // checked, also in legacy script.
+	    generate_SETTYPE(cctx, lhs->lhs_type);
+	else if (lhs->lhs_type->tt_type == VAR_TUPLE
+					&& lhs->lhs_type->tt_argcount != 0)
 	    generate_SETTYPE(cctx, lhs->lhs_type);
 	else if (inferred_type != NULL
 		&& (inferred_type->tt_type == VAR_DICT
@@ -3495,8 +3540,12 @@
 		&& inferred_type->tt_member != NULL
 		&& inferred_type->tt_member != &t_unknown
 		&& inferred_type->tt_member != &t_any)
-	    // Set the type in the list or dict, so that it can be checked,
-	    // also in legacy script.
+	    // Set the type in the list or dict, so that it can be
+	    // checked, also in legacy script.
+	    generate_SETTYPE(cctx, inferred_type);
+	else if (inferred_type != NULL
+				&& inferred_type->tt_type == VAR_TUPLE
+				&& inferred_type->tt_argcount > 0)
 	    generate_SETTYPE(cctx, inferred_type);
 
 	if (!cac->cac_skip_store &&
@@ -4030,13 +4079,15 @@
 	else
 	    push_default_value(cctx, m->ocm_type->tt_type, FALSE, NULL);
 
-	if ((m->ocm_type->tt_type == VAR_DICT
-		    || m->ocm_type->tt_type == VAR_LIST)
-		&& m->ocm_type->tt_member != NULL
-		&& m->ocm_type->tt_member != &t_any
-		&& m->ocm_type->tt_member != &t_unknown)
-	    // Set the type in the list or dict, so that it can be checked,
-	    // also in legacy script.
+	if (((m->ocm_type->tt_type == VAR_DICT
+			|| m->ocm_type->tt_type == VAR_LIST)
+		    && m->ocm_type->tt_member != NULL
+		    && m->ocm_type->tt_member != &t_any
+		    && m->ocm_type->tt_member != &t_unknown)
+		|| (m->ocm_type->tt_type == VAR_TUPLE
+		    && m->ocm_type->tt_argcount > 0))
+	    // Set the type in the list, tuple or dict, so that it can be
+	    // checked, also in legacy script.
 	    generate_SETTYPE(cctx, m->ocm_type);
 
 	generate_STORE_THIS(cctx, i);
diff --git a/src/vim9execute.c b/src/vim9execute.c
index c0c3103..55f9d43 100644
--- a/src/vim9execute.c
+++ b/src/vim9execute.c
@@ -110,6 +110,11 @@
 // Get pointer to a local variable on the stack.  Negative for arguments.
 #define STACK_TV_VAR(idx) (((typval_T *)ectx->ec_stack.ga_data) + ectx->ec_frame_idx + STACK_FRAME_SIZE + idx)
 
+// Return value for functions used to execute instructions
+#define EXEC_FAIL	0
+#define EXEC_OK		1
+#define EXEC_DONE	2
+
     void
 to_string_error(vartype_T vartype)
 {
@@ -207,6 +212,46 @@
 }
 
 /*
+ * Create a new tuple from "count" items at the bottom of the stack.
+ * When "count" is zero an empty tuple is added to the stack.
+ * When "count" is -1 a NULL tuple is added to the stack.
+ */
+    static int
+exe_newtuple(int count, ectx_T *ectx)
+{
+    tuple_T	*tuple = NULL;
+    int		idx;
+    typval_T	*tv;
+
+    if (count >= 0)
+    {
+	tuple = tuple_alloc_with_items(count);
+	if (tuple == NULL)
+	    return FAIL;
+	for (idx = 0; idx < count; ++idx)
+	    tuple_set_item(tuple, idx, STACK_TV_BOT(idx - count));
+    }
+
+    if (count > 0)
+	ectx->ec_stack.ga_len -= count - 1;
+    else if (GA_GROW_FAILS(&ectx->ec_stack, 1))
+    {
+	tuple_unref(tuple);
+	return FAIL;
+    }
+    else
+	++ectx->ec_stack.ga_len;
+    tv = STACK_TV_BOT(-1);
+    tv->v_type = VAR_TUPLE;
+    tv->vval.v_tuple = tuple;
+    tv->v_lock = 0;
+    if (tuple != NULL)
+	++tuple->tv_refcount;
+
+    return OK;
+}
+
+/*
  * Implementation of ISN_NEWDICT.
  * Returns FAIL on total failure, MAYBE on error.
  */
@@ -923,7 +968,7 @@
 	int	    i;
 
 	for (i = 0; i < funcstack->fs_ga.ga_len; ++i)
-	    if (set_ref_in_item(stack + i, copyID, NULL, NULL))
+	    if (set_ref_in_item(stack + i, copyID, NULL, NULL, NULL))
 		return TRUE;  // abort
     }
     return FALSE;
@@ -1718,6 +1763,10 @@
 	    if (tv->vval.v_list == NULL && sv->sv_type != &t_list_empty)
 		(void)rettv_list_alloc(tv);
 	    break;
+	case VAR_TUPLE:
+	    if (tv->vval.v_tuple == NULL && sv->sv_type != &t_tuple_empty)
+		(void)rettv_tuple_alloc(tv);
+	    break;
 	case VAR_DICT:
 	    if (tv->vval.v_dict == NULL && sv->sv_type != &t_dict_empty)
 		(void)rettv_dict_alloc(tv);
@@ -2464,6 +2513,11 @@
 	    clear_tv(&otv[lidx]);
 	    otv[lidx] = *tv;
 	}
+	else if (dest_type == VAR_TUPLE)
+	{
+	    emsg(_(e_tuple_is_immutable));
+	    status = FAIL;
+	}
 	else
 	{
 	    status = FAIL;
@@ -2802,6 +2856,20 @@
 	    ++ectx->ec_stack.ga_len;
 	}
     }
+    else if (ltv->v_type == VAR_TUPLE)
+    {
+	tuple_T *tuple = ltv->vval.v_tuple;
+
+	// push the next item from the tuple
+	++idxtv->vval.v_number;
+	if (tuple == NULL || idxtv->vval.v_number >= TUPLE_LEN(tuple))
+	    jump = TRUE;
+	else
+	{
+	    copy_tv(TUPLE_ITEM(tuple, idxtv->vval.v_number), STACK_TV_BOT(0));
+	    ++ectx->ec_stack.ga_len;
+	}
+    }
     else if (ltv->v_type == VAR_STRING)
     {
 	char_u	*str = ltv->vval.v_string;
@@ -3066,7 +3134,7 @@
 	int	    i;
 
 	for (i = 0; i < loopvars->lvs_ga.ga_len; ++i)
-	    if (set_ref_in_item(stack + i, copyID, NULL, NULL))
+	    if (set_ref_in_item(stack + i, copyID, NULL, NULL, NULL))
 		return TRUE;  // abort
     }
     return FALSE;
@@ -3305,6 +3373,145 @@
 }
 
 /*
+ * Execute the ISN_UNPACK instruction for a List
+ */
+    static int
+exec_unpack_list(ectx_T *ectx, isn_T *iptr, typval_T *tv)
+{
+    int		count = iptr->isn_arg.unpack.unp_count;
+    int		semicolon = iptr->isn_arg.unpack.unp_semicolon;
+    list_T	*l;
+    listitem_T	*li;
+    int		i;
+
+    l = tv->vval.v_list;
+    if (l == NULL
+	    || l->lv_len < (semicolon ? count - 1 : count))
+    {
+	SOURCING_LNUM = iptr->isn_lnum;
+	emsg(_(e_list_value_does_not_have_enough_items));
+	return EXEC_FAIL;
+    }
+    else if (!semicolon && l->lv_len > count)
+    {
+	SOURCING_LNUM = iptr->isn_lnum;
+	emsg(_(e_list_value_has_more_items_than_targets));
+	return EXEC_FAIL;
+    }
+
+    CHECK_LIST_MATERIALIZE(l);
+    if (GA_GROW_FAILS(&ectx->ec_stack, count - 1))
+	return EXEC_DONE;
+    ectx->ec_stack.ga_len += count - 1;
+
+    // Variable after semicolon gets a list with the remaining
+    // items.
+    if (semicolon)
+    {
+	list_T	*rem_list =
+	    list_alloc_with_items(l->lv_len - count + 1);
+
+	if (rem_list == NULL)
+	    return EXEC_DONE;
+	tv = STACK_TV_BOT(-count);
+	tv->vval.v_list = rem_list;
+	++rem_list->lv_refcount;
+	tv->v_lock = 0;
+	li = l->lv_first;
+	for (i = 0; i < count - 1; ++i)
+	    li = li->li_next;
+	for (i = 0; li != NULL; ++i)
+	{
+	    typval_T tvcopy;
+
+	    copy_tv(&li->li_tv, &tvcopy);
+	    list_set_item(rem_list, i, &tvcopy);
+	    li = li->li_next;
+	}
+	--count;
+    }
+
+    // Produce the values in reverse order, first item last.
+    li = l->lv_first;
+    for (i = 0; i < count; ++i)
+    {
+	tv = STACK_TV_BOT(-i - 1);
+	copy_tv(&li->li_tv, tv);
+	li = li->li_next;
+    }
+
+    list_unref(l);
+
+    return EXEC_OK;
+}
+
+/*
+ * Execute the ISN_UNPACK instruction for a Tuple
+ */
+    static int
+exec_unpack_tuple(ectx_T *ectx, isn_T *iptr, typval_T *tv)
+{
+    int		count = iptr->isn_arg.unpack.unp_count;
+    int		semicolon = iptr->isn_arg.unpack.unp_semicolon;
+    tuple_T	*tuple;
+    int		i;
+
+    tuple = tv->vval.v_tuple;
+    if (tuple == NULL
+	    || TUPLE_LEN(tuple) < (semicolon ? count - 1 : count))
+    {
+	SOURCING_LNUM = iptr->isn_lnum;
+	emsg(_(e_more_targets_than_tuple_items));
+	return EXEC_FAIL;
+    }
+    else if (!semicolon && TUPLE_LEN(tuple) > count)
+    {
+	SOURCING_LNUM = iptr->isn_lnum;
+	emsg(_(e_less_targets_than_tuple_items));
+	return EXEC_FAIL;
+    }
+
+    if (GA_GROW_FAILS(&ectx->ec_stack, count - 1))
+	return EXEC_DONE;
+    ectx->ec_stack.ga_len += count - 1;
+
+    // Variable after semicolon gets a list with the remaining
+    // items.
+    if (semicolon)
+    {
+	list_T	*rem_list =
+	    list_alloc_with_items(TUPLE_LEN(tuple) - count + 1);
+
+	if (rem_list == NULL)
+	    return EXEC_DONE;
+	tv = STACK_TV_BOT(-count);
+	tv->v_type = VAR_LIST;
+	tv->vval.v_list = rem_list;
+	++rem_list->lv_refcount;
+	tv->v_lock = 0;
+	for (i = count - 1; i < TUPLE_LEN(tuple); ++i)
+	{
+	    typval_T tvcopy;
+
+	    copy_tv(TUPLE_ITEM(tuple, i), &tvcopy);
+	    list_set_item(rem_list, i - (count - 1), &tvcopy);
+	}
+	--count;
+    }
+
+    // Produce the values in reverse order, first item last.
+    for (i = 0; i < count; ++i)
+    {
+	tv = STACK_TV_BOT(-i - 1);
+	copy_tv(TUPLE_ITEM(tuple, i), tv);
+    }
+
+    tuple_unref(tuple);
+
+    return EXEC_OK;
+}
+
+/*
  * Execute instructions in execution context "ectx".
  * Return OK or FAIL;
  */
@@ -4534,6 +4741,12 @@
 		    goto theend;
 		break;
 
+	    // create a tuple from items on the stack
+	    case ISN_NEWTUPLE:
+		if (exe_newtuple(iptr->isn_arg.number, ectx) == FAIL)
+		    goto theend;
+		break;
+
 	    // create a dict from items on the stack
 	    case ISN_NEWDICT:
 		{
@@ -4938,9 +5151,9 @@
 		    size_t argidx = ufunc->uf_def_args.ga_len
 					+ iptr->isn_arg.jumparg.jump_arg_off
 					+ STACK_FRAME_SIZE;
-		    type_T *t = ufunc->uf_arg_types[argidx];
+		    type_T *tuple = ufunc->uf_arg_types[argidx];
 		    CLEAR_POINTER(tv);
-		    tv->v_type = t->tt_type;
+		    tv->v_type = tuple->tt_type;
 		}
 
 		if (iptr->isn_type == ISN_JUMP_IF_ARG_SET ? arg_set : !arg_set)
@@ -5299,6 +5512,7 @@
 		break;
 
 	    case ISN_COMPARELIST:
+	    case ISN_COMPARETUPLE:
 	    case ISN_COMPAREDICT:
 	    case ISN_COMPAREFUNC:
 	    case ISN_COMPARESTRING:
@@ -5318,6 +5532,11 @@
 			status = typval_compare_list(tv1, tv2,
 							   exprtype, ic, &res);
 		    }
+		    else if (iptr->isn_type == ISN_COMPARETUPLE)
+		    {
+			status = typval_compare_tuple(tv1, tv2,
+							   exprtype, ic, &res);
+		    }
 		    else if (iptr->isn_type == ISN_COMPAREDICT)
 		    {
 			status = typval_compare_dict(tv1, tv2,
@@ -5370,6 +5589,7 @@
 		break;
 
 	    case ISN_ADDLIST:
+	    case ISN_ADDTUPLE:
 	    case ISN_ADDBLOB:
 		{
 		    typval_T *tv1 = STACK_TV_BOT(-2);
@@ -5385,6 +5605,8 @@
 			else
 			    eval_addlist(tv1, tv2);
 		    }
+		    else if (iptr->isn_type == ISN_ADDTUPLE)
+			eval_addtuple(tv1, tv2);
 		    else
 			eval_addblob(tv1, tv2);
 		    clear_tv(tv2);
@@ -5455,6 +5677,14 @@
 			    --ectx->ec_stack.ga_len;
 			    break;
 			}
+			else if (tv1->v_type == VAR_TUPLE
+						&& tv2->v_type == VAR_TUPLE)
+			{
+			    eval_addtuple(tv1, tv2);
+			    clear_tv(tv2);
+			    --ectx->ec_stack.ga_len;
+			    break;
+			}
 			else if (tv1->v_type == VAR_BLOB
 						    && tv2->v_type == VAR_BLOB)
 			{
@@ -5574,20 +5804,25 @@
 
 	    case ISN_LISTINDEX:
 	    case ISN_LISTSLICE:
+	    case ISN_TUPLEINDEX:
+	    case ISN_TUPLESLICE:
 	    case ISN_BLOBINDEX:
 	    case ISN_BLOBSLICE:
 		{
 		    int		is_slice = iptr->isn_type == ISN_LISTSLICE
-					    || iptr->isn_type == ISN_BLOBSLICE;
+				    || iptr->isn_type == ISN_TUPLESLICE
+				    || iptr->isn_type == ISN_BLOBSLICE;
 		    int		is_blob = iptr->isn_type == ISN_BLOBINDEX
 					    || iptr->isn_type == ISN_BLOBSLICE;
+		    int		is_tuple = iptr->isn_type == ISN_TUPLEINDEX
+				    || iptr->isn_type == ISN_TUPLESLICE;
 		    varnumber_T	n1, n2;
 		    typval_T	*val_tv;
 
 		    // list index: list is at stack-2, index at stack-1
 		    // list slice: list is at stack-3, indexes at stack-2 and
 		    // stack-1
-		    // Same for blob.
+		    // Same for tuple and blob.
 		    val_tv = is_slice ? STACK_TV_BOT(-3) : STACK_TV_BOT(-2);
 
 		    tv = STACK_TV_BOT(-1);
@@ -5610,6 +5845,12 @@
 						    n1, n2, FALSE, tv) == FAIL)
 			    goto on_error;
 		    }
+		    else if (is_tuple)
+		    {
+			if (tuple_slice_or_index(val_tv->vval.v_tuple,
+				is_slice, n1, n2, FALSE, tv, TRUE) == FAIL)
+			    goto on_error;
+		    }
 		    else
 		    {
 			if (list_slice_or_index(val_tv->vval.v_list, is_slice,
@@ -5648,24 +5889,48 @@
 
 	    case ISN_SLICE:
 		{
-		    list_T	*list;
 		    int		count = iptr->isn_arg.number;
 
 		    // type will have been checked to be a list
 		    tv = STACK_TV_BOT(-1);
-		    list = tv->vval.v_list;
-
-		    // no error for short list, expect it to be checked earlier
-		    if (list != NULL && list->lv_len >= count)
+		    if (tv->v_type == VAR_LIST)
 		    {
-			list_T	*newlist = list_slice(list,
-						      count, list->lv_len - 1);
+			list_T *list = tv->vval.v_list;
 
-			if (newlist != NULL)
+			// no error for short list, expect it to be checked
+			// earlier
+			if (list != NULL && list->lv_len >= count)
 			{
-			    list_unref(list);
-			    tv->vval.v_list = newlist;
-			    ++newlist->lv_refcount;
+			    list_T	*newlist = list_slice(list,
+				    count, list->lv_len - 1);
+
+			    if (newlist != NULL)
+			    {
+				list_unref(list);
+				tv->vval.v_list = newlist;
+				++newlist->lv_refcount;
+			    }
+			}
+		    }
+		    else
+		    {
+			tuple_T *tuple = tv->vval.v_tuple;
+
+			// no error for short tuple, expect it to be checked
+			// earlier
+			if (tuple != NULL && TUPLE_LEN(tuple) >= count)
+			{
+			    tuple_T *newtuple;
+
+			    newtuple = tuple_slice(tuple, count,
+							TUPLE_LEN(tuple) - 1);
+			    if (newtuple != NULL)
+			    {
+				tuple_unref(tuple);
+				tv->v_type = VAR_TUPLE;
+				tv->vval.v_tuple = newtuple;
+				++newtuple->tv_refcount;
+			    }
 			}
 		    }
 		}
@@ -5674,17 +5939,24 @@
 	    case ISN_GETITEM:
 		{
 		    listitem_T	*li;
+		    typval_T	*item_tv;
 		    getitem_T	*gi = &iptr->isn_arg.getitem;
 
 		    // Get list item: list is at stack-1, push item.
 		    // List type and length is checked for when compiling.
 		    tv = STACK_TV_BOT(-1 - gi->gi_with_op);
-		    li = list_find(tv->vval.v_list, gi->gi_index);
+		    if (tv->v_type == VAR_LIST)
+		    {
+			li = list_find(tv->vval.v_list, gi->gi_index);
+			item_tv = &li->li_tv;
+		    }
+		    else
+			item_tv = TUPLE_ITEM(tv->vval.v_tuple, gi->gi_index);
 
 		    if (GA_GROW_FAILS(&ectx->ec_stack, 1))
 			goto theend;
 		    ++ectx->ec_stack.ga_len;
-		    copy_tv(&li->li_tv, STACK_TV_BOT(-1));
+		    copy_tv(item_tv, STACK_TV_BOT(-1));
 
 		    // Useful when used in unpack assignment.  Reset at
 		    // ISN_DROP.
@@ -5920,17 +6192,40 @@
 		{
 		    int	    min_len = iptr->isn_arg.checklen.cl_min_len;
 		    list_T  *list = NULL;
+		    tuple_T *tuple = NULL;
+		    int	    len = 0;
 
 		    tv = STACK_TV_BOT(-1);
+
+		    int		len_check_failed = FALSE;
 		    if (tv->v_type == VAR_LIST)
-			    list = tv->vval.v_list;
-		    if (list == NULL || list->lv_len < min_len
+		    {
+			list = tv->vval.v_list;
+			if (list == NULL || list->lv_len < min_len
 			    || (list->lv_len > min_len
 					&& !iptr->isn_arg.checklen.cl_more_OK))
+			    len_check_failed = TRUE;
+			if (list != NULL)
+			    len = list->lv_len;
+		    }
+		    else if (tv->v_type == VAR_TUPLE)
+		    {
+			tuple = tv->vval.v_tuple;
+			if (tuple == NULL || TUPLE_LEN(tuple) < min_len
+			    || (TUPLE_LEN(tuple) > min_len
+					&& !iptr->isn_arg.checklen.cl_more_OK))
+			    len_check_failed = TRUE;
+			if (tuple != NULL)
+			    len = TUPLE_LEN(tuple);
+		    }
+		    else
+			len_check_failed = TRUE;
+
+		    if (len_check_failed)
 		    {
 			SOURCING_LNUM = iptr->isn_lnum;
 			semsg(_(e_expected_nr_items_but_got_nr),
-				     min_len, list == NULL ? 0 : list->lv_len);
+				     min_len, len);
 			goto on_error;
 		    }
 		}
@@ -6026,78 +6321,25 @@
 		break;
 
 	    case ISN_UNPACK:
+		// Check there is a valid list to unpack.
+		tv = STACK_TV_BOT(-1);
+		if (tv->v_type != VAR_LIST && tv->v_type != VAR_TUPLE)
 		{
-		    int		count = iptr->isn_arg.unpack.unp_count;
-		    int		semicolon = iptr->isn_arg.unpack.unp_semicolon;
-		    list_T	*l;
-		    listitem_T	*li;
-		    int		i;
+		    SOURCING_LNUM = iptr->isn_lnum;
+		    emsg(_(e_for_argument_must_be_sequence_of_lists_or_tuples));
+		    goto on_error;
+		}
 
-		    // Check there is a valid list to unpack.
-		    tv = STACK_TV_BOT(-1);
-		    if (tv->v_type != VAR_LIST)
-		    {
-			SOURCING_LNUM = iptr->isn_lnum;
-			emsg(_(e_for_argument_must_be_sequence_of_lists));
+		int rc;
+		if (tv->v_type == VAR_LIST)
+		    rc = exec_unpack_list(ectx, iptr, tv);
+		else
+		    rc = exec_unpack_tuple(ectx, iptr, tv);
+		if (rc != EXEC_OK)
+		{
+		    if (rc == EXEC_FAIL)
 			goto on_error;
-		    }
-		    l = tv->vval.v_list;
-		    if (l == NULL
-				|| l->lv_len < (semicolon ? count - 1 : count))
-		    {
-			SOURCING_LNUM = iptr->isn_lnum;
-			emsg(_(e_list_value_does_not_have_enough_items));
-			goto on_error;
-		    }
-		    else if (!semicolon && l->lv_len > count)
-		    {
-			SOURCING_LNUM = iptr->isn_lnum;
-			emsg(_(e_list_value_has_more_items_than_targets));
-			goto on_error;
-		    }
-
-		    CHECK_LIST_MATERIALIZE(l);
-		    if (GA_GROW_FAILS(&ectx->ec_stack, count - 1))
-			goto theend;
-		    ectx->ec_stack.ga_len += count - 1;
-
-		    // Variable after semicolon gets a list with the remaining
-		    // items.
-		    if (semicolon)
-		    {
-			list_T	*rem_list =
-				  list_alloc_with_items(l->lv_len - count + 1);
-
-			if (rem_list == NULL)
-			    goto theend;
-			tv = STACK_TV_BOT(-count);
-			tv->vval.v_list = rem_list;
-			++rem_list->lv_refcount;
-			tv->v_lock = 0;
-			li = l->lv_first;
-			for (i = 0; i < count - 1; ++i)
-			    li = li->li_next;
-			for (i = 0; li != NULL; ++i)
-			{
-			    typval_T tvcopy;
-
-			    copy_tv(&li->li_tv, &tvcopy);
-			    list_set_item(rem_list, i, &tvcopy);
-			    li = li->li_next;
-			}
-			--count;
-		    }
-
-		    // Produce the values in reverse order, first item last.
-		    li = l->lv_first;
-		    for (i = 0; i < count; ++i)
-		    {
-			tv = STACK_TV_BOT(-i - 1);
-			copy_tv(&li->li_tv, tv);
-			li = li->li_next;
-		    }
-
-		    list_unref(l);
+		    goto theend;
 		}
 		break;
 
@@ -7183,6 +7425,10 @@
 		smsg("%s%4d NEWLIST size %lld", pfx, current,
 					  (varnumber_T)(iptr->isn_arg.number));
 		break;
+	    case ISN_NEWTUPLE:
+		smsg("%s%4d NEWTUPLE size %lld", pfx, current,
+					  (varnumber_T)(iptr->isn_arg.number));
+		break;
 	    case ISN_NEWDICT:
 		smsg("%s%4d NEWDICT size %lld", pfx, current,
 					  (varnumber_T)(iptr->isn_arg.number));
@@ -7474,6 +7720,7 @@
 	    case ISN_COMPARESTRING:
 	    case ISN_COMPAREBLOB:
 	    case ISN_COMPARELIST:
+	    case ISN_COMPARETUPLE:
 	    case ISN_COMPAREDICT:
 	    case ISN_COMPAREFUNC:
 	    case ISN_COMPAREOBJECT:
@@ -7512,6 +7759,7 @@
 						  type = "COMPARESTRING"; break;
 			   case ISN_COMPAREBLOB: type = "COMPAREBLOB"; break;
 			   case ISN_COMPARELIST: type = "COMPARELIST"; break;
+			   case ISN_COMPARETUPLE: type = "COMPARETUPLE"; break;
 			   case ISN_COMPAREDICT: type = "COMPAREDICT"; break;
 			   case ISN_COMPAREFUNC: type = "COMPAREFUNC"; break;
 			   case ISN_COMPAREOBJECT:
@@ -7525,6 +7773,7 @@
 		   break;
 
 	    case ISN_ADDLIST: smsg("%s%4d ADDLIST", pfx, current); break;
+	    case ISN_ADDTUPLE: smsg("%s%4d ADDTUPLE", pfx, current); break;
 	    case ISN_ADDBLOB: smsg("%s%4d ADDBLOB", pfx, current); break;
 
 	    // expression operations
@@ -7540,6 +7789,8 @@
 	    case ISN_BLOBAPPEND: smsg("%s%4d BLOBAPPEND", pfx, current); break;
 	    case ISN_LISTINDEX: smsg("%s%4d LISTINDEX", pfx, current); break;
 	    case ISN_LISTSLICE: smsg("%s%4d LISTSLICE", pfx, current); break;
+	    case ISN_TUPLEINDEX: smsg("%s%4d TUPLEINDEX", pfx, current); break;
+	    case ISN_TUPLESLICE: smsg("%s%4d TUPLESLICE", pfx, current); break;
 	    case ISN_ANYINDEX: smsg("%s%4d ANYINDEX", pfx, current); break;
 	    case ISN_ANYSLICE: smsg("%s%4d ANYSLICE", pfx, current); break;
 	    case ISN_SLICE: smsg("%s%4d SLICE %lld",
@@ -7817,6 +8068,8 @@
 	    return tv->vval.v_string != NULL && *tv->vval.v_string != NUL;
 	case VAR_LIST:
 	    return tv->vval.v_list != NULL && tv->vval.v_list->lv_len > 0;
+	case VAR_TUPLE:
+	    return tuple_len(tv->vval.v_tuple) > 0;
 	case VAR_DICT:
 	    return tv->vval.v_dict != NULL
 				    && tv->vval.v_dict->dv_hashtab.ht_used > 0;
diff --git a/src/vim9expr.c b/src/vim9expr.c
index f875bc4..68de736 100644
--- a/src/vim9expr.c
+++ b/src/vim9expr.c
@@ -73,7 +73,74 @@
 }
 
 /*
- * Compile getting a member from a list/dict/string/blob.  Stack has the
+ * Compile getting a member from a tuple.  Stack has the indexable value and
+ * the index or the two indexes of a slice.
+ */
+    static int
+compile_tuple_member(
+    type2_T	*typep,
+    int		is_slice,
+    cctx_T	*cctx)
+{
+    if (is_slice)
+    {
+	if (generate_instr_drop(cctx, ISN_TUPLESLICE, 2) == FAIL)
+	    return FAIL;
+	// a copy is made so the member type is no longer declared
+	if (typep->type_decl->tt_type == VAR_TUPLE)
+	    typep->type_decl = &t_tuple_any;
+
+	// a copy is made, the composite is no longer "const"
+	if (typep->type_curr->tt_flags & TTFLAG_CONST)
+	{
+	    type_T *type = copy_type(typep->type_curr, cctx->ctx_type_list);
+
+	    if (type != typep->type_curr)  // did get a copy
+	    {
+		type->tt_flags &= ~(TTFLAG_CONST | TTFLAG_STATIC);
+		typep->type_curr = type;
+	    }
+	}
+    }
+    else
+    {
+	if (typep->type_curr->tt_type == VAR_TUPLE)
+	{
+	    if (typep->type_curr->tt_argcount == 1)
+	    {
+		if (typep->type_curr->tt_flags & TTFLAG_VARARGS)
+		    typep->type_curr
+				= typep->type_curr->tt_args[0]->tt_member;
+		else
+		    typep->type_curr = typep->type_curr->tt_args[0];
+	    }
+	    else
+		typep->type_curr = &t_any;
+	    if (typep->type_decl->tt_type == VAR_TUPLE)
+	    {
+		if (typep->type_decl->tt_argcount == 1)
+		{
+		    if (typep->type_decl->tt_flags & TTFLAG_VARARGS)
+			typep->type_decl
+				= typep->type_decl->tt_args[0]->tt_member;
+		    else
+			typep->type_decl = typep->type_decl->tt_args[0];
+		}
+		else
+		    typep->type_curr = &t_any;
+	    }
+	    else
+		typep->type_decl = typep->type_curr;
+	}
+	if (generate_instr_drop(cctx, ISN_TUPLEINDEX, 1) == FAIL)
+	    return FAIL;
+    }
+
+    return OK;
+}
+
+/*
+ * Compile getting a member from a list/tuple/dict/string/blob.  Stack has the
  * indexable value and the index or the two indexes of a slice.
  * "keeping_dict" is used for dict[func](arg) to pass dict to func.
  */
@@ -85,7 +152,7 @@
     vartype_T	vartype;
     type_T	*idxtype;
 
-    // We can index a list, dict and blob.  If we don't know the type
+    // We can index a list, tuple, dict and blob.  If we don't know the type
     // we can use the index value type.  If we still don't know use an "ANY"
     // instruction.
     // TODO: what about the decl type?
@@ -97,7 +164,8 @@
 		|| typep->type_curr->tt_type == VAR_UNKNOWN)
 						       && idxtype == &t_string)
 	vartype = VAR_DICT;
-    if (vartype == VAR_STRING || vartype == VAR_LIST || vartype == VAR_BLOB)
+    if (vartype == VAR_STRING || vartype == VAR_LIST || vartype == VAR_BLOB
+						|| vartype == VAR_TUPLE)
     {
 	if (need_type(idxtype, &t_number, FALSE,
 					    -1, 0, cctx, FALSE, FALSE) == FAIL)
@@ -174,6 +242,11 @@
 		return FAIL;
 	}
     }
+    else if (vartype == VAR_TUPLE)
+    {
+	if (compile_tuple_member(typep, is_slice, cctx) == FAIL)
+	    return FAIL;
+    }
     else if (vartype == VAR_LIST || typep->type_curr->tt_type == VAR_ANY
 				 || typep->type_curr->tt_type == VAR_UNKNOWN)
     {
@@ -1466,6 +1539,82 @@
 }
 
 /*
+ * parse a tuple: (expr, expr)
+ * "*arg" points to the ','.
+ * ppconst->pp_is_const is set if all the items are constants.
+ */
+    static int
+compile_tuple(
+    char_u **arg,
+    cctx_T *cctx,
+    ppconst_T *ppconst,
+    int first_item_const)
+{
+    char_u	*p = *arg + 1;
+    char_u	*whitep = *arg + 1;
+    int		count = 0;
+    int		is_const;
+    int		is_all_const = TRUE;	// reset when non-const encountered
+    int		must_end = FALSE;
+
+    if (**arg != ')')
+    {
+	if (*p != ')' && !IS_WHITE_OR_NUL(*p))
+	{
+	    semsg(_(e_white_space_required_after_str_str), ",", p - 1);
+	    return FAIL;
+	}
+	count = 1;	// the first tuple item is already processed
+	is_all_const = first_item_const;
+	for (;;)
+	{
+	    if (may_get_next_line(whitep, &p, cctx) == FAIL)
+	    {
+		semsg(_(e_missing_end_of_tuple_rsp_str), *arg);
+		return FAIL;
+	    }
+	    if (*p == ',')
+	    {
+		semsg(_(e_no_white_space_allowed_before_str_str), ",", p);
+		return FAIL;
+	    }
+	    if (*p == ')')
+	    {
+		++p;
+		break;
+	    }
+	    if (must_end)
+	    {
+		semsg(_(e_missing_comma_in_tuple_str), p);
+		return FAIL;
+	    }
+	    if (compile_expr0_ext(&p, cctx, &is_const) == FAIL)
+		return FAIL;
+	    if (!is_const)
+		is_all_const = FALSE;
+	    ++count;
+	    if (*p == ',')
+	    {
+		++p;
+		if (*p != ')' && !IS_WHITE_OR_NUL(*p))
+		{
+		    semsg(_(e_white_space_required_after_str_str), ",", p - 1);
+		    return FAIL;
+		}
+	    }
+	    else
+		must_end = TRUE;
+	    whitep = p;
+	    p = skipwhite(p);
+	}
+    }
+    *arg = p;
+
+    ppconst->pp_is_const = is_all_const;
+    return generate_NEWTUPLE(cctx, count, FALSE);
+}
+
+/*
  * Parse a lambda: "(arg, arg) => expr"
  * "*arg" points to the '('.
  * Returns OK/FAIL when a lambda is recognized, NOTDONE if it's not a lambda.
@@ -2168,6 +2317,11 @@
 
     if (may_get_next_line_error(p, arg, cctx) == FAIL)
 	return FAIL;
+
+    if (**arg == ')')
+	// empty tuple
+	return compile_tuple(arg, cctx, ppconst, FALSE);
+
     if (ppconst->pp_used <= PPSIZE - 10)
     {
 	ret = compile_expr1(arg, cctx, ppconst);
@@ -2181,6 +2335,15 @@
     }
     if (may_get_next_line_error(*arg, arg, cctx) == FAIL)
 	return FAIL;
+    if (ret == OK && **arg == ',')
+    {
+	// tuple
+	int is_const = ppconst->pp_used > 0 || ppconst->pp_is_const;
+	if (generate_ppconst(cctx, ppconst) == FAIL)
+	    return FAIL;
+	return compile_tuple(arg, cctx, ppconst, is_const);
+    }
+
     if (**arg == ')')
 	++*arg;
     else if (ret == OK)
@@ -2440,6 +2603,7 @@
 	    int		is_slice = FALSE;
 
 	    // list index: list[123]
+	    // tuple index: tuple[123]
 	    // dict member: dict[key]
 	    // string index: text[123]
 	    // blob index: blob[123]
diff --git a/src/vim9instr.c b/src/vim9instr.c
index 3da56bf..1b322f6 100644
--- a/src/vim9instr.c
+++ b/src/vim9instr.c
@@ -224,6 +224,7 @@
 
 	// conversion possible when tolerant
 	case VAR_LIST:
+	case VAR_TUPLE:
 	case VAR_DICT:
 			 if (tostring_flags & TOSTRING_TOLERANT)
 			 {
@@ -281,6 +282,58 @@
 }
 
 /*
+ * Append the tuple item types from "tuple_type" to the grow array "gap".
+ */
+    static int
+ga_append_tuple_types(type_T *tuple_type, garray_T *gap)
+{
+    for (int i = 0; i < tuple_type->tt_argcount; i++)
+    {
+	if (ga_grow(gap, 1) == FAIL)
+	    return FAIL;
+
+	((type_T **)gap->ga_data)[gap->ga_len] = tuple_type->tt_args[i];
+	gap->ga_len++;
+    }
+
+    return OK;
+}
+
+/*
+ * When concatenating two tuples, the resulting tuple gets a union of item
+ * types from both the tuples.  This function sets the union tuple type in the
+ * stack.
+ *
+ * Returns OK on success and FAIL on memory allocation failure.
+ */
+    static int
+set_tuple_union_type_on_stack(type_T *type1, type_T *type2, cctx_T *cctx)
+{
+    // The concatenated tuple has the union of types from both the tuples
+    garray_T	tuple_types_ga;
+
+    ga_init2(&tuple_types_ga, sizeof(type_T *), 10);
+
+    if (type1->tt_argcount > 0)
+	ga_append_tuple_types(type1, &tuple_types_ga);
+    if (!(type1->tt_flags & TTFLAG_VARARGS) && (type2->tt_argcount > 0))
+	ga_append_tuple_types(type2, &tuple_types_ga);
+
+    type_T *new_tuple_type = get_tuple_type(&tuple_types_ga,
+							cctx->ctx_type_list);
+    // result inherits the variadic flag from the operands
+    new_tuple_type->tt_flags |= (type1->tt_flags & TTFLAG_VARARGS)
+					| (type2->tt_flags & TTFLAG_VARARGS);
+
+    // set the type on the stack for the resulting tuple
+    set_type_on_stack(cctx, new_tuple_type, 0);
+
+    ga_clear(&tuple_types_ga);
+
+    return OK;
+}
+
+/*
  * Generate instruction for "+".  For a list this creates a new list.
  */
     int
@@ -294,11 +347,12 @@
     isn_T	*isn = generate_instr_drop(cctx,
 		      vartype == VAR_NUMBER ? ISN_OPNR
 		    : vartype == VAR_LIST ? ISN_ADDLIST
+		    : vartype == VAR_TUPLE ? ISN_ADDTUPLE
 		    : vartype == VAR_BLOB ? ISN_ADDBLOB
 		    : vartype == VAR_FLOAT ? ISN_OPFLOAT
 		    : ISN_OPANY, 1);
 
-    if (vartype != VAR_LIST && vartype != VAR_BLOB
+    if (vartype != VAR_LIST && vartype != VAR_BLOB && vartype != VAR_TUPLE
 	    && type1->tt_type != VAR_ANY
 	    && type1->tt_type != VAR_UNKNOWN
 	    && type2->tt_type != VAR_ANY
@@ -320,6 +374,14 @@
 	    && type1->tt_type == VAR_LIST && type2->tt_type == VAR_LIST
 	    && type1->tt_member != type2->tt_member)
 	set_type_on_stack(cctx, &t_list_any, 0);
+    else if (vartype == VAR_TUPLE)
+    {
+	if (!check_tuples_addable(type1, type2))
+	    return FAIL;
+
+	if (set_tuple_union_type_on_stack(type1, type2, cctx) == FAIL)
+	    return FAIL;
+    }
 
     return isn == NULL ? FAIL : OK;
 }
@@ -335,6 +397,7 @@
     if (type1->tt_type == type2->tt_type
 	    && (type1->tt_type == VAR_NUMBER
 		|| type1->tt_type == VAR_LIST
+		|| type1->tt_type == VAR_TUPLE
 		|| type1->tt_type == VAR_FLOAT
 		|| type1->tt_type == VAR_BLOB))
 	return type1->tt_type;
@@ -461,6 +524,7 @@
 	    case VAR_STRING: isntype = ISN_COMPARESTRING; break;
 	    case VAR_BLOB: isntype = ISN_COMPAREBLOB; break;
 	    case VAR_LIST: isntype = ISN_COMPARELIST; break;
+	    case VAR_TUPLE: isntype = ISN_COMPARETUPLE; break;
 	    case VAR_DICT: isntype = ISN_COMPAREDICT; break;
 	    case VAR_FUNC: isntype = ISN_COMPAREFUNC; break;
 	    case VAR_OBJECT: isntype = ISN_COMPAREOBJECT; break;
@@ -744,6 +808,11 @@
 		iemsg("non-empty list constant not supported");
 	    generate_NEWLIST(cctx, 0, TRUE);
 	    break;
+	case VAR_TUPLE:
+	    if (tv->vval.v_tuple != NULL)
+		iemsg("non-empty tuple constant not supported");
+	    generate_NEWTUPLE(cctx, 0, TRUE);
+	    break;
 	case VAR_DICT:
 	    if (tv->vval.v_dict != NULL)
 		iemsg("non-empty dict constant not supported");
@@ -1009,7 +1078,7 @@
 
     RETURN_OK_IF_SKIP(cctx);
 
-    item_type = type->tt_member;
+    item_type = get_item_type(type);
     if ((isn = generate_instr(cctx, ISN_GETITEM)) == NULL)
 	return FAIL;
     isn->isn_arg.getitem.gi_index = index;
@@ -1370,6 +1439,45 @@
 }
 
 /*
+ * Generate an ISN_NEWTUPLE instruction for "count" items.
+ * "use_null" is TRUE for null_tuple.
+ */
+    int
+generate_NEWTUPLE(cctx_T *cctx, int count, int use_null)
+{
+    isn_T	*isn;
+    type_T	*type;
+    type_T	*decl_type;
+
+    RETURN_OK_IF_SKIP(cctx);
+    if ((isn = generate_instr(cctx, ISN_NEWTUPLE)) == NULL)
+	return FAIL;
+    isn->isn_arg.number = use_null ? -1 : count;
+
+    // Get the member type and the declared member type from all the items on
+    // the stack.
+    garray_T	tuple_types_ga;
+    ga_init2(&tuple_types_ga, sizeof(type_T *), 10);
+
+    if (get_tuple_type_from_stack(count, &tuple_types_ga, cctx) < 0)
+    {
+	ga_clear(&tuple_types_ga);
+	return FAIL;
+    }
+
+    type = get_tuple_type(&tuple_types_ga, cctx->ctx_type_list);
+    decl_type = &t_tuple_any;
+
+    ga_clear(&tuple_types_ga);
+
+    // drop the value types
+    cctx->ctx_type_stack.ga_len -= count;
+
+    // add the tuple type to the type stack
+    return push_type_stack2(cctx, type, decl_type);
+}
+
+/*
  * Generate an ISN_NEWDICT instruction.
  * "use_null" is TRUE for null_dict.
  */
@@ -2738,6 +2846,7 @@
 	case ISN_2STRING_ANY:
 	case ISN_ADDBLOB:
 	case ISN_ADDLIST:
+	case ISN_ADDTUPLE:
 	case ISN_ANYINDEX:
 	case ISN_ANYSLICE:
 	case ISN_BCALL:
@@ -2756,6 +2865,7 @@
 	case ISN_COMPAREFLOAT:
 	case ISN_COMPAREFUNC:
 	case ISN_COMPARELIST:
+	case ISN_COMPARETUPLE:
 	case ISN_COMPARENR:
 	case ISN_COMPARENULL:
 	case ISN_COMPAREOBJECT:
@@ -2787,6 +2897,8 @@
 	case ISN_LISTAPPEND:
 	case ISN_LISTINDEX:
 	case ISN_LISTSLICE:
+	case ISN_TUPLEINDEX:
+	case ISN_TUPLESLICE:
 	case ISN_LOAD:
 	case ISN_LOADBDICT:
 	case ISN_LOADGDICT:
@@ -2800,6 +2912,7 @@
 	case ISN_NEGATENR:
 	case ISN_NEWDICT:
 	case ISN_NEWLIST:
+	case ISN_NEWTUPLE:
 	case ISN_NEWPARTIAL:
 	case ISN_OPANY:
 	case ISN_OPFLOAT:
diff --git a/src/vim9script.c b/src/vim9script.c
index 3035889..cdacf0b 100644
--- a/src/vim9script.c
+++ b/src/vim9script.c
@@ -1127,6 +1127,7 @@
     "null_dict",
     "null_function",
     "null_list",
+    "null_tuple",
     "null_partial",
     "null_string",
     "null_channel",
diff --git a/src/vim9type.c b/src/vim9type.c
index 1f044d3..abf4daf 100644
--- a/src/vim9type.c
+++ b/src/vim9type.c
@@ -202,7 +202,7 @@
 {
     return type->tt_member != NULL
 		&& (type->tt_member->tt_type == VAR_DICT
-				       || type->tt_member->tt_type == VAR_LIST)
+				|| type->tt_member->tt_type == VAR_LIST)
 		&& type->tt_member->tt_member != NULL
 		&& type->tt_member->tt_member != &t_any
 		&& type->tt_member->tt_member != &t_unknown;
@@ -262,7 +262,37 @@
 }
 
 /*
- * Set the type of "tv" to "type" if it is a list or dict.
+ * Set the type of Tuple "tuple" to "type"
+ */
+    static void
+set_tv_type_tuple(tuple_T *tuple, type_T *type)
+{
+    if (tuple->tv_type == type)
+	return;
+
+    free_type(tuple->tv_type);
+    tuple->tv_type = alloc_type(type);
+
+    if (type->tt_argcount <= 0)
+	return;
+
+    // recursively set the type of list items
+    type_T	*item_type;
+    for (int i = 0; i < tuple_len(tuple); i++)
+    {
+	if ((type->tt_flags & TTFLAG_VARARGS) && (i >= type->tt_argcount - 1))
+	    // For a variadic tuple, the last type is a List.  So use the
+	    // List member type.
+	    item_type = type->tt_args[type->tt_argcount - 1]->tt_member;
+	else
+	    item_type = type->tt_args[i];
+
+	set_tv_type(TUPLE_ITEM(tuple, i), item_type);
+    }
+}
+
+/*
+ * Set the type of "tv" to "type" if it is a list or tuple or dict.
  */
     void
 set_tv_type(typval_T *tv, type_T *type)
@@ -276,6 +306,31 @@
 	set_tv_type_dict(tv->vval.v_dict, type);
     else if (tv->v_type == VAR_LIST && tv->vval.v_list != NULL)
 	set_tv_type_list(tv->vval.v_list, type);
+    else if (tv->v_type == VAR_TUPLE && tv->vval.v_tuple != NULL)
+	set_tv_type_tuple(tv->vval.v_tuple, type);
+}
+
+/*
+ * For a tuple type, reserve space for "typecount" types (including the
+ * repeated type).
+ */
+    static int
+tuple_type_add_types(
+    type_T	*tupletype,
+    int		typecount,
+    garray_T	*type_gap)
+{
+    // To make it easy to free the space needed for the types, add the
+    // pointer to type_gap.
+    if (ga_grow(type_gap, 1) == FAIL)
+	return FAIL;
+    tupletype->tt_args = ALLOC_CLEAR_MULT(type_T *, typecount);
+    if (tupletype->tt_args == NULL)
+	return FAIL;
+    ((type_T **)type_gap->ga_data)[type_gap->ga_len] =
+						(void *)tupletype->tt_args;
+    ++type_gap->ga_len;
+    return OK;
 }
 
     type_T *
@@ -307,6 +362,41 @@
     return type;
 }
 
+/*
+ * Create and return a tuple type from the tuple item types in
+ * "tuple_types_ga".
+ */
+    type_T *
+get_tuple_type(
+    garray_T	*tuple_types_gap,
+    garray_T	*type_gap)
+{
+    type_T	*type;
+    type_T	**tuple_types = tuple_types_gap->ga_data;
+    int		typecount = tuple_types_gap->ga_len;
+
+    // recognize commonly used types
+    if (typecount == 0)
+	return &t_tuple_any;
+
+    // Not a common type, create a new entry.
+    type = get_type_ptr(type_gap);
+    if (type == NULL)
+	return &t_any;
+    type->tt_type = VAR_TUPLE;
+    type->tt_member = NULL;
+    if (typecount > 0)
+    {
+	if (tuple_type_add_types(type, typecount, type_gap) == FAIL)
+	    return NULL;
+	mch_memmove(type->tt_args, tuple_types, sizeof(type_T *) * typecount);
+    }
+    type->tt_argcount = typecount;
+    type->tt_flags = 0;
+
+    return type;
+}
+
     type_T *
 get_dict_type(type_T *member_type, garray_T *type_gap)
 {
@@ -354,6 +444,23 @@
 }
 
 /*
+ * Allocate a new type for a tuple.
+ */
+    static type_T *
+alloc_tuple_type(int typecount, garray_T *type_gap)
+{
+    type_T *type = get_type_ptr(type_gap);
+
+    if (type == NULL)
+	return &t_any;
+    type->tt_type = VAR_TUPLE;
+    type->tt_member = NULL;
+    type->tt_argcount = typecount;
+    type->tt_args = NULL;
+    return type;
+}
+
+/*
  * Get a function type, based on the return type "ret_type".
  * "argcount" must be -1 or 0, a predefined type can be used.
  */
@@ -507,6 +614,64 @@
 }
 
 /*
+ * Get a type_T for a Tuple typval in "tv".
+ * When "flags" has TVTT_DO_MEMBER also get the member type, otherwise use
+ * "any".
+ * When "flags" has TVTT_MORE_SPECIFIC get the more specific member type if it
+ * is "any".
+ */
+    static type_T *
+tuple_typval2type(typval_T *tv, int copyID, garray_T *type_gap, int flags)
+{
+    tuple_T	*tuple = tv->vval.v_tuple;
+    int		len = tuple_len(tuple);
+    type_T	*type = NULL;
+
+    // An empty tuple has type tuple<unknown>, unless the type was specified
+    // and is not tuple<any>.  This matters when assigning to a variable
+    // with a specific tuple type.
+    if (tuple == NULL || (len == 0 && (tuple->tv_type == NULL
+				|| tuple->tv_type->tt_argcount == 0)))
+	return &t_tuple_empty;
+
+    if ((flags & TVTT_DO_MEMBER) == 0)
+	return &t_tuple_any;
+
+    // If the type is tuple<any> go through the members, it may end up a
+    // more specific type.
+    if (tuple->tv_type != NULL && (len == 0
+					|| (flags & TVTT_MORE_SPECIFIC) == 0))
+	// make a copy, tv_type may be freed if the tuple is freed
+	return copy_type_deep(tuple->tv_type, type_gap);
+
+    if (tuple->tv_copyID == copyID)
+	// avoid recursion
+	return &t_tuple_any;
+
+    tuple->tv_copyID = copyID;
+
+    garray_T	tuple_types_ga;
+    ga_init2(&tuple_types_ga, sizeof(type_T *), 10);
+    for (int i = 0; i < len; i++)
+    {
+	type = typval2type(TUPLE_ITEM(tuple, i), copyID, type_gap,
+							TVTT_DO_MEMBER);
+	if (ga_grow(&tuple_types_ga, 1) == FAIL)
+	{
+	    ga_clear(&tuple_types_ga);
+	    return NULL;
+	}
+	((type_T **)tuple_types_ga.ga_data)[tuple_types_ga.ga_len] = type;
+	tuple_types_ga.ga_len++;
+    }
+
+    type_T *tuple_type = get_tuple_type(&tuple_types_ga, type_gap);
+    ga_clear(&tuple_types_ga);
+
+    return tuple_type;
+}
+
+/*
  * Get a type_T for a Dict typval in "tv".
  * When "flags" has TVTT_DO_MEMBER also get the member type, otherwise use
  * "any".
@@ -723,6 +888,9 @@
 	case VAR_LIST:
 	    return list_typval2type(tv, copyID, type_gap, flags);
 
+	case VAR_TUPLE:
+	    return tuple_typval2type(tv, copyID, type_gap, flags);
+
 	case VAR_DICT:
 	    return dict_typval2type(tv, copyID, type_gap, flags);
 
@@ -950,6 +1118,63 @@
 }
 
 /*
+ * Check if the expected and actual types match for a tuple
+ */
+    static int
+check_tuple_type_maybe(
+    type_T	*expected,
+    type_T	*actual,
+    where_T	where)
+{
+    if (expected->tt_argcount == -1 || actual->tt_argcount == -1
+		|| expected->tt_args == NULL || actual->tt_args == NULL)
+	return OK;
+
+    // For a non-variadic tuple, the number of items must match
+    if (!(expected->tt_flags & TTFLAG_VARARGS)
+	    && expected->tt_argcount != actual->tt_argcount)
+	return FAIL;
+
+    // compare the type of each tuple item
+    for (int i = 0; i < actual->tt_argcount; ++i)
+    {
+	type_T	*exp_type;
+	type_T	*actual_type;
+
+	if (expected->tt_flags & TTFLAG_VARARGS)
+	{
+	    if (i < expected->tt_argcount - 1)
+		exp_type = expected->tt_args[i];
+	    else
+		// For a variadic tuple, the last type is a List.  So use the
+		// List member type.
+		exp_type = expected->tt_args[expected->tt_argcount - 1]->tt_member;
+	}
+	else
+	    exp_type = expected->tt_args[i];
+
+	if (actual->tt_flags & TTFLAG_VARARGS)
+	{
+	    if (i < actual->tt_argcount - 1)
+		actual_type = actual->tt_args[i];
+	    else
+		// For a variadic tuple, the last type is a List.  So use the
+		// List member type.
+		actual_type = actual->tt_args[actual->tt_argcount - 1]->tt_member;
+	}
+	else
+	    actual_type = actual->tt_args[i];
+
+	// Allow for using "any" type for a tuple item
+	if (actual->tt_args[i] != &t_any && check_type(exp_type, actual_type,
+						FALSE, where) == FAIL)
+	    return FAIL;
+    }
+
+    return OK;
+}
+
+/*
  * Check if the expected and actual types match.
  * Does not allow for assigning "any" to a specific type.
  * When "argidx" > 0 it is included in the error message.
@@ -1018,6 +1243,8 @@
 		ret = check_type_maybe(expected->tt_member, actual->tt_member,
 								 FALSE, where);
 	}
+	else if (expected->tt_type == VAR_TUPLE && actual != &t_any)
+	    ret =  check_tuple_type_maybe(expected, actual, where);
 	else if (expected->tt_type == VAR_FUNC && actual != &t_any)
 	{
 	    // If the return type is unknown it can be anything, including
@@ -1200,11 +1427,33 @@
     // Skip over "<type>"; this is permissive about white space.
     if (*skipwhite(p) == '<')
     {
-	p = skipwhite(p);
-	p = skip_type(skipwhite(p + 1), FALSE);
-	p = skipwhite(p);
-	if (*p == '>')
-	    ++p;
+	if (STRNCMP("tuple", start, 5) == 0)
+	{
+	    // handle tuple<{type1}, {type2}, ....<type>>
+	    p = skipwhite(p + 1);
+	    while (*p != '>' && *p != NUL)
+	    {
+		char_u *sp = p;
+
+		if (STRNCMP(p, "...", 3) == 0)
+		    p += 3;
+		p = skip_type(p, TRUE);
+		if (p == sp)
+		    return p;  // syntax error
+		if (*p == ',')
+		    p = skipwhite(p + 1);
+	    }
+	    if (*p == '>')
+		p++;
+	}
+	else
+	{
+	    p = skipwhite(p);
+	    p = skip_type(skipwhite(p + 1), FALSE);
+	    p = skipwhite(p);
+	    if (*p == '>')
+		++p;
+	}
     }
     else if ((*p == '(' || (*p == ':' && VIM_ISWHITE(p[1])))
 					     && STRNCMP("func", start, 4) == 0)
@@ -1423,6 +1672,116 @@
 }
 
 /*
+ * Parse a "tuple" type at "*arg" and advance over it.
+ * When "give_error" is TRUE give error messages, otherwise be quiet.
+ * Return NULL for failure.
+ */
+    static type_T *
+parse_type_tuple(char_u **arg, garray_T *type_gap, int give_error)
+{
+    char_u	*p;
+    type_T	*type;
+    type_T	*ret_type = NULL;
+    int		typecount = -1;
+    int		flags = 0;
+    garray_T	tuple_types_ga;
+
+    ga_init2(&tuple_types_ga, sizeof(type_T *), 10);
+
+    // tuple<{type}, {type}>
+    // tuple<{type}, ...{type}>
+    if (**arg != '<')
+    {
+	if (give_error)
+	{
+	    if (*skipwhite(*arg) == '<')
+		semsg(_(e_no_white_space_allowed_before_str_str), "<", *arg);
+	    else
+		semsg(_(e_missing_type_after_str), "tuple");
+	}
+
+	// only "tuple" is specified
+	return NULL;
+    }
+
+    p = ++*arg;
+    typecount = 0;
+    while (*p != NUL && *p != '>')
+    {
+	if (STRNCMP(p, "...", 3) == 0)
+	{
+	    flags |= TTFLAG_VARARGS;
+	    p += 3;
+	}
+
+	type = parse_type(&p, type_gap, give_error);
+	if (type == NULL)
+	    goto on_err;
+
+	if ((flags & TTFLAG_VARARGS) != 0 && type->tt_type != VAR_LIST)
+	{
+	    char *tofree;
+	    semsg(_(e_variadic_tuple_must_end_with_list_type_str),
+						type_name(type, &tofree));
+	    vim_free(tofree);
+	    goto on_err;
+	}
+
+	// Add the item type
+	if (ga_grow(&tuple_types_ga, 1) == FAIL)
+	    goto on_err;
+	((type_T **)tuple_types_ga.ga_data)[tuple_types_ga.ga_len] = type;
+	tuple_types_ga.ga_len++;
+	typecount++;
+
+	// Nothing comes after "...{type}".
+	if (flags & TTFLAG_VARARGS)
+	    break;
+
+	if (*p != ',' && *skipwhite(p) == ',')
+	{
+	    if (give_error)
+		semsg(_(e_no_white_space_allowed_before_str_str), ",", p);
+	    goto on_err;
+	}
+	if (*p == ',')
+	{
+	    ++p;
+	    if (!VIM_ISWHITE(*p))
+	    {
+		if (give_error)
+		    semsg(_(e_white_space_required_after_str_str),
+			    ",", p - 1);
+		goto on_err;
+	    }
+	}
+	p = skipwhite(p);
+    }
+
+    p = skipwhite(p);
+    if (*p != '>' || typecount <= 0)
+    {
+	if (give_error)
+	    semsg(_(e_missing_type_after_str), p);
+	goto on_err;
+    }
+    *arg = p + 1;
+
+    ret_type = alloc_tuple_type(typecount, type_gap);
+    ret_type->tt_flags = flags;
+    ret_type->tt_argcount = typecount;
+    if (tuple_type_add_types(ret_type, typecount, type_gap) == FAIL)
+	return NULL;
+    mch_memmove(ret_type->tt_args, tuple_types_ga.ga_data,
+						sizeof(type_T *) * typecount);
+
+on_err:
+    ga_clear(&tuple_types_ga);
+
+    return ret_type;
+}
+
+/*
  * Parse a user defined type at "*arg" and advance over it.
  * It can be a class or an interface or a typealias name, possibly imported.
  * Return NULL if a type is not found.
@@ -1577,6 +1936,13 @@
 		return &t_string;
 	    }
 	    break;
+	case 't':
+	    if (len == 5 && STRNCMP(*arg, "tuple", len) == 0)
+	    {
+		*arg += len;
+		return parse_type_tuple(arg, type_gap, give_error);
+	    }
+	    break;
 	case 'v':
 	    if (len == 4 && STRNCMP(*arg, "void", len) == 0)
 	    {
@@ -1625,6 +1991,18 @@
 	case VAR_LIST:
 	case VAR_DICT:
 	    return equal_type(type1->tt_member, type2->tt_member, flags);
+	case VAR_TUPLE:
+	    if (type1->tt_argcount != type2->tt_argcount)
+		return FALSE;
+	    if (type1->tt_argcount < 0
+			   || type1->tt_args == NULL || type2->tt_args == NULL)
+		return TRUE;
+	    for (i = 0; i < type1->tt_argcount; ++i)
+		if ((flags & ETYPE_ARG_UNKNOWN) == 0
+			&& !equal_type(type1->tt_args[i], type2->tt_args[i],
+									flags))
+		    return FALSE;
+	    return TRUE;
 	case VAR_FUNC:
 	case VAR_PARTIAL:
 	    if (!equal_type(type1->tt_member, type2->tt_member, flags)
@@ -1725,7 +2103,8 @@
 
     if (type1->tt_type == type2->tt_type)
     {
-	if (type1->tt_type == VAR_LIST || type2->tt_type == VAR_DICT)
+	if (type1->tt_type == VAR_LIST
+		|| type1->tt_type == VAR_DICT)
 	{
 	    type_T *common;
 
@@ -1736,8 +2115,7 @@
 		*dest = get_dict_type(common, type_gap);
 	    return;
 	}
-
-	if (type1->tt_type == VAR_FUNC)
+	else if (type1->tt_type == VAR_FUNC)
 	{
 	    common_type_var_func(type1, type2, dest, type_gap);
 	    return;
@@ -1748,6 +2126,26 @@
 }
 
 /*
+ * Return the item type of a List, Dict or a Tuple
+ */
+    type_T *
+get_item_type(type_T *type)
+{
+    if (type->tt_type == VAR_TUPLE)
+    {
+	if (type->tt_argcount != 1)
+	    return &t_any;
+
+	if (type->tt_flags & TTFLAG_VARARGS)
+	    return type->tt_args[0]->tt_member;
+	else
+	    return type->tt_args[0];
+    }
+
+    return type->tt_member;
+}
+
+/*
  * Push an entry onto the type stack.  "type" used both for the current type
  * and the declared type.
  * Returns FAIL when out of memory.
@@ -1864,6 +2262,40 @@
     return result;
 }
 
+/*
+ * Get the types of items in a tuple on the stack of "cctx".
+ * Returns the number of types.  Returns -1 on failure.
+ */
+    int
+get_tuple_type_from_stack(
+    int		count,
+    garray_T	*tuple_types_gap,
+    cctx_T	*cctx)
+{
+    garray_T	*stack = &cctx->ctx_type_stack;
+    type2_T	*typep;
+    type_T	*type = NULL;
+
+    // Use "unknown" for an empty tuple
+    if (count == 0)
+	return 0;
+
+    // Find the common type from following items.
+    typep = ((type2_T *)stack->ga_data) + stack->ga_len;
+    for (int i = 0; i < count; i++)
+    {
+	type = (typep - (count - i))->type_curr;
+	if (check_type_is_value(type) == FAIL)
+	    return -1;
+	if (ga_grow(tuple_types_gap, 1) == FAIL)
+	    return -1;
+	((type_T **)tuple_types_gap->ga_data)[tuple_types_gap->ga_len] = type;
+	tuple_types_gap->ga_len++;
+    }
+
+    return tuple_types_gap->ga_len;
+}
+
     char *
 vartype_name(vartype_T type)
 {
@@ -1881,6 +2313,7 @@
 	case VAR_JOB: return "job";
 	case VAR_CHANNEL: return "channel";
 	case VAR_LIST: return "list";
+	case VAR_TUPLE: return "tuple";
 	case VAR_DICT: return "dict";
 	case VAR_INSTR: return "instr";
 	case VAR_CLASS: return "class";
@@ -1919,6 +2352,65 @@
 }
 
 /*
+ * Return the type name of a tuple.
+ * The result may be in allocated memory, in which case "tofree" is set.
+ */
+    static char *
+type_name_tuple(type_T *type, char **tofree)
+{
+    garray_T    ga;
+    int		i;
+    int		varargs = (type->tt_flags & TTFLAG_VARARGS) ? 1 : 0;
+    char	*arg_free = NULL;
+
+    ga_init2(&ga, 1, 100);
+    if (ga_grow(&ga, 20) == FAIL)
+	goto failed;
+    STRCPY(ga.ga_data, "tuple<");
+    ga.ga_len += 6;
+
+    if (type->tt_argcount <= 0)
+	// empty tuple
+	ga_concat(&ga, (char_u *)"any");
+    else
+    {
+	if (type->tt_args == NULL)
+	    ga_concat(&ga, (char_u *)"[unknown]");
+	else
+	{
+	    for (i = 0; i < type->tt_argcount; ++i)
+	    {
+		char	*arg_type;
+		int	len;
+
+		arg_type = type_name(type->tt_args[i], &arg_free);
+		if (i > 0)
+		{
+		    STRCPY((char *)ga.ga_data + ga.ga_len, ", ");
+		    ga.ga_len += 2;
+		}
+		len = (int)STRLEN(arg_type);
+		if (ga_grow(&ga, len + 8) == FAIL)
+		    goto failed;
+		if (varargs && i == type->tt_argcount - 1)
+		    ga_concat(&ga, (char_u *)"...");
+		ga_concat(&ga, (char_u *)arg_type);
+		VIM_CLEAR(arg_free);
+	    }
+	}
+    }
+
+    STRCPY((char *)ga.ga_data + ga.ga_len, ">");
+    *tofree = ga.ga_data;
+    return ga.ga_data;
+
+failed:
+    vim_free(arg_free);
+    ga_clear(&ga);
+    return "[unknown]";
+}
+
+/*
  * Return the type name of a Class (class<name>) or Object (object<name>).
  * The result may be in allocated memory, in which case "tofree" is set.
  */
@@ -2035,6 +2527,9 @@
 	case VAR_DICT:
 	    return type_name_list_or_dict(name, type, tofree);
 
+	case VAR_TUPLE:
+	    return type_name_tuple(type, tofree);
+
 	case VAR_CLASS:
 	case VAR_OBJECT:
 	    return type_name_class_or_obj(name, type, tofree);
diff --git a/src/viminfo.c b/src/viminfo.c
index 5f1ad74..44d5487 100644
--- a/src/viminfo.c
+++ b/src/viminfo.c
@@ -1263,6 +1263,7 @@
 		case 'L': type = VAR_LIST; break;
 		case 'B': type = VAR_BLOB; break;
 		case 'X': type = VAR_SPECIAL; break;
+		case 'T': type = VAR_TUPLE; break;
 	    }
 
 	    tab = vim_strchr(tab, '\t');
@@ -1270,7 +1271,8 @@
 	    {
 		tv.v_type = type;
 		if (type == VAR_STRING || type == VAR_DICT
-			|| type == VAR_LIST || type == VAR_BLOB)
+			|| type == VAR_LIST || type == VAR_BLOB
+			|| type == VAR_TUPLE)
 		    tv.vval.v_string = viminfo_readstring(virp,
 				       (int)(tab - virp->vir_line + 1), TRUE);
 		else if (type == VAR_FLOAT)
@@ -1282,7 +1284,7 @@
 					     || tv.vval.v_number == VVAL_TRUE))
 			tv.v_type = VAR_BOOL;
 		}
-		if (type == VAR_DICT || type == VAR_LIST)
+		if (type == VAR_DICT || type == VAR_LIST || type == VAR_TUPLE)
 		{
 		    typval_T *etv = eval_expr(tv.vval.v_string, NULL);
 
@@ -1370,7 +1372,7 @@
 
 			      s = "DIC";
 			      if (di != NULL && !set_ref_in_ht(
-						 &di->dv_hashtab, copyID, NULL)
+					 &di->dv_hashtab, copyID, NULL, NULL)
 				      && di->dv_copyID == copyID)
 				  // has a circular reference, can't turn the
 				  // value into a string
@@ -1384,13 +1386,27 @@
 
 			      s = "LIS";
 			      if (l != NULL && !set_ref_in_list_items(
-							       l, copyID, NULL)
+						       l, copyID, NULL, NULL)
 				      && l->lv_copyID == copyID)
 				  // has a circular reference, can't turn the
 				  // value into a string
 				  continue;
 			      break;
 			  }
+		    case VAR_TUPLE:
+			  {
+			      tuple_T	*tuple = this_var->di_tv.vval.v_tuple;
+			      int	copyID = get_copyID();
+
+			      s = "TUP";
+			      if (tuple != NULL && !set_ref_in_tuple_items(
+					       tuple, copyID, NULL, NULL)
+				      && tuple->tv_copyID == copyID)
+				  // has a circular reference, can't turn the
+				  // value into a string
+				  continue;
+			      break;
+			  }
 		    case VAR_BLOB:    s = "BLO"; break;
 		    case VAR_BOOL:    s = "XPL"; break;  // backwards compat.
 		    case VAR_SPECIAL: s = "XPL"; break;