Change to a different hash table implementation

This commit is contained in:
2025-09-23 14:24:13 -07:00
parent 733c4103a3
commit f961673670
2 changed files with 219 additions and 185 deletions

View File

@ -3,7 +3,6 @@
// used by static function registering macros
#include "read.h" // IWYU pragma: keep
#include <assert.h>
#include <ctype.h>
#include <stdarg.h>
#include <stdio.h>
@ -106,6 +105,14 @@ void *lisp_realloc(void *old_ptr, size_t size) {
return new_ptr;
}
void *lisp_malloc0(size_t size) {
void *ptr = lisp_malloc(size);
if (ptr && size) {
memset(ptr, 0, size);
}
return ptr;
}
void garbage_collect(void) {
last_gc = bytes_allocated;
refcount_garbage_collect();
@ -136,12 +143,20 @@ static bool held_refs_callback(void *obj, RefcountList **held, void *ignored) {
}
return true;
}
case TYPE_HASHTABLE:
HASHTABLE_FOREACH(key, val, obj) {
*held = refcount_list_push(*held, key);
*held = refcount_list_push(*held, val);
case TYPE_HASHTABLE: {
LispHashtable *ht = obj;
HT_FOREACH_VALID_INDEX(obj, i) {
*held = refcount_list_push(*held, HASH_KEY(obj, i));
*held = refcount_list_push(*held, HASH_VALUE(obj, i));
}
if (ht->eq_fn != Qstrings_equal) {
*held = refcount_list_push(*held, ht->eq_fn);
}
if (ht->hash_fn != Qhash_string) {
*held = refcount_list_push(*held, ht->hash_fn);
}
return true;
}
case TYPE_FUNCTION: {
LispFunction *fn = obj;
*held = refcount_list_push(*held, fn->name);
@ -192,16 +207,8 @@ static void free_obj_callback(void *obj, void *ignored) {
}
} break;
case TYPE_HASHTABLE: {
LispHashtable *tbl = obj;
for (size_t i = 0; i < tbl->table_size; ++i) {
struct HashtableBucket *cur = tbl->data[i];
while (cur) {
struct HashtableBucket *next = cur->next;
lisp_free(cur);
cur = next;
}
}
lisp_free(tbl->data);
LispHashtable *ht = obj;
lisp_free(ht->key_vals);
} break;
case TYPE_FUNCTION:
case TYPE_SYMBOL:
@ -353,12 +360,20 @@ LispVal *make_lisp_function(LispVal *name, LispVal *return_tag, LispVal *args,
LispVal *make_lisp_hashtable(LispVal *eq_fn, LispVal *hash_fn) {
CONSTRUCT_OBJECT(self, LispHashtable, TYPE_HASHTABLE);
self->table_size = LISP_HASHTABLE_INITIAL_SIZE;
self->data =
lisp_malloc(sizeof(struct HashtableBucket *) * self->table_size);
memset(self->data, 0, sizeof(struct HashtableBucket *) * self->table_size);
self->key_vals =
lisp_malloc0(sizeof(struct HashtableEntry) * self->table_size);
self->count = 0;
self->eq_fn = eq_fn;
self->hash_fn = hash_fn;
// needed during early initialization
if (eq_fn == Qstrings_equal) {
self->eq_fn = eq_fn;
} else {
self->eq_fn = refcount_ref(eq_fn);
}
if (hash_fn == Qhash_string) {
self->hash_fn = hash_fn;
} else {
self->hash_fn = refcount_ref(hash_fn);
}
return LISPVAL(self);
}
@ -541,10 +556,7 @@ static LispVal **process_builtin_args(LispVal *fname, LispFunction *func,
(func->n_req + func->n_opt + ((LispHashtable *) func->kwargs)->count
+ !NILP(func->rest_arg));
*nargs = raw_count;
LispVal **vec = lisp_malloc(sizeof(LispVal *) * raw_count);
if (raw_count) {
memset(vec, 0, sizeof(LispVal *) * raw_count);
}
LispVal **vec = lisp_malloc0(sizeof(LispVal *) * raw_count);
LispVal *rest = Qnil;
LispVal *rest_end = Qnil;
size_t have_count = 0;
@ -741,10 +753,9 @@ static void process_lisp_args(LispVal *fname, LispFunction *func, LispVal *args,
if (!NILP(rargs)) {
goto missing_required;
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
HASHTABLE_FOREACH(arg, desc_lv, func->kwargs) {
struct OptArgDesc *oad = USERPTR(struct OptArgDesc, desc_lv);
HT_FOREACH_VALID_INDEX(func->kwargs, i) {
struct OptArgDesc *oad =
USERPTR(struct OptArgDesc, HASH_VALUE(func->kwargs, i));
// only check the current function's lexenv and not its parents'
if (NILP(gethash(added_kwds, oad->name, Qnil))) {
LispVal *eval_res = Feval(oad->default_form);
@ -755,7 +766,6 @@ static void process_lisp_args(LispVal *fname, LispFunction *func, LispVal *args,
}
}
}
#pragma GCC diagnostic pop
FOREACH(arg, oargs) {
struct OptArgDesc *oad = USERPTR(struct OptArgDesc, arg);
LispVal *default_val = Feval(oad->default_form);
@ -917,10 +927,7 @@ DEFUN(eval_in_env, "eval-in-env", (LispVal * form, LispVal *lexenv)) {
}
case TYPE_VECTOR: {
LispVector *vec = (LispVector *) form;
LispVal **elts = lisp_malloc(sizeof(LispVal *) * vec->length);
if (elts) { // in case length is 0
memset(elts, 0, sizeof(LispVal *) * vec->length);
}
LispVal **elts = lisp_malloc0(sizeof(LispVal *) * vec->length);
WITH_PUSH_FRAME(Qnil, Qnil, true, {
struct UnrefListData uld;
uld.vals = elts;
@ -2171,17 +2178,13 @@ DEFUN(mapsymbols, "mapsymbols", (LispVal * func, LispVal *package)) {
} else {
pkg = (LispPackage *) normalize_package(package);
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
// TODO make hash tables not crash if modified during a loop
WITH_CLEANUP(pkg, {
IGNORE();
HASHTABLE_FOREACH(name, sym, pkg->obarray) {
LispVal *args = const_list(true, 1, sym);
HT_FOREACH_VALID_INDEX(pkg->obarray, i) {
LispVal *args = const_list(true, 1, HASH_VALUE(pkg->obarray, i));
refcount_unref(Ffuncall(func, args));
}
});
#pragma GCC diagnostic pop
return Qnil;
}
@ -2461,7 +2464,7 @@ LispVal *intern(const char *name, size_t length, bool take, LispVal *package,
}
// #######################
// # Hashtable Functions #
// # Hash Table Functions #
// #######################
DEFUN(hashtablep, "hashtablep", (LispVal * val)) {
return LISP_BOOL(HASHTABLEP(val));
@ -2473,28 +2476,8 @@ DEFUN(make_hashtable, "make-hashtable", (LispVal * hash_fn, LispVal *eq_fn)) {
DEFUN(copy_hash_table, "copy-hash-table", (LispVal * table)) {
CHECK_TYPE(TYPE_HASHTABLE, table);
LispHashtable *orig = (LispHashtable *) table;
CONSTRUCT_OBJECT(copy, LispHashtable, TYPE_HASHTABLE);
copy->table_size = orig->table_size;
copy->data =
lisp_malloc(sizeof(struct HashtableBucket *) * copy->table_size);
memset(copy->data, 0, sizeof(struct HashtableBucket *) * copy->table_size);
copy->count = orig->count;
copy->eq_fn = orig->eq_fn;
copy->hash_fn = orig->hash_fn;
for (size_t i = 0; i < orig->table_size; ++i) {
for (struct HashtableBucket *bucket = orig->data[i]; bucket;
bucket = bucket->next) {
struct HashtableBucket *new_bucket =
lisp_malloc(sizeof(struct HashtableBucket));
new_bucket->hash = bucket->hash;
new_bucket->key = refcount_ref(bucket->key);
new_bucket->value = refcount_ref(bucket->value);
new_bucket->next = copy->data[i];
copy->data[i] = new_bucket;
}
}
return LISPVAL(copy);
// TODO implement
return Qnil;
}
DEFUN(hash_table_count, "hash-table-count", (LispVal * table)) {
@ -2510,20 +2493,16 @@ DEFUN(gethash, "gethash", (LispVal * table, LispVal *key, LispVal *def)) {
return refcount_ref(gethash(table, key, def));
}
DEFUN(remhash, "remhash", (LispVal * table, LispVal *key)) {
return refcount_ref(remhash(table, key));
}
static bool hash_table_eq(LispHashtable *self, LispVal *v1, LispVal *v2) {
if (NILP(self->eq_fn)) {
static bool hash_table_eq(LispVal *eq_fn, LispVal *v1, LispVal *v2) {
if (NILP(eq_fn)) {
return v1 == v2;
} else if (self->eq_fn == Qstrings_equal) {
} else if (eq_fn == Qstrings_equal) {
return !NILP(Fstrings_equal(v1, v2));
} else {
LispVal *eq_obj;
LispVal *args = const_list(true, 2, v1, v2);
WITH_CLEANUP(args, {
eq_obj = Ffuncall(self->eq_fn, args); //
eq_obj = Ffuncall(eq_fn, args); //
});
bool result = !NILP(eq_obj);
refcount_unref(eq_obj);
@ -2555,107 +2534,143 @@ static uint64_t hash_table_hash(LispHashtable *self, LispVal *key) {
}
}
static struct HashtableBucket *
find_hash_table_bucket(LispHashtable *self, LispVal *key, uint64_t hash) {
struct HashtableBucket *cur = self->data[hash % self->table_size];
while (cur) {
if (hash_table_eq(self, key, cur->key)) {
return cur;
}
cur = cur->next;
static ptrdiff_t hash_table_find_entry(struct HashtableEntry *entries,
size_t size, LispVal *eq_fn,
LispVal *key, uint64_t hash) {
size_t i = hash % size;
while (entries[i].key && !hash_table_eq(eq_fn, key, entries[i].key)) {
i = (i + 1) % size;
}
return NULL;
return i;
}
static void hash_table_rehash(LispHashtable *self, size_t new_size) {
struct HashtableBucket **new_data =
lisp_malloc(sizeof(struct HashtableBucket *) * new_size);
memset(new_data, 0, sizeof(struct HashtableBucket *) * new_size);
for (size_t i = 0; i < self->table_size; ++i) {
struct HashtableBucket *cur = self->data[i];
while (cur) {
struct HashtableBucket *next = cur->next;
cur->next = new_data[cur->hash % new_size];
new_data[cur->hash % new_size] = cur;
cur = next;
DEFUN(remhash, "remhash", (LispVal * table, LispVal *key)) {
CHECK_TYPE(TYPE_HASHTABLE, table);
LispHashtable *self = (LispHashtable *) table;
uint64_t hash = hash_table_hash(self, key);
ptrdiff_t i = hash_table_find_entry(self->key_vals, self->table_size,
self->eq_fn, key, hash);
if (HASH_SLOT_UNSET_P(self, i)) {
return Qnil;
}
refcount_unref(self->key_vals[i].key);
self->key_vals[i].key = NULL;
LispVal *retval = self->key_vals[i].value;
--self->count;
// fixup the table
for (size_t j = (i + 1) % self->table_size; !HASH_SLOT_UNSET_P(self, j);
j = (j + 1) % self->table_size) {
size_t k = HASH_HASH(self, j) % self->table_size;
if ((i <= j && i < k && k <= j) || (i > j && (k <= j || i < k))) {
// https://en.wikipedia.org/wiki/Open_addressing
// test if the value actually should come before i or after j
continue;
}
self->key_vals[i].hash = HASH_HASH(self, j);
self->key_vals[i].key = HASH_KEY(self, j);
self->key_vals[i].value = HASH_VALUE(self, j);
self->key_vals[j].key = NULL;
i = j;
}
return retval;
}
void free_hash_table_data_array(void *data) {
struct HashtableDataArray *arr = data;
for (size_t i = 0; i < arr->size; ++i) {
refcount_unref(arr->entries[i].key);
refcount_unref(arr->entries[i].value);
}
lisp_free(arr->entries);
}
// we assume the table is not full
// return true if we added a new entry, false otherwise
static bool puthash_to_array(LispVal *eq_fn, struct HashtableEntry *key_vals,
size_t table_size, LispVal *key, uint64_t hash,
LispVal *value) {
ptrdiff_t i = hash_table_find_entry(key_vals, table_size, eq_fn, key, hash);
if (!key_vals[i].key) {
key_vals[i].key = refcount_ref(key);
key_vals[i].hash = hash;
key_vals[i].value = refcount_ref(value);
return true;
} else {
refcount_unref(key_vals[i].key);
key_vals[i].key = refcount_ref(key);
refcount_unref(key_vals[i].value);
key_vals[i].value = refcount_ref(value);
return false;
}
}
static void rehash_to(LispHashtable *self, size_t new_size) {
struct HashtableEntry *new_data =
lisp_malloc0(sizeof(struct HashtableEntry) * new_size);
struct HashtableDataArray data_arr = {.size = new_size,
.entries = new_data};
void *cl_handler;
if (the_stack) {
cl_handler = register_cleanup(&free_hash_table_data_array, &data_arr);
}
size_t new_count = 0; // this should be the same, but just in case the user
// violates the rules of immutability
HT_FOREACH_VALID_INDEX(self, i) {
LispVal *key = HASH_KEY(self, i);
uint64_t hash = HASH_HASH(self, i);
LispVal *value = HASH_VALUE(self, i);
if (puthash_to_array(self->eq_fn, new_data, new_size, key, hash,
value)) {
++new_count;
}
}
free(self->data);
self->data = new_data;
if (the_stack) {
cancel_cleanup(cl_handler);
}
free_hash_table_data_array(&(struct HashtableDataArray) {
.size = self->table_size, .entries = self->key_vals});
self->key_vals = new_data;
self->table_size = new_size;
self->count = new_count;
}
static inline void maybe_rehash(LispHashtable *self) {
if (HASH_TABLE_LOAD_FACTOR(self) >= 0.5) {
rehash_to(self, self->table_size * LISP_HASHTABLE_GROWTH_FACTOR);
} /* else if (HASH_TABLE_LOAD_FACTOR(self) <= 0.1
&& self->table_size > LISP_HASHTABLE_INITIAL_SIZE) {
rehash_to(self, self->table_size / LISP_HASHTABLE_GROWTH_FACTOR);
} */
}
LispVal *puthash(LispVal *table, LispVal *key, LispVal *value) {
CHECK_TYPE(TYPE_HASHTABLE, table);
LispHashtable *self = (LispHashtable *) table;
maybe_rehash(self);
uint64_t hash = hash_table_hash(self, key);
struct HashtableBucket *cur_bucket =
find_hash_table_bucket(self, key, hash);
if (cur_bucket) {
refcount_ref(value);
refcount_unref(cur_bucket->value);
cur_bucket->value = value;
} else {
cur_bucket = lisp_malloc(sizeof(struct HashtableBucket));
cur_bucket->next = self->data[hash % self->table_size];
cur_bucket->hash = hash;
cur_bucket->key = refcount_ref(key);
cur_bucket->value = refcount_ref(value);
self->data[hash % self->table_size] = cur_bucket;
if (puthash_to_array(self->eq_fn, self->key_vals, self->table_size, key,
hash, value)) {
++self->count;
if ((double) self->count / self->table_size
>= LISP_HASHTABLE_GROWTH_THRESHOLD) {
hash_table_rehash(self,
LISP_HASHTABLE_GROWTH_FACTOR * self->table_size);
}
}
return table;
return value;
}
LispVal *gethash(LispVal *table, LispVal *key, LispVal *def) {
CHECK_TYPE(TYPE_HASHTABLE, table);
assert(HASH_TABLE_LOAD_FACTOR(table) < 0.95); // infinite loop otherwise
LispHashtable *self = (LispHashtable *) table;
uint64_t hash = hash_table_hash(self, key);
struct HashtableBucket *cur_bucket =
find_hash_table_bucket(self, key, hash);
if (cur_bucket) {
return cur_bucket->value;
ptrdiff_t i = hash_table_find_entry(self->key_vals, self->table_size,
self->eq_fn, key, hash);
if (HASH_SLOT_UNSET_P(self, i)) {
return def;
} else {
return HASH_VALUE(self, i);
}
return def;
}
LispVal *remhash(LispVal *table, LispVal *key) {
CHECK_TYPE(TYPE_HASHTABLE, table);
LispHashtable *self = (LispHashtable *) table;
uint64_t hash = hash_table_hash(self, key);
struct HashtableBucket *cur_bucket = self->data[hash % self->table_size];
if (cur_bucket && hash_table_eq(self, cur_bucket->key, key)) {
self->data[hash % self->table_size] = cur_bucket->next;
refcount_unref(cur_bucket->key);
refcount_unref(cur_bucket->value);
lisp_free(cur_bucket);
--self->count;
} else {
struct HashtableBucket *prev_bucket = cur_bucket;
cur_bucket = cur_bucket->next;
while (cur_bucket) {
if (hash_table_eq(self, cur_bucket->key, key)) {
prev_bucket->next = cur_bucket->next;
refcount_unref(cur_bucket->key);
refcount_unref(cur_bucket->value);
lisp_free(cur_bucket);
--self->count;
break;
}
}
}
if ((double) self->count / self->table_size
<= LISP_HASHTABLE_SHRINK_THRESHOLD
&& self->table_size > LISP_HASHTABLE_INITIAL_SIZE) {
hash_table_rehash(self,
self->table_size / LISP_HASHTABLE_GROWTH_FACTOR);
}
return table;
return refcount_unref(Fremhash(table, key));
}
// #####################
@ -3097,11 +3112,11 @@ void debug_dump(FILE *stream, void *obj, bool newline) {
void debug_print_hashtable(FILE *stream, LispVal *table) {
debug_dump(stream, table, true);
HASHTABLE_FOREACH(key, val, table) {
HT_FOREACH_VALID_INDEX(table, i) {
fprintf(stream, "- ");
debug_dump(stream, key, false);
debug_dump(stream, HASH_KEY(table, i), false);
fprintf(stream, " = ");
debug_dump(stream, val, true);
debug_dump(stream, HASH_VALUE(table, i), true);
}
}
@ -3126,12 +3141,9 @@ void debug_print_tree(FILE *stream, void *obj) {
// ################
static void register_symbols_and_functions(void) {
// don't intern Qunbound!
puthash(((LispPackage *) system_package)->obarray,
LISPVAL(((LispSymbol *) Qnil)->name), Qnil);
((LispSymbol *) Qnil)->package = refcount_ref(system_package);
puthash(((LispPackage *) system_package)->obarray,
LISPVAL(((LispSymbol *) Qt)->name), Qt);
((LispSymbol *) Qt)->package = refcount_ref(system_package);
REGISTER_DO_INTERN(nil, system_package);
REGISTER_DO_INTERN(t, system_package);
REGISTER_SYMBOL(opt);
REGISTER_SYMBOL(allow_other_keys);
REGISTER_SYMBOL(key);

View File

@ -1,6 +1,7 @@
#ifndef INCLUDED_LISP_H
#define INCLUDED_LISP_H
#include <assert.h>
#include <refcount/refcount.h>
#include <setjmp.h>
#include <stdarg.h>
@ -125,23 +126,20 @@ typedef struct {
LispVal *lexenv;
} LispFunction;
struct HashtableBucket {
struct HashtableBucket *next;
#define LISP_HASHTABLE_INITIAL_SIZE 32
#define LISP_HASHTABLE_GROWTH_FACTOR 2
struct HashtableEntry {
uint64_t hash;
LispVal *key;
LispVal *value;
};
#define LISP_HASHTABLE_INITIAL_SIZE 32
#define LISP_HASHTABLE_GROWTH_FACTOR 2
#define LISP_HASHTABLE_GROWTH_THRESHOLD 0.75f
#define LISP_HASHTABLE_SHRINK_THRESHOLD 0.25f
typedef struct {
LISP_OBJECT_HEADER;
struct HashtableBucket **data;
size_t table_size;
size_t table_size; // number of buckets
struct HashtableEntry *key_vals; // array of key, hash, value
size_t count;
LispVal *eq_fn;
LispVal *hash_fn;
@ -306,11 +304,13 @@ inline static bool NUMBERP(LispVal *v) {
refcount_init_static(Q##sym); \
refcount_init_static(((LispSymbol *) Q##sym)->name); \
}
#define REGISTER_SYMBOL_INTO(sym, pkg) \
REGISTER_SYMBOL_NOINTERN(sym) \
#define REGISTER_DO_INTERN(sym, pkg) \
((LispSymbol *) Q##sym)->package = refcount_ref(pkg); \
puthash(((LispPackage *) pkg)->obarray, \
LISPVAL(((LispSymbol *) Q##sym)->name), Q##sym);
#define REGISTER_SYMBOL_INTO(sym, pkg) \
REGISTER_SYMBOL_NOINTERN(sym) \
REGISTER_DO_INTERN(sym, pkg)
#define REGISTER_SYMBOL(sym) REGISTER_SYMBOL_INTO(sym, system_package)
#define REGISTER_STATIC_FUNCTION(name, args, docstr) \
REGISTER_SYMBOL_NOINTERN(name); \
@ -333,22 +333,11 @@ inline static bool NUMBERP(LispVal *v) {
// ###############
// # Loop macros #
// ###############
#define HASHTABLE_FOREACH(key_var, val_var, table) \
for (struct { \
LispHashtable *ht; \
size_t i; \
} __l = {.ht = (void *) table, .i = 0}; \
__l.i < __l.ht->table_size; ++__l.i) \
for (LispVal *__b = (void *) __l.ht->data[__l.i], \
*key_var = __b ? ((struct HashtableBucket *) __b)->key \
: NULL, \
*val_var = __b ? ((struct HashtableBucket *) __b)->value \
: NULL; \
__b; __b = (void *) ((struct HashtableBucket *) __b)->next, \
key_var = __b ? ((struct HashtableBucket *) __b)->key \
: NULL, \
val_var = __b ? ((struct HashtableBucket *) __b)->value \
: NULL)
#define HT_FOREACH_VALID_INDEX(table, i_var) \
for (size_t i_var = 0; i_var < ((LispHashtable *) (table))->table_size; \
++i_var) \
if (!HASH_SLOT_UNSET_P((table), i_var))
#define FOREACH(var, list) \
for (LispVal *__foreach_cur = list, *var = HEAD(list); \
!NILP(__foreach_cur); \
@ -362,6 +351,7 @@ inline static bool NUMBERP(LispVal *v) {
#define GC_EVERY_N_BYTES 1024 * 80
void *lisp_malloc(size_t size);
void *lisp_realloc(void *old_ptr, size_t size);
void *lisp_malloc0(size_t size);
#define lisp_free free
void garbage_collect(void);
@ -528,7 +518,7 @@ LispVal *intern(const char *name, size_t length, bool take, LispVal *package,
bool included_too);
// #######################
// # Hashtable Functions #
// # Hash Table Functions #
// #######################
DECLARE_FUNCTION(hashtablep, (LispVal * val));
DECLARE_FUNCTION(make_hashtable, (LispVal * hash_fn, LispVal *eq_fn));
@ -537,6 +527,11 @@ DECLARE_FUNCTION(hash_table_count, (LispVal * table));
DECLARE_FUNCTION(puthash, (LispVal * table, LispVal *key, LispVal *value));
DECLARE_FUNCTION(gethash, (LispVal * table, LispVal *key, LispVal *def));
DECLARE_FUNCTION(remhash, (LispVal * table, LispVal *key));
struct HashtableDataArray {
size_t size;
struct HashtableEntry *entries;
};
void free_hash_table_data_array(void *data);
// Don't ref their return value
LispVal *puthash(LispVal *table, LispVal *key, LispVal *value);
@ -760,7 +755,8 @@ static inline void push_to_lexenv(LispVal **lexenv, LispVal *key,
refcount_unref(old);
}
// These are like the internal functions, but they don't ref their return value
// These are like the internal functions, but they don't ref their
// return value
static inline LispVal *HEAD(LispVal *list) {
if (NILP(list)) {
return Qnil;
@ -788,6 +784,32 @@ static inline LispVal *TAIL_SAFE(LispVal *list) {
return ((LispPair *) list)->tail;
}
static inline double HASH_TABLE_LOAD_FACTOR(void *obj) {
assert(HASHTABLEP(obj));
LispHashtable *table = obj;
return (double) table->count / table->table_size;
}
static inline bool HASH_SLOT_UNSET_P(void *obj, size_t i) {
assert(HASHTABLEP(obj));
LispHashtable *table = obj;
return !table->key_vals[i].key;
}
static inline LispVal *HASH_KEY(void *obj, size_t i) {
assert(HASHTABLEP(obj));
LispHashtable *table = obj;
return table->key_vals[i].key;
}
static inline LispVal *HASH_VALUE(void *obj, size_t i) {
assert(HASHTABLEP(obj));
LispHashtable *table = obj;
return table->key_vals[i].value;
}
static inline uint64_t HASH_HASH(void *obj, size_t i) {
assert(HASHTABLEP(obj));
LispHashtable *table = obj;
return table->key_vals[i].hash;
}
// ###################
// # Debug Functions #
// ###################