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/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;