src/kv_list.c

Sun, 23 Nov 2025 13:15:19 +0100

author
Mike Becker <universe@uap-core.de>
date
Sun, 23 Nov 2025 13:15:19 +0100
changeset 1508
dfc0ddd9571e
parent 1482
6769cb72521b
permissions
-rw-r--r--

optimize sorted insertion by using the infimum instead of the supremum

The reason is that the supremum returns the equal element with the smallest index, and we want the largest.
Therefore, we use the infimum, which already gives us the largest index when there are equal elements, and increase the index by one. The infimum is also guaranteed to exist in that case.

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2025 Mike Becker, Olaf Wintermann All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   1. Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *
 *   2. Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include "cx/kv_list.h"
#include "cx/hash_map.h"
#include "cx/linked_list.h"

#include <string.h>
#include <assert.h>

typedef struct cx_kv_list_s cx_kv_list;

struct cx_kv_list_map_s {
    struct cx_hash_map_s map_base;
    /** Back-reference to the list. */
    cx_kv_list *list;
};

struct cx_kv_list_s {
    struct cx_linked_list_s list;
    /** The lookup map - stores pointers to the nodes. */
    struct cx_kv_list_map_s *map;
    const cx_list_class *list_methods;
    const cx_map_class *map_methods;
    cx_destructor_func list_destr;
    cx_destructor_func2 list_destr2;
    void *list_destr_data;
    cx_destructor_func map_destr;
    cx_destructor_func2 map_destr2;
    void *map_destr_data;
};

static void cx_kv_list_destructor_wrapper(void *list_ptr, void *elem) {
    const cx_kv_list *kv_list = list_ptr;
    // list destructor is already called with proper deref of the elem
    if (kv_list->list_destr) {
        kv_list->list_destr(elem);
    }
    if (kv_list->list_destr2) {
        kv_list->list_destr2(kv_list->list_destr_data, elem);
    }
    if (kv_list->map_destr) {
        kv_list->map_destr(elem);
    }
    if (kv_list->map_destr2) {
        kv_list->map_destr2(kv_list->map_destr_data, elem);
    }
}

static void cx_kv_list_update_destructors(cx_kv_list *list) {
    // we copy the destructors to our custom fields and register
    // an own destructor function which invokes all these
    if (list->list.base.collection.simple_destructor != NULL) {
        list->list_destr = list->list.base.collection.simple_destructor;
        list->list.base.collection.simple_destructor = NULL;
    }
    if (list->list.base.collection.advanced_destructor != cx_kv_list_destructor_wrapper) {
        list->list_destr2 = list->list.base.collection.advanced_destructor;
        list->list_destr_data = list->list.base.collection.destructor_data;
        list->list.base.collection.advanced_destructor = cx_kv_list_destructor_wrapper;
        list->list.base.collection.destructor_data = list;
    }
    if (list->map->map_base.base.collection.simple_destructor != NULL) {
        list->map_destr = list->map->map_base.base.collection.simple_destructor;
        list->map->map_base.base.collection.simple_destructor = NULL;
    }
    if (list->map->map_base.base.collection.advanced_destructor != NULL) {
        list->map_destr2 = list->map->map_base.base.collection.advanced_destructor;
        list->map_destr_data = list->map->map_base.base.collection.destructor_data;
        list->map->map_base.base.collection.advanced_destructor = NULL;
        list->map->map_base.base.collection.destructor_data = NULL;
    }
}

static CxHashKey *cx_kv_list_loc_key(cx_kv_list *list, void *node_data) {
    return (CxHashKey*)((char*)node_data + list->list.base.collection.elem_size);
}

static void cx_kvl_deallocate(struct cx_list_s *list) {
    cx_kv_list *kv_list = (cx_kv_list*)list;
    // patch the destructors
    cx_kv_list_update_destructors(kv_list);
    kv_list->map_methods->deallocate(&kv_list->map->map_base.base);
    // then free the list, now the destructors may be called
    kv_list->list_methods->deallocate(list);
}

static void *cx_kvl_insert_element(
        struct cx_list_s *list,
        size_t index,
        const void *data
) {
    cx_kv_list *kv_list = (cx_kv_list*)list;
    return kv_list->list_methods->insert_element(list, index, data);
}

