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