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)