static size_t cx_kvl_insert_array(
        struct cx_list_s *list,
        size_t index,
        const void *data,
        size_t n
) {
    cx_kv_list *kv_list = (cx_kv_list*)list;
    return kv_list->list_methods->insert_array(list, index, data, n);
}

static size_t cx_kvl_insert_sorted(
        struct cx_list_s *list,
        const void *sorted_data,
        size_t n
) {
    cx_kv_list *kv_list = (cx_kv_list*)list;
    return kv_list->list_methods->insert_sorted(list, sorted_data, n);
}

static size_t cx_kvl_insert_unique(
        struct cx_list_s *list,
        const void *sorted_data,
        size_t n
) {
    cx_kv_list *kv_list = (cx_kv_list*)list;
    return kv_list->list_methods->insert_unique(list, sorted_data, n);
}

static int cx_kvl_insert_iter(
        struct cx_iterator_s *iter,
        const void *elem,
        int prepend
) {
    cx_kv_list *kv_list = iter->src_handle;
    return kv_list->list_methods->insert_iter(iter, elem, prepend);
}

static size_t cx_kvl_remove(
        struct cx_list_s *list,
        size_t index,
        size_t num,
        void *targetbuf
) {
    cx_kv_list *kv_list = (cx_kv_list*)list;
    // patch the destructors
    // we also have to do that when targetbuf is NULL,
    // because we do not want wrong destructors to be called when we remove keys from the map
    cx_kv_list_update_destructors(kv_list);
    // iterate through the elements first and remove their keys from the map
    CxIterator iter = kv_list->list_methods->iterator(list, index, false);
    for (size_t i = 0; i < num && cxIteratorValid(iter); i++) {
        void *node_data = cxIteratorCurrent(iter);
        CxHashKey *key = cx_kv_list_loc_key(kv_list, node_data);
        // when the hash is zero, there is no key assigned to that element
        if (key->hash != 0) {
            kv_list->map_methods->remove(&kv_list->map->map_base.base, *key, NULL);
        }
        cxIteratorNext(iter);
    }
    return kv_list->list_methods->remove(list, index, num, targetbuf);
}

static void cx_kvl_clear(struct cx_list_s *list) {
    cx_kv_list *kv_list = (cx_kv_list*)list;
    // patch the destructors
    cx_kv_list_update_destructors(kv_list);
    // clear the list
    kv_list->list_methods->clear(list);
    // then clear the map
    kv_list->map_methods->clear(&kv_list->map->map_base.base);
}

static int cx_kvl_swap(
        struct cx_list_s *list,
        size_t i,
        size_t j
) {
    cx_kv_list *kv_list = (cx_kv_list*)list;
    return kv_list->list_methods->swap(list, i, j);
}

static void *cx_kvl_at(
        const struct cx_list_s *list,
        size_t index
) {
    const cx_kv_list *kv_list = (const cx_kv_list*)list;
    return kv_list->list_methods->at(list, index);
}

static size_t cx_kvl_find_remove(
        struct cx_list_s *list,
        const void *elem,
        bool remove
) {
    cx_kv_list *kv_list = (cx_kv_list*)list;
    // we do not use the original list methods,
    // because that would need two passes for removal
    // (the first to find the index, the second to get a pointer)
    if (list->collection.size == 0) return 0;

    size_t index;
    cx_linked_list *ll = &kv_list->list;
    char *node = cx_linked_list_find(
            ll->begin,
            ll->loc_next, ll->loc_data,
            list->collection.cmpfunc, elem,
            &index
    );
    if (node == NULL) {
        return list->collection.size;
    }
    if (remove) {
        cx_kv_list_update_destructors(kv_list);
        cx_invoke_advanced_destructor(list, node + ll->loc_data);
        cx_linked_list_remove(&ll->begin, &ll->end,
                              ll->loc_prev, ll->loc_next, node);
        CxHashKey *key = cx_kv_list_loc_key(kv_list, node + ll->loc_data);
        if (key->hash != 0) {
            kv_list->map_methods->remove(&kv_list->map->map_base.base, *key, NULL);
        }
        list->collection.size--;
        cxFree(list->collection.allocator, node);
    }
    return index;
}

