From 16f6d62ef80d1ff231ee8a62a6174c21e4c1f095 Mon Sep 17 00:00:00 2001 From: Fabrizio Pietrucci Date: Sun, 22 Mar 2026 13:02:43 +0100 Subject: [PATCH 1/6] Add `Ext_Array` macro to simplify array definitions --- examples/01_dynamic_array.c | 4 ++++ extlib.h | 28 +++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/examples/01_dynamic_array.c b/examples/01_dynamic_array.c index e484283..5e7a7d3 100644 --- a/examples/01_dynamic_array.c +++ b/examples/01_dynamic_array.c @@ -11,6 +11,8 @@ #define EXTLIB_IMPL #include "../extlib.h" +// Could also create this typedef with `Array` utility macro: +// typedef Array(StringSlice) Lines; typedef struct { StringSlice *items; size_t size, capacity; @@ -22,6 +24,8 @@ static int qsort_cmp(const void *a, const void *b) { } int main(void) { + // Could also define this dynamic array inline with: + // Array(StringSlice) lines = {0}; Lines lines = {0}; StringBuffer file = {0}; diff --git a/extlib.h b/extlib.h index 366eaa1..4310921 100644 --- a/extlib.h +++ b/extlib.h @@ -822,7 +822,7 @@ char *ext_arena_vsprintf(Ext_Arena *a, const char *fmt, va_list ap); // The dynamic array integrates with the `Allocator` interface and the context to support custom // allocators for its backing array. // -// USAGE; +// USAGE: //```c // typedef struct { // int* items; @@ -849,6 +849,31 @@ char *ext_arena_vsprintf(Ext_Arena *a, const char *fmt, va_list ap); #define EXT_ARRAY_INIT_CAP 8 #endif // EXT_ARRAY_INIT_CAP +// Utility macro for defining a dynamic array in-line. +// The array is defined as an anonymous struct. +// +// USAGE: +// ```c +// Array(int) int_array = {0}; +// array_push(&int_array, 1); +// array_push(&int_array, 2); +// ... +// ``` +// +// You can also use this macro to `typedef` the array definition so you can re-use it multiple +// times: +// ```c +// typedef Array(int) Int_Array; +// ... +// Int_Array int_array = {0}; +// ``` +#define Ext_Array(T) \ + struct { \ + T *items; \ + size_t capacity, size; \ + Ext_Allocator *allocator; \ + } + // Macro to iterate over all elements // // USAGE @@ -3689,6 +3714,7 @@ static inline int ext_dbg_unknown(const char *name, const char *file, int line, #define arena_vsprintf ext_arena_vsprintf #endif // EXTLIB_NO_STD +#define Array Ext_Array #define array_foreach ext_array_foreach #define array_reserve ext_array_reserve #define array_reserve_exact ext_array_reserve_exact From 4f1cf7a059cf0c6c6ca975855c6c3dd541e0474d Mon Sep 17 00:00:00 2001 From: Fabrizio Pietrucci Date: Sun, 22 Mar 2026 13:20:24 +0100 Subject: [PATCH 2/6] Remove `typedef` from `EXT_STATIC_ASSERT` in c99 compatibility mode. --- extlib.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/extlib.h b/extlib.h index 4310921..bf901f9 100644 --- a/extlib.h +++ b/extlib.h @@ -238,10 +238,10 @@ void assert(int c); // TODO: are we sure we want to require wasm embedder to pr #include #define EXT_STATIC_ASSERT static_assert #else -#define EXT_STATIC_ASSERT(cond, msg) \ - typedef struct { \ - int static_assertion_failed : !!(cond); \ - } EXT_CONCAT_(EXT_CONCAT_(static_assertion_failed_, __COUNTER__), __LINE__) +#define EXT_STATIC_ASSERT(cond, msg) \ + struct EXT_CONCAT_(EXT_CONCAT_(static_assertion_failed_, __COUNTER__), __LINE__) { \ + int static_assertion_failed : !!(cond); \ + } #endif // defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) && !defined(EXTLIB_NO_STD) // Debug macro: prints an expression to stderr and returns its value. From 7a6e57e80d68a3a622f12e7d5ef18d162acb4990 Mon Sep 17 00:00:00 2001 From: Fabrizio Pietrucci Date: Sun, 22 Mar 2026 15:50:42 +0100 Subject: [PATCH 3/6] Reworking hashmap implementation --- examples/02_hashmap.c | 3 +- extlib.h | 570 ++++++++++++++++++++++++++++++++---------- test/test.c | 211 ++++++++++++++++ 3 files changed, 645 insertions(+), 139 deletions(-) diff --git a/examples/02_hashmap.c b/examples/02_hashmap.c index d176e4c..a04c613 100644 --- a/examples/02_hashmap.c +++ b/examples/02_hashmap.c @@ -121,8 +121,7 @@ int main(int argc, char **argv) { StringSlice word = ss_split_once_ws(&file_slice); if(word.size == 0) continue; - WordFreq *e; - hmap_get_default_ss(&words_freq, word, 0, &e); + WordFreq *e = hmap_getp_default_ss(&words_freq, word, 0); ASSERT(e != NULL, "default entry shouldn't be NULL"); e->value++; diff --git a/extlib.h b/extlib.h index bf901f9..5d7b1e5 100644 --- a/extlib.h +++ b/extlib.h @@ -1378,162 +1378,303 @@ int ext_cmd_write(const char *cmd, const void *data, size_t size); // temp_reset(&map); // ``` +// Declares a hashmap entry struct with fields `key` (type K) and `value` (type V). +// Useful as the type argument to `hmap_foreach` when using `Ext_HashMap`: +// +// ```c +// hmap_foreach(Ext_Entry(StringSlice, int), it, &map) { ... } +// ``` +// +// Note: each expansion of `Ext_Entry(K, V)` produces a distinct anonymous struct type. Pointer +// assignments between two separate expansions require a cast. For direct variable storage, prefer +// typedef-ing the entry type once. +#define Ext_Entry(K, V) \ + struct { \ + K key; \ + V value; \ + } + +// Declares a hashmap struct for key type K and value type V, using `Ext_Entry(K, V)` for the +// entries array. The generated struct has the same layout as a manually-declared hashmap struct. +// +// Use `hmap_getp` / `hmap_getp_default` with inline maps to avoid needing a named entry type: +// +// ```c +// Ext_HashMap(StringSlice, int) word_count = {0}; +// hmap_getp_default_ss(&word_count, word, 0)->value++; +// +// hmap_foreach(Ext_Entry(StringSlice, int), it, &word_count) { +// printf("%zu\n", (size_t)it->value); +// } +// ``` +#define Ext_HashMap(K, V) \ + struct { \ + Ext_Entry(K, V) * entries; \ + size_t *hashes; \ + size_t size, capacity; \ + Ext_Allocator *allocator; \ + } + // Read as: size * 0.75, i.e. a load factor of 75% // This is basically doing: // size / 2 + size / 4 = (3 * size) / 4 #define EXT_HMAP_MAX_ENTRY_LOAD(size) (((size) >> 1) + ((size) >> 2)) // Puts an entry into the hashmap. -// `hash_fn` is expected to be a function (or function-like macro) -// that takes a key by pointer and returns an hash of it. `cmp_fn` is also expected to be a -// function (or function-like macro) that takes two keys by pointer and compares them, returning -// 0 if they're euqal, -1 if a is less than b, 1 if a is greater than b. -// -// You probably want to use the non-ex version of this function (ext_hmap_put, ext_hmap_put_cstr -// or ext_hmap_put_ss) unless you specifically need to customize the way entries are hashed or -// compared. +// `hash_fn` and `cmp_fn` are functions or function-like macros: +// size_t hash_fn(KeyType *key) +// int cmp_fn (KeyType *key_a, KeyType *key_b) — returns 0 if equal +// Both receive a pointer to the key field (first field of the entry). +// You probably want ext_hmap_put / ext_hmap_put_cstr / ext_hmap_put_ss instead. #define ext_hmap_put_ex(hmap, entry_key, entry_val, hash_fn, cmp_fn) \ do { \ if((hmap)->size >= EXT_HMAP_MAX_ENTRY_LOAD((hmap)->capacity + 1)) { \ ext_hmap_grow_((void **)&(hmap)->entries, sizeof(*(hmap)->entries), &(hmap)->hashes, \ &(hmap)->capacity, (Ext_Allocator **)&(hmap)->allocator); \ - ext_hmap_tombs_(hmap) = (hmap)->size; \ } \ ext_hmap_tmp_(hmap).key = (entry_key); \ ext_hmap_tmp_(hmap).value = (entry_val); \ - size_t hash = hash_fn(&ext_hmap_tmp_(hmap).key); \ - if(hash < 2) hash += 2; \ - ext_hmap_find_index_(hmap, &ext_hmap_tmp_(hmap).key, hash, cmp_fn); \ - if(!EXT_HMAP_IS_VALID((hmap)->hashes[idx_])) { \ - (hmap)->size++; \ - if(!EXT_HMAP_IS_TOMB((hmap)->hashes[idx_])) ext_hmap_tombs_(hmap)++; \ - } \ + size_t hash_ = hash_fn(&ext_hmap_tmp_(hmap).key); \ + if(hash_ < 2) hash_ += 2; \ + ext__hmap_probe_((hmap)->hashes, (hmap)->capacity, hash_, \ + cmp_fn(&ext_hmap_tmp_(hmap).key, &(hmap)->entries[i_ + 1].key) == 0); \ + size_t idx_ = (hmap)->hashes[0]; \ + if(!EXT_HMAP_IS_VALID((hmap)->hashes[idx_])) (hmap)->size++; \ (hmap)->entries[idx_] = ext_hmap_tmp_(hmap); \ - (hmap)->hashes[idx_] = hash; \ + (hmap)->hashes[idx_] = hash_; \ } while(0) -// Gets an entry from the hashmap. -// The retrieved entry is put in `out` if found, otherwise `out` is set to NULL. -// `hash_fn` is expected to be a function (or function-like macro) that takes a key by pointer -// and returns an hash of it. `cmp_fn` is also expected to be a function (or function-like -// macro) that takes two keys by pointer and compares them, returning 0 if they're euqal, -1 if -// a is less than b, 1 if a is greater than b. -// -// You probably want to use the non-ex version of this function (ext_hmap_get, ext_hmap_get_cstr -// or ext_hmap_get_ss) unless you specifically need to customize the way entries are hashed or -// compared. -#define ext_hmap_get_ex(hmap, entry_key, out, hash_fn, cmp_fn) \ - do { \ - if(!(hmap)->size) { \ - *(out) = NULL; \ - break; \ - } \ - ext_hmap_tmp_(hmap).key = (entry_key); \ - size_t hash = hash_fn(&ext_hmap_tmp_(hmap).key); \ - if(hash < 2) hash += 2; \ - ext_hmap_find_index_(hmap, &ext_hmap_tmp_(hmap).key, hash, cmp_fn); \ - if(EXT_HMAP_IS_VALID((hmap)->hashes[idx_])) { \ - *(out) = &(hmap)->entries[idx_]; \ - } else { \ - *(out) = NULL; \ - } \ +// Gets an entry from the hashmap. Sets `*out` to a pointer to the found entry, or NULL. +// See ext_hmap_put_ex for hash_fn / cmp_fn conventions. +// You probably want ext_hmap_get / ext_hmap_get_cstr / ext_hmap_get_ss instead. +#define ext_hmap_get_ex(hmap, entry_key, out, hash_fn, cmp_fn) \ + do { \ + if(!(hmap)->size) { \ + *(out) = NULL; \ + break; \ + } \ + ext_hmap_tmp_(hmap).key = (entry_key); \ + size_t hash_ = hash_fn(&ext_hmap_tmp_(hmap).key); \ + if(hash_ < 2) hash_ += 2; \ + ext__hmap_probe_((hmap)->hashes, (hmap)->capacity, hash_, \ + cmp_fn(&ext_hmap_tmp_(hmap).key, &(hmap)->entries[i_ + 1].key) == 0); \ + size_t idx_ = (hmap)->hashes[0]; \ + *(out) = EXT_HMAP_IS_VALID((hmap)->hashes[idx_]) ? &(hmap)->entries[idx_] : NULL; \ } while(0) -// Gets an entry from the hashmap, creating a new one in if it is not found. -// The retrieved (or newly created) entry is put in `out`. -// `hash_fn` is expected to be a function (or function-like macro) that takes a key by pointer -// and returns an hash of it. `cmp_fn` is also expected to be a function (or function-like -// macro) that takes two keys by pointer and compares them, returning 0 if they're euqal, -1 if -// a is less than b, 1 if a is greater than b. -// -// You probably want to use the non-ex version of this function (ext_hmap_get, ext_hmap_get_cstr -// or ext_hmap_get_ss) unless you specifically need to customize the way entries are hashed or -// compared. +// Gets an entry from the hashmap, inserting a default if not found. Sets `*out` to the entry. +// See ext_hmap_put_ex for hash_fn / cmp_fn conventions. +// You probably want ext_hmap_get_default / _cstr / _ss instead. #define ext_hmap_get_default_ex(hmap, entry_key, entry_val, out, hash_fn, cmp_fn) \ do { \ if((hmap)->size >= EXT_HMAP_MAX_ENTRY_LOAD((hmap)->capacity + 1)) { \ ext_hmap_grow_((void **)&(hmap)->entries, sizeof(*(hmap)->entries), &(hmap)->hashes, \ &(hmap)->capacity, (Ext_Allocator **)&(hmap)->allocator); \ - ext_hmap_tombs_(hmap) = (hmap)->size; \ } \ ext_hmap_tmp_(hmap).key = (entry_key); \ ext_hmap_tmp_(hmap).value = (entry_val); \ - size_t hash = hash_fn(&ext_hmap_tmp_(hmap).key); \ - if(hash < 2) hash += 2; \ - ext_hmap_find_index_(hmap, &ext_hmap_tmp_(hmap).key, hash, cmp_fn); \ + size_t hash_ = hash_fn(&ext_hmap_tmp_(hmap).key); \ + if(hash_ < 2) hash_ += 2; \ + ext__hmap_probe_((hmap)->hashes, (hmap)->capacity, hash_, \ + cmp_fn(&ext_hmap_tmp_(hmap).key, &(hmap)->entries[i_ + 1].key) == 0); \ + size_t idx_ = (hmap)->hashes[0]; \ if(!EXT_HMAP_IS_VALID((hmap)->hashes[idx_])) { \ - (hmap)->size++; \ - if(!EXT_HMAP_IS_TOMB((hmap)->hashes[idx_])) ext_hmap_tombs_(hmap)++; \ (hmap)->entries[idx_] = ext_hmap_tmp_(hmap); \ - (hmap)->hashes[idx_] = hash; \ + (hmap)->hashes[idx_] = hash_; \ + (hmap)->size++; \ } \ *(out) = &(hmap)->entries[idx_]; \ } while(0) // Deletes an entry from the hashmap. -// `hash_fn` is expected to be a function (or function-like macro) that takes the entry and -// returns an hash of it. `cmp_fn` is also expected to be a function (or function-like macro) -// that takes two entries and compares them, returning 0 if they're euqal, -1 if a is less than -// b, 1 if a is greater than b. -// -// You probably want to use the non-ex version of this function (ext_hmap_delete, -// ext_hmap_delete_cstr or ext_hmap_delete_ss) unless you specifically need to customize the way -// entries are hashed or compared. -#define ext_hmap_delete_ex(hmap, entry_key, hash_fn, cmp_fn) \ - do { \ - if(!(hmap)->size) break; \ - ext_hmap_tmp_(hmap).key = (entry_key); \ - size_t hash = hash_fn(&ext_hmap_tmp_(hmap).key); \ - if(hash < 2) hash += 2; \ - ext_hmap_find_index_(hmap, &ext_hmap_tmp_(hmap).key, hash, cmp_fn); \ - if(EXT_HMAP_IS_VALID((hmap)->hashes[idx_])) { \ - (hmap)->hashes[idx_] = EXT_HMAP_TOMB_MARK; \ - (hmap)->size--; \ - } \ +// See ext_hmap_put_ex for hash_fn / cmp_fn conventions. +// You probably want ext_hmap_delete / ext_hmap_delete_cstr / ext_hmap_delete_ss instead. +#define ext_hmap_delete_ex(hmap, entry_key, hash_fn, cmp_fn) \ + do { \ + if(!(hmap)->size) break; \ + ext_hmap_tmp_(hmap).key = (entry_key); \ + size_t hash_ = hash_fn(&ext_hmap_tmp_(hmap).key); \ + if(hash_ < 2) hash_ += 2; \ + ext__hmap_probe_((hmap)->hashes, (hmap)->capacity, hash_, \ + cmp_fn(&ext_hmap_tmp_(hmap).key, &(hmap)->entries[i_ + 1].key) == 0); \ + size_t idx_ = (hmap)->hashes[0]; \ + if(EXT_HMAP_IS_VALID((hmap)->hashes[idx_])) { \ + (hmap)->hashes[idx_] = EXT_HMAP_TOMB_MARK; \ + (hmap)->size--; \ + } \ } while(0) -// Puts an entry into the hashmap. keys are compared with `memcmp`. +// Puts an entry into the hashmap. Keys are compared byte-for-byte with memcmp. #define ext_hmap_put(hmap, entry_key, entry_val) \ ext_hmap_put_ex(hmap, entry_key, entry_val, ext_hmap_hash_bytes_, ext_hmap_memcmp_) -// Gets an entry from the hashmap. keys are compared with `memcmp`. + +// Gets an entry from the hashmap. Sets `*out` to a pointer to the entry, or NULL. +// Keys are compared byte-for-byte with memcmp. #define ext_hmap_get(hmap, entry_key, out) \ ext_hmap_get_ex(hmap, entry_key, out, ext_hmap_hash_bytes_, ext_hmap_memcmp_) -// Gets an entry from the hashmap, creating a new one if not found. keys are compared with -// `memcmp`. + +// Gets an entry from the hashmap, inserting a default if not found. +// Keys are compared byte-for-byte with memcmp. #define ext_hmap_get_default(hmap, entry_key, entry_val, out) \ ext_hmap_get_default_ex(hmap, entry_key, entry_val, out, ext_hmap_hash_bytes_, ext_hmap_memcmp_) -// Deletes an entry from the hashmap. keys are compared with `memcmp`. + +// Deletes an entry from the hashmap. Keys are compared byte-for-byte with memcmp. #define ext_hmap_delete(hmap, entry_key) \ ext_hmap_delete_ex(hmap, entry_key, ext_hmap_hash_bytes_, ext_hmap_memcmp_) -// Puts an entry into the hashmap. keys are compared with `strcmp`. +// Puts an entry into the hashmap. Keys are compared with strcmp. #define ext_hmap_put_cstr(hmap, entry_key, entry_val) \ ext_hmap_put_ex(hmap, entry_key, entry_val, ext_hmap_hash_cstr_, ext_hmap_strcmp_) -// Gets an entry from the hashmap. keys are compared with `strcmp`. + +// Gets an entry from the hashmap. Keys are compared with strcmp. #define ext_hmap_get_cstr(hmap, entry_key, out) \ ext_hmap_get_ex(hmap, entry_key, out, ext_hmap_hash_cstr_, ext_hmap_strcmp_) -// Gets an entry from the hashmap, creating a new one if not found. keys are compared with -// `strcmp`. + +// Gets an entry from the hashmap, inserting a default if not found. Keys are compared with +// strcmp. #define ext_hmap_get_default_cstr(hmap, entry_key, entry_val, out) \ ext_hmap_get_default_ex(hmap, entry_key, entry_val, out, ext_hmap_hash_cstr_, ext_hmap_strcmp_) -// Deletes an entry from the hashmap. keys are compared with `strcmp`. + +// Deletes an entry from the hashmap. Keys are compared with strcmp. #define ext_hmap_delete_cstr(hmap, entry_key) \ ext_hmap_delete_ex(hmap, entry_key, ext_hmap_hash_cstr_, ext_hmap_strcmp_) -// Puts an entry into the hashmap. keys are compared with `ss_cmp`. +// Puts an entry into the hashmap. Keys are compared with ext_ss_cmp. #define ext_hmap_put_ss(hmap, entry_key, entry_val) \ ext_hmap_put_ex(hmap, entry_key, entry_val, ext_hmap_hash_ss_, ext_hmap_sscmp_) -// Gets an entry from the hashmap. keys are compared with `ext_ss_cmp`. + +// Gets an entry from the hashmap. Keys are compared with ext_ss_cmp. #define ext_hmap_get_ss(hmap, entry_key, out) \ ext_hmap_get_ex(hmap, entry_key, out, ext_hmap_hash_ss_, ext_hmap_sscmp_) -// Gets an entry from the hashmap, creating a new one if not found. keys are compared with -// `ss_cmp`. + +// Gets an entry from the hashmap, inserting a default if not found. +// Keys are compared with ext_ss_cmp. #define ext_hmap_get_default_ss(hmap, entry_key, entry_val, out) \ ext_hmap_get_default_ex(hmap, entry_key, entry_val, out, ext_hmap_hash_ss_, ext_hmap_sscmp_) -// Delets an entry from the hashmap. keys are compared with `ss_cmp`. + +// Deletes an entry from the hashmap. Keys are compared with ext_ss_cmp. #define ext_hmap_delete_ss(hmap, entry_key) \ ext_hmap_delete_ex(hmap, entry_key, ext_hmap_hash_ss_, ext_hmap_sscmp_) +// Expression-form lookup: evaluates to a pointer to the matching entry, or +// NULL if not found. Unlike `ext_hmap_get*`, these macros are expressions +// (not statements) and therefore work as struct-field initialisers, function +// arguments, and with anonymous entry types produced by `Ext_HashMap(K, V)`. +// +// The typed NULL is achieved via the C99 null-pointer-constant ternary rule +// (6.5.15p6): `cond ? typed_ptr : (void*)0` has the type of `typed_ptr`. +// +// `ext_hmap_getp_default*` always inserts a default entry when the key is +// absent, so they never return NULL. Use them with `Ext_HashMap` to avoid +// needing a named entry typedef: +// +// ```c +// Ext_HashMap(const char *, int) freq = {0}; +// hmap_getp_default_cstr(&freq, word, 0)->value++; +// ``` + +// Returns a pointer to the entry with the given key, or NULL. +// Keys are compared byte-for-byte with memcmp. +#define ext_hmap_getp(hmap, entry_key) \ + ((hmap)->size == 0 \ + ? (void *)0 \ + : (ext_hmap_tmp_(hmap).key = (entry_key), \ + ext__hmap_find_bytes_((hmap)->entries, (hmap)->hashes, (hmap)->capacity, \ + sizeof(*(hmap)->entries), sizeof(ext_hmap_tmp_(hmap).key)), \ + EXT_HMAP_IS_VALID((hmap)->hashes[(hmap)->hashes[0]]) \ + ? (hmap)->entries + (hmap)->hashes[0] \ + : (void *)0)) + +// Returns a pointer to the entry with the given key (cstr variant), or NULL. +#define ext_hmap_getp_cstr(hmap, entry_key) \ + ((hmap)->size == 0 ? (void *)0 \ + : (ext_hmap_tmp_(hmap).key = (entry_key), \ + ext__hmap_find_cstr_((hmap)->entries, (hmap)->hashes, (hmap)->capacity, \ + sizeof(*(hmap)->entries)), \ + EXT_HMAP_IS_VALID((hmap)->hashes[(hmap)->hashes[0]]) \ + ? (hmap)->entries + (hmap)->hashes[0] \ + : (void *)0)) + +// Returns a pointer to the entry with the given key (StringSlice variant), or NULL. +#define ext_hmap_getp_ss(hmap, entry_key) \ + ((hmap)->size == 0 ? (void *)0 \ + : (ext_hmap_tmp_(hmap).key = (entry_key), \ + ext__hmap_find_ss_((hmap)->entries, (hmap)->hashes, (hmap)->capacity, \ + sizeof(*(hmap)->entries)), \ + EXT_HMAP_IS_VALID((hmap)->hashes[(hmap)->hashes[0]]) \ + ? (hmap)->entries + (hmap)->hashes[0] \ + : (void *)0)) + +// Returns a pointer to the entry with the given key (custom hash/cmp), or NULL. +// `hash_fn` must be a function pointer `size_t (*)(const void *)`; +// `cmp_fn` must be a function pointer `int (*)(const void *, const void *)`. +// Both receive a pointer to the key (the first field of the entry). +#define ext_hmap_getp_ex(hmap, entry_key, hash_fn, cmp_fn) \ + ((hmap)->size == 0 ? (void *)0 \ + : (ext_hmap_tmp_(hmap).key = (entry_key), \ + ext__hmap_find_ex_((hmap)->entries, (hmap)->hashes, (hmap)->capacity, \ + sizeof(*(hmap)->entries), (hash_fn), (cmp_fn)), \ + EXT_HMAP_IS_VALID((hmap)->hashes[(hmap)->hashes[0]]) \ + ? (hmap)->entries + (hmap)->hashes[0] \ + : (void *)0)) + +// Returns a pointer to the existing entry with the given key, inserting a new +// entry with `entry_val` as the value when absent. Keys are compared with +// memcmp. +// +// The grow check runs first so that key/value are written into the (possibly +// reallocated) tmp slot after any reallocation. +#define ext_hmap_getp_default(hmap, entry_key, entry_val) \ + ((hmap)->size >= EXT_HMAP_MAX_ENTRY_LOAD((hmap)->capacity + 1) \ + ? (ext_hmap_grow_((void **)&(hmap)->entries, sizeof(*(hmap)->entries), &(hmap)->hashes, \ + &(hmap)->capacity, (Ext_Allocator **)&(hmap)->allocator), \ + 0) \ + : 0, \ + ext_hmap_tmp_(hmap).key = (entry_key), ext_hmap_tmp_(hmap).value = (entry_val), \ + ext__hmap_get_default_find_bytes_((hmap)->entries, (hmap)->hashes, (hmap)->capacity, \ + sizeof(*(hmap)->entries), sizeof(ext_hmap_tmp_(hmap).key), \ + &(hmap)->size), \ + (hmap)->entries + (hmap)->hashes[0]) + +// Returns a pointer to the existing entry (cstr variant), inserting with +// `entry_val` when absent. +#define ext_hmap_getp_default_cstr(hmap, entry_key, entry_val) \ + ((hmap)->size >= EXT_HMAP_MAX_ENTRY_LOAD((hmap)->capacity + 1) \ + ? (ext_hmap_grow_((void **)&(hmap)->entries, sizeof(*(hmap)->entries), &(hmap)->hashes, \ + &(hmap)->capacity, (Ext_Allocator **)&(hmap)->allocator), \ + 0) \ + : 0, \ + ext_hmap_tmp_(hmap).key = (entry_key), ext_hmap_tmp_(hmap).value = (entry_val), \ + ext__hmap_get_default_find_cstr_((hmap)->entries, (hmap)->hashes, (hmap)->capacity, \ + sizeof(*(hmap)->entries), &(hmap)->size), \ + (hmap)->entries + (hmap)->hashes[0]) + +// Returns a pointer to the existing entry (StringSlice variant), inserting +// with `entry_val` when absent. +#define ext_hmap_getp_default_ss(hmap, entry_key, entry_val) \ + ((hmap)->size >= EXT_HMAP_MAX_ENTRY_LOAD((hmap)->capacity + 1) \ + ? (ext_hmap_grow_((void **)&(hmap)->entries, sizeof(*(hmap)->entries), &(hmap)->hashes, \ + &(hmap)->capacity, (Ext_Allocator **)&(hmap)->allocator), \ + 0) \ + : 0, \ + ext_hmap_tmp_(hmap).key = (entry_key), ext_hmap_tmp_(hmap).value = (entry_val), \ + ext__hmap_get_default_find_ss_((hmap)->entries, (hmap)->hashes, (hmap)->capacity, \ + sizeof(*(hmap)->entries), &(hmap)->size), \ + (hmap)->entries + (hmap)->hashes[0]) + +// Returns a pointer to the existing entry (custom hash/cmp), inserting with +// `entry_val` when absent. +#define ext_hmap_getp_default_ex(hmap, entry_key, entry_val, hash_fn, cmp_fn) \ + ((hmap)->size >= EXT_HMAP_MAX_ENTRY_LOAD((hmap)->capacity + 1) \ + ? (ext_hmap_grow_((void **)&(hmap)->entries, sizeof(*(hmap)->entries), &(hmap)->hashes, \ + &(hmap)->capacity, (Ext_Allocator **)&(hmap)->allocator), \ + 0) \ + : 0, \ + ext_hmap_tmp_(hmap).key = (entry_key), ext_hmap_tmp_(hmap).value = (entry_val), \ + ext__hmap_get_default_find_ex_((hmap)->entries, (hmap)->hashes, (hmap)->capacity, \ + sizeof(*(hmap)->entries), &(hmap)->size, (hash_fn), (cmp_fn)), \ + (hmap)->entries + (hmap)->hashes[0]) + // Clears the hashmap #define ext_hmap_clear(hmap) \ do { \ @@ -1600,32 +1741,7 @@ EXT_STATIC_ASSERT(((EXT_HMAP_INIT_CAPACITY) & (EXT_HMAP_INIT_CAPACITY - 1)) == 0 void ext_hmap_grow_(void **entries, size_t entries_sz, size_t **hashes, size_t *cap, Ext_Allocator **a); -#define ext_hmap_tmp_(map) ((map)->entries[EXT_HMAP_TMP_SLOT]) -#define ext_hmap_tombs_(map) ((map)->hashes[EXT_HMAP_TMP_SLOT]) - -#define ext_hmap_find_index_(map, entry_key, hash, cmp_fn) \ - size_t idx_ = 0; \ - { \ - size_t i_ = ((hash) & (map)->capacity); \ - bool tomb_found_ = false; \ - size_t tomb_idx_ = 0; \ - for(;;) { \ - size_t buck = (map)->hashes[i_ + 1]; \ - if(!EXT_HMAP_IS_VALID(buck)) { \ - if(EXT_HMAP_IS_EMPTY(buck)) { \ - idx_ = tomb_found_ ? tomb_idx_ : i_ + 1; \ - break; \ - } else if(!tomb_found_) { \ - tomb_found_ = true; \ - tomb_idx_ = i_ + 1; \ - } \ - } else if(buck == hash && cmp_fn((entry_key), &(map)->entries[i_ + 1].key) == 0) { \ - idx_ = i_ + 1; \ - break; \ - } \ - i_ = ((i_ + 1) & (map)->capacity); \ - } \ - } +#define ext_hmap_tmp_(map) ((map)->entries[EXT_HMAP_TMP_SLOT]) #define ext_hmap_hash_bytes_(k) ext_hash_bytes_((k), sizeof(*(k))) #define ext_hmap_hash_cstr_(k) ext_hash_cstr_(*(k)) @@ -1634,6 +1750,43 @@ void ext_hmap_grow_(void **entries, size_t entries_sz, size_t **hashes, size_t * #define ext_hmap_strcmp_(k1, k2) strcmp(*(k1), *(k2)) #define ext_hmap_sscmp_(k1, k2) ext_ss_cmp(*(k1), *(k2)) +// Core probe loop used by all hashmap operations. +// +// Scans the bucket array starting from hash_ & cap_, writing the result slot +// index to hashes_[0]. After the macro: +// EXT_HMAP_IS_VALID(hashes_[hashes_[0]]) == true => hit (existing entry) +// EXT_HMAP_IS_VALID(hashes_[hashes_[0]]) == false => miss (best insertion slot) +// +// Parameters: +// hashes_ — hashes array; hashes_[0] receives the output slot index +// cap_ — capacity mask (capacity is always a power-of-two minus 1) +// hash_ — pre-computed hash (must be >= 2 before calling) +// key_eq_ — boolean expression, true when the candidate matches the lookup +// key; may reference i_, the current 0-based probe index, so the +// candidate entry lives at entries[i_ + 1] +#define ext__hmap_probe_(hashes_, cap_, hash_, key_eq_) \ + { \ + size_t i_ = (hash_) & (cap_); \ + bool tomb_found_ = false; \ + size_t tomb_idx_ = 0; \ + for(;;) { \ + size_t buck_ = (hashes_)[i_ + 1]; \ + if(!EXT_HMAP_IS_VALID(buck_)) { \ + if(EXT_HMAP_IS_EMPTY(buck_)) { \ + (hashes_)[0] = tomb_found_ ? tomb_idx_ : i_ + 1; \ + break; \ + } else if(!tomb_found_) { \ + tomb_found_ = true; \ + tomb_idx_ = i_ + 1; \ + } \ + } else if(buck_ == (hash_) && (key_eq_)) { \ + (hashes_)[0] = i_ + 1; \ + break; \ + } \ + i_ = (i_ + 1) & (cap_); \ + } \ + } + #ifdef __GNUC__ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-function" @@ -1857,6 +2010,141 @@ static inline size_t ext_hash_bytes_(const void *p, size_t len) { // End of stbds.h // ----------------------------------------------------------------------------- +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" +#endif // __GNUC__ + +// ----------------------------------------------------------------------------- +// Thin inline helpers for expression-form getp / getp_default macros. +// +// Expression macros (getp, getp_default) cannot use compound statements, so +// they delegate to these inline functions. Each helper stores the result slot +// index in hashes[0] via ext__hmap_probe_ and returns the computed hash so +// callers can write it without recomputing. +// +// On entry the key (and for get_default variants, the value) must already be +// stored in entries[0] (the tmp slot). + +// Bytes key (compared with memcmp over key_sz bytes). +static inline size_t ext__hmap_find_bytes_(const void *entries, size_t *hashes, size_t cap, + size_t entry_sz, size_t key_sz) { + size_t hash = ext_hash_bytes_(entries, key_sz); + if(hash < 2) hash += 2; + ext__hmap_probe_(hashes, cap, hash, + memcmp((const char *)entries + (i_ + 1) * entry_sz, entries, key_sz) == 0); + return hash; +} + +// Cstr key (compared with strcmp). +static inline size_t ext__hmap_find_cstr_(const void *entries, size_t *hashes, size_t cap, + size_t entry_sz) { + const char *key = *(const char *const *)entries; + size_t hash = ext_hash_cstr_(key); + if(hash < 2) hash += 2; + ext__hmap_probe_(hashes, cap, hash, + strcmp(*(const char *const *)((const char *)entries + (i_ + 1) * entry_sz), + key) == 0); + return hash; +} + +// StringSlice key (compared with ext_ss_cmp). +static inline size_t ext__hmap_find_ss_(const void *entries, size_t *hashes, size_t cap, + size_t entry_sz) { + const Ext_StringSlice *key = (const Ext_StringSlice *)entries; + size_t hash = ext_hash_bytes_(key->data, key->size); + if(hash < 2) hash += 2; + ext__hmap_probe_( + hashes, cap, hash, + ext_ss_cmp(*(const Ext_StringSlice *)((const char *)entries + (i_ + 1) * entry_sz), *key) == + 0); + return hash; +} + +// Custom hash_fn / cmp_fn (each receives a pointer to the key at entries[0]). +static inline size_t ext__hmap_find_ex_(const void *entries, size_t *hashes, size_t cap, + size_t entry_sz, size_t (*hash_fn)(const void *), + int (*cmp_fn)(const void *, const void *)) { + size_t hash = hash_fn(entries); + if(hash < 2) hash += 2; + ext__hmap_probe_(hashes, cap, hash, + cmp_fn((const char *)entries + (i_ + 1) * entry_sz, entries) == 0); + return hash; +} + +// Get-or-insert variants: probe, then insert entries[0] if the key is absent. + +// Bytes key. +static inline void ext__hmap_get_default_find_bytes_(void *entries, size_t *hashes, size_t cap, + size_t entry_sz, size_t key_sz, size_t *size) { + size_t hash = ext_hash_bytes_(entries, key_sz); + if(hash < 2) hash += 2; + ext__hmap_probe_(hashes, cap, hash, + memcmp((const char *)entries + (i_ + 1) * entry_sz, entries, key_sz) == 0); + size_t idx = hashes[0]; + if(!EXT_HMAP_IS_VALID(hashes[idx])) { + memcpy((char *)entries + idx * entry_sz, entries, entry_sz); + hashes[idx] = hash; + (*size)++; + } +} + +// Cstr key. +static inline void ext__hmap_get_default_find_cstr_(void *entries, size_t *hashes, size_t cap, + size_t entry_sz, size_t *size) { + const char *key = *(const char *const *)entries; + size_t hash = ext_hash_cstr_(key); + if(hash < 2) hash += 2; + ext__hmap_probe_(hashes, cap, hash, + strcmp(*(const char *const *)((const char *)entries + (i_ + 1) * entry_sz), + key) == 0); + size_t idx = hashes[0]; + if(!EXT_HMAP_IS_VALID(hashes[idx])) { + memcpy((char *)entries + idx * entry_sz, entries, entry_sz); + hashes[idx] = hash; + (*size)++; + } +} + +// StringSlice key. +static inline void ext__hmap_get_default_find_ss_(void *entries, size_t *hashes, size_t cap, + size_t entry_sz, size_t *size) { + const Ext_StringSlice *key = (const Ext_StringSlice *)entries; + size_t hash = ext_hash_bytes_(key->data, key->size); + if(hash < 2) hash += 2; + ext__hmap_probe_( + hashes, cap, hash, + ext_ss_cmp(*(const Ext_StringSlice *)((const char *)entries + (i_ + 1) * entry_sz), *key) == + 0); + size_t idx = hashes[0]; + if(!EXT_HMAP_IS_VALID(hashes[idx])) { + memcpy((char *)entries + idx * entry_sz, entries, entry_sz); + hashes[idx] = hash; + (*size)++; + } +} + +// Custom hash_fn / cmp_fn. +static inline void ext__hmap_get_default_find_ex_(void *entries, size_t *hashes, size_t cap, + size_t entry_sz, size_t *size, + size_t (*hash_fn)(const void *), + int (*cmp_fn)(const void *, const void *)) { + size_t hash = hash_fn(entries); + if(hash < 2) hash += 2; + ext__hmap_probe_(hashes, cap, hash, + cmp_fn((const char *)entries + (i_ + 1) * entry_sz, entries) == 0); + size_t idx = hashes[0]; + if(!EXT_HMAP_IS_VALID(hashes[idx])) { + memcpy((char *)entries + idx * entry_sz, entries, entry_sz); + hashes[idx] = hash; + (*size)++; + } +} + +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif // __GNUC__ + #define ext__return_tox_(a, b, c, d, ...) d #define ext__return_to1_(result_) ext__return_to3_(result_, exit, res) #define ext__return_to2_(result_, label_) ext__return_to3_(result_, label_, res) @@ -3835,24 +4123,32 @@ static inline int ext_dbg_unknown(const char *name, const char *file, int line, #define cmd_write ext_cmd_write #endif -#define hmap_foreach ext_hmap_foreach -#define hmap_end ext_hmap_end -#define hmap_begin ext_hmap_begin -#define hmap_next ext_hmap_next -#define hmap_put ext_hmap_put -#define hmap_get ext_hmap_get -#define hmap_get_default ext_hmap_get_default -#define hmap_delete ext_hmap_delete -#define hmap_put_cstr ext_hmap_put_cstr -#define hmap_get_cstr ext_hmap_get_cstr -#define hmap_get_default_cstr ext_hmap_get_default_cstr -#define hmap_delete_cstr ext_hmap_delete_cstr -#define hmap_put_ss ext_hmap_put_ss -#define hmap_get_ss ext_hmap_get_ss -#define hmap_get_default_ss ext_hmap_get_default_ss -#define hmap_delete_ss ext_hmap_delete_ss -#define hmap_clear ext_hmap_clear -#define hmap_free ext_hmap_free +#define Entry Ext_Entry +#define HashMap Ext_HashMap +#define hmap_foreach ext_hmap_foreach +#define hmap_end ext_hmap_end +#define hmap_begin ext_hmap_begin +#define hmap_next ext_hmap_next +#define hmap_put ext_hmap_put +#define hmap_get ext_hmap_get +#define hmap_get_default ext_hmap_get_default +#define hmap_delete ext_hmap_delete +#define hmap_put_cstr ext_hmap_put_cstr +#define hmap_get_cstr ext_hmap_get_cstr +#define hmap_get_default_cstr ext_hmap_get_default_cstr +#define hmap_delete_cstr ext_hmap_delete_cstr +#define hmap_put_ss ext_hmap_put_ss +#define hmap_get_ss ext_hmap_get_ss +#define hmap_get_default_ss ext_hmap_get_default_ss +#define hmap_delete_ss ext_hmap_delete_ss +#define hmap_getp ext_hmap_getp +#define hmap_getp_cstr ext_hmap_getp_cstr +#define hmap_getp_ss ext_hmap_getp_ss +#define hmap_getp_default ext_hmap_getp_default +#define hmap_getp_default_cstr ext_hmap_getp_default_cstr +#define hmap_getp_default_ss ext_hmap_getp_default_ss +#define hmap_clear ext_hmap_clear +#define hmap_free ext_hmap_free #endif // EXTLIB_NO_SHORTHANDS #endif // EXTLIB_H diff --git a/test/test.c b/test/test.c index 66b0de9..7d0db3c 100644 --- a/test/test.c +++ b/test/test.c @@ -1772,6 +1772,217 @@ CTEST(hmap, delete_ss) { temp_reset(); } +// ----------------------------------------------------------------------------- +// Expression-form get (hmap_getp) and get-or-insert (hmap_getp_default) + +CTEST(hmap, getp_bytes) { + HashMap(int, int) map = {0}; + + // Empty map must return NULL without touching a NULL entries pointer. + ASSERT_TRUE(hmap_getp(&map, 1) == NULL); + + for(int i = 1; i <= 22; i++) { + hmap_put(&map, i, i * 10); + } + + // Hit: correct key and value, inline mutation visible on next lookup. + ASSERT_TRUE(hmap_getp(&map, 5) != NULL); + ASSERT_TRUE(hmap_getp(&map, 5)->key == 5); + ASSERT_TRUE(hmap_getp(&map, 5)->value == 50); + hmap_getp(&map, 5)->value = 99; + ASSERT_TRUE(hmap_getp(&map, 5)->value == 99); + + // Same key returns the same pointer (stable storage, no realloc triggered here). + // Note: two getp calls must not share an expression — both write to the tmp slot. + void *p3a = hmap_getp(&map, 3); + void *p3b = hmap_getp(&map, 3); + ASSERT_TRUE(p3a == p3b); + + // Miss on absent key. + ASSERT_TRUE(hmap_getp(&map, 100) == NULL); + + hmap_free(&map); +} + +CTEST(hmap, getp_bytes_tombstones) { + HashMap(int, int) map = {0}; + + for(int i = 1; i <= 50; i++) { + hmap_put(&map, i, i * 10); + } + + // Create tombstones in the lower half. + for(int i = 1; i <= 25; i++) { + hmap_delete(&map, i); + } + + // Deleted entries are not found (probe must continue past tombstones). + for(int i = 1; i <= 25; i++) { + ASSERT_TRUE(hmap_getp(&map, i) == NULL); + } + + // Entries whose probe chain passes through tombstones are still found. + for(int i = 26; i <= 50; i++) { + ASSERT_TRUE(hmap_getp(&map, i) != NULL); + ASSERT_TRUE(hmap_getp(&map, i)->value == i * 10); + } + + hmap_free(&map); +} + +CTEST(hmap, getp_default_bytes) { + HashMap(int, int) map = {0}; + + // First access on an empty map: triggers grow then inserts with default. + hmap_getp_default(&map, 1, 100)->value += 5; + ASSERT_TRUE(map.size == 1); + + // Second access: key already present, default is ignored, value unchanged. + ASSERT_TRUE(hmap_getp_default(&map, 1, 0)->value == 105); + ASSERT_TRUE(map.size == 1); + + // Accessing a key with a large default does not overwrite the stored value. + (void)hmap_getp_default(&map, 1, 999); + ASSERT_TRUE(hmap_getp(&map, 1)->value == 105); + + // Insert enough keys to trigger at least one grow. + for(int i = 2; i <= 22; i++) { + (void)hmap_getp_default(&map, i, i * 10); + } + ASSERT_TRUE(map.size == 22); + + // After a grow the previously inserted key is accessible under its new address. + ASSERT_TRUE(hmap_getp(&map, 1)->value == 105); + for(int i = 2; i <= 22; i++) { + ASSERT_TRUE(hmap_getp(&map, i) != NULL); + ASSERT_TRUE(hmap_getp(&map, i)->value == i * 10); + } + + hmap_free(&map); +} + +CTEST(hmap, getp_default_bytes_tombstone_reuse) { + HashMap(int, int) map = {0}; + + for(int i = 1; i <= 20; i++) { + hmap_put(&map, i, i); + } + + for(int i = 1; i <= 10; i++) { + hmap_delete(&map, i); + } + ASSERT_TRUE(map.size == 10); + + // Re-inserting deleted keys via getp_default should reuse tombstone slots. + for(int i = 1; i <= 10; i++) { + (void)hmap_getp_default(&map, i, i * 100); + } + ASSERT_TRUE(map.size == 20); + + for(int i = 1; i <= 10; i++) { + ASSERT_TRUE(hmap_getp(&map, i) != NULL); + ASSERT_TRUE(hmap_getp(&map, i)->value == i * 100); + } + + hmap_free(&map); +} + +CTEST(hmap, getp_cstr) { + HashMap(const char *, int) map = {0}; + + ASSERT_TRUE(hmap_getp_cstr(&map, "missing") == NULL); + + for(int i = 0; i < 22; i++) { + hmap_put_cstr(&map, temp_sprintf("key %d", i), i * 10); + } + + ASSERT_TRUE(hmap_getp_cstr(&map, "key 3") != NULL); + ASSERT_TRUE(hmap_getp_cstr(&map, "key 3")->value == 30); + hmap_getp_cstr(&map, "key 3")->value = 77; + ASSERT_TRUE(hmap_getp_cstr(&map, "key 3")->value == 77); + + ASSERT_TRUE(hmap_getp_cstr(&map, "key 99") == NULL); + + hmap_free(&map); + temp_reset(); +} + +CTEST(hmap, getp_default_cstr) { + HashMap(const char *, int) map = {0}; + + // Word-count pattern. + const char *words[] = {"foo", "bar", "foo", "baz", "foo", "bar"}; + for(size_t i = 0; i < 6; i++) { + hmap_getp_default_cstr(&map, words[i], 0)->value++; + } + + ASSERT_TRUE(map.size == 3); + ASSERT_TRUE(hmap_getp_cstr(&map, "foo")->value == 3); + ASSERT_TRUE(hmap_getp_cstr(&map, "bar")->value == 2); + ASSERT_TRUE(hmap_getp_cstr(&map, "baz")->value == 1); + + hmap_free(&map); + temp_reset(); +} + +CTEST(hmap, getp_ss) { + HashMap(StringSlice, int) map = {0}; + + ASSERT_TRUE(hmap_getp_ss(&map, ss_from_cstr("missing")) == NULL); + + for(int i = 0; i < 22; i++) { + hmap_put_ss(&map, ss_from_cstr(temp_sprintf("key %d", i)), i * 10); + } + + ASSERT_TRUE(hmap_getp_ss(&map, ss_from_cstr("key 7")) != NULL); + ASSERT_TRUE(hmap_getp_ss(&map, ss_from_cstr("key 7"))->value == 70); + hmap_getp_ss(&map, ss_from_cstr("key 7"))->value = 55; + ASSERT_TRUE(hmap_getp_ss(&map, ss_from_cstr("key 7"))->value == 55); + + ASSERT_TRUE(hmap_getp_ss(&map, ss_from_cstr("key 99")) == NULL); + + hmap_free(&map); + temp_reset(); +} + +CTEST(hmap, getp_default_ss) { + HashMap(StringSlice, int) map = {0}; + + // Word-count pattern — primary use case for getp_default. + const char *words[] = {"foo", "bar", "foo", "baz", "foo", "bar"}; + for(size_t i = 0; i < 6; i++) { + hmap_getp_default_ss(&map, ss_from_cstr(words[i]), 0)->value++; + } + + ASSERT_TRUE(map.size == 3); + ASSERT_TRUE(hmap_getp_ss(&map, ss_from_cstr("foo"))->value == 3); + ASSERT_TRUE(hmap_getp_ss(&map, ss_from_cstr("bar"))->value == 2); + ASSERT_TRUE(hmap_getp_ss(&map, ss_from_cstr("baz"))->value == 1); + + hmap_free(&map); + temp_reset(); +} + +CTEST(hmap, entry_macro_foreach) { + HashMap(int, int) map = {0}; + + for(int i = 0; i < 20; i++) { + hmap_put(&map, i, i * 2); + } + + // Entry(K, V) used as the type argument; valid because hmap_begin returns void*. + int count = 0; + int sum = 0; + hmap_foreach(Entry(int, int), it, &map) { + count++; + sum += it->value; + } + ASSERT_TRUE(count == 20); + ASSERT_TRUE(sum == 380); // 2 * (0+1+...+19) = 2 * 190 + + hmap_free(&map); +} + CTEST(defer, loop) { char *str = ext_strdup("hello, world"); defer_loop((void)0, ext_free(str, 13)) { From bacf65c8c7ce7fe4e378196c5693b1cbb39fd4f1 Mon Sep 17 00:00:00 2001 From: Fabrizio Pietrucci Date: Sun, 22 Mar 2026 16:11:08 +0100 Subject: [PATCH 4/6] Cleaned new hashmap implementation --- examples/02_hashmap.c | 2 +- extlib.h | 699 +++++++++++++++--------------------------- test/test.c | 164 +++++----- 3 files changed, 319 insertions(+), 546 deletions(-) diff --git a/examples/02_hashmap.c b/examples/02_hashmap.c index a04c613..c99617b 100644 --- a/examples/02_hashmap.c +++ b/examples/02_hashmap.c @@ -121,7 +121,7 @@ int main(int argc, char **argv) { StringSlice word = ss_split_once_ws(&file_slice); if(word.size == 0) continue; - WordFreq *e = hmap_getp_default_ss(&words_freq, word, 0); + WordFreq *e = hmap_get_default_ss(&words_freq, word, 0); ASSERT(e != NULL, "default entry shouldn't be NULL"); e->value++; diff --git a/extlib.h b/extlib.h index 5d7b1e5..eb7f52c 100644 --- a/extlib.h +++ b/extlib.h @@ -45,6 +45,7 @@ * * v1.4.1: * - Minor tweaks to allocation functions - move some of them to be `inline` + * * v1.4.0: * - Simplified arena allocator * - Fixes in temp allocator and arena allocator @@ -168,6 +169,10 @@ static inline size_t strlen(const char *s) { while(s[len] != '\0') len++; return len; } +static inline int strcmp(const char *l, const char *r) { + for(; *l == *r && *l; l++, r++); + return *(unsigned char *)l - *(unsigned char *)r; +} void assert(int c); // TODO: are we sure we want to require wasm embedder to provide `assert`? #endif // EXTLIB_NO_STD @@ -822,6 +827,8 @@ char *ext_arena_vsprintf(Ext_Arena *a, const char *fmt, va_list ap); // The dynamic array integrates with the `Allocator` interface and the context to support custom // allocators for its backing array. // +// The dynamic arrays macro expect a specific struct layout (see USAGE). +// // USAGE: //```c // typedef struct { @@ -1346,7 +1353,9 @@ int ext_cmd_write(const char *cmd, const void *data, size_t size); // The hashmap integrates with the `Allocator` interface and the context to support custom // allocators for its backing entry and hashes array. // -// USAGE +// The hashmap, like the dynamic array, expects a specific struct layout (see USAGE). +// +// USAGE: // ```c // typedef struct { // int key; @@ -1360,11 +1369,10 @@ int ext_cmd_write(const char *cmd, const void *data, size_t size); // Allocator *allocator; // } IntMap; // -// IntHashMap map = {0}; +// IntMap map = {0}; // hmap_put(&map, 1, 10); // -// IntEntry* e; -// hmap_get(&map, 1, &e); +// IntEntry* e = hmap_get(&map, 1); // if(e != NULL) { // Found! // printf("key = %d value = %d", e->key, e->value); // } @@ -1378,35 +1386,27 @@ int ext_cmd_write(const char *cmd, const void *data, size_t size); // temp_reset(&map); // ``` -// Declares a hashmap entry struct with fields `key` (type K) and `value` (type V). -// Useful as the type argument to `hmap_foreach` when using `Ext_HashMap`: -// -// ```c -// hmap_foreach(Ext_Entry(StringSlice, int), it, &map) { ... } -// ``` -// -// Note: each expansion of `Ext_Entry(K, V)` produces a distinct anonymous struct type. Pointer -// assignments between two separate expansions require a cast. For direct variable storage, prefer -// typedef-ing the entry type once. -#define Ext_Entry(K, V) \ - struct { \ - K key; \ - V value; \ - } - -// Declares a hashmap struct for key type K and value type V, using `Ext_Entry(K, V)` for the -// entries array. The generated struct has the same layout as a manually-declared hashmap struct. -// -// Use `hmap_getp` / `hmap_getp_default` with inline maps to avoid needing a named entry type: +// Declares a hashmap struct in-line for key type K and value type V, using `Ext_Entry(K, V)` for +// the entries array. The generated struct has the same layout as a manually-declared hashmap struct +// (see above). // +// USAGE: // ```c // Ext_HashMap(StringSlice, int) word_count = {0}; -// hmap_getp_default_ss(&word_count, word, 0)->value++; +// hmap_get_default_ss(&word_count, word, 0)->value++; // // hmap_foreach(Ext_Entry(StringSlice, int), it, &word_count) { // printf("%zu\n", (size_t)it->value); // } // ``` +// +// You can also use this macro to `typedef` the hashmap definition so you can re-use it multiple +// times: +// ```c +// typedef HashMap(int, int) Int_HashMap; +// ... +// Int_HashMap int_map = {0}; +// ``` #define Ext_HashMap(K, V) \ struct { \ Ext_Entry(K, V) * entries; \ @@ -1415,265 +1415,159 @@ int ext_cmd_write(const char *cmd, const void *data, size_t size); Ext_Allocator *allocator; \ } +// Declares a hashmap entry struct with fields `key` (type K) and `value` (type V). +// Useful as the type argument to `hmap_foreach` when using `Ext_HashMap`: +// +// ```c +// hmap_foreach(Ext_Entry(StringSlice, int), it, &map) { ... } +// ``` +// +// NOTE: each expansion of `Ext_Entry(K, V)` produces a distinct anonymous struct type. Pointer +// assignments between two separate expansions require a cast. For direct variable storage, prefer +// typedef-ing the hashmap type once or using manual struct layout. +#define Ext_Entry(K, V) \ + struct { \ + K key; \ + V value; \ + } + // Read as: size * 0.75, i.e. a load factor of 75% // This is basically doing: // size / 2 + size / 4 = (3 * size) / 4 #define EXT_HMAP_MAX_ENTRY_LOAD(size) (((size) >> 1) + ((size) >> 2)) -// Puts an entry into the hashmap. -// `hash_fn` and `cmp_fn` are functions or function-like macros: -// size_t hash_fn(KeyType *key) -// int cmp_fn (KeyType *key_a, KeyType *key_b) — returns 0 if equal -// Both receive a pointer to the key field (first field of the entry). -// You probably want ext_hmap_put / ext_hmap_put_cstr / ext_hmap_put_ss instead. -#define ext_hmap_put_ex(hmap, entry_key, entry_val, hash_fn, cmp_fn) \ - do { \ - if((hmap)->size >= EXT_HMAP_MAX_ENTRY_LOAD((hmap)->capacity + 1)) { \ - ext_hmap_grow_((void **)&(hmap)->entries, sizeof(*(hmap)->entries), &(hmap)->hashes, \ - &(hmap)->capacity, (Ext_Allocator **)&(hmap)->allocator); \ - } \ - ext_hmap_tmp_(hmap).key = (entry_key); \ - ext_hmap_tmp_(hmap).value = (entry_val); \ - size_t hash_ = hash_fn(&ext_hmap_tmp_(hmap).key); \ - if(hash_ < 2) hash_ += 2; \ - ext__hmap_probe_((hmap)->hashes, (hmap)->capacity, hash_, \ - cmp_fn(&ext_hmap_tmp_(hmap).key, &(hmap)->entries[i_ + 1].key) == 0); \ - size_t idx_ = (hmap)->hashes[0]; \ - if(!EXT_HMAP_IS_VALID((hmap)->hashes[idx_])) (hmap)->size++; \ - (hmap)->entries[idx_] = ext_hmap_tmp_(hmap); \ - (hmap)->hashes[idx_] = hash_; \ +// Puts an entry into the hashmap with custom hash and compare functions. +// +// `hash_fn` and `cmp_fn` are functions: +// size_t hash_fn(KeyType *key, size_t key_size) — returns hash of the key +// int cmp_fn (KeyType *key_a, KeyType *key_b, size_t key_size) — returns 0 if equal +// Both receive a pointer to the key field (first field of the entry) along with its size +// (sizeof(KeyType)). +// +// You probably want to use ext_hmap_put / ext_hmap_put_cstr / ext_hmap_put_ss instead. +#define ext_hmap_put_ex(hmap, entry_key, entry_val, hash_fn, cmp_fn) \ + do { \ + if((hmap)->size >= EXT_HMAP_MAX_ENTRY_LOAD((hmap)->capacity + 1)) { \ + ext__hmap_grow_((void **)&(hmap)->entries, sizeof(*(hmap)->entries), &(hmap)->hashes, \ + &(hmap)->capacity, (Ext_Allocator **)&(hmap)->allocator); \ + } \ + ext__hmap_tmp_(hmap).key = (entry_key); \ + ext__hmap_tmp_(hmap).value = (entry_val); \ + size_t hash_ = hash_fn(&ext__hmap_tmp_(hmap).key, sizeof(ext__hmap_tmp_(hmap).key)); \ + if(hash_ < 2) hash_ += 2; \ + ext__hmap_find_((hmap)->entries, (hmap)->hashes, (hmap)->capacity, \ + sizeof(*(hmap)->entries), sizeof((hmap)->entries[EXT_HMAP_TMP_SLOT].key), \ + (hash_fn), (cmp_fn)); \ + size_t idx_ = (hmap)->hashes[EXT_HMAP_TMP_SLOT]; \ + if(!EXT_HMAP_IS_VALID((hmap)->hashes[idx_])) (hmap)->size++; \ + (hmap)->entries[idx_] = ext__hmap_tmp_(hmap); \ + (hmap)->hashes[idx_] = hash_; \ } while(0) -// Gets an entry from the hashmap. Sets `*out` to a pointer to the found entry, or NULL. -// See ext_hmap_put_ex for hash_fn / cmp_fn conventions. -// You probably want ext_hmap_get / ext_hmap_get_cstr / ext_hmap_get_ss instead. -#define ext_hmap_get_ex(hmap, entry_key, out, hash_fn, cmp_fn) \ - do { \ - if(!(hmap)->size) { \ - *(out) = NULL; \ - break; \ - } \ - ext_hmap_tmp_(hmap).key = (entry_key); \ - size_t hash_ = hash_fn(&ext_hmap_tmp_(hmap).key); \ - if(hash_ < 2) hash_ += 2; \ - ext__hmap_probe_((hmap)->hashes, (hmap)->capacity, hash_, \ - cmp_fn(&ext_hmap_tmp_(hmap).key, &(hmap)->entries[i_ + 1].key) == 0); \ - size_t idx_ = (hmap)->hashes[0]; \ - *(out) = EXT_HMAP_IS_VALID((hmap)->hashes[idx_]) ? &(hmap)->entries[idx_] : NULL; \ - } while(0) +// Returns a pointer to the entry with the given key (custom hash/cmp), or NULL. +// `hash_fn` must be a function pointer `size_t (*)(const void *, size_t)`; +// `cmp_fn` must be a function pointer `int (*)(const void *, const void *, size_t)`. +// Both receive a pointer to the key (the first field of the entry) along with its size +// (sizeof(KeyType)). +#define ext_hmap_get_ex(hmap, entry_key, hash_fn, cmp_fn) \ + ((hmap)->size == 0 \ + ? (void *)0 \ + : (ext__hmap_tmp_(hmap).key = (entry_key), \ + ext__hmap_find_((hmap)->entries, (hmap)->hashes, (hmap)->capacity, \ + sizeof(*(hmap)->entries), \ + sizeof((hmap)->entries[EXT_HMAP_TMP_SLOT].key), (hash_fn), (cmp_fn)), \ + EXT_HMAP_IS_VALID((hmap)->hashes[(hmap)->hashes[EXT_HMAP_TMP_SLOT]]) \ + ? (hmap)->entries + (hmap)->hashes[EXT_HMAP_TMP_SLOT] \ + : NULL)) -// Gets an entry from the hashmap, inserting a default if not found. Sets `*out` to the entry. -// See ext_hmap_put_ex for hash_fn / cmp_fn conventions. -// You probably want ext_hmap_get_default / _cstr / _ss instead. -#define ext_hmap_get_default_ex(hmap, entry_key, entry_val, out, hash_fn, cmp_fn) \ - do { \ - if((hmap)->size >= EXT_HMAP_MAX_ENTRY_LOAD((hmap)->capacity + 1)) { \ - ext_hmap_grow_((void **)&(hmap)->entries, sizeof(*(hmap)->entries), &(hmap)->hashes, \ - &(hmap)->capacity, (Ext_Allocator **)&(hmap)->allocator); \ - } \ - ext_hmap_tmp_(hmap).key = (entry_key); \ - ext_hmap_tmp_(hmap).value = (entry_val); \ - size_t hash_ = hash_fn(&ext_hmap_tmp_(hmap).key); \ - if(hash_ < 2) hash_ += 2; \ - ext__hmap_probe_((hmap)->hashes, (hmap)->capacity, hash_, \ - cmp_fn(&ext_hmap_tmp_(hmap).key, &(hmap)->entries[i_ + 1].key) == 0); \ - size_t idx_ = (hmap)->hashes[0]; \ - if(!EXT_HMAP_IS_VALID((hmap)->hashes[idx_])) { \ - (hmap)->entries[idx_] = ext_hmap_tmp_(hmap); \ - (hmap)->hashes[idx_] = hash_; \ - (hmap)->size++; \ - } \ - *(out) = &(hmap)->entries[idx_]; \ - } while(0) +// Returns a pointer to the entry with the given key, or NULL. +// Keys are hashed and compared byte-for-byte with memcmp. +#define ext_hmap_get(hmap, entry_key) \ + ext_hmap_get_ex(hmap, entry_key, ext__hmap_hash_bytes_, ext__hmap_cmp_bytes_) + +// Returns a pointer to the entry with the given key (cstr variant), or NULL. +// Keys are compared with strcmp and hashed with `ext_hash_cstr_` +#define ext_hmap_get_cstr(hmap, entry_key) \ + ext_hmap_get_ex(hmap, entry_key, ext__hmap_hash_cstr_, ext__hmap_cmp_cstr_) + +// Returns a pointer to the entry with the given key (StringSlice variant), or NULL. +// Keys are compared with ext_ss_cmp and hashed with ext_hash_bytes_ on the slice's `data` member. +#define ext_hmap_get_ss(hmap, entry_key) \ + ext_hmap_get_ex(hmap, entry_key, ext__hmap_hash_ss_, ext__hmap_cmp_ss_) + +// Returns a pointer to the existing entry (with custom hash/cmp functions), inserting with +// `entry_val` when absent. +// Refer to `ext_hmap_get_ex` for function signatures. +#define ext_hmap_get_default_ex(hmap, entry_key, entry_val, hash_fn, cmp_fn) \ + ((hmap)->size >= EXT_HMAP_MAX_ENTRY_LOAD((hmap)->capacity + 1) \ + ? (ext__hmap_grow_((void **)&(hmap)->entries, sizeof(*(hmap)->entries), &(hmap)->hashes, \ + &(hmap)->capacity, (Ext_Allocator **)&(hmap)->allocator), \ + 0) \ + : 0, \ + ext__hmap_tmp_(hmap).key = (entry_key), ext__hmap_tmp_(hmap).value = (entry_val), \ + ext__hmap_find_default_((hmap)->entries, (hmap)->hashes, &(hmap)->size, (hmap)->capacity, \ + sizeof(*(hmap)->entries), sizeof((hmap)->entries[0].key), (hash_fn), \ + (cmp_fn)), \ + (hmap)->entries + (hmap)->hashes[EXT_HMAP_TMP_SLOT]) + +// Returns a pointer to the existing entry with the given key, inserting a new +// entry with `entry_val` as the value when absent. Keys are compared with +// memcmp. +#define ext_hmap_get_default(hmap, entry_key, entry_val) \ + ext_hmap_get_default_ex(hmap, entry_key, entry_val, ext__hmap_hash_bytes_, ext__hmap_cmp_bytes_) + +// Returns a pointer to the existing entry (cstr variant), inserting with +// `entry_val` when absent. +#define ext_hmap_get_default_cstr(hmap, entry_key, entry_val) \ + ext_hmap_get_default_ex(hmap, entry_key, entry_val, ext__hmap_hash_cstr_, ext__hmap_cmp_cstr_) + +// Returns a pointer to the existing entry (StringSlice variant), inserting +// with `entry_val` when absent. +#define ext_hmap_get_default_ss(hmap, entry_key, entry_val) \ + ext_hmap_get_default_ex(hmap, entry_key, entry_val, ext__hmap_hash_ss_, ext__hmap_cmp_ss_) // Deletes an entry from the hashmap. // See ext_hmap_put_ex for hash_fn / cmp_fn conventions. // You probably want ext_hmap_delete / ext_hmap_delete_cstr / ext_hmap_delete_ss instead. -#define ext_hmap_delete_ex(hmap, entry_key, hash_fn, cmp_fn) \ - do { \ - if(!(hmap)->size) break; \ - ext_hmap_tmp_(hmap).key = (entry_key); \ - size_t hash_ = hash_fn(&ext_hmap_tmp_(hmap).key); \ - if(hash_ < 2) hash_ += 2; \ - ext__hmap_probe_((hmap)->hashes, (hmap)->capacity, hash_, \ - cmp_fn(&ext_hmap_tmp_(hmap).key, &(hmap)->entries[i_ + 1].key) == 0); \ - size_t idx_ = (hmap)->hashes[0]; \ - if(EXT_HMAP_IS_VALID((hmap)->hashes[idx_])) { \ - (hmap)->hashes[idx_] = EXT_HMAP_TOMB_MARK; \ - (hmap)->size--; \ - } \ +#define ext_hmap_delete_ex(hmap, entry_key, hash_fn, cmp_fn) \ + do { \ + if(!(hmap)->size) break; \ + ext__hmap_tmp_(hmap).key = (entry_key); \ + size_t hash_ = hash_fn(&ext__hmap_tmp_(hmap).key, sizeof(ext__hmap_tmp_(hmap).key)); \ + if(hash_ < 2) hash_ += 2; \ + ext__hmap_find_((hmap)->entries, (hmap)->hashes, (hmap)->capacity, \ + sizeof(*(hmap)->entries), sizeof((hmap)->entries[EXT_HMAP_TMP_SLOT].key), \ + (hash_fn), (cmp_fn)); \ + size_t idx_ = (hmap)->hashes[EXT_HMAP_TMP_SLOT]; \ + if(EXT_HMAP_IS_VALID((hmap)->hashes[idx_])) { \ + (hmap)->hashes[idx_] = EXT_HMAP_TOMB_MARK; \ + (hmap)->size--; \ + } \ } while(0) // Puts an entry into the hashmap. Keys are compared byte-for-byte with memcmp. #define ext_hmap_put(hmap, entry_key, entry_val) \ - ext_hmap_put_ex(hmap, entry_key, entry_val, ext_hmap_hash_bytes_, ext_hmap_memcmp_) - -// Gets an entry from the hashmap. Sets `*out` to a pointer to the entry, or NULL. -// Keys are compared byte-for-byte with memcmp. -#define ext_hmap_get(hmap, entry_key, out) \ - ext_hmap_get_ex(hmap, entry_key, out, ext_hmap_hash_bytes_, ext_hmap_memcmp_) - -// Gets an entry from the hashmap, inserting a default if not found. -// Keys are compared byte-for-byte with memcmp. -#define ext_hmap_get_default(hmap, entry_key, entry_val, out) \ - ext_hmap_get_default_ex(hmap, entry_key, entry_val, out, ext_hmap_hash_bytes_, ext_hmap_memcmp_) + ext_hmap_put_ex(hmap, entry_key, entry_val, ext__hmap_hash_bytes_, ext__hmap_cmp_bytes_) // Deletes an entry from the hashmap. Keys are compared byte-for-byte with memcmp. #define ext_hmap_delete(hmap, entry_key) \ - ext_hmap_delete_ex(hmap, entry_key, ext_hmap_hash_bytes_, ext_hmap_memcmp_) + ext_hmap_delete_ex(hmap, entry_key, ext__hmap_hash_bytes_, ext__hmap_cmp_bytes_) // Puts an entry into the hashmap. Keys are compared with strcmp. #define ext_hmap_put_cstr(hmap, entry_key, entry_val) \ - ext_hmap_put_ex(hmap, entry_key, entry_val, ext_hmap_hash_cstr_, ext_hmap_strcmp_) - -// Gets an entry from the hashmap. Keys are compared with strcmp. -#define ext_hmap_get_cstr(hmap, entry_key, out) \ - ext_hmap_get_ex(hmap, entry_key, out, ext_hmap_hash_cstr_, ext_hmap_strcmp_) - -// Gets an entry from the hashmap, inserting a default if not found. Keys are compared with -// strcmp. -#define ext_hmap_get_default_cstr(hmap, entry_key, entry_val, out) \ - ext_hmap_get_default_ex(hmap, entry_key, entry_val, out, ext_hmap_hash_cstr_, ext_hmap_strcmp_) + ext_hmap_put_ex(hmap, entry_key, entry_val, ext__hmap_hash_cstr_, ext__hmap_cmp_cstr_) // Deletes an entry from the hashmap. Keys are compared with strcmp. #define ext_hmap_delete_cstr(hmap, entry_key) \ - ext_hmap_delete_ex(hmap, entry_key, ext_hmap_hash_cstr_, ext_hmap_strcmp_) + ext_hmap_delete_ex(hmap, entry_key, ext__hmap_hash_cstr_, ext__hmap_cmp_cstr_) // Puts an entry into the hashmap. Keys are compared with ext_ss_cmp. #define ext_hmap_put_ss(hmap, entry_key, entry_val) \ - ext_hmap_put_ex(hmap, entry_key, entry_val, ext_hmap_hash_ss_, ext_hmap_sscmp_) - -// Gets an entry from the hashmap. Keys are compared with ext_ss_cmp. -#define ext_hmap_get_ss(hmap, entry_key, out) \ - ext_hmap_get_ex(hmap, entry_key, out, ext_hmap_hash_ss_, ext_hmap_sscmp_) - -// Gets an entry from the hashmap, inserting a default if not found. -// Keys are compared with ext_ss_cmp. -#define ext_hmap_get_default_ss(hmap, entry_key, entry_val, out) \ - ext_hmap_get_default_ex(hmap, entry_key, entry_val, out, ext_hmap_hash_ss_, ext_hmap_sscmp_) + ext_hmap_put_ex(hmap, entry_key, entry_val, ext__hmap_hash_ss_, ext__hmap_cmp_ss_) // Deletes an entry from the hashmap. Keys are compared with ext_ss_cmp. #define ext_hmap_delete_ss(hmap, entry_key) \ - ext_hmap_delete_ex(hmap, entry_key, ext_hmap_hash_ss_, ext_hmap_sscmp_) - -// Expression-form lookup: evaluates to a pointer to the matching entry, or -// NULL if not found. Unlike `ext_hmap_get*`, these macros are expressions -// (not statements) and therefore work as struct-field initialisers, function -// arguments, and with anonymous entry types produced by `Ext_HashMap(K, V)`. -// -// The typed NULL is achieved via the C99 null-pointer-constant ternary rule -// (6.5.15p6): `cond ? typed_ptr : (void*)0` has the type of `typed_ptr`. -// -// `ext_hmap_getp_default*` always inserts a default entry when the key is -// absent, so they never return NULL. Use them with `Ext_HashMap` to avoid -// needing a named entry typedef: -// -// ```c -// Ext_HashMap(const char *, int) freq = {0}; -// hmap_getp_default_cstr(&freq, word, 0)->value++; -// ``` - -// Returns a pointer to the entry with the given key, or NULL. -// Keys are compared byte-for-byte with memcmp. -#define ext_hmap_getp(hmap, entry_key) \ - ((hmap)->size == 0 \ - ? (void *)0 \ - : (ext_hmap_tmp_(hmap).key = (entry_key), \ - ext__hmap_find_bytes_((hmap)->entries, (hmap)->hashes, (hmap)->capacity, \ - sizeof(*(hmap)->entries), sizeof(ext_hmap_tmp_(hmap).key)), \ - EXT_HMAP_IS_VALID((hmap)->hashes[(hmap)->hashes[0]]) \ - ? (hmap)->entries + (hmap)->hashes[0] \ - : (void *)0)) - -// Returns a pointer to the entry with the given key (cstr variant), or NULL. -#define ext_hmap_getp_cstr(hmap, entry_key) \ - ((hmap)->size == 0 ? (void *)0 \ - : (ext_hmap_tmp_(hmap).key = (entry_key), \ - ext__hmap_find_cstr_((hmap)->entries, (hmap)->hashes, (hmap)->capacity, \ - sizeof(*(hmap)->entries)), \ - EXT_HMAP_IS_VALID((hmap)->hashes[(hmap)->hashes[0]]) \ - ? (hmap)->entries + (hmap)->hashes[0] \ - : (void *)0)) - -// Returns a pointer to the entry with the given key (StringSlice variant), or NULL. -#define ext_hmap_getp_ss(hmap, entry_key) \ - ((hmap)->size == 0 ? (void *)0 \ - : (ext_hmap_tmp_(hmap).key = (entry_key), \ - ext__hmap_find_ss_((hmap)->entries, (hmap)->hashes, (hmap)->capacity, \ - sizeof(*(hmap)->entries)), \ - EXT_HMAP_IS_VALID((hmap)->hashes[(hmap)->hashes[0]]) \ - ? (hmap)->entries + (hmap)->hashes[0] \ - : (void *)0)) - -// Returns a pointer to the entry with the given key (custom hash/cmp), or NULL. -// `hash_fn` must be a function pointer `size_t (*)(const void *)`; -// `cmp_fn` must be a function pointer `int (*)(const void *, const void *)`. -// Both receive a pointer to the key (the first field of the entry). -#define ext_hmap_getp_ex(hmap, entry_key, hash_fn, cmp_fn) \ - ((hmap)->size == 0 ? (void *)0 \ - : (ext_hmap_tmp_(hmap).key = (entry_key), \ - ext__hmap_find_ex_((hmap)->entries, (hmap)->hashes, (hmap)->capacity, \ - sizeof(*(hmap)->entries), (hash_fn), (cmp_fn)), \ - EXT_HMAP_IS_VALID((hmap)->hashes[(hmap)->hashes[0]]) \ - ? (hmap)->entries + (hmap)->hashes[0] \ - : (void *)0)) - -// Returns a pointer to the existing entry with the given key, inserting a new -// entry with `entry_val` as the value when absent. Keys are compared with -// memcmp. -// -// The grow check runs first so that key/value are written into the (possibly -// reallocated) tmp slot after any reallocation. -#define ext_hmap_getp_default(hmap, entry_key, entry_val) \ - ((hmap)->size >= EXT_HMAP_MAX_ENTRY_LOAD((hmap)->capacity + 1) \ - ? (ext_hmap_grow_((void **)&(hmap)->entries, sizeof(*(hmap)->entries), &(hmap)->hashes, \ - &(hmap)->capacity, (Ext_Allocator **)&(hmap)->allocator), \ - 0) \ - : 0, \ - ext_hmap_tmp_(hmap).key = (entry_key), ext_hmap_tmp_(hmap).value = (entry_val), \ - ext__hmap_get_default_find_bytes_((hmap)->entries, (hmap)->hashes, (hmap)->capacity, \ - sizeof(*(hmap)->entries), sizeof(ext_hmap_tmp_(hmap).key), \ - &(hmap)->size), \ - (hmap)->entries + (hmap)->hashes[0]) - -// Returns a pointer to the existing entry (cstr variant), inserting with -// `entry_val` when absent. -#define ext_hmap_getp_default_cstr(hmap, entry_key, entry_val) \ - ((hmap)->size >= EXT_HMAP_MAX_ENTRY_LOAD((hmap)->capacity + 1) \ - ? (ext_hmap_grow_((void **)&(hmap)->entries, sizeof(*(hmap)->entries), &(hmap)->hashes, \ - &(hmap)->capacity, (Ext_Allocator **)&(hmap)->allocator), \ - 0) \ - : 0, \ - ext_hmap_tmp_(hmap).key = (entry_key), ext_hmap_tmp_(hmap).value = (entry_val), \ - ext__hmap_get_default_find_cstr_((hmap)->entries, (hmap)->hashes, (hmap)->capacity, \ - sizeof(*(hmap)->entries), &(hmap)->size), \ - (hmap)->entries + (hmap)->hashes[0]) - -// Returns a pointer to the existing entry (StringSlice variant), inserting -// with `entry_val` when absent. -#define ext_hmap_getp_default_ss(hmap, entry_key, entry_val) \ - ((hmap)->size >= EXT_HMAP_MAX_ENTRY_LOAD((hmap)->capacity + 1) \ - ? (ext_hmap_grow_((void **)&(hmap)->entries, sizeof(*(hmap)->entries), &(hmap)->hashes, \ - &(hmap)->capacity, (Ext_Allocator **)&(hmap)->allocator), \ - 0) \ - : 0, \ - ext_hmap_tmp_(hmap).key = (entry_key), ext_hmap_tmp_(hmap).value = (entry_val), \ - ext__hmap_get_default_find_ss_((hmap)->entries, (hmap)->hashes, (hmap)->capacity, \ - sizeof(*(hmap)->entries), &(hmap)->size), \ - (hmap)->entries + (hmap)->hashes[0]) - -// Returns a pointer to the existing entry (custom hash/cmp), inserting with -// `entry_val` when absent. -#define ext_hmap_getp_default_ex(hmap, entry_key, entry_val, hash_fn, cmp_fn) \ - ((hmap)->size >= EXT_HMAP_MAX_ENTRY_LOAD((hmap)->capacity + 1) \ - ? (ext_hmap_grow_((void **)&(hmap)->entries, sizeof(*(hmap)->entries), &(hmap)->hashes, \ - &(hmap)->capacity, (Ext_Allocator **)&(hmap)->allocator), \ - 0) \ - : 0, \ - ext_hmap_tmp_(hmap).key = (entry_key), ext_hmap_tmp_(hmap).value = (entry_val), \ - ext__hmap_get_default_find_ex_((hmap)->entries, (hmap)->hashes, (hmap)->capacity, \ - sizeof(*(hmap)->entries), &(hmap)->size, (hash_fn), (cmp_fn)), \ - (hmap)->entries + (hmap)->hashes[0]) + ext_hmap_delete_ex(hmap, entry_key, ext__hmap_hash_ss_, ext__hmap_cmp_ss_) // Clears the hashmap #define ext_hmap_clear(hmap) \ @@ -1715,11 +1609,11 @@ int ext_cmd_write(const char *cmd, const void *data, size_t size); // } // ``` #define ext_hmap_end(hmap) \ - ext_hmap_end_((hmap)->entries, (hmap)->capacity, sizeof(*(hmap)->entries)) + ext__hmap_end_((hmap)->entries, (hmap)->capacity, sizeof(*(hmap)->entries)) #define ext_hmap_begin(hmap) \ - ext_hmap_begin_((hmap)->entries, (hmap)->hashes, (hmap)->capacity, sizeof(*(hmap)->entries)) + ext__hmap_begin_((hmap)->entries, (hmap)->hashes, (hmap)->capacity, sizeof(*(hmap)->entries)) #define ext_hmap_next(hmap, it) \ - ext_hmap_next_((hmap)->entries, (hmap)->hashes, it, (hmap)->capacity, sizeof(*(hmap)->entries)) + ext__hmap_next_((hmap)->entries, (hmap)->hashes, it, (hmap)->capacity, sizeof(*(hmap)->entries)) #ifndef EXT_HMAP_INIT_CAPACITY #define EXT_HMAP_INIT_CAPACITY 8 @@ -1738,84 +1632,40 @@ EXT_STATIC_ASSERT(((EXT_HMAP_INIT_CAPACITY) & (EXT_HMAP_INIT_CAPACITY - 1)) == 0 #define EXT_HMAP_IS_EMPTY(h) ((h) == EXT_HMAP_EMPTY_MARK) #define EXT_HMAP_IS_VALID(h) (!EXT_HMAP_IS_EMPTY(h) && !EXT_HMAP_IS_TOMB(h)) -void ext_hmap_grow_(void **entries, size_t entries_sz, size_t **hashes, size_t *cap, - Ext_Allocator **a); - -#define ext_hmap_tmp_(map) ((map)->entries[EXT_HMAP_TMP_SLOT]) +void ext__hmap_grow_(void **entries, size_t entries_sz, size_t **hashes, size_t *cap, + Ext_Allocator **a); -#define ext_hmap_hash_bytes_(k) ext_hash_bytes_((k), sizeof(*(k))) -#define ext_hmap_hash_cstr_(k) ext_hash_cstr_(*(k)) -#define ext_hmap_hash_ss_(k) ext_hash_bytes_((k)->data, (k)->size) -#define ext_hmap_memcmp_(k1, k2) memcmp((k1), (k2), sizeof(*(k1))) -#define ext_hmap_strcmp_(k1, k2) strcmp(*(k1), *(k2)) -#define ext_hmap_sscmp_(k1, k2) ext_ss_cmp(*(k1), *(k2)) - -// Core probe loop used by all hashmap operations. -// -// Scans the bucket array starting from hash_ & cap_, writing the result slot -// index to hashes_[0]. After the macro: -// EXT_HMAP_IS_VALID(hashes_[hashes_[0]]) == true => hit (existing entry) -// EXT_HMAP_IS_VALID(hashes_[hashes_[0]]) == false => miss (best insertion slot) -// -// Parameters: -// hashes_ — hashes array; hashes_[0] receives the output slot index -// cap_ — capacity mask (capacity is always a power-of-two minus 1) -// hash_ — pre-computed hash (must be >= 2 before calling) -// key_eq_ — boolean expression, true when the candidate matches the lookup -// key; may reference i_, the current 0-based probe index, so the -// candidate entry lives at entries[i_ + 1] -#define ext__hmap_probe_(hashes_, cap_, hash_, key_eq_) \ - { \ - size_t i_ = (hash_) & (cap_); \ - bool tomb_found_ = false; \ - size_t tomb_idx_ = 0; \ - for(;;) { \ - size_t buck_ = (hashes_)[i_ + 1]; \ - if(!EXT_HMAP_IS_VALID(buck_)) { \ - if(EXT_HMAP_IS_EMPTY(buck_)) { \ - (hashes_)[0] = tomb_found_ ? tomb_idx_ : i_ + 1; \ - break; \ - } else if(!tomb_found_) { \ - tomb_found_ = true; \ - tomb_idx_ = i_ + 1; \ - } \ - } else if(buck_ == (hash_) && (key_eq_)) { \ - (hashes_)[0] = i_ + 1; \ - break; \ - } \ - i_ = (i_ + 1) & (cap_); \ - } \ - } +#define ext__hmap_tmp_(map) ((map)->entries[EXT_HMAP_TMP_SLOT]) #ifdef __GNUC__ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-function" #endif // __GNUC__ -static inline void *ext_hmap_end_(const void *entries, size_t cap, size_t sz) { +static inline void *ext__hmap_end_(const void *entries, size_t cap, size_t sz) { return entries ? (char *)entries + (cap + 2) * sz : NULL; } -static inline void *ext_hmap_begin_(const void *entries, const size_t *hashes, size_t cap, - size_t sz) { +static inline void *ext__hmap_begin_(const void *entries, const size_t *hashes, size_t cap, + size_t sz) { if(!entries) return NULL; for(size_t i = 1; i <= cap + 1; i++) { if(EXT_HMAP_IS_VALID(hashes[i])) { return (char *)entries + i * sz; } } - return ext_hmap_end_(entries, cap, sz); + return ext__hmap_end_(entries, cap, sz); } -static inline void *ext_hmap_next_(const void *entries, const size_t *hashes, const void *it, - size_t cap, size_t sz) { +static inline void *ext__hmap_next_(const void *entries, const size_t *hashes, const void *it, + size_t cap, size_t sz) { size_t curr = ((char *)it - (char *)entries) / sz; for(size_t idx = curr + 1; idx <= cap + 1; idx++) { if(EXT_HMAP_IS_VALID(hashes[idx])) { return (char *)entries + idx * sz; } } - return ext_hmap_end_(entries, cap, sz); + return ext__hmap_end_(entries, cap, sz); } // ----------------------------------------------------------------------------- @@ -2000,147 +1850,91 @@ static inline size_t ext_hash_bytes_(const void *p, size_t len) { } #endif } -#ifdef _MSC_VER -#pragma warning(pop) -#endif // _MSC_VER -#ifdef __GNUC__ -#pragma GCC diagnostic pop -#endif // __GNUC__ // End of stbds.h // ----------------------------------------------------------------------------- -#ifdef __GNUC__ -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-function" -#endif // __GNUC__ - // ----------------------------------------------------------------------------- -// Thin inline helpers for expression-form getp / getp_default macros. +// Concrete hash / compare functions for the built-in key types. // -// Expression macros (getp, getp_default) cannot use compound statements, so -// they delegate to these inline functions. Each helper stores the result slot -// index in hashes[0] via ext__hmap_probe_ and returns the computed hash so -// callers can write it without recomputing. -// -// On entry the key (and for get_default variants, the value) must already be -// stored in entries[0] (the tmp slot). +// Both functions follow the same convention used by the _ex macros: +// hash_fn(entry, key_sz) — entry points to the full entry; key_sz carries +// sizeof(key) for the bytes variant and is ignored by cstr/ss. +// cmp_fn(entry_a, entry_b, key_sz) — returns 0 on a key match. -// Bytes key (compared with memcmp over key_sz bytes). -static inline size_t ext__hmap_find_bytes_(const void *entries, size_t *hashes, size_t cap, - size_t entry_sz, size_t key_sz) { - size_t hash = ext_hash_bytes_(entries, key_sz); - if(hash < 2) hash += 2; - ext__hmap_probe_(hashes, cap, hash, - memcmp((const char *)entries + (i_ + 1) * entry_sz, entries, key_sz) == 0); - return hash; +static inline size_t ext__hmap_hash_bytes_(const void *entry, size_t key_sz) { + return ext_hash_bytes_(entry, key_sz); } - -// Cstr key (compared with strcmp). -static inline size_t ext__hmap_find_cstr_(const void *entries, size_t *hashes, size_t cap, - size_t entry_sz) { - const char *key = *(const char *const *)entries; - size_t hash = ext_hash_cstr_(key); - if(hash < 2) hash += 2; - ext__hmap_probe_(hashes, cap, hash, - strcmp(*(const char *const *)((const char *)entries + (i_ + 1) * entry_sz), - key) == 0); - return hash; +static inline size_t ext__hmap_hash_cstr_(const void *entry, size_t key_sz) { + (void)key_sz; + return ext_hash_cstr_(*(const char *const *)entry); } - -// StringSlice key (compared with ext_ss_cmp). -static inline size_t ext__hmap_find_ss_(const void *entries, size_t *hashes, size_t cap, - size_t entry_sz) { - const Ext_StringSlice *key = (const Ext_StringSlice *)entries; - size_t hash = ext_hash_bytes_(key->data, key->size); - if(hash < 2) hash += 2; - ext__hmap_probe_( - hashes, cap, hash, - ext_ss_cmp(*(const Ext_StringSlice *)((const char *)entries + (i_ + 1) * entry_sz), *key) == - 0); - return hash; +static inline size_t ext__hmap_hash_ss_(const void *entry, size_t key_sz) { + (void)key_sz; + const Ext_StringSlice *ss = (const Ext_StringSlice *)entry; + return ext_hash_bytes_(ss->data, ss->size); } - -// Custom hash_fn / cmp_fn (each receives a pointer to the key at entries[0]). -static inline size_t ext__hmap_find_ex_(const void *entries, size_t *hashes, size_t cap, - size_t entry_sz, size_t (*hash_fn)(const void *), - int (*cmp_fn)(const void *, const void *)) { - size_t hash = hash_fn(entries); - if(hash < 2) hash += 2; - ext__hmap_probe_(hashes, cap, hash, - cmp_fn((const char *)entries + (i_ + 1) * entry_sz, entries) == 0); - return hash; +static inline int ext__hmap_cmp_bytes_(const void *ea, const void *eb, size_t key_sz) { + return memcmp(ea, eb, key_sz); } - -// Get-or-insert variants: probe, then insert entries[0] if the key is absent. - -// Bytes key. -static inline void ext__hmap_get_default_find_bytes_(void *entries, size_t *hashes, size_t cap, - size_t entry_sz, size_t key_sz, size_t *size) { - size_t hash = ext_hash_bytes_(entries, key_sz); - if(hash < 2) hash += 2; - ext__hmap_probe_(hashes, cap, hash, - memcmp((const char *)entries + (i_ + 1) * entry_sz, entries, key_sz) == 0); - size_t idx = hashes[0]; - if(!EXT_HMAP_IS_VALID(hashes[idx])) { - memcpy((char *)entries + idx * entry_sz, entries, entry_sz); - hashes[idx] = hash; - (*size)++; - } +static inline int ext__hmap_cmp_cstr_(const void *ea, const void *eb, size_t key_sz) { + (void)key_sz; + return strcmp(*(const char *const *)ea, *(const char *const *)eb); } - -// Cstr key. -static inline void ext__hmap_get_default_find_cstr_(void *entries, size_t *hashes, size_t cap, - size_t entry_sz, size_t *size) { - const char *key = *(const char *const *)entries; - size_t hash = ext_hash_cstr_(key); - if(hash < 2) hash += 2; - ext__hmap_probe_(hashes, cap, hash, - strcmp(*(const char *const *)((const char *)entries + (i_ + 1) * entry_sz), - key) == 0); - size_t idx = hashes[0]; - if(!EXT_HMAP_IS_VALID(hashes[idx])) { - memcpy((char *)entries + idx * entry_sz, entries, entry_sz); - hashes[idx] = hash; - (*size)++; - } +static inline int ext__hmap_cmp_ss_(const void *ea, const void *eb, size_t key_sz) { + (void)key_sz; + return ext_ss_cmp(*(const Ext_StringSlice *)ea, *(const Ext_StringSlice *)eb); } -// StringSlice key. -static inline void ext__hmap_get_default_find_ss_(void *entries, size_t *hashes, size_t cap, - size_t entry_sz, size_t *size) { - const Ext_StringSlice *key = (const Ext_StringSlice *)entries; - size_t hash = ext_hash_bytes_(key->data, key->size); +// Probes for the key in entries[EXT_HMAP_TMP_SLOT]; writes the slot index to +// hashes[EXT_HMAP_TMP_SLOT] and returns the computed hash. +static inline size_t ext__hmap_find_(const void *entries, size_t *hashes, size_t cap, + size_t entry_sz, size_t key_sz, + size_t (*hash_fn)(const void *, size_t), + int (*cmp_fn)(const void *, const void *, size_t)) { + size_t hash = hash_fn(entries, key_sz); if(hash < 2) hash += 2; - ext__hmap_probe_( - hashes, cap, hash, - ext_ss_cmp(*(const Ext_StringSlice *)((const char *)entries + (i_ + 1) * entry_sz), *key) == - 0); - size_t idx = hashes[0]; - if(!EXT_HMAP_IS_VALID(hashes[idx])) { - memcpy((char *)entries + idx * entry_sz, entries, entry_sz); - hashes[idx] = hash; - (*size)++; + + size_t idx = hash & cap; + bool tomb_found = 0; + size_t tomb_idx = 0; + for(;;) { + size_t bucket = hashes[idx + 1]; + if(!EXT_HMAP_IS_VALID(bucket)) { + if(EXT_HMAP_IS_EMPTY(bucket)) { + hashes[EXT_HMAP_TMP_SLOT] = tomb_found ? tomb_idx : idx + 1; + break; + } else if(!tomb_found) { + tomb_found = 1; + tomb_idx = idx + 1; + } + } else if(bucket == (hash) && + cmp_fn(entries, (const char *)entries + (idx + 1) * entry_sz, key_sz) == 0) { + hashes[EXT_HMAP_TMP_SLOT] = idx + 1; + break; + } + idx = (idx + 1) & (cap); } + + return hash; } -// Custom hash_fn / cmp_fn. -static inline void ext__hmap_get_default_find_ex_(void *entries, size_t *hashes, size_t cap, - size_t entry_sz, size_t *size, - size_t (*hash_fn)(const void *), - int (*cmp_fn)(const void *, const void *)) { - size_t hash = hash_fn(entries); - if(hash < 2) hash += 2; - ext__hmap_probe_(hashes, cap, hash, - cmp_fn((const char *)entries + (i_ + 1) * entry_sz, entries) == 0); - size_t idx = hashes[0]; +static inline void ext__hmap_find_default_(const void *entries, size_t *hashes, + size_t *hashmap_size, size_t cap, size_t entry_sz, + size_t key_sz, size_t (*hash_fn)(const void *, size_t), + int (*cmp_fn)(const void *, const void *, size_t)) { + size_t hash = ext__hmap_find_(entries, hashes, cap, entry_sz, key_sz, hash_fn, cmp_fn); + size_t idx = hashes[EXT_HMAP_TMP_SLOT]; if(!EXT_HMAP_IS_VALID(hashes[idx])) { memcpy((char *)entries + idx * entry_sz, entries, entry_sz); hashes[idx] = hash; - (*size)++; + (*hashmap_size)++; } } +#ifdef _MSC_VER +#pragma warning(pop) +#endif // _MSC_VER #ifdef __GNUC__ #pragma GCC diagnostic pop #endif // __GNUC__ @@ -2155,6 +1949,7 @@ static inline void ext__hmap_get_default_find_ex_(void *entries, size_t *hashes, } while(0) #ifdef EXTLIB_IMPL + // ----------------------------------------------------------------------------- // SECTION: Logging // @@ -3725,8 +3520,8 @@ exit:; // ----------------------------------------------------------------------------- // SECTION: Hashmap // -void ext_hmap_grow_(void **entries, size_t entries_sz, size_t **hashes, size_t *cap, - Ext_Allocator **a) { +void ext__hmap_grow_(void **entries, size_t entries_sz, size_t **hashes, size_t *cap, + Ext_Allocator **a) { size_t newcap = *cap ? (*cap + 1) * 2 : EXT_HMAP_INIT_CAPACITY; size_t newsz = (newcap + 1) * entries_sz; size_t pad = EXT_ALIGN_PAD(newsz, sizeof(size_t)); @@ -4123,32 +3918,26 @@ static inline int ext_dbg_unknown(const char *name, const char *file, int line, #define cmd_write ext_cmd_write #endif -#define Entry Ext_Entry -#define HashMap Ext_HashMap -#define hmap_foreach ext_hmap_foreach -#define hmap_end ext_hmap_end -#define hmap_begin ext_hmap_begin -#define hmap_next ext_hmap_next -#define hmap_put ext_hmap_put -#define hmap_get ext_hmap_get -#define hmap_get_default ext_hmap_get_default -#define hmap_delete ext_hmap_delete -#define hmap_put_cstr ext_hmap_put_cstr -#define hmap_get_cstr ext_hmap_get_cstr -#define hmap_get_default_cstr ext_hmap_get_default_cstr -#define hmap_delete_cstr ext_hmap_delete_cstr -#define hmap_put_ss ext_hmap_put_ss -#define hmap_get_ss ext_hmap_get_ss -#define hmap_get_default_ss ext_hmap_get_default_ss -#define hmap_delete_ss ext_hmap_delete_ss -#define hmap_getp ext_hmap_getp -#define hmap_getp_cstr ext_hmap_getp_cstr -#define hmap_getp_ss ext_hmap_getp_ss -#define hmap_getp_default ext_hmap_getp_default -#define hmap_getp_default_cstr ext_hmap_getp_default_cstr -#define hmap_getp_default_ss ext_hmap_getp_default_ss -#define hmap_clear ext_hmap_clear -#define hmap_free ext_hmap_free +#define Entry Ext_Entry +#define HashMap Ext_HashMap +#define hmap_foreach ext_hmap_foreach +#define hmap_end ext_hmap_end +#define hmap_begin ext_hmap_begin +#define hmap_next ext_hmap_next +#define hmap_put ext_hmap_put +#define hmap_get ext_hmap_get +#define hmap_get_default ext_hmap_get_default +#define hmap_delete ext_hmap_delete +#define hmap_put_cstr ext_hmap_put_cstr +#define hmap_get_cstr ext_hmap_get_cstr +#define hmap_get_default_cstr ext_hmap_get_default_cstr +#define hmap_delete_cstr ext_hmap_delete_cstr +#define hmap_put_ss ext_hmap_put_ss +#define hmap_get_ss ext_hmap_get_ss +#define hmap_get_default_ss ext_hmap_get_default_ss +#define hmap_delete_ss ext_hmap_delete_ss +#define hmap_clear ext_hmap_clear +#define hmap_free ext_hmap_free #endif // EXTLIB_NO_SHORTHANDS #endif // EXTLIB_H diff --git a/test/test.c b/test/test.c index 7d0db3c..0f092d8 100644 --- a/test/test.c +++ b/test/test.c @@ -1493,14 +1493,12 @@ CTEST(hmap, get_put) { hmap_put(&map, 2, 100); ASSERT_TRUE(map.size == 22); - IntEntry *entry; - hmap_get(&map, 2, &entry); + IntEntry *entry = hmap_get(&map, 2); ASSERT_TRUE(entry != NULL); ASSERT_TRUE(entry->value = 100); entry->value += 50; - IntEntry *entry2; - hmap_get(&map, 2, &entry2); + IntEntry *entry2 = hmap_get(&map, 2); ASSERT_TRUE(entry == entry2); ASSERT_TRUE(entry2->value == 150); @@ -1515,17 +1513,17 @@ CTEST(hmap, delete) { hmap_put(&map, i, i * 10); } - hmap_get(&map, 1, &e); + e = hmap_get(&map, 1); ASSERT_TRUE(e != NULL); ASSERT_TRUE(e->value == 10); ASSERT_TRUE(map.size == 49); hmap_delete(&map, 1); ASSERT_TRUE(map.size == 48); - hmap_get(&map, 1, &e); + e = hmap_get(&map, 1); ASSERT_TRUE(e == NULL); - hmap_get(&map, 4, &e); + e = hmap_get(&map, 4); ASSERT_TRUE(e != NULL); ASSERT_TRUE(e->value == 40); @@ -1534,8 +1532,7 @@ CTEST(hmap, delete) { } for(int i = 2; i < 50; i++) { - IntEntry *e; - hmap_get(&map, i, &e); + IntEntry *e = hmap_get(&map, i); ASSERT_TRUE(e == NULL); } ASSERT_TRUE(map.size == 0); @@ -1552,16 +1549,14 @@ CTEST(hmap, clear) { hmap_clear(&map); ASSERT_TRUE(map.size == 0); for(int i = 0; i < 10; i++) { - IntEntry *e; - hmap_get(&map, i, &e); + IntEntry *e = hmap_get(&map, i); ASSERT_TRUE(e == NULL); } hmap_put(&map, 3, 100); ASSERT_TRUE(map.size == 1); - IntEntry *e; - hmap_get(&map, 3, &e); + IntEntry *e = hmap_get(&map, 3); ASSERT_TRUE(e != NULL); ASSERT_TRUE(e->key == 3 && e->value == 100); @@ -1647,19 +1642,16 @@ CTEST(hmap, get_put_cstr) { hmap_put_cstr(&map, "key 2", 100); ASSERT_TRUE(map.size == 22); - StrEntry *entry; - ext_hmap_get_cstr(&map, "key 2", &entry); + StrEntry *entry = hmap_get_cstr(&map, "key 2"); ASSERT_TRUE(entry != NULL); ASSERT_TRUE(entry->value == 100); entry->value += 50; - StrEntry *entry2; - hmap_get_cstr(&map, "key 2", &entry2); + StrEntry *entry2 = hmap_get_cstr(&map, "key 2"); ASSERT_TRUE(entry == entry2); ASSERT_TRUE(entry2->value == 150); - StrEntry *entry3; - hmap_get_cstr(&map, "key 30", &entry3); + StrEntry *entry3 = hmap_get_cstr(&map, "key 30"); ASSERT_TRUE(entry3 == NULL); hmap_free(&map); @@ -1674,23 +1666,22 @@ CTEST(hmap, delete_cstr) { hmap_put_cstr(&map, temp_sprintf("key %d", i), i * 10); } - hmap_get_cstr(&map, "key 1", &e); + e = hmap_get_cstr(&map, "key 1"); ASSERT_TRUE(e != NULL); ASSERT_TRUE(e->value == 10); ASSERT_TRUE(map.size == 49); hmap_delete_cstr(&map, "key 1"); ASSERT_TRUE(map.size == 48); - hmap_get_cstr(&map, "key 1", &e); + e = hmap_get_cstr(&map, "key 1"); ASSERT_TRUE(e == NULL); for(int i = 2; i < 50; i++) { hmap_delete_cstr(&map, temp_sprintf("key %d", i)); } for(int i = 2; i < 50; i++) { - StrEntry *e; const char *key = temp_sprintf("key %d", i); - hmap_get_cstr(&map, key, &e); + StrEntry *e = hmap_get_cstr(&map, key); ASSERT_TRUE(e == NULL); } ASSERT_TRUE(map.size == 0); @@ -1721,19 +1712,16 @@ CTEST(hmap, get_put_ss) { hmap_put_ss(&map, ss_from_cstr("key 2"), 100); ASSERT_TRUE(map.size == 22); - SliceEntry *entry; - ext_hmap_get_ss(&map, ss_from_cstr("key 2"), &entry); + SliceEntry *entry = hmap_get_ss(&map, ss_from_cstr("key 2")); ASSERT_TRUE(entry != NULL); ASSERT_TRUE(entry->value == 100); entry->value += 50; - SliceEntry *entry2; - hmap_get_ss(&map, ss_from_cstr("key 2"), &entry2); + SliceEntry *entry2 = hmap_get_ss(&map, ss_from_cstr("key 2")); ASSERT_TRUE(entry == entry2); ASSERT_TRUE(entry2->value == 150); - SliceEntry *entry3; - hmap_get_ss(&map, ss_from_cstr("key 30"), &entry3); + SliceEntry *entry3 = hmap_get_ss(&map, ss_from_cstr("key 30")); ASSERT_TRUE(entry3 == NULL); hmap_free(&map); @@ -1748,22 +1736,21 @@ CTEST(hmap, delete_ss) { hmap_put_ss(&map, ss_from_cstr(temp_sprintf("key %d", i)), i * 10); } - hmap_get_ss(&map, ss_from_cstr("key 1"), &e); + e = hmap_get_ss(&map, ss_from_cstr("key 1")); ASSERT_TRUE(e != NULL); ASSERT_TRUE(e->value == 10); ASSERT_TRUE(map.size == 49); hmap_delete_ss(&map, ss_from_cstr("key 1")); ASSERT_TRUE(map.size == 48); - hmap_get_ss(&map, ss_from_cstr("key 1"), &e); + e = hmap_get_ss(&map, ss_from_cstr("key 1")); ASSERT_FALSE(e != NULL); for(int i = 2; i < 50; i++) { hmap_delete_ss(&map, ss_from_cstr(temp_sprintf("key %d", i))); } for(int i = 2; i < 50; i++) { - SliceEntry *e; - hmap_get_ss(&map, ss_from_cstr(temp_sprintf("key %d", i)), &e); + SliceEntry *e = hmap_get_ss(&map, ss_from_cstr(temp_sprintf("key %d", i))); ASSERT_TRUE(e == NULL); } ASSERT_TRUE(map.size == 0); @@ -1772,39 +1759,36 @@ CTEST(hmap, delete_ss) { temp_reset(); } -// ----------------------------------------------------------------------------- -// Expression-form get (hmap_getp) and get-or-insert (hmap_getp_default) - -CTEST(hmap, getp_bytes) { +CTEST(hmap, get_bytes) { HashMap(int, int) map = {0}; // Empty map must return NULL without touching a NULL entries pointer. - ASSERT_TRUE(hmap_getp(&map, 1) == NULL); + ASSERT_TRUE(hmap_get(&map, 1) == NULL); for(int i = 1; i <= 22; i++) { hmap_put(&map, i, i * 10); } // Hit: correct key and value, inline mutation visible on next lookup. - ASSERT_TRUE(hmap_getp(&map, 5) != NULL); - ASSERT_TRUE(hmap_getp(&map, 5)->key == 5); - ASSERT_TRUE(hmap_getp(&map, 5)->value == 50); - hmap_getp(&map, 5)->value = 99; - ASSERT_TRUE(hmap_getp(&map, 5)->value == 99); + ASSERT_TRUE(hmap_get(&map, 5) != NULL); + ASSERT_TRUE(hmap_get(&map, 5)->key == 5); + ASSERT_TRUE(hmap_get(&map, 5)->value == 50); + hmap_get(&map, 5)->value = 99; + ASSERT_TRUE(hmap_get(&map, 5)->value == 99); // Same key returns the same pointer (stable storage, no realloc triggered here). - // Note: two getp calls must not share an expression — both write to the tmp slot. - void *p3a = hmap_getp(&map, 3); - void *p3b = hmap_getp(&map, 3); + // Note: two get calls must not share an expression — both write to the tmp slot. + void *p3a = hmap_get(&map, 3); + void *p3b = hmap_get(&map, 3); ASSERT_TRUE(p3a == p3b); // Miss on absent key. - ASSERT_TRUE(hmap_getp(&map, 100) == NULL); + ASSERT_TRUE(hmap_get(&map, 100) == NULL); hmap_free(&map); } -CTEST(hmap, getp_bytes_tombstones) { +CTEST(hmap, get_bytes_tombstones) { HashMap(int, int) map = {0}; for(int i = 1; i <= 50; i++) { @@ -1818,50 +1802,50 @@ CTEST(hmap, getp_bytes_tombstones) { // Deleted entries are not found (probe must continue past tombstones). for(int i = 1; i <= 25; i++) { - ASSERT_TRUE(hmap_getp(&map, i) == NULL); + ASSERT_TRUE(hmap_get(&map, i) == NULL); } // Entries whose probe chain passes through tombstones are still found. for(int i = 26; i <= 50; i++) { - ASSERT_TRUE(hmap_getp(&map, i) != NULL); - ASSERT_TRUE(hmap_getp(&map, i)->value == i * 10); + ASSERT_TRUE(hmap_get(&map, i) != NULL); + ASSERT_TRUE(hmap_get(&map, i)->value == i * 10); } hmap_free(&map); } -CTEST(hmap, getp_default_bytes) { +CTEST(hmap, get_default_bytes) { HashMap(int, int) map = {0}; // First access on an empty map: triggers grow then inserts with default. - hmap_getp_default(&map, 1, 100)->value += 5; + hmap_get_default(&map, 1, 100)->value += 5; ASSERT_TRUE(map.size == 1); // Second access: key already present, default is ignored, value unchanged. - ASSERT_TRUE(hmap_getp_default(&map, 1, 0)->value == 105); + ASSERT_TRUE(hmap_get_default(&map, 1, 0)->value == 105); ASSERT_TRUE(map.size == 1); // Accessing a key with a large default does not overwrite the stored value. - (void)hmap_getp_default(&map, 1, 999); - ASSERT_TRUE(hmap_getp(&map, 1)->value == 105); + (void)hmap_get_default(&map, 1, 999); + ASSERT_TRUE(hmap_get(&map, 1)->value == 105); // Insert enough keys to trigger at least one grow. for(int i = 2; i <= 22; i++) { - (void)hmap_getp_default(&map, i, i * 10); + (void)hmap_get_default(&map, i, i * 10); } ASSERT_TRUE(map.size == 22); // After a grow the previously inserted key is accessible under its new address. - ASSERT_TRUE(hmap_getp(&map, 1)->value == 105); + ASSERT_TRUE(hmap_get(&map, 1)->value == 105); for(int i = 2; i <= 22; i++) { - ASSERT_TRUE(hmap_getp(&map, i) != NULL); - ASSERT_TRUE(hmap_getp(&map, i)->value == i * 10); + ASSERT_TRUE(hmap_get(&map, i) != NULL); + ASSERT_TRUE(hmap_get(&map, i)->value == i * 10); } hmap_free(&map); } -CTEST(hmap, getp_default_bytes_tombstone_reuse) { +CTEST(hmap, get_default_bytes_tombstone_reuse) { HashMap(int, int) map = {0}; for(int i = 1; i <= 20; i++) { @@ -1873,91 +1857,91 @@ CTEST(hmap, getp_default_bytes_tombstone_reuse) { } ASSERT_TRUE(map.size == 10); - // Re-inserting deleted keys via getp_default should reuse tombstone slots. + // Re-inserting deleted keys via get_default should reuse tombstone slots. for(int i = 1; i <= 10; i++) { - (void)hmap_getp_default(&map, i, i * 100); + (void)hmap_get_default(&map, i, i * 100); } ASSERT_TRUE(map.size == 20); for(int i = 1; i <= 10; i++) { - ASSERT_TRUE(hmap_getp(&map, i) != NULL); - ASSERT_TRUE(hmap_getp(&map, i)->value == i * 100); + ASSERT_TRUE(hmap_get(&map, i) != NULL); + ASSERT_TRUE(hmap_get(&map, i)->value == i * 100); } hmap_free(&map); } -CTEST(hmap, getp_cstr) { +CTEST(hmap, get_cstr) { HashMap(const char *, int) map = {0}; - ASSERT_TRUE(hmap_getp_cstr(&map, "missing") == NULL); + ASSERT_TRUE(hmap_get_cstr(&map, "missing") == NULL); for(int i = 0; i < 22; i++) { hmap_put_cstr(&map, temp_sprintf("key %d", i), i * 10); } - ASSERT_TRUE(hmap_getp_cstr(&map, "key 3") != NULL); - ASSERT_TRUE(hmap_getp_cstr(&map, "key 3")->value == 30); - hmap_getp_cstr(&map, "key 3")->value = 77; - ASSERT_TRUE(hmap_getp_cstr(&map, "key 3")->value == 77); + ASSERT_TRUE(hmap_get_cstr(&map, "key 3") != NULL); + ASSERT_TRUE(hmap_get_cstr(&map, "key 3")->value == 30); + hmap_get_cstr(&map, "key 3")->value = 77; + ASSERT_TRUE(hmap_get_cstr(&map, "key 3")->value == 77); - ASSERT_TRUE(hmap_getp_cstr(&map, "key 99") == NULL); + ASSERT_TRUE(hmap_get_cstr(&map, "key 99") == NULL); hmap_free(&map); temp_reset(); } -CTEST(hmap, getp_default_cstr) { +CTEST(hmap, get_default_cstr) { HashMap(const char *, int) map = {0}; // Word-count pattern. const char *words[] = {"foo", "bar", "foo", "baz", "foo", "bar"}; for(size_t i = 0; i < 6; i++) { - hmap_getp_default_cstr(&map, words[i], 0)->value++; + hmap_get_default_cstr(&map, words[i], 0)->value++; } ASSERT_TRUE(map.size == 3); - ASSERT_TRUE(hmap_getp_cstr(&map, "foo")->value == 3); - ASSERT_TRUE(hmap_getp_cstr(&map, "bar")->value == 2); - ASSERT_TRUE(hmap_getp_cstr(&map, "baz")->value == 1); + ASSERT_TRUE(hmap_get_cstr(&map, "foo")->value == 3); + ASSERT_TRUE(hmap_get_cstr(&map, "bar")->value == 2); + ASSERT_TRUE(hmap_get_cstr(&map, "baz")->value == 1); hmap_free(&map); temp_reset(); } -CTEST(hmap, getp_ss) { +CTEST(hmap, get_ss) { HashMap(StringSlice, int) map = {0}; - ASSERT_TRUE(hmap_getp_ss(&map, ss_from_cstr("missing")) == NULL); + ASSERT_TRUE(hmap_get_ss(&map, ss_from_cstr("missing")) == NULL); for(int i = 0; i < 22; i++) { hmap_put_ss(&map, ss_from_cstr(temp_sprintf("key %d", i)), i * 10); } - ASSERT_TRUE(hmap_getp_ss(&map, ss_from_cstr("key 7")) != NULL); - ASSERT_TRUE(hmap_getp_ss(&map, ss_from_cstr("key 7"))->value == 70); - hmap_getp_ss(&map, ss_from_cstr("key 7"))->value = 55; - ASSERT_TRUE(hmap_getp_ss(&map, ss_from_cstr("key 7"))->value == 55); + ASSERT_TRUE(hmap_get_ss(&map, ss_from_cstr("key 7")) != NULL); + ASSERT_TRUE(hmap_get_ss(&map, ss_from_cstr("key 7"))->value == 70); + hmap_get_ss(&map, ss_from_cstr("key 7"))->value = 55; + ASSERT_TRUE(hmap_get_ss(&map, ss_from_cstr("key 7"))->value == 55); - ASSERT_TRUE(hmap_getp_ss(&map, ss_from_cstr("key 99")) == NULL); + ASSERT_TRUE(hmap_get_ss(&map, ss_from_cstr("key 99")) == NULL); hmap_free(&map); temp_reset(); } -CTEST(hmap, getp_default_ss) { +CTEST(hmap, get_default_ss) { HashMap(StringSlice, int) map = {0}; - // Word-count pattern — primary use case for getp_default. + // Word-count pattern — primary use case for get_default. const char *words[] = {"foo", "bar", "foo", "baz", "foo", "bar"}; for(size_t i = 0; i < 6; i++) { - hmap_getp_default_ss(&map, ss_from_cstr(words[i]), 0)->value++; + hmap_get_default_ss(&map, ss_from_cstr(words[i]), 0)->value++; } ASSERT_TRUE(map.size == 3); - ASSERT_TRUE(hmap_getp_ss(&map, ss_from_cstr("foo"))->value == 3); - ASSERT_TRUE(hmap_getp_ss(&map, ss_from_cstr("bar"))->value == 2); - ASSERT_TRUE(hmap_getp_ss(&map, ss_from_cstr("baz"))->value == 1); + ASSERT_TRUE(hmap_get_ss(&map, ss_from_cstr("foo"))->value == 3); + ASSERT_TRUE(hmap_get_ss(&map, ss_from_cstr("bar"))->value == 2); + ASSERT_TRUE(hmap_get_ss(&map, ss_from_cstr("baz"))->value == 1); hmap_free(&map); temp_reset(); From 3ab212b665ab8e5bd1efcbf515538478440b1e6b Mon Sep 17 00:00:00 2001 From: Fabrizio Pietrucci Date: Sun, 22 Mar 2026 18:27:36 +0100 Subject: [PATCH 5/6] Remove deprecated functions and macros --- extlib.h | 7 ------- 1 file changed, 7 deletions(-) diff --git a/extlib.h b/extlib.h index eb7f52c..78f6000 100644 --- a/extlib.h +++ b/extlib.h @@ -366,7 +366,6 @@ void assert(int c); // TODO: are we sure we want to require wasm embedder to pr // } // ``` #define ext_defer_loop(begin, end) for(int i__ = ((begin), 0); i__ != 1; i__ = ((end), 1)) -#define EXT_DEFER_LOOP ext_defer_loop // Assigns passed in value to variable, and jumps to label. // @@ -605,12 +604,6 @@ inline void *ext_memdup(const void *mem, size_t size) { return ext_allocator_memdup(ext_context->alloc, mem, size); } -// Backward compatibility: old _alloc suffix functions now use ext_allocator_* internally -// Note: parameter order changed - allocator is now first parameter -// DEPRECATED: Use ext_allocator_strdup and ext_allocator_memdup instead -#define ext_strdup_alloc(s, a) ext_allocator_strdup(a, s) -#define ext_memdup_alloc(mem, size, a) ext_allocator_memdup(a, mem, size) - // A default allocator that uses malloc/realloc/free. // It is the allocator configured in the context at program start. typedef struct Ext_DefaultAllocator { From c3765a901b05a2559250ab809423f74254efaad0 Mon Sep 17 00:00:00 2001 From: Fabrizio Pietrucci Date: Sun, 22 Mar 2026 18:46:08 +0100 Subject: [PATCH 6/6] Bump version --- extlib.h | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/extlib.h b/extlib.h index 78f6000..7f6fa66 100644 --- a/extlib.h +++ b/extlib.h @@ -1,5 +1,5 @@ /** - * extlib v1.4.1 - c extended library + * extlib v2.0.0 - c extended library * * Single-header-file library that provides functionality that extends the standard c library. * Features: @@ -43,6 +43,32 @@ * * Changelog: * + * v2.0.0: + * - Breaking changes on hashmap implementation: + * Reworked the hashmap implementation to make possible the usage of `ext_hmap_get` and + * `ext_hmap_get_default` family of functions as expressions. Now it is possible to do + * things like: + * ```c + * Entry* e = hmap_get_cstr(&hmap, "key"); + * if(e != NULL) { + * ... + * } + * ``` + * + * - Added `Ext_Array` and `Ext_HashMap`/`Ext_Entry` macros to define required struct layout + * for both dynamic arrays and hashmap in-line. + * This makes it possible to skip declaring the full struct by hand when we do not need + * to reference it by name (i.e. create variables to it, pass it to functions, etc.): + * ```c + * Array(int) int_array = {0}; + * HashMap(char*, int) int_map = {0}; + * ``` + * Declaring full struct layout by hand is still supported. + * + * - Breaking change: removed deprecated functions and macros: + * 1. ext_strdup_alloc and ext_memdup_alloc + * 2. EXT_DEFER_LOOP + * * v1.4.1: * - Minor tweaks to allocation functions - move some of them to be `inline` * @@ -454,7 +480,7 @@ Ext_Context *ext_pop_context(void); // } // // ... context automatically popped // ``` -#define EXT_PUSH_CONTEXT(ctx) EXT_DEFER_LOOP(ext_push_context(ctx), ext_pop_context()) +#define EXT_PUSH_CONTEXT(ctx) ext_defer_loop(ext_push_context(ctx), ext_pop_context()) // Utility macro to push/pop context with an allocator between code. // Simplifies pushing when the only thing you want to customize is the allocator. @@ -469,7 +495,7 @@ Ext_Context *ext_pop_context(void); #define EXT_PUSH_ALLOCATOR(allocator) \ Ext_Context EXT_CONCAT_(ctx_, __LINE__) = *ext_context; \ EXT_CONCAT_(ctx_, __LINE__).alloc = (allocator); \ - EXT_DEFER_LOOP(ext_push_context(&EXT_CONCAT_(ctx_, __LINE__)), ext_pop_context()) + ext_defer_loop(ext_push_context(&EXT_CONCAT_(ctx_, __LINE__)), ext_pop_context()) // Utility macro to push/pop a context with the given logging level set. // Simplifies pushing when the only thing you want to customize is the logging level. @@ -485,7 +511,7 @@ Ext_Context *ext_pop_context(void); #define EXT_LOGGING_LEVEL(level) \ Ext_Context EXT_CONCAT_(ctx_, __LINE__) = *ext_context; \ EXT_CONCAT_(ctx_, __LINE__).log_level = (level); \ - EXT_DEFER_LOOP(ext_push_context(&EXT_CONCAT_(ctx_, __LINE__)), ext_pop_context()) + ext_defer_loop(ext_push_context(&EXT_CONCAT_(ctx_, __LINE__)), ext_pop_context()) // ----------------------------------------------------------------------------- // SECTION: Allocators @@ -3716,7 +3742,6 @@ static inline int ext_dbg_unknown(const char *name, const char *file, int line, #define GiB EXT_GiB #define PRINTF_FORMAT EXT_PRINTF_FORMAT #define defer_loop ext_defer_loop -#define DEFER_LOOP EXT_DEFER_LOOP #define return_exit ext_return_exit #define DEBUG EXT_DEBUG