// This is an open source non-commercial project. Dear PVS-Studio, please check // it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com #include #include #include #include #include #include #include "lauxlib.h" #include "nvim/ascii.h" #include "nvim/assert.h" #include "nvim/charset.h" #include "nvim/eval.h" #include "nvim/eval/encode.h" #include "nvim/eval/executor.h" #include "nvim/eval/gc.h" #include "nvim/eval/typval.h" #include "nvim/eval/typval_encode.h" #include "nvim/eval/userfunc.h" #include "nvim/eval/vars.h" #include "nvim/garray.h" #include "nvim/gettext.h" #include "nvim/globals.h" #include "nvim/hashtab.h" #include "nvim/lib/queue.h" #include "nvim/lua/executor.h" #include "nvim/macros.h" #include "nvim/mbyte.h" #include "nvim/mbyte_defs.h" #include "nvim/memory.h" #include "nvim/message.h" #include "nvim/os/input.h" #include "nvim/pos.h" #include "nvim/types.h" #include "nvim/vim.h" #ifdef INCLUDE_GENERATED_DECLARATIONS # include "eval/typval.c.generated.h" #endif static char e_string_required_for_argument_nr[] = N_("E1174: String required for argument %d"); static char e_non_empty_string_required_for_argument_nr[] = N_("E1175: Non-empty string required for argument %d"); static char e_number_required_for_argument_nr[] = N_("E1210: Number required for argument %d"); static char e_string_or_list_required_for_argument_nr[] = N_("E1222: String or List required for argument %d"); bool tv_in_free_unref_items = false; // TODO(ZyX-I): Remove DICT_MAXNEST, make users be non-recursive instead #define DICT_MAXNEST 100 const char *const tv_empty_string = ""; //{{{1 Lists //{{{2 List log #ifdef LOG_LIST_ACTIONS ListLog *list_log_first = NULL; ListLog *list_log_last = NULL; /// Write list log to the given file /// /// @param[in] fname File to write log to. Will be appended to if already /// present. void list_write_log(const char *const fname) FUNC_ATTR_NONNULL_ALL { FileDescriptor fp; const int fo_ret = file_open(&fp, fname, kFileCreate|kFileAppend, 0600); if (fo_ret != 0) { semsg(_("E5142: Failed to open file %s: %s"), fname, os_strerror(fo_ret)); return; } for (ListLog *chunk = list_log_first; chunk != NULL;) { for (size_t i = 0; i < chunk->size; i++) { char buf[10 + 1 + ((16 + 3) * 3) + (8 + 2) + 2]; // act : hex " c:" len "[]" "\n\0" const ListLogEntry entry = chunk->entries[i]; const size_t snp_len = (size_t)snprintf(buf, sizeof(buf), "%-10.10s: l:%016" PRIxPTR "[%08d] 1:%016" PRIxPTR " 2:%016" PRIxPTR "\n", entry.action, entry.l, entry.len, entry.li1, entry.li2); assert(snp_len + 1 == sizeof(buf)); const ptrdiff_t fw_ret = file_write(&fp, buf, snp_len); if (fw_ret != (ptrdiff_t)snp_len) { assert(fw_ret < 0); if (i) { memmove(chunk->entries, chunk->entries + i, sizeof(chunk->entries[0]) * (chunk->size - i)); chunk->size -= i; } semsg(_("E5143: Failed to write to file %s: %s"), fname, os_strerror((int)fw_ret)); return; } } list_log_first = chunk->next; xfree(chunk); chunk = list_log_first; } const int fc_ret = file_close(&fp, true); if (fc_ret != 0) { semsg(_("E5144: Failed to close file %s: %s"), fname, os_strerror(fc_ret)); } } # ifdef EXITFREE /// Free list log void list_free_log(void) { for (ListLog *chunk = list_log_first; chunk != NULL;) { list_log_first = chunk->next; xfree(chunk); chunk = list_log_first; } } # endif #endif //{{{2 List item /// Allocate a list item /// /// @warning Allocated item is not initialized, do not forget to initialize it /// and specifically set lv_lock. /// /// @return [allocated] new list item. static listitem_T *tv_list_item_alloc(void) FUNC_ATTR_NONNULL_RET FUNC_ATTR_MALLOC { return xmalloc(sizeof(listitem_T)); } /// Remove a list item from a List and free it /// /// Also clears the value. /// /// @param[out] l List to remove item from. /// @param[in,out] item Item to remove. /// /// @return Pointer to the list item just after removed one, NULL if removed /// item was the last one. listitem_T *tv_list_item_remove(list_T *const l, listitem_T *const item) FUNC_ATTR_NONNULL_ALL { listitem_T *const next_item = TV_LIST_ITEM_NEXT(l, item); tv_list_drop_items(l, item, item); tv_clear(TV_LIST_ITEM_TV(item)); xfree(item); return next_item; } //{{{2 List watchers /// Add a watcher to a list /// /// @param[out] l List to add watcher to. /// @param[in] lw Watcher to add. void tv_list_watch_add(list_T *const l, listwatch_T *const lw) FUNC_ATTR_NONNULL_ALL { lw->lw_next = l->lv_watch; l->lv_watch = lw; } /// Remove a watcher from a list /// /// Does not give a warning if watcher was not found. /// /// @param[out] l List to remove watcher from. /// @param[in] lwrem Watcher to remove. void tv_list_watch_remove(list_T *const l, listwatch_T *const lwrem) FUNC_ATTR_NONNULL_ALL { listwatch_T **lwp = &l->lv_watch; for (listwatch_T *lw = l->lv_watch; lw != NULL; lw = lw->lw_next) { if (lw == lwrem) { *lwp = lw->lw_next; break; } lwp = &lw->lw_next; } } /// Advance watchers to the next item /// /// Used just before removing an item from a list. /// /// @param[out] l List from which item is removed. /// @param[in] item List item being removed. void tv_list_watch_fix(list_T *const l, const listitem_T *const item) FUNC_ATTR_NONNULL_ALL { for (listwatch_T *lw = l->lv_watch; lw != NULL; lw = lw->lw_next) { if (lw->lw_item == item) { lw->lw_item = item->li_next; } } } //{{{2 Alloc/free /// Allocate an empty list /// /// Caller should take care of the reference count. /// /// @param[in] len Expected number of items to be populated before list /// becomes accessible from VimL. It is still valid to /// underpopulate a list, value only controls how many elements /// will be allocated in advance. Currently does nothing. /// @see ListLenSpecials. /// /// @return [allocated] new list. list_T *tv_list_alloc(const ptrdiff_t len) FUNC_ATTR_NONNULL_RET { list_T *const list = xcalloc(1, sizeof(list_T)); // Prepend the list to the list of lists for garbage collection. if (gc_first_list != NULL) { gc_first_list->lv_used_prev = list; } list->lv_used_prev = NULL; list->lv_used_next = gc_first_list; gc_first_list = list; list_log(list, NULL, (void *)(uintptr_t)len, "alloc"); list->lua_table_ref = LUA_NOREF; return list; } /// Initialize a static list with 10 items /// /// @param[out] sl Static list to initialize. void tv_list_init_static10(staticList10_T *const sl) FUNC_ATTR_NONNULL_ALL { #define SL_SIZE ARRAY_SIZE(sl->sl_items) list_T *const l = &sl->sl_list; CLEAR_POINTER(sl); l->lv_first = &sl->sl_items[0]; l->lv_last = &sl->sl_items[SL_SIZE - 1]; l->lv_refcount = DO_NOT_FREE_CNT; tv_list_set_lock(l, VAR_FIXED); sl->sl_list.lv_len = 10; sl->sl_items[0].li_prev = NULL; sl->sl_items[0].li_next = &sl->sl_items[1]; sl->sl_items[SL_SIZE - 1].li_prev = &sl->sl_items[SL_SIZE - 2]; sl->sl_items[SL_SIZE - 1].li_next = NULL; for (size_t i = 1; i < SL_SIZE - 1; i++) { listitem_T *const li = &sl->sl_items[i]; li->li_prev = li - 1; li->li_next = li + 1; } list_log((const list_T *)sl, &sl->sl_items[0], &sl->sl_items[SL_SIZE - 1], "s10init"); #undef SL_SIZE } /// Initialize static list with undefined number of elements /// /// @param[out] l List to initialize. void tv_list_init_static(list_T *const l) FUNC_ATTR_NONNULL_ALL { CLEAR_POINTER(l); l->lv_refcount = DO_NOT_FREE_CNT; list_log(l, NULL, NULL, "sinit"); } /// Free items contained in a list /// /// @param[in,out] l List to clear. void tv_list_free_contents(list_T *const l) FUNC_ATTR_NONNULL_ALL { list_log(l, NULL, NULL, "freecont"); for (listitem_T *item = l->lv_first; item != NULL; item = l->lv_first) { // Remove the item before deleting it. l->lv_first = item->li_next; tv_clear(&item->li_tv); xfree(item); } l->lv_len = 0; l->lv_idx_item = NULL; l->lv_last = NULL; assert(l->lv_watch == NULL); } /// Free a list itself, ignoring items it contains /// /// Ignores the reference count. /// /// @param[in,out] l List to free. void tv_list_free_list(list_T *const l) FUNC_ATTR_NONNULL_ALL { // Remove the list from the list of lists for garbage collection. if (l->lv_used_prev == NULL) { gc_first_list = l->lv_used_next; } else { l->lv_used_prev->lv_used_next = l->lv_used_next; } if (l->lv_used_next != NULL) { l->lv_used_next->lv_used_prev = l->lv_used_prev; } list_log(l, NULL, NULL, "freelist"); NLUA_CLEAR_REF(l->lua_table_ref); xfree(l); } /// Free a list, including all items it points to /// /// Ignores the reference count. Does not do anything if /// tv_in_free_unref_items is true. /// /// @param[in,out] l List to free. void tv_list_free(list_T *const l) FUNC_ATTR_NONNULL_ALL { if (tv_in_free_unref_items) { return; } tv_list_free_contents(l); tv_list_free_list(l); } /// Unreference a list /// /// Decrements the reference count and frees when it becomes zero or less. /// /// @param[in,out] l List to unreference. void tv_list_unref(list_T *const l) { if (l != NULL && --l->lv_refcount <= 0) { tv_list_free(l); } } //{{{2 Add/remove /// Remove items "item" to "item2" from list "l" /// /// @warning Does not free the listitem or the value! /// /// @param[out] l List to remove from. /// @param[in] item First item to remove. /// @param[in] item2 Last item to remove. void tv_list_drop_items(list_T *const l, listitem_T *const item, listitem_T *const item2) FUNC_ATTR_NONNULL_ALL { list_log(l, item, item2, "drop"); // Notify watchers. for (listitem_T *ip = item; ip != item2->li_next; ip = ip->li_next) { l->lv_len--; tv_list_watch_fix(l, ip); } if (item2->li_next == NULL) { l->lv_last = item->li_prev; } else { item2->li_next->li_prev = item->li_prev; } if (item->li_prev == NULL) { l->lv_first = item2->li_next; } else { item->li_prev->li_next = item2->li_next; } l->lv_idx_item = NULL; list_log(l, l->lv_first, l->lv_last, "afterdrop"); } /// Like tv_list_drop_items, but also frees all removed items void tv_list_remove_items(list_T *const l, listitem_T *const item, listitem_T *const item2) FUNC_ATTR_NONNULL_ALL { list_log(l, item, item2, "remove"); tv_list_drop_items(l, item, item2); for (listitem_T *li = item;;) { tv_clear(TV_LIST_ITEM_TV(li)); listitem_T *const nli = li->li_next; xfree(li); if (li == item2) { break; } li = nli; } } /// Move items "item" to "item2" from list "l" to the end of the list "tgt_l" /// /// @param[out] l List to move from. /// @param[in] item First item to move. /// @param[in] item2 Last item to move. /// @param[out] tgt_l List to move to. /// @param[in] cnt Number of items moved. void tv_list_move_items(list_T *const l, listitem_T *const item, listitem_T *const item2, list_T *const tgt_l, const int cnt) FUNC_ATTR_NONNULL_ALL { list_log(l, item, item2, "move"); tv_list_drop_items(l, item, item2); item->li_prev = tgt_l->lv_last; item2->li_next = NULL; if (tgt_l->lv_last == NULL) { tgt_l->lv_first = item; } else { tgt_l->lv_last->li_next = item; } tgt_l->lv_last = item2; tgt_l->lv_len += cnt; list_log(tgt_l, tgt_l->lv_first, tgt_l->lv_last, "movetgt"); } /// Insert list item /// /// @param[out] l List to insert to. /// @param[in,out] ni Item to insert. /// @param[in] item Item to insert before. If NULL, inserts at the end of the /// list. void tv_list_insert(list_T *const l, listitem_T *const ni, listitem_T *const item) FUNC_ATTR_NONNULL_ARG(1, 2) { if (item == NULL) { // Append new item at end of list. tv_list_append(l, ni); } else { // Insert new item before existing item. ni->li_prev = item->li_prev; ni->li_next = item; if (item->li_prev == NULL) { l->lv_first = ni; l->lv_idx++; } else { item->li_prev->li_next = ni; l->lv_idx_item = NULL; } item->li_prev = ni; l->lv_len++; list_log(l, ni, item, "insert"); } } /// Insert VimL value into a list /// /// @param[out] l List to insert to. /// @param[in,out] tv Value to insert. Is copied (@see tv_copy()) to an /// allocated listitem_T and inserted. /// @param[in] item Item to insert before. If NULL, inserts at the end of the /// list. void tv_list_insert_tv(list_T *const l, typval_T *const tv, listitem_T *const item) { listitem_T *const ni = tv_list_item_alloc(); tv_copy(tv, &ni->li_tv); tv_list_insert(l, ni, item); } /// Append item to the end of list /// /// @param[out] l List to append to. /// @param[in,out] item Item to append. void tv_list_append(list_T *const l, listitem_T *const item) FUNC_ATTR_NONNULL_ALL { list_log(l, item, NULL, "append"); if (l->lv_last == NULL) { // empty list l->lv_first = item; l->lv_last = item; item->li_prev = NULL; } else { l->lv_last->li_next = item; item->li_prev = l->lv_last; l->lv_last = item; } l->lv_len++; item->li_next = NULL; } /// Append VimL value to the end of list /// /// @param[out] l List to append to. /// @param[in,out] tv Value to append. Is copied (@see tv_copy()) to an /// allocated listitem_T. void tv_list_append_tv(list_T *const l, typval_T *const tv) FUNC_ATTR_NONNULL_ALL { listitem_T *const li = tv_list_item_alloc(); tv_copy(tv, TV_LIST_ITEM_TV(li)); tv_list_append(l, li); } /// Like tv_list_append_tv(), but tv is moved to a list /// /// This means that it is no longer valid to use contents of the typval_T after /// function exits. void tv_list_append_owned_tv(list_T *const l, typval_T tv) FUNC_ATTR_NONNULL_ALL { listitem_T *const li = tv_list_item_alloc(); *TV_LIST_ITEM_TV(li) = tv; tv_list_append(l, li); } /// Append a list to a list as one item /// /// @param[out] l List to append to. /// @param[in,out] itemlist List to append. Reference count is increased. void tv_list_append_list(list_T *const l, list_T *const itemlist) FUNC_ATTR_NONNULL_ARG(1) { tv_list_append_owned_tv(l, (typval_T) { .v_type = VAR_LIST, .v_lock = VAR_UNLOCKED, .vval.v_list = itemlist, }); tv_list_ref(itemlist); } /// Append a dictionary to a list /// /// @param[out] l List to append to. /// @param[in,out] dict Dictionary to append. Reference count is increased. void tv_list_append_dict(list_T *const l, dict_T *const dict) FUNC_ATTR_NONNULL_ARG(1) { tv_list_append_owned_tv(l, (typval_T) { .v_type = VAR_DICT, .v_lock = VAR_UNLOCKED, .vval.v_dict = dict, }); if (dict != NULL) { dict->dv_refcount++; } } /// Make a copy of "str" and append it as an item to list "l" /// /// @param[out] l List to append to. /// @param[in] str String to append. /// @param[in] len Length of the appended string. May be -1, in this /// case string is considered to be usual zero-terminated /// string or NULL “empty” string. void tv_list_append_string(list_T *const l, const char *const str, const ssize_t len) FUNC_ATTR_NONNULL_ARG(1) { tv_list_append_owned_tv(l, (typval_T) { .v_type = VAR_STRING, .v_lock = VAR_UNLOCKED, .vval.v_string = (str == NULL ? NULL : (len >= 0 ? xmemdupz(str, (size_t)len) : xstrdup(str))), }); } /// Append given string to the list /// /// Unlike list_append_string this function does not copy the string. /// /// @param[out] l List to append to. /// @param[in] str String to append. void tv_list_append_allocated_string(list_T *const l, char *const str) FUNC_ATTR_NONNULL_ARG(1) { tv_list_append_owned_tv(l, (typval_T) { .v_type = VAR_STRING, .v_lock = VAR_UNLOCKED, .vval.v_string = str, }); } /// Append number to the list /// /// @param[out] l List to append to. /// @param[in] n Number to append. Will be recorded in the allocated /// listitem_T. void tv_list_append_number(list_T *const l, const varnumber_T n) { tv_list_append_owned_tv(l, (typval_T) { .v_type = VAR_NUMBER, .v_lock = VAR_UNLOCKED, .vval.v_number = n, }); } //{{{2 Operations on the whole list /// Make a copy of list /// /// @param[in] conv If non-NULL, then all internal strings will be converted. /// Only used when `deep` is true. /// @param[in] orig Original list to copy. /// @param[in] deep If false, then shallow copy will be done. /// @param[in] copyID See var_item_copy(). /// /// @return Copied list. May be NULL in case original list is NULL or some /// failure happens. The refcount of the new list is set to 1. list_T *tv_list_copy(const vimconv_T *const conv, list_T *const orig, const bool deep, const int copyID) FUNC_ATTR_WARN_UNUSED_RESULT { if (orig == NULL) { return NULL; } list_T *copy = tv_list_alloc(tv_list_len(orig)); tv_list_ref(copy); if (copyID != 0) { // Do this before adding the items, because one of the items may // refer back to this list. orig->lv_copyID = copyID; orig->lv_copylist = copy; } TV_LIST_ITER(orig, item, { if (got_int) { break; } listitem_T *const ni = tv_list_item_alloc(); if (deep) { if (var_item_copy(conv, TV_LIST_ITEM_TV(item), TV_LIST_ITEM_TV(ni), deep, copyID) == FAIL) { xfree(ni); goto tv_list_copy_error; } } else { tv_copy(TV_LIST_ITEM_TV(item), TV_LIST_ITEM_TV(ni)); } tv_list_append(copy, ni); }); return copy; tv_list_copy_error: tv_list_unref(copy); return NULL; } /// Flatten "list" in place to depth "maxdepth". /// Does nothing if "maxdepth" is 0. /// /// @param[in,out] list List to flatten /// @param[in] maxdepth Maximum depth that will be flattened /// /// @return OK or FAIL int tv_list_flatten(list_T *list, long maxdepth) FUNC_ATTR_NONNULL_ARG(1) FUNC_ATTR_WARN_UNUSED_RESULT { listitem_T *item; listitem_T *to_free; int n; if (maxdepth == 0) { return OK; } n = 0; item = list->lv_first; while (item != NULL) { fast_breakcheck(); if (got_int) { return FAIL; } if (item->li_tv.v_type == VAR_LIST) { listitem_T *next = item->li_next; tv_list_drop_items(list, item, item); tv_list_extend(list, item->li_tv.vval.v_list, next); tv_clear(&item->li_tv); to_free = item; if (item->li_prev == NULL) { item = list->lv_first; } else { item = item->li_prev->li_next; } xfree(to_free); if (++n >= maxdepth) { n = 0; item = next; } } else { n = 0; item = item->li_next; } } return OK; } /// Extend first list with the second /// /// @param[out] l1 List to extend. /// @param[in] l2 List to extend with. /// @param[in] bef If not NULL, extends before this item. void tv_list_extend(list_T *const l1, list_T *const l2, listitem_T *const bef) FUNC_ATTR_NONNULL_ARG(1) { int todo = tv_list_len(l2); listitem_T *const befbef = (bef == NULL ? NULL : bef->li_prev); listitem_T *const saved_next = (befbef == NULL ? NULL : befbef->li_next); // We also quit the loop when we have inserted the original item count of // the list, avoid a hang when we extend a list with itself. for (listitem_T *item = tv_list_first(l2) ; item != NULL && todo-- ; item = (item == befbef ? saved_next : item->li_next)) { tv_list_insert_tv(l1, TV_LIST_ITEM_TV(item), bef); } } /// Concatenate lists into a new list /// /// @param[in] l1 First list. /// @param[in] l2 Second list. /// @param[out] ret_tv Location where new list is saved. /// /// @return OK or FAIL. int tv_list_concat(list_T *const l1, list_T *const l2, typval_T *const tv) FUNC_ATTR_WARN_UNUSED_RESULT { list_T *l; tv->v_type = VAR_LIST; tv->v_lock = VAR_UNLOCKED; if (l1 == NULL && l2 == NULL) { l = NULL; } else if (l1 == NULL) { l = tv_list_copy(NULL, l2, false, 0); } else { l = tv_list_copy(NULL, l1, false, 0); if (l != NULL && l2 != NULL) { tv_list_extend(l, l2, NULL); } } if (l == NULL && !(l1 == NULL && l2 == NULL)) { return FAIL; } tv->vval.v_list = l; return OK; } typedef struct { char_u *s; char_u *tofree; } Join; /// Join list into a string, helper function /// /// @param[out] gap Garray where result will be saved. /// @param[in] l List to join. /// @param[in] sep Used separator. /// @param[in] join_gap Garray to keep each list item string. /// /// @return OK in case of success, FAIL otherwise. static int list_join_inner(garray_T *const gap, list_T *const l, const char *const sep, garray_T *const join_gap) FUNC_ATTR_NONNULL_ALL { size_t sumlen = 0; bool first = true; // Stringify each item in the list. TV_LIST_ITER(l, item, { if (got_int) { break; } char *s; size_t len; s = encode_tv2echo(TV_LIST_ITEM_TV(item), &len); if (s == NULL) { return FAIL; } sumlen += len; Join *const p = GA_APPEND_VIA_PTR(Join, join_gap); p->tofree = p->s = (char_u *)s; line_breakcheck(); }); // 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 += strlen(sep) * (size_t)(join_gap->ga_len - 1); } ga_grow(gap, (int)sumlen + 2); for (int i = 0; i < join_gap->ga_len && !got_int; i++) { if (first) { first = false; } else { ga_concat(gap, sep); } const Join *const p = ((const Join *)join_gap->ga_data) + i; if (p->s != NULL) { ga_concat(gap, (char *)p->s); } line_breakcheck(); } return OK; } /// Join list into a string using given separator /// /// @param[out] gap Garray where result will be saved. /// @param[in] l Joined list. /// @param[in] sep Separator. /// /// @return OK in case of success, FAIL otherwise. int tv_list_join(garray_T *const gap, list_T *const l, const char *const sep) FUNC_ATTR_NONNULL_ARG(1) { if (!tv_list_len(l)) { return OK; } garray_T join_ga; int retval; ga_init(&join_ga, (int)sizeof(Join), tv_list_len(l)); retval = list_join_inner(gap, l, sep, &join_ga); #define FREE_JOIN_TOFREE(join) xfree((join)->tofree) GA_DEEP_CLEAR(&join_ga, Join, FREE_JOIN_TOFREE); #undef FREE_JOIN_TOFREE return retval; } /// "join()" function void f_join(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) { if (argvars[0].v_type != VAR_LIST) { emsg(_(e_listreq)); return; } const char *const sep = (argvars[1].v_type == VAR_UNKNOWN ? " " : tv_get_string_chk(&argvars[1])); rettv->v_type = VAR_STRING; if (sep != NULL) { garray_T ga; ga_init(&ga, (int)sizeof(char), 80); tv_list_join(&ga, argvars[0].vval.v_list, sep); ga_append(&ga, NUL); rettv->vval.v_string = ga.ga_data; } else { rettv->vval.v_string = NULL; } } /// "list2str()" function void f_list2str(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) { garray_T ga; rettv->v_type = VAR_STRING; rettv->vval.v_string = NULL; if (argvars[0].v_type != VAR_LIST) { emsg(_(e_invarg)); return; } list_T *const l = argvars[0].vval.v_list; if (l == NULL) { return; // empty list results in empty string } ga_init(&ga, 1, 80); char buf[MB_MAXBYTES + 1]; TV_LIST_ITER_CONST(l, li, { buf[utf_char2bytes((int)tv_get_number(TV_LIST_ITEM_TV(li)), (char *)buf)] = NUL; ga_concat(&ga, (char *)buf); }); ga_append(&ga, NUL); rettv->vval.v_string = ga.ga_data; } /// "remove({list})" function void tv_list_remove(typval_T *argvars, typval_T *rettv, const char *arg_errmsg) { list_T *l; bool error = false; if (value_check_lock(tv_list_locked((l = argvars[0].vval.v_list)), arg_errmsg, TV_TRANSLATE)) { return; } long idx = tv_get_number_chk(&argvars[1], &error); listitem_T *item; if (error) { // Type error: do nothing, errmsg already given. } else if ((item = tv_list_find(l, (int)idx)) == NULL) { semsg(_(e_listidx), (int64_t)idx); } else { if (argvars[2].v_type == VAR_UNKNOWN) { // Remove one item, return its value. tv_list_drop_items(l, item, item); *rettv = *TV_LIST_ITEM_TV(item); xfree(item); } else { listitem_T *item2; // Remove range of items, return list with values. long end = tv_get_number_chk(&argvars[2], &error); if (error) { // Type error: do nothing. } else if ((item2 = tv_list_find(l, (int)end)) == NULL) { semsg(_(e_listidx), (int64_t)end); } else { int cnt = 0; listitem_T *li; for (li = item; li != NULL; li = TV_LIST_ITEM_NEXT(l, li)) { cnt++; if (li == item2) { break; } } if (li == NULL) { // Didn't find "item2" after "item". emsg(_(e_invrange)); } else { tv_list_move_items(l, item, item2, tv_list_alloc_ret(rettv, cnt), cnt); } } } } } /// struct storing information about current sort typedef struct { int item_compare_ic; bool item_compare_lc; bool item_compare_numeric; bool item_compare_numbers; bool item_compare_float; const char *item_compare_func; partial_T *item_compare_partial; dict_T *item_compare_selfdict; bool item_compare_func_err; } sortinfo_T; static sortinfo_T *sortinfo = NULL; #define ITEM_COMPARE_FAIL 999 /// Compare functions for f_sort() and f_uniq() below. static int item_compare(const void *s1, const void *s2, bool keep_zero) { ListSortItem *const si1 = (ListSortItem *)s1; ListSortItem *const si2 = (ListSortItem *)s2; typval_T *const tv1 = TV_LIST_ITEM_TV(si1->item); typval_T *const tv2 = TV_LIST_ITEM_TV(si2->item); int res; if (sortinfo->item_compare_numbers) { const varnumber_T v1 = tv_get_number(tv1); const varnumber_T v2 = tv_get_number(tv2); res = v1 == v2 ? 0 : v1 > v2 ? 1 : -1; goto item_compare_end; } if (sortinfo->item_compare_float) { const float_T v1 = tv_get_float(tv1); const float_T v2 = tv_get_float(tv2); res = v1 == v2 ? 0 : v1 > v2 ? 1 : -1; goto item_compare_end; } char *tofree1 = NULL; char *tofree2 = NULL; char *p1; char *p2; // encode_tv2string() puts quotes around a string and allocates memory. Don't // do that for string variables. Use a single quote when comparing with // a non-string to do what the docs promise. if (tv1->v_type == VAR_STRING) { if (tv2->v_type != VAR_STRING || sortinfo->item_compare_numeric) { p1 = "'"; } else { p1 = tv1->vval.v_string; } } else { tofree1 = p1 = encode_tv2string(tv1, NULL); } if (tv2->v_type == VAR_STRING) { if (tv1->v_type != VAR_STRING || sortinfo->item_compare_numeric) { p2 = "'"; } else { p2 = tv2->vval.v_string; } } else { tofree2 = p2 = encode_tv2string(tv2, NULL); } if (p1 == NULL) { p1 = ""; } if (p2 == NULL) { p2 = ""; } if (!sortinfo->item_compare_numeric) { if (sortinfo->item_compare_lc) { res = strcoll(p1, p2); } else { res = sortinfo->item_compare_ic ? STRICMP(p1, p2): strcmp(p1, p2); } } else { double n1, n2; n1 = strtod(p1, &p1); n2 = strtod(p2, &p2); res = n1 == n2 ? 0 : n1 > n2 ? 1 : -1; } xfree(tofree1); xfree(tofree2); item_compare_end: // When the result would be zero, compare the item indexes. Makes the // sort stable. if (res == 0 && !keep_zero) { // WARNING: When using uniq si1 and si2 are actually listitem_T **, no // indexes are there. res = si1->idx > si2->idx ? 1 : -1; } return res; } static int item_compare_keeping_zero(const void *s1, const void *s2) { return item_compare(s1, s2, true); } static int item_compare_not_keeping_zero(const void *s1, const void *s2) { return item_compare(s1, s2, false); } static int item_compare2(const void *s1, const void *s2, bool keep_zero) { ListSortItem *si1, *si2; int res; typval_T rettv; typval_T argv[3]; const char *func_name; partial_T *partial = sortinfo->item_compare_partial; // shortcut after failure in previous call; compare all items equal if (sortinfo->item_compare_func_err) { return 0; } si1 = (ListSortItem *)s1; si2 = (ListSortItem *)s2; if (partial == NULL) { func_name = sortinfo->item_compare_func; } else { func_name = (const char *)partial_name(partial); } // Copy the values. This is needed to be able to set v_lock to VAR_FIXED // in the copy without changing the original list items. tv_copy(TV_LIST_ITEM_TV(si1->item), &argv[0]); tv_copy(TV_LIST_ITEM_TV(si2->item), &argv[1]); rettv.v_type = VAR_UNKNOWN; // tv_clear() uses this funcexe_T funcexe = FUNCEXE_INIT; funcexe.fe_evaluate = true; funcexe.fe_partial = partial; funcexe.fe_selfdict = sortinfo->item_compare_selfdict; res = call_func(func_name, -1, &rettv, 2, argv, &funcexe); tv_clear(&argv[0]); tv_clear(&argv[1]); if (res == FAIL) { // XXX: ITEM_COMPARE_FAIL is unused res = ITEM_COMPARE_FAIL; sortinfo->item_compare_func_err = true; } else { res = (int)tv_get_number_chk(&rettv, &sortinfo->item_compare_func_err); if (res > 0) { res = 1; } else if (res < 0) { res = -1; } } if (sortinfo->item_compare_func_err) { res = ITEM_COMPARE_FAIL; // return value has wrong type } tv_clear(&rettv); // When the result would be zero, compare the pointers themselves. Makes // the sort stable. if (res == 0 && !keep_zero) { // WARNING: When using uniq si1 and si2 are actually listitem_T **, no // indexes are there. res = si1->idx > si2->idx ? 1 : -1; } return res; } static int item_compare2_keeping_zero(const void *s1, const void *s2) { return item_compare2(s1, s2, true); } static int item_compare2_not_keeping_zero(const void *s1, const void *s2) { return item_compare2(s1, s2, false); } /// "sort({list})" function static void do_sort_uniq(typval_T *argvars, typval_T *rettv, bool sort) { ListSortItem *ptrs; long len; long i; // Pointer to current info struct used in compare function. Save and restore // the current one for nested calls. sortinfo_T info; sortinfo_T *old_sortinfo = sortinfo; sortinfo = &info; const char *const arg_errmsg = (sort ? N_("sort() argument") : N_("uniq() argument")); if (argvars[0].v_type != VAR_LIST) { semsg(_(e_listarg), sort ? "sort()" : "uniq()"); } else { list_T *const l = argvars[0].vval.v_list; if (value_check_lock(tv_list_locked(l), arg_errmsg, TV_TRANSLATE)) { goto theend; } tv_list_set_ret(rettv, l); len = tv_list_len(l); if (len <= 1) { goto theend; // short list sorts pretty quickly } info.item_compare_ic = false; info.item_compare_lc = false; info.item_compare_numeric = false; info.item_compare_numbers = false; info.item_compare_float = false; info.item_compare_func = NULL; info.item_compare_partial = NULL; info.item_compare_selfdict = NULL; if (argvars[1].v_type != VAR_UNKNOWN) { // optional second argument: {func} if (argvars[1].v_type == VAR_FUNC) { info.item_compare_func = (const char *)argvars[1].vval.v_string; } else if (argvars[1].v_type == VAR_PARTIAL) { info.item_compare_partial = argvars[1].vval.v_partial; } else { bool error = false; i = tv_get_number_chk(&argvars[1], &error); if (error) { goto theend; // type error; errmsg already given } if (i == 1) { info.item_compare_ic = true; } else if (argvars[1].v_type != VAR_NUMBER) { info.item_compare_func = tv_get_string(&argvars[1]); } else if (i != 0) { emsg(_(e_invarg)); goto theend; } if (info.item_compare_func != NULL) { if (*info.item_compare_func == NUL) { // empty string means default sort info.item_compare_func = NULL; } else if (strcmp(info.item_compare_func, "n") == 0) { info.item_compare_func = NULL; info.item_compare_numeric = true; } else if (strcmp(info.item_compare_func, "N") == 0) { info.item_compare_func = NULL; info.item_compare_numbers = true; } else if (strcmp(info.item_compare_func, "f") == 0) { info.item_compare_func = NULL; info.item_compare_float = true; } else if (strcmp(info.item_compare_func, "i") == 0) { info.item_compare_func = NULL; info.item_compare_ic = true; } else if (strcmp(info.item_compare_func, "l") == 0) { info.item_compare_func = NULL; info.item_compare_lc = true; } } } if (argvars[2].v_type != VAR_UNKNOWN) { // optional third argument: {dict} if (argvars[2].v_type != VAR_DICT) { emsg(_(e_dictreq)); goto theend; } info.item_compare_selfdict = argvars[2].vval.v_dict; } } // Make an array with each entry pointing to an item in the List. ptrs = xmalloc((size_t)((unsigned)len * sizeof(ListSortItem))); if (sort) { info.item_compare_func_err = false; tv_list_item_sort(l, ptrs, ((info.item_compare_func == NULL && info.item_compare_partial == NULL) ? item_compare_not_keeping_zero : item_compare2_not_keeping_zero), &info.item_compare_func_err); if (info.item_compare_func_err) { emsg(_("E702: Sort compare function failed")); } } else { ListSorter item_compare_func_ptr; // f_uniq(): ptrs will be a stack of items to remove. info.item_compare_func_err = false; if (info.item_compare_func != NULL || info.item_compare_partial != NULL) { item_compare_func_ptr = item_compare2_keeping_zero; } else { item_compare_func_ptr = item_compare_keeping_zero; } for (listitem_T *li = TV_LIST_ITEM_NEXT(l, tv_list_first(l)) ; li != NULL;) { listitem_T *const prev_li = TV_LIST_ITEM_PREV(l, li); if (item_compare_func_ptr(&prev_li, &li) == 0) { li = tv_list_item_remove(l, li); } else { li = TV_LIST_ITEM_NEXT(l, li); } if (info.item_compare_func_err) { emsg(_("E882: Uniq compare function failed")); break; } } } xfree(ptrs); } theend: sortinfo = old_sortinfo; } /// "sort"({list})" function void f_sort(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) { do_sort_uniq(argvars, rettv, true); } /// "uniq({list})" function void f_uniq(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) { do_sort_uniq(argvars, rettv, false); } /// Check whether two lists are equal /// /// @param[in] l1 First list to compare. /// @param[in] l2 Second list to compare. /// @param[in] ic True if case is to be ignored. /// @param[in] recursive True when used recursively. /// /// @return True if lists are equal, false otherwise. bool tv_list_equal(list_T *const l1, list_T *const l2, const bool ic, const bool recursive) FUNC_ATTR_WARN_UNUSED_RESULT { if (l1 == l2) { return true; } if (tv_list_len(l1) != tv_list_len(l2)) { return false; } if (tv_list_len(l1) == 0) { // empty and NULL list are considered equal return true; } if (l1 == NULL || l2 == NULL) { return false; } listitem_T *item1 = tv_list_first(l1); listitem_T *item2 = tv_list_first(l2); for (; item1 != NULL && item2 != NULL ; (item1 = TV_LIST_ITEM_NEXT(l1, item1), item2 = TV_LIST_ITEM_NEXT(l2, item2))) { if (!tv_equal(TV_LIST_ITEM_TV(item1), TV_LIST_ITEM_TV(item2), ic, recursive)) { return false; } } assert(item1 == NULL && item2 == NULL); return true; } /// Reverse list in-place /// /// @param[in,out] l List to reverse. void tv_list_reverse(list_T *const l) { if (tv_list_len(l) <= 1) { return; } list_log(l, NULL, NULL, "reverse"); #define SWAP(a, b) \ do { \ tmp = (a); \ (a) = (b); \ (b) = tmp; \ } while (0) listitem_T *tmp; SWAP(l->lv_first, l->lv_last); for (listitem_T *li = l->lv_first; li != NULL; li = li->li_next) { SWAP(li->li_next, li->li_prev); } #undef SWAP l->lv_idx = l->lv_len - l->lv_idx - 1; } // FIXME Add unit tests for tv_list_item_sort(). /// Sort list using libc qsort /// /// @param[in,out] l List to sort, will be sorted in-place. /// @param ptrs Preallocated array of items to sort, must have at least /// tv_list_len(l) entries. Should not be initialized. /// @param[in] item_compare_func Function used to compare list items. /// @param errp Location where information about whether error occurred is /// saved by item_compare_func. If boolean there appears to be /// true list will not be modified. Must be initialized to false /// by the caller. void tv_list_item_sort(list_T *const l, ListSortItem *const ptrs, const ListSorter item_compare_func, const bool *errp) FUNC_ATTR_NONNULL_ARG(3, 4) { const int len = tv_list_len(l); if (len <= 1) { return; } list_log(l, NULL, NULL, "sort"); int i = 0; TV_LIST_ITER(l, li, { ptrs[i].item = li; ptrs[i].idx = i; i++; }); // Sort the array with item pointers. qsort(ptrs, (size_t)len, sizeof(ListSortItem), item_compare_func); if (!(*errp)) { // Clear the list and append the items in the sorted order. l->lv_first = NULL; l->lv_last = NULL; l->lv_idx_item = NULL; l->lv_len = 0; for (i = 0; i < len; i++) { tv_list_append(l, ptrs[i].item); } } } //{{{2 Indexing/searching /// Locate item with a given index in a list and return it /// /// @param[in] l List to index. /// @param[in] n Index. Negative index is counted from the end, -1 is the last /// item. /// /// @return Item at the given index or NULL if `n` is out of range. listitem_T *tv_list_find(list_T *const l, int n) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT { STATIC_ASSERT(sizeof(n) == sizeof(l->lv_idx), "n and lv_idx sizes do not match"); if (l == NULL) { return NULL; } n = tv_list_uidx(l, n); if (n == -1) { return NULL; } int idx; listitem_T *item; // When there is a cached index may start search from there. if (l->lv_idx_item != NULL) { if (n < l->lv_idx / 2) { // Closest to the start of the list. item = l->lv_first; idx = 0; } else if (n > (l->lv_idx + l->lv_len) / 2) { // Closest to the end of the list. item = l->lv_last; idx = l->lv_len - 1; } else { // Closest to the cached index. item = l->lv_idx_item; idx = l->lv_idx; } } else { if (n < l->lv_len / 2) { // Closest to the start of the list. item = l->lv_first; idx = 0; } else { // Closest to the end of the list. item = l->lv_last; idx = l->lv_len - 1; } } while (n > idx) { // Search forward. item = item->li_next; idx++; } while (n < idx) { // Search backward. item = item->li_prev; idx--; } assert(idx == n); // Cache the used index. l->lv_idx = idx; l->lv_idx_item = item; list_log(l, l->lv_idx_item, (void *)(uintptr_t)l->lv_idx, "find"); return item; } /// Get list item l[n] as a number /// /// @param[in] l List to index. /// @param[in] n Index in a list. /// @param[out] ret_error Location where 1 will be saved if index was not /// found. May be NULL. If everything is OK, /// `*ret_error` is not touched. /// /// @return Integer value at the given index or -1. varnumber_T tv_list_find_nr(list_T *const l, const int n, bool *const ret_error) FUNC_ATTR_WARN_UNUSED_RESULT { const listitem_T *const li = tv_list_find(l, n); if (li == NULL) { if (ret_error != NULL) { *ret_error = true; } return -1; } return tv_get_number_chk(TV_LIST_ITEM_TV(li), ret_error); } /// Get list item l[n] as a string /// /// @param[in] l List to index. /// @param[in] n Index in a list. /// /// @return List item string value or NULL in case of error. const char *tv_list_find_str(list_T *const l, const int n) FUNC_ATTR_WARN_UNUSED_RESULT { const listitem_T *const li = tv_list_find(l, n); if (li == NULL) { semsg(_(e_listidx), (int64_t)n); return NULL; } return tv_get_string(TV_LIST_ITEM_TV(li)); } /// Locate item in a list and return its index /// /// @param[in] l List to search. /// @param[in] item Item to search for. /// /// @return Index of an item or -1 if item is not in the list. long tv_list_idx_of_item(const list_T *const l, const listitem_T *const item) FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE { if (l == NULL) { return -1; } int idx = 0; TV_LIST_ITER_CONST(l, li, { if (li == item) { return idx; } idx++; }); return -1; } //{{{1 Dictionaries //{{{2 Dictionary watchers /// Perform all necessary cleanup for a `DictWatcher` instance /// /// @param watcher Watcher to free. static void tv_dict_watcher_free(DictWatcher *watcher) FUNC_ATTR_NONNULL_ALL { callback_free(&watcher->callback); xfree(watcher->key_pattern); xfree(watcher); } /// Add watcher to a dictionary /// /// @param[in] dict Dictionary to add watcher to. /// @param[in] key_pattern Pattern to watch for. /// @param[in] key_pattern_len Key pattern length. /// @param callback Function to be called on events. void tv_dict_watcher_add(dict_T *const dict, const char *const key_pattern, const size_t key_pattern_len, Callback callback) FUNC_ATTR_NONNULL_ARG(2) { if (dict == NULL) { return; } DictWatcher *const watcher = xmalloc(sizeof(DictWatcher)); watcher->key_pattern = xmemdupz(key_pattern, key_pattern_len); watcher->key_pattern_len = key_pattern_len; watcher->callback = callback; watcher->busy = false; watcher->needs_free = false; QUEUE_INSERT_TAIL(&dict->watchers, &watcher->node); } /// Check whether two callbacks are equal /// /// @param[in] cb1 First callback to check. /// @param[in] cb2 Second callback to check. /// /// @return True if they are equal, false otherwise. bool tv_callback_equal(const Callback *cb1, const Callback *cb2) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { if (cb1->type != cb2->type) { return false; } switch (cb1->type) { case kCallbackFuncref: return strcmp(cb1->data.funcref, cb2->data.funcref) == 0; case kCallbackPartial: // FIXME: this is inconsistent with tv_equal but is needed for precision // maybe change dictwatcheradd to return a watcher id instead? return cb1->data.partial == cb2->data.partial; case kCallbackLua: return cb1->data.luaref == cb2->data.luaref; case kCallbackNone: return true; } abort(); return false; } /// Unref/free callback void callback_free(Callback *callback) FUNC_ATTR_NONNULL_ALL { switch (callback->type) { case kCallbackFuncref: func_unref(callback->data.funcref); xfree(callback->data.funcref); break; case kCallbackPartial: partial_unref(callback->data.partial); break; case kCallbackLua: NLUA_CLEAR_REF(callback->data.luaref); break; case kCallbackNone: break; } callback->type = kCallbackNone; callback->data.funcref = NULL; } /// Copy a callback into a typval_T. void callback_put(Callback *cb, typval_T *tv) FUNC_ATTR_NONNULL_ALL { switch (cb->type) { case kCallbackPartial: tv->v_type = VAR_PARTIAL; tv->vval.v_partial = cb->data.partial; cb->data.partial->pt_refcount++; break; case kCallbackFuncref: tv->v_type = VAR_FUNC; tv->vval.v_string = xstrdup(cb->data.funcref); func_ref(cb->data.funcref); break; case kCallbackLua: // TODO(tjdevries): Unified Callback. // At this point this isn't possible, but it'd be nice to put // these handled more neatly in one place. // So instead, we just do the default and put nil default: tv->v_type = VAR_SPECIAL; tv->vval.v_special = kSpecialVarNull; break; } } // Copy callback from "src" to "dest", incrementing the refcounts. void callback_copy(Callback *dest, Callback *src) FUNC_ATTR_NONNULL_ALL { dest->type = src->type; switch (src->type) { case kCallbackPartial: dest->data.partial = src->data.partial; dest->data.partial->pt_refcount++; break; case kCallbackFuncref: dest->data.funcref = xstrdup(src->data.funcref); func_ref(src->data.funcref); break; case kCallbackLua: dest->data.luaref = api_new_luaref(src->data.luaref); break; default: dest->data.funcref = NULL; break; } } /// Generate a string description of a callback char *callback_to_string(Callback *cb) { if (cb->type == kCallbackLua) { return nlua_funcref_str(cb->data.luaref); } const size_t msglen = 100; char *msg = (char *)xmallocz(msglen); switch (cb->type) { case kCallbackFuncref: // TODO(tjdevries): Is this enough space for this? snprintf(msg, msglen, "", cb->data.funcref); break; case kCallbackPartial: snprintf(msg, msglen, "", cb->data.partial->pt_name); break; default: *msg = '\0'; break; } return msg; } /// Remove watcher from a dictionary /// /// @param dict Dictionary to remove watcher from. /// @param[in] key_pattern Pattern to remove watcher for. /// @param[in] key_pattern_len Pattern length. /// @param callback Callback to remove watcher for. /// /// @return True on success, false if relevant watcher was not found. bool tv_dict_watcher_remove(dict_T *const dict, const char *const key_pattern, const size_t key_pattern_len, Callback callback) FUNC_ATTR_NONNULL_ARG(2) { if (dict == NULL) { return false; } QUEUE *w = NULL; DictWatcher *watcher = NULL; bool matched = false; bool queue_is_busy = false; QUEUE_FOREACH(w, &dict->watchers, { watcher = tv_dict_watcher_node_data(w); if (watcher->busy) { queue_is_busy = true; } if (tv_callback_equal(&watcher->callback, &callback) && watcher->key_pattern_len == key_pattern_len && memcmp(watcher->key_pattern, key_pattern, key_pattern_len) == 0) { matched = true; break; } }) if (!matched) { return false; } if (queue_is_busy) { watcher->needs_free = true; } else { QUEUE_REMOVE(w); tv_dict_watcher_free(watcher); } return true; } /// Test if `key` matches with with `watcher->key_pattern` /// /// @param[in] watcher Watcher to check key pattern from. /// @param[in] key Key to check. /// /// @return true if key matches, false otherwise. static bool tv_dict_watcher_matches(DictWatcher *watcher, const char *const key) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE { // For now only allow very simple globbing in key patterns: a '*' at the end // of the string means it should match everything up to the '*' instead of the // whole string. const size_t len = watcher->key_pattern_len; if (len && watcher->key_pattern[len - 1] == '*') { return strncmp(key, watcher->key_pattern, len - 1) == 0; } return strcmp(key, watcher->key_pattern) == 0; } /// Send a change notification to all dictionary watchers that match given key /// /// @param[in] dict Dictionary which was modified. /// @param[in] key Key which was modified. /// @param[in] newtv New key value. /// @param[in] oldtv Old key value. void tv_dict_watcher_notify(dict_T *const dict, const char *const key, typval_T *const newtv, typval_T *const oldtv) FUNC_ATTR_NONNULL_ARG(1, 2) { typval_T argv[3]; argv[0].v_type = VAR_DICT; argv[0].v_lock = VAR_UNLOCKED; argv[0].vval.v_dict = dict; argv[1].v_type = VAR_STRING; argv[1].v_lock = VAR_UNLOCKED; argv[1].vval.v_string = xstrdup(key); argv[2].v_type = VAR_DICT; argv[2].v_lock = VAR_UNLOCKED; argv[2].vval.v_dict = tv_dict_alloc(); argv[2].vval.v_dict->dv_refcount++; if (newtv) { dictitem_T *const v = tv_dict_item_alloc_len(S_LEN("new")); tv_copy(newtv, &v->di_tv); tv_dict_add(argv[2].vval.v_dict, v); } if (oldtv && oldtv->v_type != VAR_UNKNOWN) { dictitem_T *const v = tv_dict_item_alloc_len(S_LEN("old")); tv_copy(oldtv, &v->di_tv); tv_dict_add(argv[2].vval.v_dict, v); } typval_T rettv; bool any_needs_free = false; dict->dv_refcount++; QUEUE *w; QUEUE_FOREACH(w, &dict->watchers, { DictWatcher *watcher = tv_dict_watcher_node_data(w); if (!watcher->busy && tv_dict_watcher_matches(watcher, key)) { rettv = TV_INITIAL_VALUE; watcher->busy = true; callback_call(&watcher->callback, 3, argv, &rettv); watcher->busy = false; tv_clear(&rettv); if (watcher->needs_free) { any_needs_free = true; } } }) if (any_needs_free) { QUEUE_FOREACH(w, &dict->watchers, { DictWatcher *watcher = tv_dict_watcher_node_data(w); if (watcher->needs_free) { QUEUE_REMOVE(w); tv_dict_watcher_free(watcher); } }) } tv_dict_unref(dict); for (size_t i = 1; i < ARRAY_SIZE(argv); i++) { tv_clear(argv + i); } } //{{{2 Dictionary item /// Allocate a dictionary item /// /// @note that the type and value of the item (->di_tv) still needs to /// be initialized. /// /// @param[in] key Key, is copied to the new item. /// @param[in] key_len Key length. /// /// @return [allocated] new dictionary item. dictitem_T *tv_dict_item_alloc_len(const char *const key, const size_t key_len) FUNC_ATTR_NONNULL_RET FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_MALLOC { dictitem_T *const di = xmalloc(offsetof(dictitem_T, di_key) + key_len + 1); memcpy(di->di_key, key, key_len); di->di_key[key_len] = NUL; di->di_flags = DI_FLAGS_ALLOC; di->di_tv.v_lock = VAR_UNLOCKED; di->di_tv.v_type = VAR_UNKNOWN; return di; } /// Allocate a dictionary item /// /// @note that the type and value of the item (->di_tv) still needs to /// be initialized. /// /// @param[in] key Key, is copied to the new item. /// /// @return [allocated] new dictionary item. dictitem_T *tv_dict_item_alloc(const char *const key) FUNC_ATTR_NONNULL_RET FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_MALLOC { return tv_dict_item_alloc_len(key, strlen(key)); } /// Free a dictionary item, also clearing the value /// /// @param item Item to free. void tv_dict_item_free(dictitem_T *const item) FUNC_ATTR_NONNULL_ALL { tv_clear(&item->di_tv); if (item->di_flags & DI_FLAGS_ALLOC) { xfree(item); } } /// Make a copy of a dictionary item /// /// @param[in] di Item to copy. /// /// @return [allocated] new dictionary item. dictitem_T *tv_dict_item_copy(dictitem_T *const di) FUNC_ATTR_NONNULL_RET FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { dictitem_T *const new_di = tv_dict_item_alloc((const char *)di->di_key); tv_copy(&di->di_tv, &new_di->di_tv); return new_di; } /// Remove item from dictionary and free it /// /// @param dict Dictionary to remove item from. /// @param item Item to remove. void tv_dict_item_remove(dict_T *const dict, dictitem_T *const item) FUNC_ATTR_NONNULL_ALL { hashitem_T *const hi = hash_find(&dict->dv_hashtab, (char *)item->di_key); if (HASHITEM_EMPTY(hi)) { semsg(_(e_intern2), "tv_dict_item_remove()"); } else { hash_remove(&dict->dv_hashtab, hi); } tv_dict_item_free(item); } //{{{2 Alloc/free /// Allocate an empty dictionary. /// Caller should take care of the reference count. /// /// @return [allocated] new dictionary. dict_T *tv_dict_alloc(void) FUNC_ATTR_NONNULL_RET FUNC_ATTR_WARN_UNUSED_RESULT { dict_T *const d = xcalloc(1, sizeof(dict_T)); // Add the dict to the list of dicts for garbage collection. if (gc_first_dict != NULL) { gc_first_dict->dv_used_prev = d; } d->dv_used_next = gc_first_dict; d->dv_used_prev = NULL; gc_first_dict = d; hash_init(&d->dv_hashtab); d->dv_lock = VAR_UNLOCKED; d->dv_scope = VAR_NO_SCOPE; d->dv_refcount = 0; d->dv_copyID = 0; QUEUE_INIT(&d->watchers); d->lua_table_ref = LUA_NOREF; return d; } /// Free items contained in a dictionary /// /// @param[in,out] d Dictionary to clear. void tv_dict_free_contents(dict_T *const d) FUNC_ATTR_NONNULL_ALL { // Lock the hashtab, we don't want it to resize while freeing items. hash_lock(&d->dv_hashtab); assert(d->dv_hashtab.ht_locked > 0); HASHTAB_ITER(&d->dv_hashtab, hi, { // Remove the item before deleting it, just in case there is // something recursive causing trouble. dictitem_T *const di = TV_DICT_HI2DI(hi); hash_remove(&d->dv_hashtab, hi); tv_dict_item_free(di); }); while (!QUEUE_EMPTY(&d->watchers)) { QUEUE *w = QUEUE_HEAD(&d->watchers); QUEUE_REMOVE(w); DictWatcher *watcher = tv_dict_watcher_node_data(w); tv_dict_watcher_free(watcher); } hash_clear(&d->dv_hashtab); d->dv_hashtab.ht_locked--; hash_init(&d->dv_hashtab); } /// Free a dictionary itself, ignoring items it contains /// /// Ignores the reference count. /// /// @param[in,out] d Dictionary to free. void tv_dict_free_dict(dict_T *const d) FUNC_ATTR_NONNULL_ALL { // Remove the dict from the list of dicts for garbage collection. if (d->dv_used_prev == NULL) { gc_first_dict = d->dv_used_next; } else { d->dv_used_prev->dv_used_next = d->dv_used_next; } if (d->dv_used_next != NULL) { d->dv_used_next->dv_used_prev = d->dv_used_prev; } NLUA_CLEAR_REF(d->lua_table_ref); xfree(d); } /// Free a dictionary, including all items it contains /// /// Ignores the reference count. /// /// @param d Dictionary to free. void tv_dict_free(dict_T *const d) FUNC_ATTR_NONNULL_ALL { if (!tv_in_free_unref_items) { tv_dict_free_contents(d); tv_dict_free_dict(d); } } /// Unreference a dictionary /// /// Decrements the reference count and frees dictionary when it becomes zero. /// /// @param[in] d Dictionary to operate on. void tv_dict_unref(dict_T *const d) { if (d != NULL && --d->dv_refcount <= 0) { tv_dict_free(d); } } //{{{2 Indexing/searching /// Find item in dictionary /// /// @param[in] d Dictionary to check. /// @param[in] key Dictionary key. /// @param[in] len Key length. If negative, then strlen(key) is used. /// /// @return found item or NULL if nothing was found. dictitem_T *tv_dict_find(const dict_T *const d, const char *const key, const ptrdiff_t len) FUNC_ATTR_NONNULL_ARG(2) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT { if (d == NULL) { return NULL; } hashitem_T *const hi = (len < 0 ? hash_find(&d->dv_hashtab, key) : hash_find_len(&d->dv_hashtab, key, (size_t)len)); if (HASHITEM_EMPTY(hi)) { return NULL; } return TV_DICT_HI2DI(hi); } /// Get a typval item from a dictionary and copy it into "rettv". /// /// @param[in] d Dictionary to check. /// @param[in] key Dictionary key. /// @param[in] rettv Return value. /// @return OK in case of success or FAIL if nothing was found. int tv_dict_get_tv(dict_T *d, const char *const key, typval_T *rettv) { dictitem_T *const di = tv_dict_find(d, key, -1); if (di == NULL) { return FAIL; } tv_copy(&di->di_tv, rettv); return OK; } /// Get a number item from a dictionary /// /// Returns 0 if the entry does not exist. /// /// @param[in] d Dictionary to get item from. /// @param[in] key Key to find in dictionary. /// /// @return Dictionary item. varnumber_T tv_dict_get_number(const dict_T *const d, const char *const key) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT { return tv_dict_get_number_def(d, key, 0); } /// Get a number item from a dictionary. /// /// Returns "def" if the entry doesn't exist. /// /// @param[in] d Dictionary to get item from. /// @param[in] key Key to find in dictionary. /// @param[in] def Default value. /// /// @return Dictionary item. varnumber_T tv_dict_get_number_def(const dict_T *const d, const char *const key, const int def) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT { dictitem_T *const di = tv_dict_find(d, key, -1); if (di == NULL) { return def; } return tv_get_number(&di->di_tv); } /// Converts a dict to an environment char **tv_dict_to_env(dict_T *denv) { size_t env_size = (size_t)tv_dict_len(denv); size_t i = 0; char **env = NULL; // + 1 for NULL env = xmalloc((env_size + 1) * sizeof(*env)); TV_DICT_ITER(denv, var, { const char *str = tv_get_string(&var->di_tv); assert(str); size_t len = strlen((char *)var->di_key) + strlen(str) + strlen("=") + 1; env[i] = xmalloc(len); snprintf(env[i], len, "%s=%s", (char *)var->di_key, str); i++; }); // must be null terminated env[env_size] = NULL; return env; } /// Get a string item from a dictionary /// /// @param[in] d Dictionary to get item from. /// @param[in] key Dictionary key. /// @param[in] save If true, returned string will be placed in the allocated /// memory. /// /// @return NULL if key does not exist, empty string in case of type error, /// string item value otherwise. If returned value is not NULL, it may /// be allocated depending on `save` argument. char *tv_dict_get_string(const dict_T *const d, const char *const key, const bool save) FUNC_ATTR_WARN_UNUSED_RESULT { static char numbuf[NUMBUFLEN]; const char *const s = tv_dict_get_string_buf(d, key, numbuf); if (save && s != NULL) { return xstrdup(s); } return (char *)s; } /// Get a string item from a dictionary /// /// @param[in] d Dictionary to get item from. /// @param[in] key Dictionary key. /// @param[in] numbuf Buffer for non-string items converted to strings, at /// least of #NUMBUFLEN length. /// /// @return NULL if key does not exist, empty string in case of type error, /// string item value otherwise. const char *tv_dict_get_string_buf(const dict_T *const d, const char *const key, char *const numbuf) FUNC_ATTR_WARN_UNUSED_RESULT { const dictitem_T *const di = tv_dict_find(d, key, -1); if (di == NULL) { return NULL; } return tv_get_string_buf(&di->di_tv, numbuf); } /// Get a string item from a dictionary /// /// @param[in] d Dictionary to get item from. /// @param[in] key Dictionary key. /// @param[in] key_len Key length. /// @param[in] numbuf Buffer for non-string items converted to strings, at /// least of #NUMBUFLEN length. /// @param[in] def Default return when key does not exist. /// /// @return `def` when key does not exist, /// NULL in case of type error, /// string item value in case of success. const char *tv_dict_get_string_buf_chk(const dict_T *const d, const char *const key, const ptrdiff_t key_len, char *const numbuf, const char *const def) FUNC_ATTR_WARN_UNUSED_RESULT { const dictitem_T *const di = tv_dict_find(d, key, key_len); if (di == NULL) { return def; } return tv_get_string_buf_chk(&di->di_tv, numbuf); } /// Get a function from a dictionary /// /// @param[in] d Dictionary to get callback from. /// @param[in] key Dictionary key. /// @param[in] key_len Key length, may be -1 to use strlen(). /// @param[out] result The address where a pointer to the wanted callback /// will be left. /// /// @return true/false on success/failure. bool tv_dict_get_callback(dict_T *const d, const char *const key, const ptrdiff_t key_len, Callback *const result) FUNC_ATTR_NONNULL_ARG(2, 4) FUNC_ATTR_WARN_UNUSED_RESULT { result->type = kCallbackNone; dictitem_T *const di = tv_dict_find(d, key, key_len); if (di == NULL) { return true; } if (!tv_is_func(di->di_tv) && di->di_tv.v_type != VAR_STRING) { emsg(_("E6000: Argument is not a function or function name")); return false; } typval_T tv; tv_copy(&di->di_tv, &tv); set_selfdict(&tv, d); const bool res = callback_from_typval(result, &tv); tv_clear(&tv); return res; } /// Check for adding a function to g: or l:. /// If the name is wrong give an error message and return true. int tv_dict_wrong_func_name(dict_T *d, typval_T *tv, const char *name) { return (d == &globvardict || &d->dv_hashtab == get_funccal_local_ht()) && tv_is_func(*tv) && var_wrong_func_name(name, true); } //{{{2 dict_add* /// Add item to dictionary /// /// @param[out] d Dictionary to add to. /// @param[in] item Item to add. /// /// @return FAIL if key already exists. int tv_dict_add(dict_T *const d, dictitem_T *const item) FUNC_ATTR_NONNULL_ALL { if (tv_dict_wrong_func_name(d, &item->di_tv, (const char *)item->di_key)) { return FAIL; } return hash_add(&d->dv_hashtab, (char *)item->di_key); } /// Add a list entry to dictionary /// /// @param[out] d Dictionary to add entry to. /// @param[in] key Key to add. /// @param[in] key_len Key length. /// @param list List to add. Will have reference count incremented. /// /// @return OK in case of success, FAIL when key already exists. int tv_dict_add_list(dict_T *const d, const char *const key, const size_t key_len, list_T *const list) FUNC_ATTR_NONNULL_ALL { dictitem_T *const item = tv_dict_item_alloc_len(key, key_len); item->di_tv.v_type = VAR_LIST; item->di_tv.vval.v_list = list; tv_list_ref(list); if (tv_dict_add(d, item) == FAIL) { tv_dict_item_free(item); return FAIL; } return OK; } /// Add a typval entry to dictionary. /// /// @param[out] d Dictionary to add entry to. /// @param[in] key Key to add. /// @param[in] key_len Key length. /// /// @return FAIL if out of memory or key already exists. int tv_dict_add_tv(dict_T *d, const char *key, const size_t key_len, typval_T *tv) { dictitem_T *const item = tv_dict_item_alloc_len(key, key_len); tv_copy(tv, &item->di_tv); if (tv_dict_add(d, item) == FAIL) { tv_dict_item_free(item); return FAIL; } return OK; } /// Add a dictionary entry to dictionary /// /// @param[out] d Dictionary to add entry to. /// @param[in] key Key to add. /// @param[in] key_len Key length. /// @param dict Dictionary to add. Will have reference count incremented. /// /// @return OK in case of success, FAIL when key already exists. int tv_dict_add_dict(dict_T *const d, const char *const key, const size_t key_len, dict_T *const dict) FUNC_ATTR_NONNULL_ALL { dictitem_T *const item = tv_dict_item_alloc_len(key, key_len); item->di_tv.v_type = VAR_DICT; item->di_tv.vval.v_dict = dict; dict->dv_refcount++; if (tv_dict_add(d, item) == FAIL) { tv_dict_item_free(item); return FAIL; } return OK; } /// Add a number entry to dictionary /// /// @param[out] d Dictionary to add entry to. /// @param[in] key Key to add. /// @param[in] key_len Key length. /// @param[in] nr Number to add. /// /// @return OK in case of success, FAIL when key already exists. int tv_dict_add_nr(dict_T *const d, const char *const key, const size_t key_len, const varnumber_T nr) { dictitem_T *const item = tv_dict_item_alloc_len(key, key_len); item->di_tv.v_type = VAR_NUMBER; item->di_tv.vval.v_number = nr; if (tv_dict_add(d, item) == FAIL) { tv_dict_item_free(item); return FAIL; } return OK; } /// Add a floating point number entry to dictionary /// /// @param[out] d Dictionary to add entry to. /// @param[in] key Key to add. /// @param[in] key_len Key length. /// @param[in] nr Floating point number to add. /// /// @return OK in case of success, FAIL when key already exists. int tv_dict_add_float(dict_T *const d, const char *const key, const size_t key_len, const float_T nr) { dictitem_T *const item = tv_dict_item_alloc_len(key, key_len); item->di_tv.v_type = VAR_FLOAT; item->di_tv.vval.v_float = nr; if (tv_dict_add(d, item) == FAIL) { tv_dict_item_free(item); return FAIL; } return OK; } /// Add a boolean entry to dictionary /// /// @param[out] d Dictionary to add entry to. /// @param[in] key Key to add. /// @param[in] key_len Key length. /// @param[in] val BoolVarValue to add. /// /// @return OK in case of success, FAIL when key already exists. int tv_dict_add_bool(dict_T *const d, const char *const key, const size_t key_len, BoolVarValue val) { dictitem_T *const item = tv_dict_item_alloc_len(key, key_len); item->di_tv.v_type = VAR_BOOL; item->di_tv.vval.v_bool = val; if (tv_dict_add(d, item) == FAIL) { tv_dict_item_free(item); return FAIL; } return OK; } /// Add a string entry to dictionary /// /// @see tv_dict_add_allocated_str int tv_dict_add_str(dict_T *const d, const char *const key, const size_t key_len, const char *const val) FUNC_ATTR_NONNULL_ARG(1, 2) { return tv_dict_add_str_len(d, key, key_len, val, -1); } /// Add a string entry to dictionary /// /// @param[out] d Dictionary to add entry to. /// @param[in] key Key to add. /// @param[in] key_len Key length. /// @param[in] val String to add. NULL adds empty string. /// @param[in] len Use this many bytes from `val`, or -1 for whole string. /// /// @return OK in case of success, FAIL when key already exists. int tv_dict_add_str_len(dict_T *const d, const char *const key, const size_t key_len, const char *const val, int len) FUNC_ATTR_NONNULL_ARG(1, 2) { char *s = NULL; if (val != NULL) { s = (len < 0) ? xstrdup(val) : xstrndup(val, (size_t)len); } return tv_dict_add_allocated_str(d, key, key_len, s); } /// Add a string entry to dictionary /// /// Unlike tv_dict_add_str() saves val to the new dictionary item in place of /// creating a new copy. /// /// @warning String will be freed even in case addition fails. /// /// @param[out] d Dictionary to add entry to. /// @param[in] key Key to add. /// @param[in] key_len Key length. /// @param[in] val String to add. /// /// @return OK in case of success, FAIL when key already exists. int tv_dict_add_allocated_str(dict_T *const d, const char *const key, const size_t key_len, char *const val) FUNC_ATTR_NONNULL_ARG(1, 2) { dictitem_T *const item = tv_dict_item_alloc_len(key, key_len); item->di_tv.v_type = VAR_STRING; item->di_tv.vval.v_string = val; if (tv_dict_add(d, item) == FAIL) { tv_dict_item_free(item); return FAIL; } return OK; } //{{{2 Operations on the whole dict /// Clear all the keys of a Dictionary. "d" remains a valid empty Dictionary. /// /// @param d The Dictionary to clear void tv_dict_clear(dict_T *const d) FUNC_ATTR_NONNULL_ALL { hash_lock(&d->dv_hashtab); assert(d->dv_hashtab.ht_locked > 0); HASHTAB_ITER(&d->dv_hashtab, hi, { tv_dict_item_free(TV_DICT_HI2DI(hi)); hash_remove(&d->dv_hashtab, hi); }); hash_unlock(&d->dv_hashtab); } /// Extend dictionary with items from another dictionary /// /// @param d1 Dictionary to extend. /// @param[in] d2 Dictionary to extend with. /// @param[in] action "error", "force", "move", "keep": /// e*, including "error": duplicate key gives an error. /// f*, including "force": duplicate d2 keys override d1. /// m*, including "move": move items instead of copying. /// other, including "keep": duplicate d2 keys ignored. void tv_dict_extend(dict_T *const d1, dict_T *const d2, const char *const action) FUNC_ATTR_NONNULL_ALL { const bool watched = tv_dict_is_watched(d1); const char *const arg_errmsg = _("extend() argument"); const size_t arg_errmsg_len = strlen(arg_errmsg); if (*action == 'm') { hash_lock(&d2->dv_hashtab); // don't rehash on hash_remove() } HASHTAB_ITER(&d2->dv_hashtab, hi2, { dictitem_T *const di2 = TV_DICT_HI2DI(hi2); dictitem_T *const di1 = tv_dict_find(d1, (const char *)di2->di_key, -1); // Check the key to be valid when adding to any scope. if (d1->dv_scope != VAR_NO_SCOPE && !valid_varname((const char *)di2->di_key)) { break; } if (di1 == NULL) { if (*action == 'm') { // Cheap way to move a dict item from "d2" to "d1". // If dict_add() fails then "d2" won't be empty. dictitem_T *const new_di = di2; if (tv_dict_add(d1, new_di) == OK) { hash_remove(&d2->dv_hashtab, hi2); tv_dict_watcher_notify(d1, (const char *)new_di->di_key, &new_di->di_tv, NULL); } } else { dictitem_T *const new_di = tv_dict_item_copy(di2); if (tv_dict_add(d1, new_di) == FAIL) { tv_dict_item_free(new_di); } else if (watched) { tv_dict_watcher_notify(d1, (const char *)new_di->di_key, &new_di->di_tv, NULL); } } } else if (*action == 'e') { semsg(_("E737: Key already exists: %s"), di2->di_key); break; } else if (*action == 'f' && di2 != di1) { typval_T oldtv; if (value_check_lock(di1->di_tv.v_lock, arg_errmsg, arg_errmsg_len) || var_check_ro(di1->di_flags, arg_errmsg, arg_errmsg_len)) { break; } // Disallow replacing a builtin function. if (tv_dict_wrong_func_name(d1, &di2->di_tv, (const char *)di2->di_key)) { break; } if (watched) { tv_copy(&di1->di_tv, &oldtv); } tv_clear(&di1->di_tv); tv_copy(&di2->di_tv, &di1->di_tv); if (watched) { tv_dict_watcher_notify(d1, (const char *)di1->di_key, &di1->di_tv, &oldtv); tv_clear(&oldtv); } } }); if (*action == 'm') { hash_unlock(&d2->dv_hashtab); } } /// Compare two dictionaries /// /// @param[in] d1 First dictionary. /// @param[in] d2 Second dictionary. /// @param[in] ic True if case is to be ignored. /// @param[in] recursive True when used recursively. bool tv_dict_equal(dict_T *const d1, dict_T *const d2, const bool ic, const bool recursive) FUNC_ATTR_WARN_UNUSED_RESULT { if (d1 == d2) { return true; } if (tv_dict_len(d1) != tv_dict_len(d2)) { return false; } if (tv_dict_len(d1) == 0) { // empty and NULL dicts are considered equal return true; } if (d1 == NULL || d2 == NULL) { return false; } TV_DICT_ITER(d1, di1, { dictitem_T *const di2 = tv_dict_find(d2, (const char *)di1->di_key, -1); if (di2 == NULL) { return false; } if (!tv_equal(&di1->di_tv, &di2->di_tv, ic, recursive)) { return false; } }); return true; } /// Make a copy of dictionary /// /// @param[in] conv If non-NULL, then all internal strings will be converted. /// @param[in] orig Original dictionary to copy. /// @param[in] deep If false, then shallow copy will be done. /// @param[in] copyID See var_item_copy(). /// /// @return Copied dictionary. May be NULL in case original dictionary is NULL /// or some failure happens. The refcount of the new dictionary is set /// to 1. dict_T *tv_dict_copy(const vimconv_T *const conv, dict_T *const orig, const bool deep, const int copyID) { if (orig == NULL) { return NULL; } dict_T *copy = tv_dict_alloc(); if (copyID != 0) { orig->dv_copyID = copyID; orig->dv_copydict = copy; } TV_DICT_ITER(orig, di, { if (got_int) { break; } dictitem_T *new_di; if (conv == NULL || conv->vc_type == CONV_NONE) { new_di = tv_dict_item_alloc((const char *)di->di_key); } else { size_t len = strlen((char *)di->di_key); char *const key = (char *)string_convert(conv, (char *)di->di_key, &len); if (key == NULL) { new_di = tv_dict_item_alloc_len((const char *)di->di_key, len); } else { new_di = tv_dict_item_alloc_len(key, len); xfree(key); } } if (deep) { if (var_item_copy(conv, &di->di_tv, &new_di->di_tv, deep, copyID) == FAIL) { xfree(new_di); break; } } else { tv_copy(&di->di_tv, &new_di->di_tv); } if (tv_dict_add(copy, new_di) == FAIL) { tv_dict_item_free(new_di); break; } }); copy->dv_refcount++; if (got_int) { tv_dict_unref(copy); copy = NULL; } return copy; } /// Set all existing keys in "dict" as read-only. /// /// This does not protect against adding new keys to the Dictionary. /// /// @param dict The dict whose keys should be frozen. void tv_dict_set_keys_readonly(dict_T *const dict) FUNC_ATTR_NONNULL_ALL { TV_DICT_ITER(dict, di, { di->di_flags |= DI_FLAGS_RO | DI_FLAGS_FIX; }); } //{{{1 Blobs //{{{2 Alloc/free /// Allocate an empty blob. /// /// Caller should take care of the reference count. /// /// @return [allocated] new blob. blob_T *tv_blob_alloc(void) FUNC_ATTR_NONNULL_RET { blob_T *const blob = xcalloc(1, sizeof(blob_T)); ga_init(&blob->bv_ga, 1, 100); return blob; } /// Free a blob. Ignores the reference count. /// /// @param[in,out] b Blob to free. void tv_blob_free(blob_T *const b) FUNC_ATTR_NONNULL_ALL { ga_clear(&b->bv_ga); xfree(b); } /// Unreference a blob. /// /// Decrements the reference count and frees blob when it becomes zero. /// /// @param[in,out] b Blob to operate on. void tv_blob_unref(blob_T *const b) { if (b != NULL && --b->bv_refcount <= 0) { tv_blob_free(b); } } //{{{2 Operations on the whole blob /// Check whether two blobs are equal. /// /// @param[in] b1 First blob. /// @param[in] b2 Second blob. /// /// @return true if blobs are equal, false otherwise. bool tv_blob_equal(const blob_T *const b1, const blob_T *const b2) FUNC_ATTR_WARN_UNUSED_RESULT { const int len1 = tv_blob_len(b1); const int len2 = tv_blob_len(b2); // empty and NULL are considered the same if (len1 == 0 && len2 == 0) { return true; } if (b1 == b2) { return true; } if (len1 != len2) { return false; } for (int i = 0; i < b1->bv_ga.ga_len; i++) { if (tv_blob_get(b1, i) != tv_blob_get(b2, i)) { return false; } } return true; } /// "remove({blob})" function void tv_blob_remove(typval_T *argvars, typval_T *rettv, const char *arg_errmsg) { blob_T *const b = argvars[0].vval.v_blob; if (b != NULL && value_check_lock(b->bv_lock, arg_errmsg, TV_TRANSLATE)) { return; } bool error = false; long idx = tv_get_number_chk(&argvars[1], &error); if (!error) { const int len = tv_blob_len(b); if (idx < 0) { // count from the end idx = len + idx; } if (idx < 0 || idx >= len) { semsg(_(e_blobidx), (int64_t)idx); return; } if (argvars[2].v_type == VAR_UNKNOWN) { // Remove one item, return its value. uint8_t *const p = (uint8_t *)b->bv_ga.ga_data; rettv->vval.v_number = (varnumber_T)(*(p + idx)); memmove(p + idx, p + idx + 1, (size_t)(len - idx - 1)); b->bv_ga.ga_len--; } else { // Remove range of items, return blob with values. long end = tv_get_number_chk(&argvars[2], &error); if (error) { return; } if (end < 0) { // count from the end end = len + end; } if (end >= len || idx > end) { semsg(_(e_blobidx), (int64_t)end); return; } blob_T *const blob = tv_blob_alloc(); blob->bv_ga.ga_len = (int)(end - idx + 1); ga_grow(&blob->bv_ga, (int)(end - idx + 1)); uint8_t *const p = (uint8_t *)b->bv_ga.ga_data; memmove(blob->bv_ga.ga_data, p + idx, (size_t)(end - idx + 1)); tv_blob_set_ret(rettv, blob); if (len - end - 1 > 0) { memmove(p + idx, p + end + 1, (size_t)(len - end - 1)); } b->bv_ga.ga_len -= (int)(end - idx + 1); } } } //{{{1 Generic typval operations //{{{2 Init/alloc/clear //{{{3 Alloc /// Allocate an empty list for a return value /// /// Also sets reference count. /// /// @param[out] ret_tv Structure where list is saved. /// @param[in] len Expected number of items to be populated before list /// becomes accessible from VimL. It is still valid to /// underpopulate a list, value only controls how many elements /// will be allocated in advance. @see ListLenSpecials. /// /// @return [allocated] pointer to the created list. list_T *tv_list_alloc_ret(typval_T *const ret_tv, const ptrdiff_t len) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_RET { list_T *const l = tv_list_alloc(len); tv_list_set_ret(ret_tv, l); ret_tv->v_lock = VAR_UNLOCKED; return l; } dict_T *tv_dict_alloc_lock(VarLockStatus lock) FUNC_ATTR_NONNULL_RET { dict_T *const d = tv_dict_alloc(); d->dv_lock = lock; return d; } /// Allocate an empty dictionary for a return value /// /// Also sets reference count. /// /// @param[out] ret_tv Structure where dictionary is saved. void tv_dict_alloc_ret(typval_T *const ret_tv) FUNC_ATTR_NONNULL_ALL { dict_T *const d = tv_dict_alloc_lock(VAR_UNLOCKED); tv_dict_set_ret(ret_tv, d); } /// Turn a dictionary into a list /// /// @param[in] tv Dictionary to convert. Is checked for actually being /// a dictionary, will give an error if not. /// @param[out] rettv Location where result will be saved. /// @param[in] what What to save in rettv. static void tv_dict_list(typval_T *const tv, typval_T *const rettv, const DictListType what) { if (tv->v_type != VAR_DICT) { emsg(_(e_dictreq)); return; } if (tv->vval.v_dict == NULL) { return; } tv_list_alloc_ret(rettv, tv_dict_len(tv->vval.v_dict)); TV_DICT_ITER(tv->vval.v_dict, di, { typval_T tv_item = { .v_lock = VAR_UNLOCKED }; switch (what) { case kDictListKeys: tv_item.v_type = VAR_STRING; tv_item.vval.v_string = xstrdup((char *)di->di_key); break; case kDictListValues: tv_copy(&di->di_tv, &tv_item); break; case kDictListItems: { // items() list_T *const sub_l = tv_list_alloc(2); tv_item.v_type = VAR_LIST; tv_item.vval.v_list = sub_l; tv_list_ref(sub_l); tv_list_append_owned_tv(sub_l, (typval_T) { .v_type = VAR_STRING, .v_lock = VAR_UNLOCKED, .vval.v_string = xstrdup((const char *)di->di_key), }); tv_list_append_tv(sub_l, &di->di_tv); break; } } tv_list_append_owned_tv(rettv->vval.v_list, tv_item); }); } /// "items(dict)" function void f_items(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) { tv_dict_list(argvars, rettv, 2); } /// "keys()" function void f_keys(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) { tv_dict_list(argvars, rettv, 0); } /// "values(dict)" function void f_values(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) { tv_dict_list(argvars, rettv, 1); } /// "has_key()" function void f_has_key(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) { if (argvars[0].v_type != VAR_DICT) { emsg(_(e_dictreq)); return; } if (argvars[0].vval.v_dict == NULL) { return; } rettv->vval.v_number = tv_dict_find(argvars[0].vval.v_dict, tv_get_string(&argvars[1]), -1) != NULL; } /// "remove({dict})" function void tv_dict_remove(typval_T *argvars, typval_T *rettv, const char *arg_errmsg) { dict_T *d; if (argvars[2].v_type != VAR_UNKNOWN) { semsg(_(e_toomanyarg), "remove()"); } else if ((d = argvars[0].vval.v_dict) != NULL && !value_check_lock(d->dv_lock, arg_errmsg, TV_TRANSLATE)) { const char *key = tv_get_string_chk(&argvars[1]); if (key != NULL) { dictitem_T *di = tv_dict_find(d, key, -1); if (di == NULL) { semsg(_(e_dictkey), key); } else if (!var_check_fixed(di->di_flags, arg_errmsg, TV_TRANSLATE) && !var_check_ro(di->di_flags, arg_errmsg, TV_TRANSLATE)) { *rettv = di->di_tv; di->di_tv = TV_INITIAL_VALUE; tv_dict_item_remove(d, di); if (tv_dict_is_watched(d)) { tv_dict_watcher_notify(d, key, NULL, rettv); } } } } } /// Allocate an empty blob for a return value. /// /// Also sets reference count. /// /// @param[out] ret_tv Structure where blob is saved. void tv_blob_alloc_ret(typval_T *const ret_tv) FUNC_ATTR_NONNULL_ALL { blob_T *const b = tv_blob_alloc(); tv_blob_set_ret(ret_tv, b); } /// Copy a blob typval to a different typval. /// /// @param[in] from Blob object to copy from. /// @param[out] to Blob object to copy to. void tv_blob_copy(typval_T *const from, typval_T *const to) FUNC_ATTR_NONNULL_ALL { assert(from->v_type == VAR_BLOB); to->v_type = VAR_BLOB; to->v_lock = VAR_UNLOCKED; if (from->vval.v_blob == NULL) { to->vval.v_blob = NULL; } else { tv_blob_alloc_ret(to); int len = from->vval.v_blob->bv_ga.ga_len; if (len > 0) { to->vval.v_blob->bv_ga.ga_data = xmemdup(from->vval.v_blob->bv_ga.ga_data, (size_t)len); } to->vval.v_blob->bv_ga.ga_len = len; to->vval.v_blob->bv_ga.ga_maxlen = len; } } //{{{3 Clear #define TYPVAL_ENCODE_ALLOW_SPECIALS false #define TYPVAL_ENCODE_CONV_NIL(tv) \ do { \ (tv)->vval.v_special = kSpecialVarNull; \ (tv)->v_lock = VAR_UNLOCKED; \ } while (0) #define TYPVAL_ENCODE_CONV_BOOL(tv, num) \ do { \ (tv)->vval.v_bool = kBoolVarFalse; \ (tv)->v_lock = VAR_UNLOCKED; \ } while (0) #define TYPVAL_ENCODE_CONV_NUMBER(tv, num) \ do { \ (void)(num); \ (tv)->vval.v_number = 0; \ (tv)->v_lock = VAR_UNLOCKED; \ } while (0) #define TYPVAL_ENCODE_CONV_UNSIGNED_NUMBER(tv, num) #define TYPVAL_ENCODE_CONV_FLOAT(tv, flt) \ do { \ (tv)->vval.v_float = 0; \ (tv)->v_lock = VAR_UNLOCKED; \ } while (0) #define TYPVAL_ENCODE_CONV_STRING(tv, buf, len) \ do { \ xfree(buf); \ (tv)->vval.v_string = NULL; \ (tv)->v_lock = VAR_UNLOCKED; \ } while (0) #define TYPVAL_ENCODE_CONV_STR_STRING(tv, buf, len) #define TYPVAL_ENCODE_CONV_EXT_STRING(tv, buf, len, type) #define TYPVAL_ENCODE_CONV_BLOB(tv, blob, len) \ do { \ tv_blob_unref((tv)->vval.v_blob); \ (tv)->vval.v_blob = NULL; \ (tv)->v_lock = VAR_UNLOCKED; \ } while (0) static inline int _nothing_conv_func_start(typval_T *const tv, char *const fun) FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_ALWAYS_INLINE FUNC_ATTR_NONNULL_ARG(1) { tv->v_lock = VAR_UNLOCKED; if (tv->v_type == VAR_PARTIAL) { partial_T *const pt_ = tv->vval.v_partial; if (pt_ != NULL && pt_->pt_refcount > 1) { pt_->pt_refcount--; tv->vval.v_partial = NULL; return OK; } } else { func_unref(fun); if ((const char *)fun != tv_empty_string) { xfree(fun); } tv->vval.v_string = NULL; } return NOTDONE; } #define TYPVAL_ENCODE_CONV_FUNC_START(tv, fun) \ do { \ if (_nothing_conv_func_start(tv, fun) != NOTDONE) { \ return OK; \ } \ } while (0) #define TYPVAL_ENCODE_CONV_FUNC_BEFORE_ARGS(tv, len) #define TYPVAL_ENCODE_CONV_FUNC_BEFORE_SELF(tv, len) static inline void _nothing_conv_func_end(typval_T *const tv, const int copyID) FUNC_ATTR_ALWAYS_INLINE FUNC_ATTR_NONNULL_ALL { if (tv->v_type == VAR_PARTIAL) { partial_T *const pt = tv->vval.v_partial; if (pt == NULL) { return; } // Dictionary should already be freed by the time. // If it was not freed then it is a part of the reference cycle. assert(pt->pt_dict == NULL || pt->pt_dict->dv_copyID == copyID); pt->pt_dict = NULL; // As well as all arguments. pt->pt_argc = 0; assert(pt->pt_refcount <= 1); partial_unref(pt); tv->vval.v_partial = NULL; assert(tv->v_lock == VAR_UNLOCKED); } } #define TYPVAL_ENCODE_CONV_FUNC_END(tv) _nothing_conv_func_end(tv, copyID) #define TYPVAL_ENCODE_CONV_EMPTY_LIST(tv) \ do { \ tv_list_unref((tv)->vval.v_list); \ (tv)->vval.v_list = NULL; \ (tv)->v_lock = VAR_UNLOCKED; \ } while (0) static inline void _nothing_conv_empty_dict(typval_T *const tv, dict_T **const dictp) FUNC_ATTR_ALWAYS_INLINE FUNC_ATTR_NONNULL_ARG(2) { tv_dict_unref(*dictp); *dictp = NULL; if (tv != NULL) { tv->v_lock = VAR_UNLOCKED; } } #define TYPVAL_ENCODE_CONV_EMPTY_DICT(tv, dict) \ do { \ assert((void *)&(dict) != (void *)&TYPVAL_ENCODE_NODICT_VAR); \ _nothing_conv_empty_dict(tv, ((dict_T **)&(dict))); \ } while (0) static inline int _nothing_conv_real_list_after_start(typval_T *const tv, MPConvStackVal *const mpsv) FUNC_ATTR_ALWAYS_INLINE FUNC_ATTR_WARN_UNUSED_RESULT { assert(tv != NULL); tv->v_lock = VAR_UNLOCKED; if (tv->vval.v_list->lv_refcount > 1) { tv->vval.v_list->lv_refcount--; tv->vval.v_list = NULL; mpsv->data.l.li = NULL; return OK; } return NOTDONE; } #define TYPVAL_ENCODE_CONV_LIST_START(tv, len) #define TYPVAL_ENCODE_CONV_REAL_LIST_AFTER_START(tv, mpsv) \ do { \ if (_nothing_conv_real_list_after_start(tv, &(mpsv)) != NOTDONE) { \ goto typval_encode_stop_converting_one_item; \ } \ } while (0) #define TYPVAL_ENCODE_CONV_LIST_BETWEEN_ITEMS(tv) static inline void _nothing_conv_list_end(typval_T *const tv) FUNC_ATTR_ALWAYS_INLINE { if (tv == NULL) { return; } assert(tv->v_type == VAR_LIST); list_T *const list = tv->vval.v_list; tv_list_unref(list); tv->vval.v_list = NULL; } #define TYPVAL_ENCODE_CONV_LIST_END(tv) _nothing_conv_list_end(tv) static inline int _nothing_conv_real_dict_after_start(typval_T *const tv, dict_T **const dictp, const void *const nodictvar, MPConvStackVal *const mpsv) FUNC_ATTR_ALWAYS_INLINE FUNC_ATTR_WARN_UNUSED_RESULT { if (tv != NULL) { tv->v_lock = VAR_UNLOCKED; } if ((const void *)dictp != nodictvar && (*dictp)->dv_refcount > 1) { (*dictp)->dv_refcount--; *dictp = NULL; mpsv->data.d.todo = 0; return OK; } return NOTDONE; } #define TYPVAL_ENCODE_CONV_DICT_START(tv, dict, len) #define TYPVAL_ENCODE_CONV_REAL_DICT_AFTER_START(tv, dict, mpsv) \ do { \ if (_nothing_conv_real_dict_after_start(tv, (dict_T **)&(dict), \ (void *)&TYPVAL_ENCODE_NODICT_VAR, &(mpsv)) \ != NOTDONE) { \ goto typval_encode_stop_converting_one_item; \ } \ } while (0) #define TYPVAL_ENCODE_SPECIAL_DICT_KEY_CHECK(tv, dict) #define TYPVAL_ENCODE_CONV_DICT_AFTER_KEY(tv, dict) #define TYPVAL_ENCODE_CONV_DICT_BETWEEN_ITEMS(tv, dict) static inline void _nothing_conv_dict_end(typval_T *const tv, dict_T **const dictp, const void *const nodictvar) FUNC_ATTR_ALWAYS_INLINE { if ((const void *)dictp != nodictvar) { tv_dict_unref(*dictp); *dictp = NULL; } } #define TYPVAL_ENCODE_CONV_DICT_END(tv, dict) \ _nothing_conv_dict_end(tv, (dict_T **)&(dict), \ (void *)&TYPVAL_ENCODE_NODICT_VAR) #define TYPVAL_ENCODE_CONV_RECURSE(val, conv_type) #define TYPVAL_ENCODE_SCOPE static #define TYPVAL_ENCODE_NAME nothing #define TYPVAL_ENCODE_FIRST_ARG_TYPE const void *const #define TYPVAL_ENCODE_FIRST_ARG_NAME ignored #include "nvim/eval/typval_encode.c.h" #undef TYPVAL_ENCODE_SCOPE #undef TYPVAL_ENCODE_NAME #undef TYPVAL_ENCODE_FIRST_ARG_TYPE #undef TYPVAL_ENCODE_FIRST_ARG_NAME #undef TYPVAL_ENCODE_ALLOW_SPECIALS #undef TYPVAL_ENCODE_CONV_NIL #undef TYPVAL_ENCODE_CONV_BOOL #undef TYPVAL_ENCODE_CONV_NUMBER #undef TYPVAL_ENCODE_CONV_UNSIGNED_NUMBER #undef TYPVAL_ENCODE_CONV_FLOAT #undef TYPVAL_ENCODE_CONV_STRING #undef TYPVAL_ENCODE_CONV_STR_STRING #undef TYPVAL_ENCODE_CONV_EXT_STRING #undef TYPVAL_ENCODE_CONV_BLOB #undef TYPVAL_ENCODE_CONV_FUNC_START #undef TYPVAL_ENCODE_CONV_FUNC_BEFORE_ARGS #undef TYPVAL_ENCODE_CONV_FUNC_BEFORE_SELF #undef TYPVAL_ENCODE_CONV_FUNC_END #undef TYPVAL_ENCODE_CONV_EMPTY_LIST #undef TYPVAL_ENCODE_CONV_EMPTY_DICT #undef TYPVAL_ENCODE_CONV_LIST_START #undef TYPVAL_ENCODE_CONV_REAL_LIST_AFTER_START #undef TYPVAL_ENCODE_CONV_LIST_BETWEEN_ITEMS #undef TYPVAL_ENCODE_CONV_LIST_END #undef TYPVAL_ENCODE_CONV_DICT_START #undef TYPVAL_ENCODE_CONV_REAL_DICT_AFTER_START #undef TYPVAL_ENCODE_SPECIAL_DICT_KEY_CHECK #undef TYPVAL_ENCODE_CONV_DICT_AFTER_KEY #undef TYPVAL_ENCODE_CONV_DICT_BETWEEN_ITEMS #undef TYPVAL_ENCODE_CONV_DICT_END #undef TYPVAL_ENCODE_CONV_RECURSE /// Free memory for a variable value and set the value to NULL or 0 /// /// @param[in,out] tv Value to free. void tv_clear(typval_T *const tv) { if (tv != NULL && tv->v_type != VAR_UNKNOWN) { // WARNING: do not translate the string here, gettext is slow and function // is used *very* often. At the current state encode_vim_to_nothing() does // not error out and does not use the argument anywhere. // // If situation changes and this argument will be used, translate it in the // place where it is used. const int evn_ret = encode_vim_to_nothing(NULL, tv, "tv_clear() argument"); (void)evn_ret; assert(evn_ret == OK); } } //{{{3 Free /// Free allocated VimL object and value stored inside /// /// @param tv Object to free. void tv_free(typval_T *tv) { if (tv != NULL) { switch (tv->v_type) { case VAR_PARTIAL: partial_unref(tv->vval.v_partial); break; case VAR_FUNC: func_unref(tv->vval.v_string); FALLTHROUGH; case VAR_STRING: xfree(tv->vval.v_string); break; case VAR_BLOB: tv_blob_unref(tv->vval.v_blob); break; case VAR_LIST: tv_list_unref(tv->vval.v_list); break; case VAR_DICT: tv_dict_unref(tv->vval.v_dict); break; case VAR_BOOL: case VAR_SPECIAL: case VAR_NUMBER: case VAR_FLOAT: case VAR_UNKNOWN: break; } xfree(tv); } } //{{{3 Copy /// Copy typval from one location to another /// /// When needed allocates string or increases reference count. Does not make /// a copy of a container, but copies its reference! /// /// It is OK for `from` and `to` to point to the same location; this is used to /// make a copy later. /// /// @param[in] from Location to copy from. /// @param[out] to Location to copy to. void tv_copy(const typval_T *const from, typval_T *const to) { to->v_type = from->v_type; to->v_lock = VAR_UNLOCKED; memmove(&to->vval, &from->vval, sizeof(to->vval)); switch (from->v_type) { case VAR_NUMBER: case VAR_FLOAT: case VAR_BOOL: case VAR_SPECIAL: break; case VAR_STRING: case VAR_FUNC: if (from->vval.v_string != NULL) { to->vval.v_string = xstrdup(from->vval.v_string); if (from->v_type == VAR_FUNC) { func_ref(to->vval.v_string); } } break; case VAR_PARTIAL: if (to->vval.v_partial != NULL) { to->vval.v_partial->pt_refcount++; } break; case VAR_BLOB: if (from->vval.v_blob != NULL) { to->vval.v_blob->bv_refcount++; } break; case VAR_LIST: tv_list_ref(to->vval.v_list); break; case VAR_DICT: if (from->vval.v_dict != NULL) { to->vval.v_dict->dv_refcount++; } break; case VAR_UNKNOWN: semsg(_(e_intern2), "tv_copy(UNKNOWN)"); break; } } //{{{2 Locks /// Lock or unlock an item /// /// @param[out] tv Item to (un)lock. /// @param[in] deep Levels to (un)lock, -1 to (un)lock everything. /// @param[in] lock True if it is needed to lock an item, false to unlock. /// @param[in] check_refcount If true, do not lock a list or dict with a /// reference count larger than 1. void tv_item_lock(typval_T *const tv, const int deep, const bool lock, const bool check_refcount) FUNC_ATTR_NONNULL_ALL { // TODO(ZyX-I): Make this not recursive static int recurse = 0; if (recurse >= DICT_MAXNEST) { emsg(_("E743: variable nested too deep for (un)lock")); return; } if (deep == 0) { return; } recurse++; // lock/unlock the item itself #define CHANGE_LOCK(lock, var) \ do { \ (var) = ((VarLockStatus[]) { \ [VAR_UNLOCKED] = ((lock) ? VAR_LOCKED : VAR_UNLOCKED), \ [VAR_LOCKED] = ((lock) ? VAR_LOCKED : VAR_UNLOCKED), \ [VAR_FIXED] = VAR_FIXED, \ })[var]; \ } while (0) CHANGE_LOCK(lock, tv->v_lock); switch (tv->v_type) { case VAR_BLOB: { blob_T *const b = tv->vval.v_blob; if (b != NULL && !(check_refcount && b->bv_refcount > 1)) { CHANGE_LOCK(lock, b->bv_lock); } break; } case VAR_LIST: { list_T *const l = tv->vval.v_list; if (l != NULL && !(check_refcount && l->lv_refcount > 1)) { CHANGE_LOCK(lock, l->lv_lock); if (deep < 0 || deep > 1) { // Recursive: lock/unlock the items the List contains. TV_LIST_ITER(l, li, { tv_item_lock(TV_LIST_ITEM_TV(li), deep - 1, lock, check_refcount); }); } } break; } case VAR_DICT: { dict_T *const d = tv->vval.v_dict; if (d != NULL && !(check_refcount && d->dv_refcount > 1)) { CHANGE_LOCK(lock, d->dv_lock); if (deep < 0 || deep > 1) { // recursive: lock/unlock the items the List contains TV_DICT_ITER(d, di, { tv_item_lock(&di->di_tv, deep - 1, lock, check_refcount); }); } } break; } case VAR_NUMBER: case VAR_FLOAT: case VAR_STRING: case VAR_FUNC: case VAR_PARTIAL: case VAR_BOOL: case VAR_SPECIAL: break; case VAR_UNKNOWN: abort(); } #undef CHANGE_LOCK recurse--; } /// Check whether VimL value is locked itself or refers to a locked container /// /// @warning Fixed container is not the same as locked. /// /// @param[in] tv Value to check. /// /// @return True if value is locked, false otherwise. bool tv_islocked(const typval_T *const tv) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL { return ((tv->v_lock == VAR_LOCKED) || (tv->v_type == VAR_LIST && (tv_list_locked(tv->vval.v_list) == VAR_LOCKED)) || (tv->v_type == VAR_DICT && tv->vval.v_dict != NULL && (tv->vval.v_dict->dv_lock == VAR_LOCKED))); } /// Return true if typval is locked /// /// Also gives an error message when typval is locked. /// /// @param[in] tv Typval. /// @param[in] name Variable name, used in the error message. /// @param[in] name_len Variable name length. Use #TV_TRANSLATE to translate /// variable name and compute the length. Use #TV_CSTRING /// to compute the length with strlen() without /// translating. /// /// Both #TV_… values are used for optimization purposes: /// variable name with its length is needed only in case /// of error, when no error occurs computing them is /// a waste of CPU resources. This especially applies to /// gettext. /// /// @return true if variable is locked, false otherwise. bool tv_check_lock(const typval_T *tv, const char *name, size_t name_len) FUNC_ATTR_WARN_UNUSED_RESULT { VarLockStatus lock = VAR_UNLOCKED; switch (tv->v_type) { case VAR_BLOB: if (tv->vval.v_blob != NULL) { lock = tv->vval.v_blob->bv_lock; } break; case VAR_LIST: if (tv->vval.v_list != NULL) { lock = tv->vval.v_list->lv_lock; } break; case VAR_DICT: if (tv->vval.v_dict != NULL) { lock = tv->vval.v_dict->dv_lock; } break; default: break; } return value_check_lock(tv->v_lock, name, name_len) || (lock != VAR_UNLOCKED && value_check_lock(lock, name, name_len)); } /// @return true if variable "name" has a locked (immutable) value bool value_check_lock(VarLockStatus lock, const char *name, size_t name_len) { const char *error_message = NULL; switch (lock) { case VAR_UNLOCKED: return false; case VAR_LOCKED: error_message = N_("E741: Value is locked: %.*s"); break; case VAR_FIXED: error_message = N_("E742: Cannot change value of %.*s"); break; } assert(error_message != NULL); if (name == NULL) { name = _("Unknown"); name_len = strlen(name); } else if (name_len == TV_TRANSLATE) { name = _(name); name_len = strlen(name); } else if (name_len == TV_CSTRING) { name_len = strlen(name); } semsg(_(error_message), (int)name_len, name); return true; } //{{{2 Comparison static int tv_equal_recurse_limit; /// Compare two VimL values /// /// Like "==", but strings and numbers are different, as well as floats and /// numbers. /// /// @warning Too nested structures may be considered equal even if they are not. /// /// @param[in] tv1 First value to compare. /// @param[in] tv2 Second value to compare. /// @param[in] ic True if case is to be ignored. /// @param[in] recursive True when used recursively. /// /// @return true if values are equal. bool tv_equal(typval_T *const tv1, typval_T *const tv2, const bool ic, const bool recursive) FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL { // TODO(ZyX-I): Make this not recursive static int recursive_cnt = 0; // Catch recursive loops. if (!(tv_is_func(*tv1) && tv_is_func(*tv2)) && tv1->v_type != tv2->v_type) { return false; } // Catch lists and dicts that have an endless loop by limiting // recursiveness to a limit. We guess they are equal then. // A fixed limit has the problem of still taking an awful long time. // Reduce the limit every time running into it. That should work fine for // deeply linked structures that are not recursively linked and catch // recursiveness quickly. if (!recursive) { tv_equal_recurse_limit = 1000; } if (recursive_cnt >= tv_equal_recurse_limit) { tv_equal_recurse_limit--; return true; } switch (tv1->v_type) { case VAR_LIST: { recursive_cnt++; const bool r = tv_list_equal(tv1->vval.v_list, tv2->vval.v_list, ic, true); recursive_cnt--; return r; } case VAR_DICT: { recursive_cnt++; const bool r = tv_dict_equal(tv1->vval.v_dict, tv2->vval.v_dict, ic, true); recursive_cnt--; return r; } case VAR_PARTIAL: case VAR_FUNC: { if ((tv1->v_type == VAR_PARTIAL && tv1->vval.v_partial == NULL) || (tv2->v_type == VAR_PARTIAL && tv2->vval.v_partial == NULL)) { return false; } recursive_cnt++; const bool r = func_equal(tv1, tv2, ic); recursive_cnt--; return r; } case VAR_BLOB: return tv_blob_equal(tv1->vval.v_blob, tv2->vval.v_blob); case VAR_NUMBER: return tv1->vval.v_number == tv2->vval.v_number; case VAR_FLOAT: return tv1->vval.v_float == tv2->vval.v_float; case VAR_STRING: { char buf1[NUMBUFLEN]; char buf2[NUMBUFLEN]; const char *s1 = tv_get_string_buf(tv1, buf1); const char *s2 = tv_get_string_buf(tv2, buf2); return mb_strcmp_ic(ic, s1, s2) == 0; } case VAR_BOOL: return tv1->vval.v_bool == tv2->vval.v_bool; case VAR_SPECIAL: return tv1->vval.v_special == tv2->vval.v_special; case VAR_UNKNOWN: // VAR_UNKNOWN can be the result of an invalid expression, let’s say it // does not equal anything, not even self. return false; } abort(); return false; } //{{{2 Type checks /// Check that given value is a number or string /// /// Error messages are compatible with tv_get_number() previously used for the /// same purpose in buf*() functions. Special values are not accepted (previous /// behaviour: silently fail to find buffer). /// /// @param[in] tv Value to check. /// /// @return true if everything is OK, false otherwise. bool tv_check_str_or_nr(const typval_T *const tv) FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL { switch (tv->v_type) { case VAR_NUMBER: case VAR_STRING: return true; case VAR_FLOAT: emsg(_("E805: Expected a Number or a String, Float found")); return false; case VAR_PARTIAL: case VAR_FUNC: emsg(_("E703: Expected a Number or a String, Funcref found")); return false; case VAR_LIST: emsg(_("E745: Expected a Number or a String, List found")); return false; case VAR_DICT: emsg(_("E728: Expected a Number or a String, Dictionary found")); return false; case VAR_BLOB: emsg(_("E974: Expected a Number or a String, Blob found")); return false; case VAR_BOOL: emsg(_("E5299: Expected a Number or a String, Boolean found")); return false; case VAR_SPECIAL: emsg(_("E5300: Expected a Number or a String")); return false; case VAR_UNKNOWN: semsg(_(e_intern2), "tv_check_str_or_nr(UNKNOWN)"); return false; } abort(); return false; } #define FUNC_ERROR "E703: Using a Funcref as a Number" static const char *const num_errors[] = { [VAR_PARTIAL]= N_(FUNC_ERROR), [VAR_FUNC]= N_(FUNC_ERROR), [VAR_LIST]= N_("E745: Using a List as a Number"), [VAR_DICT]= N_("E728: Using a Dictionary as a Number"), [VAR_FLOAT]= N_("E805: Using a Float as a Number"), [VAR_BLOB]= N_("E974: Using a Blob as a Number"), [VAR_UNKNOWN]= N_("E685: using an invalid value as a Number"), }; #undef FUNC_ERROR /// Check that given value is a number or can be converted to it /// /// Error messages are compatible with tv_get_number_chk() previously used for /// the same purpose. /// /// @param[in] tv Value to check. /// /// @return true if everything is OK, false otherwise. bool tv_check_num(const typval_T *const tv) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { switch (tv->v_type) { case VAR_NUMBER: case VAR_BOOL: case VAR_SPECIAL: case VAR_STRING: return true; case VAR_FUNC: case VAR_PARTIAL: case VAR_LIST: case VAR_DICT: case VAR_FLOAT: case VAR_BLOB: case VAR_UNKNOWN: emsg(_(num_errors[tv->v_type])); return false; } abort(); return false; } #define FUNC_ERROR "E729: using Funcref as a String" static const char *const str_errors[] = { [VAR_PARTIAL]= N_(FUNC_ERROR), [VAR_FUNC]= N_(FUNC_ERROR), [VAR_LIST]= N_("E730: using List as a String"), [VAR_DICT]= N_("E731: using Dictionary as a String"), [VAR_FLOAT]= ((const char *)e_float_as_string), [VAR_BLOB]= N_("E976: using Blob as a String"), [VAR_UNKNOWN]= N_("E908: using an invalid value as a String"), }; #undef FUNC_ERROR /// Check that given value is a VimL String or can be "cast" to it. /// /// Error messages are compatible with tv_get_string_chk() previously used for /// the same purpose. /// /// @param[in] tv Value to check. /// /// @return true if everything is OK, false otherwise. bool tv_check_str(const typval_T *const tv) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { switch (tv->v_type) { case VAR_NUMBER: case VAR_BOOL: case VAR_SPECIAL: case VAR_STRING: return true; case VAR_PARTIAL: case VAR_FUNC: case VAR_LIST: case VAR_DICT: case VAR_FLOAT: case VAR_BLOB: case VAR_UNKNOWN: emsg(_(str_errors[tv->v_type])); return false; } abort(); return false; } //{{{2 Get /// Get the number value of a VimL object /// /// @note Use tv_get_number_chk() if you need to determine whether there was an /// error. /// /// @param[in] tv Object to get value from. /// /// @return Number value: vim_str2nr() output for VAR_STRING objects, value /// for VAR_NUMBER objects, -1 for other types. varnumber_T tv_get_number(const typval_T *const tv) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { bool error = false; return tv_get_number_chk(tv, &error); } /// Get the number value of a VimL object /// /// @param[in] tv Object to get value from. /// @param[out] ret_error If type error occurred then `true` will be written /// to this location. Otherwise it is not touched. /// /// @note Needs to be initialized to `false` to be /// useful. /// /// @return Number value: vim_str2nr() output for VAR_STRING objects, value /// for VAR_NUMBER objects, -1 (ret_error == NULL) or 0 (otherwise) for /// other types. varnumber_T tv_get_number_chk(const typval_T *const tv, bool *const ret_error) FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ARG(1) { switch (tv->v_type) { case VAR_FUNC: case VAR_PARTIAL: case VAR_LIST: case VAR_DICT: case VAR_BLOB: case VAR_FLOAT: emsg(_(num_errors[tv->v_type])); break; case VAR_NUMBER: return tv->vval.v_number; case VAR_STRING: { varnumber_T n = 0; if (tv->vval.v_string != NULL) { vim_str2nr(tv->vval.v_string, NULL, NULL, STR2NR_ALL, &n, NULL, 0, false); } return n; } case VAR_BOOL: return tv->vval.v_bool == kBoolVarTrue ? 1 : 0; case VAR_SPECIAL: return 0; case VAR_UNKNOWN: semsg(_(e_intern2), "tv_get_number(UNKNOWN)"); break; } if (ret_error != NULL) { *ret_error = true; } return (ret_error == NULL ? -1 : 0); } /// Get the line number from VimL object /// /// @param[in] tv Object to get value from. Is expected to be a number or /// a special string like ".", "$", … (works with current buffer /// only). /// /// @return Line number or -1 or 0. linenr_T tv_get_lnum(const typval_T *const tv) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { const int did_emsg_before = did_emsg; linenr_T lnum = (linenr_T)tv_get_number_chk(tv, NULL); if (lnum <= 0 && did_emsg_before == did_emsg && tv->v_type != VAR_NUMBER) { int fnum; // No valid number, try using same function as line() does. pos_T *const fp = var2fpos(tv, true, &fnum, false); if (fp != NULL) { lnum = fp->lnum; } } return lnum; } /// Get the floating-point value of a VimL object /// /// Raises an error if object is not number or floating-point. /// /// @param[in] tv Object to get value of. /// /// @return Floating-point value of the variable or zero. float_T tv_get_float(const typval_T *const tv) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { switch (tv->v_type) { case VAR_NUMBER: return (float_T)(tv->vval.v_number); case VAR_FLOAT: return tv->vval.v_float; case VAR_PARTIAL: case VAR_FUNC: emsg(_("E891: Using a Funcref as a Float")); break; case VAR_STRING: emsg(_("E892: Using a String as a Float")); break; case VAR_LIST: emsg(_("E893: Using a List as a Float")); break; case VAR_DICT: emsg(_("E894: Using a Dictionary as a Float")); break; case VAR_BOOL: emsg(_("E362: Using a boolean value as a Float")); break; case VAR_SPECIAL: emsg(_("E907: Using a special value as a Float")); break; case VAR_BLOB: emsg(_("E975: Using a Blob as a Float")); break; case VAR_UNKNOWN: semsg(_(e_intern2), "tv_get_float(UNKNOWN)"); break; } return 0; } /// Give an error and return FAIL unless "args[idx]" is a string. int tv_check_for_string_arg(const typval_T *const args, const int idx) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE { if (args[idx].v_type != VAR_STRING) { semsg(_(e_string_required_for_argument_nr), idx + 1); return FAIL; } return OK; } /// Give an error and return FAIL unless "args[idx]" is a non-empty string. int tv_check_for_nonempty_string_arg(const typval_T *const args, const int idx) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE { if (tv_check_for_string_arg(args, idx) == FAIL) { return FAIL; } if (args[idx].vval.v_string == NULL || *args[idx].vval.v_string == NUL) { semsg(_(e_non_empty_string_required_for_argument_nr), idx + 1); return FAIL; } return OK; } /// Give an error and return FAIL unless "args[idx]" is a number. int tv_check_for_number_arg(const typval_T *const args, const int idx) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE { if (args[idx].v_type != VAR_NUMBER) { semsg(_(e_number_required_for_argument_nr), idx + 1); return FAIL; } return OK; } /// Check for an optional number argument at "idx" int tv_check_for_opt_number_arg(const typval_T *const args, const int idx) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE { return (args[idx].v_type == VAR_UNKNOWN || tv_check_for_number_arg(args, idx) != FAIL) ? OK : FAIL; } /// Give an error and return FAIL unless "args[idx]" is a string or a list. int tv_check_for_string_or_list_arg(const typval_T *const args, const int idx) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE { if (args[idx].v_type != VAR_STRING && args[idx].v_type != VAR_LIST) { semsg(_(e_string_or_list_required_for_argument_nr), idx + 1); return FAIL; } return OK; } /// Get the string value of a "stringish" VimL object. /// /// @param[in] tv Object to get value of. /// @param buf Buffer used to hold numbers and special variables converted to /// string. When function encounters one of these stringified value /// will be written to buf and buf will be returned. /// /// Buffer must have NUMBUFLEN size. /// /// @return Object value if it is VAR_STRING object, number converted to /// a string for VAR_NUMBER, v: variable name for VAR_SPECIAL or NULL. const char *tv_get_string_buf_chk(const typval_T *const tv, char *const buf) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { switch (tv->v_type) { case VAR_NUMBER: snprintf(buf, NUMBUFLEN, "%" PRIdVARNUMBER, tv->vval.v_number); // -V576 return buf; case VAR_STRING: if (tv->vval.v_string != NULL) { return (const char *)tv->vval.v_string; } return ""; case VAR_BOOL: STRCPY(buf, encode_bool_var_names[tv->vval.v_bool]); return buf; case VAR_SPECIAL: STRCPY(buf, encode_special_var_names[tv->vval.v_special]); return buf; case VAR_PARTIAL: case VAR_FUNC: case VAR_LIST: case VAR_DICT: case VAR_FLOAT: case VAR_BLOB: case VAR_UNKNOWN: emsg(_(str_errors[tv->v_type])); return NULL; } abort(); return NULL; } /// Get the string value of a "stringish" VimL object. /// /// @warning For number and special values it uses a single, static buffer. It /// may be used only once, next call to tv_get_string may reuse it. Use /// tv_get_string_buf() if you need to use tv_get_string() output after /// calling it again. /// /// @param[in] tv Object to get value of. /// /// @return Object value if it is VAR_STRING object, number converted to /// a string for VAR_NUMBER, v: variable name for VAR_SPECIAL or NULL. const char *tv_get_string_chk(const typval_T *const tv) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { static char mybuf[NUMBUFLEN]; return tv_get_string_buf_chk(tv, mybuf); } /// Get the string value of a "stringish" VimL object. /// /// @warning For number and special values it uses a single, static buffer. It /// may be used only once, next call to tv_get_string may reuse it. Use /// tv_get_string_buf() if you need to use tv_get_string() output after /// calling it again. /// /// @note tv_get_string_chk() and tv_get_string_buf_chk() are similar, but /// return NULL on error. /// /// @param[in] tv Object to get value of. /// /// @return Object value if it is VAR_STRING object, number converted to /// a string for VAR_NUMBER, v: variable name for VAR_SPECIAL or empty /// string. const char *tv_get_string(const typval_T *const tv) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_RET FUNC_ATTR_WARN_UNUSED_RESULT { static char mybuf[NUMBUFLEN]; return tv_get_string_buf((typval_T *)tv, mybuf); } /// Get the string value of a "stringish" VimL object. /// /// @note tv_get_string_chk() and tv_get_string_buf_chk() are similar, but /// return NULL on error. /// /// @param[in] tv Object to get value of. /// @param buf Buffer used to hold numbers and special variables converted to /// string. When function encounters one of these stringified value /// will be written to buf and buf will be returned. /// /// Buffer must have NUMBUFLEN size. /// /// @return Object value if it is VAR_STRING object, number converted to /// a string for VAR_NUMBER, v: variable name for VAR_SPECIAL or empty /// string. const char *tv_get_string_buf(const typval_T *const tv, char *const buf) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_RET FUNC_ATTR_WARN_UNUSED_RESULT { const char *const res = tv_get_string_buf_chk(tv, buf); return res != NULL ? res : ""; }