static void cx_kvl_sort(struct cx_list_s *list) {
    cx_kv_list *kv_list = (cx_kv_list*)list;
    kv_list->list_methods->sort(list);
}

static void cx_kvl_reverse(struct cx_list_s *list) {
    cx_kv_list *kv_list = (cx_kv_list*)list;
    kv_list->list_methods->reverse(list);
}

static void cx_kvl_list_iter_next(void *it) {
    struct cx_iterator_s *iter = it;
    if (iter->base.remove) {
        // remove the assigned key from the map before calling the actual function
        cx_kv_list *kv_list = iter->src_handle;
        cx_kv_list_update_destructors(kv_list);
        char *node = iter->elem_handle;
        CxHashKey *key = cx_kv_list_loc_key(kv_list, node + kv_list->list.loc_data);
        if (key->hash != 0) {
            kv_list->map_methods->remove(&kv_list->map->map_base.base, *key, NULL);
        }
    }
    // note that we do not clear the remove flag, because the next_impl will do that
    iter->base.next_impl(it);
}

static struct cx_iterator_s cx_kvl_iterator(
        const struct cx_list_s *list,
        size_t index,
        bool backward
) {
    const cx_kv_list *kv_list = (const cx_kv_list*)list;
    struct cx_iterator_s iter = kv_list->list_methods->iterator(list, index, backward);
    iter.base.next_impl = iter.base.next;
    iter.base.next = cx_kvl_list_iter_next;
    return iter;
}

static void cx_kvl_map_deallocate(struct cx_map_s *map) {
    cx_kv_list *kv_list = ((struct cx_kv_list_map_s*)map)->list;
    kv_list->map_methods->deallocate(map);
    kv_list->list_methods->deallocate(&kv_list->list.base);
}

static void cx_kvl_map_clear(struct cx_map_s *map) {
    cx_kv_list *kv_list = ((struct cx_kv_list_map_s*)map)->list;
    cx_kv_list_update_destructors(kv_list);
    kv_list->list_methods->clear(&kv_list->list.base);
    kv_list->map_methods->clear(map);
}

static void *cx_kvl_map_put(CxMap *map, CxHashKey key, void *value) {
    cx_kv_list *kv_list = ((struct cx_kv_list_map_s*)map)->list;
    // if the hash has not yet been computed, do it now
    if (key.hash == 0) {
        cx_hash_murmur(&key);
    }

    // reserve memory in the map first
    void **map_data = kv_list->map_methods->put(map, key, NULL);
    if (map_data == NULL) return NULL; // LCOV_EXCL_LINE

    // insert the data into the list (which most likely destroys the sorted property)
    kv_list->list.base.collection.sorted = false;
    void *node_data = kv_list->list_methods->insert_element(
        &kv_list->list.base, kv_list->list.base.collection.size,
        kv_list->list.base.collection.store_pointer ? &value : value);
    if (node_data == NULL) { // LCOV_EXCL_START
        // non-destructively remove the key again
        kv_list->map_methods->remove(&kv_list->map->map_base.base, key, &map_data);
        return NULL;
    } // LCOV_EXCL_STOP

    // write the node pointer to the map entry
    *map_data = node_data;

    // copy the key to the node data
    CxHashKey *key_ptr = cx_kv_list_loc_key(kv_list, node_data);
    *key_ptr = key;

    // we must return node_data here and not map_data,
    // because the node_data is the actual element of this collection
    return node_data;
}

void *cx_kvl_map_get(const CxMap *map, CxHashKey key) {
    cx_kv_list *kv_list = ((struct cx_kv_list_map_s*)map)->list;
    void *node_data = kv_list->map_methods->get(map, key);
    if (node_data == NULL) return NULL; // LCOV_EXCL_LINE
    // return the node data
    return kv_list->list.base.collection.store_pointer ? *(void**)node_data : node_data;
}

int cx_kvl_map_remove(CxMap *map, CxHashKey key, void *targetbuf) {
    cx_kv_list *kv_list = ((struct cx_kv_list_map_s*)map)->list;

    void *node_data;
    if (kv_list->map_methods->remove(map, key, &node_data)) {
        return 1;
    }
    // we cannot just call a list method (because we don't have the index)
    // and tbh. we also don't want to (because it's not performant when we
    // can have the node ptr directly instead)
    // therefore, we re-implement the logic ourselves

    // check if the outside caller want's us to return or to destroy the element
    if (targetbuf == NULL) {
        // patch the destructors and invoke them through the wrapper
        cx_kv_list_update_destructors(kv_list);
        cx_invoke_advanced_destructor(&kv_list->list.base, node_data);
    } else {
        // copy the element to the target buffer
        memcpy(targetbuf, node_data, kv_list->list.base.collection.elem_size);
    }

    // calculate the address of the node
    void *node_ptr = (char*)node_data - kv_list->list.loc_data;

    // unlink the node
    cx_linked_list_remove(
        &kv_list->list.begin,
        &kv_list->list.end,
        kv_list->list.loc_prev,
        kv_list->list.loc_next,
        node_ptr
    );

    // decrement the list's size
    kv_list->list.base.collection.size--;

    // deallocate the node
    cxFree(kv_list->list.base.collection.allocator, node_ptr);

    return 0;
}

static void *cx_kvl_iter_current_entry(const void *it) {
    const CxMapIterator *iter = it;
    return (void*)&iter->entry;
}

static void *cx_kvl_iter_current_key(const void *it) {
    const CxMapEntry *entry = cx_kvl_iter_current_entry(it);
    return (void*)entry->key;
}

static void *cx_kvl_iter_current_value(const void *it) {
    const CxMapEntry *entry = cx_kvl_iter_current_entry(it);
    return entry->value;
}

static void cx_kvl_iter_next(void *it) {
    CxMapIterator *iter = it;
    cx_kv_list *kv_list = ((struct cx_kv_list_map_s*)iter->map)->list;

    // find the next list entry that has a key assigned
    CxHashKey *key = NULL;
    char *next = iter->elem;
    while (true) {
        next = *(char**)(next + kv_list->list.loc_next);
        if (next == NULL) break;
        key = cx_kv_list_loc_key(kv_list, next + kv_list->list.loc_data);
        if (key->hash != 0) break;
    }

    // remove previous element if requested
    if (iter->base.remove) {
        iter->base.remove = false;
        cx_kv_list_update_destructors(kv_list);
        char *elem = iter->elem;
        char *elem_data = elem + kv_list->list.loc_data;
        CxHashKey *elem_key = cx_kv_list_loc_key(kv_list, elem_data);
        // key is guaranteed to exist because iterator only iterates over elems with a key
        kv_list->map_methods->remove(&kv_list->map->map_base.base, *elem_key, NULL);
        cx_invoke_advanced_destructor(&kv_list->list.base, elem_data);
        cx_linked_list_remove(
            &kv_list->list.begin,
            &kv_list->list.end,
            kv_list->list.loc_prev,
            kv_list->list.loc_next,
            elem
        );
        cxFree(kv_list->list.base.collection.allocator, elem);
        kv_list->list.base.collection.size--;
        iter->index--;
        iter->elem_count--;
    }

    // advance to the next element, if any
    if (next == NULL) {
        iter->index = kv_list->list.base.collection.size;
        iter->elem = NULL;
        iter->entry = (CxMapEntry){NULL, NULL};
        return;
    }
    iter->index++;
    iter->elem = next;
    iter->entry.key = key;
    if (kv_list->list.base.collection.store_pointer) {
        iter->entry.value = *(void**)(next + kv_list->list.loc_data);
    } else {
        iter->entry.value = (void*)(next + kv_list->list.loc_data);
    }
}

static bool cx_kvl_iter_valid(const void *it) {
    const CxMapIterator *iter = it;
    return iter->elem != NULL;
}

CxMapIterator cx_kvl_map_iterator(const CxMap *map, enum cx_map_iterator_type type) {
    CxMapIterator iter = {0};

    iter.type = type;
    iter.map = (CxMap*)map;
    // although we iterate over the list, we only report that many elements that have a key in the map
    iter.elem_count = map->collection.size;

    switch (type) {
        case CX_MAP_ITERATOR_PAIRS:
            iter.elem_size = sizeof(CxMapEntry);
            iter.base.current = cx_kvl_iter_current_entry;
            break;
        case CX_MAP_ITERATOR_KEYS:
            iter.elem_size = sizeof(CxHashKey);
            iter.base.current = cx_kvl_iter_current_key;
            break;
        case CX_MAP_ITERATOR_VALUES:
            iter.elem_size = map->collection.elem_size;
            iter.base.current = cx_kvl_iter_current_value;
            break;
        default:
            assert(false); // LCOV_EXCL_LINE
    }

    iter.base.allow_remove = true;
    iter.base.next = cx_kvl_iter_next;
    iter.base.valid = cx_kvl_iter_valid;

    // find the first list entry that has a key assigned
    cx_kv_list *kv_list = ((struct cx_kv_list_map_s*)map)->list;
    CxHashKey *key = NULL;
    char *next = kv_list->list.begin;
    while (next != NULL) {
        key = cx_kv_list_loc_key(kv_list, next + kv_list->list.loc_data);
        if (key->hash != 0) break;
        next = *(char**)(next + kv_list->list.loc_next);
    }
    if (next == NULL) {
        iter.elem = NULL;
        iter.entry = (CxMapEntry){NULL, NULL};
    } else {
        iter.elem = next;
        iter.entry.key = key;
        if (kv_list->list.base.collection.store_pointer) {
            iter.entry.value = *(void**)(next + kv_list->list.loc_data);
        } else {
            iter.entry.value = (void*)(next + kv_list->list.loc_data);
        }
    }

    return iter;
}

static int cx_kvl_change_capacity(struct cx_list_s *list,
        cx_attr_unused size_t cap) {
    // since our backing list is a linked list, we don't need to do much here,
    // but rehashing the map is quite useful
    cx_kv_list *kv_list = (cx_kv_list*)list;
    cxMapRehash(&kv_list->map->map_base.base);
    return 0;
}

static cx_list_class cx_kv_list_class = {
    cx_kvl_deallocate,
    cx_kvl_insert_element,
    cx_kvl_insert_array,
    cx_kvl_insert_sorted,
    cx_kvl_insert_unique,
    cx_kvl_insert_iter,
    cx_kvl_remove,
    cx_kvl_clear,
    cx_kvl_swap,
    cx_kvl_at,
    cx_kvl_find_remove,
    cx_kvl_sort,
    NULL,
    cx_kvl_reverse,
    cx_kvl_change_capacity,
    cx_kvl_iterator,
};

static cx_map_class cx_kv_map_class = {
    cx_kvl_map_deallocate,
    cx_kvl_map_clear,
    cx_kvl_map_put,
    cx_kvl_map_get,
    cx_kvl_map_remove,
    cx_kvl_map_iterator,
};

CxList *cxKvListCreate(
        const CxAllocator *allocator,
        cx_compare_func comparator,
        size_t elem_size
) {
    if (allocator == NULL) {
        allocator = cxDefaultAllocator;
    }

    // create a normal linked list and a normal hash map, first
    CxList *list = cxLinkedListCreate(allocator, comparator, elem_size);
    if (list == NULL) return NULL; // LCOV_EXCL_LINE
    cx_linked_list *ll = (cx_linked_list*)list;
    ll->extra_data_len = sizeof(CxHashKey);
    CxMap *map = cxHashMapCreate(allocator, CX_STORE_POINTERS, 0);
    if (map == NULL) { // LCOV_EXCL_START
        cxListFree(list);
        return NULL;
    } // LCOV_EXCL_STOP

    // patch the kv-list class with the compare function of the linked list
    // this allows cxListCompare() to optimize comparisons between linked lists and kv-list
    cx_kv_list_class.compare = list->cl->compare;

    // reallocate the map to add memory for the list back-reference
    struct cx_kv_list_map_s *kv_map = cxRealloc(allocator, map, sizeof(struct cx_kv_list_map_s));
    
    // reallocate the list to add memory for storing the metadata
    cx_kv_list *kv_list = cxRealloc(allocator, list, sizeof(struct cx_kv_list_s));

    // if any of the reallocations failed, we bail out
    if (kv_map != NULL && kv_list != NULL) {
        map = (CxMap*) kv_map;
        list = (CxList*) kv_list;
    } else { // LCOV_EXCL_START
        cxListFree(list);
        cxMapFree(map);
        return NULL;
    } // LCOV_EXCL_STOP

    // zero the custom destructor information
    memset((char*)kv_list + offsetof(cx_kv_list, list_destr), 0, sizeof(void*)*6);

    // combine the list and the map aspect
    kv_list->map = kv_map;
    kv_map->list = kv_list;

    // remember the base methods and override them
    kv_list->map_methods = map->cl;
    map->cl = &cx_kv_map_class;
    if (list->climpl == NULL) {
        kv_list->list_methods = list->cl;
        list->cl = &cx_kv_list_class;
    } else {
        kv_list->list_methods = list->climpl;
        list->climpl = &cx_kv_list_class;
    }

    return list;
}

CxMap *cxKvListCreateAsMap(
        const CxAllocator *allocator,
        cx_compare_func comparator,
        size_t elem_size
) {
    CxList *list = cxKvListCreate(allocator, comparator, elem_size);
    return list == NULL ? NULL : cxKvListAsMap(list);
}

CxList *cxKvListAsList(CxMap *map) {
    return &((struct cx_kv_list_map_s*)map)->list->list.base;
}

CxMap *cxKvListAsMap(CxList *list) {
    return &((cx_kv_list*)list)->map->map_base.base;
}

int cx_kv_list_set_key(CxList *list, size_t index, CxHashKey key) {
    cx_kv_list *kv_list = (cx_kv_list*)list;
    void *node_data = kv_list->list_methods->at(list, index);
    if (node_data == NULL) {
        return 1;
    }
    // if the hash has not yet been computed, do it now
    if (key.hash == 0) {
        cx_hash_murmur(&key);
    }

    // check if the key is already assigned
    void *existing = kv_list->map_methods->get(&kv_list->map->map_base.base, key);
    if (existing == node_data) {
        return 0; // nothing to do
    }
    if (existing != NULL) {
        // the key is already assigned to another node, we disallow re-assignment
        return 1;
    }

    // add the key to the map;
    if (NULL == kv_list->map_methods->put(&kv_list->map->map_base.base, key, node_data)) {
        return 1; // LCOV_EXCL_LINE
    }

    // write the key to the list's node
    CxHashKey *loc_key = cx_kv_list_loc_key(kv_list, node_data);
    *loc_key = key;

    return 0;
}

int cxKvListRemoveKey(CxList *list, size_t index) {
    cx_kv_list *kv_list = (cx_kv_list*)list;
    void *node_data = kv_list->list_methods->at(list, index);
    if (node_data == NULL) {
        return 1;
    }
    CxHashKey *loc_key = cx_kv_list_loc_key(kv_list, node_data);
    if (loc_key->hash == 0) {
        return 0;
    }
    kv_list->map_methods->remove(&kv_list->map->map_base.base, *loc_key, NULL);
    // also zero the memory in the list node,
    // but don't free the key data (that was done by the map remove)
    memset(loc_key, 0, sizeof(CxHashKey));
    return 0;
}

const CxHashKey *cxKvListGetKey(CxList *list, size_t index) {
    cx_kv_list *kv_list = (cx_kv_list*)list;
    void *node_data = kv_list->list_methods->at(list, index);
    if (node_data == NULL) {
        return NULL;
    }
    CxHashKey *key = cx_kv_list_loc_key(kv_list, node_data);
    if (key->hash == 0) {
        return NULL;
    }
    return key;
}

int cx_kv_list_insert(CxList *list, size_t index, CxHashKey key, void *value) {
    // assume we are losing the sorted property
    list->collection.sorted = false;

    cx_kv_list *kv_list = (cx_kv_list*)list;

    // reserve memory in the map
    void **map_data = kv_list->map_methods->put(&kv_list->map->map_base.base, key, NULL);
    if (map_data == NULL) return 1; // LCOV_EXCL_LINE

    // insert the node
    void *node_data = kv_list->list_methods->insert_element(&kv_list->list.base, index,
        kv_list->list.base.collection.store_pointer ? &value : value);
    if (node_data == NULL) { // LCOV_EXCL_START
        // non-destructively remove the key again
        kv_list->map_methods->remove(&kv_list->map->map_base.base, key, &map_data);
        return 1;
    } // LCOV_EXCL_STOP
    *map_data = node_data;

    // write the key to the node
    CxHashKey *loc_key = cx_kv_list_loc_key(kv_list, node_data);
    *loc_key = key;

    return 0;
}

mercurial