factor out 'allocation pool' code from string + hash table
authorcassowarii <cassowary@cassowary.me>
Tue, 30 Jun 2026 22:03:16 +0000 (15:03 -0700)
committercassowarii <cassowary@cassowary.me>
Tue, 30 Jun 2026 22:03:16 +0000 (15:03 -0700)
Makefile
src/data/hashtable.c
src/data/string.c
src/data/value.h
src/mem/mem.h
src/mem/pool.c [new file with mode: 0644]
src/mem/pool.h [new file with mode: 0644]

index a58e1c1..85b4716 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -14,12 +14,14 @@ build/a.out: $(OBJECTS)
        @mkdir -p $(dir $@)
        $(CC) $(CFLAGS) -I$(SRC) $(OBJECTS) $(LIBS) -o $@
 
-.PHONY: parsedebug optimized
+.PHONY: parsedebug optimized dev
 parsedebug: CFLAGS += -DPARSEDEBUG
-parsedebug: build/a.out
+parsedebug: clean build/a.out
 
 optimized: CFLAGS=-Wall -O3 -flto
-optimized: build/a.out
+optimized: clean build/a.out
+
+dev: clean build/a.out
 
 $(OBJ)/%.o: $(SRC)/%.c
        @mkdir -p $(dir $@)
index 6cef9a3..355e119 100644 (file)
@@ -6,7 +6,7 @@
 #define INLINE_TABLE_LENGTH 64
 
 /* TODO eliminate globals */
-sbBuffer g_hashtable_blocks = {0};
+sbPool g_hashtable_pool = {0};
 
 typedef struct hashentry {
   hV key;
@@ -20,19 +20,12 @@ typedef struct hashtbl {
   flag allocated;
   flag is_inline;
   union {
+    /* TODO should be SoA instead of AoS */
     hashentry inline_entries[INLINE_TABLE_LENGTH];
     hashentry *external_entries;
   };
 } hashtbl;
 
-typedef struct hashblk {
-  usize id;
-  usize used_count;
-  usize last_index;
-  hashtbl tables[HASHES_PER_BLOCK];
-} hashblk;
-
-static hashblk *alloc_new_block();
 static hashtbl *new_tbl(usize initial_size);
 static hashentry *get_entry_ptr_for_tbl(hashtbl *t, usize *length_out);
 static hashtbl *find_tbl_for_handle(hHash handle);
@@ -43,12 +36,11 @@ static hashentry *set_key_in_array(hashentry *entries, usize length, hV *key, hV
 static hashentry *find_entry_by_key(hashtbl *t, hV *key);
 
 void sbHash_sys_init() {
-  sbBuffer_initialize(&g_hashtable_blocks, 8 * sizeof(hashblk*));
-  alloc_new_block();
+  sbPool_initialize(&g_hashtable_pool, sizeof(hashtbl), HASHES_PER_BLOCK);
 }
 
 void sbHash_sys_deinit() {
-  //sbBuffer_deinitialize(&g_hashtable_blocks);
+  //sbPool_deinitialize(&g_hashtable_pool);
 }
 
 /* https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function */
@@ -113,54 +105,6 @@ void sbHash_delete(hHash h, hV *key, hV *value) {
 
 /* --- */
 
-static hashblk *alloc_new_block() {
-  usize nblocks = g_hashtable_blocks.size / sizeof(hashblk*);
-  hashblk *new_block = calloc(1, sizeof(hashblk));
-  new_block->id = nblocks;
-  sbBuffer_append(&g_hashtable_blocks, &new_block, sizeof(hashblk*));
-  return new_block;
-}
-
-static hashblk *find_free_block() {
-  usize nblocks = g_hashtable_blocks.size / sizeof(hashblk*);
-  hashblk *free_block = NULL;
-  for (usize i = 0; i < nblocks; i++) {
-    if (((hashblk**)g_hashtable_blocks.data)[i]->used_count < HASHES_PER_BLOCK) {
-      free_block = ((hashblk**)g_hashtable_blocks.data)[i];
-      break;
-    }
-  }
-
-  if (free_block == NULL) {
-    free_block = alloc_new_block();
-  }
-
-  return free_block;
-}
-
-static hashblk *get_block(usize index) {
-  usize nblocks = g_hashtable_blocks.size / sizeof(hashblk*);
-  if (index > nblocks) PANIC("request for a hash block (#%zu) that does not exist!", index);
-  return ((hashblk**)g_hashtable_blocks.data)[index];
-}
-
-static hashtbl *find_free_entry(hashblk *blk) {
-  if (blk->used_count >= HASHES_PER_BLOCK) PANIC("request for new table in full hashblk!");
-
-  while (blk->tables[blk->last_index].allocated) {
-    blk->last_index++;
-    blk->last_index %= HASHES_PER_BLOCK;
-  }
-  blk->tables[blk->last_index].allocated = 1;
-  blk->used_count ++;
-
-  hashtbl *result = &blk->tables[blk->last_index];
-
-  result->handle = blk->id * HASHES_PER_BLOCK + blk->last_index;
-
-  return result;
-}
-
 static void set_hashtbl_size(hashtbl *t, usize new_size, flag rehash_all) {
   if (new_size < INLINE_TABLE_LENGTH) new_size = INLINE_TABLE_LENGTH;
   if (new_size < t->size) PANIC("cannot shrink hashtable allocation");
@@ -274,12 +218,13 @@ static hashentry delete_key(hashtbl *t, hV *key) {
 }
 
 static hashtbl *find_tbl_for_handle(hHash handle) {
-  hashblk *block = get_block(handle / HASHES_PER_BLOCK);
-  return &block->tables[handle % HASHES_PER_BLOCK];
+  return sbPool_get_entry(&g_hashtable_pool, handle);
 }
 
 static hashtbl *new_tbl(usize initial_size) {
-  hashtbl *t = find_free_entry(find_free_block());
+  usize index;
+  hashtbl *t = sbPool_alloc(&g_hashtable_pool, &index);
   set_hashtbl_size(t, initial_size, FALSE);
+  t->handle = index;
   return t;
 }
index db710d7..38796df 100644 (file)
 
 /* TODO: Move these globals into some kind of "execution context" struct
  * that we can pass around to be reentrant */
-/* also TODO a better (slightly more annoying to code) way to structure
- * this would be SoA style: instead of storing used_count and allocated
- * inside the block and entry respectively, store them in a separate
- * array next to the pointers that we can scan quickly. less important
- * for allocated since it's already in the strblk anyway, but for used_count
- * this could be important because the counts will otherwise be scattered
- * to the four winds */
-sbBuffer g_string_blocks;
-usize g_free_block_index;
+sbPool g_string_pool;
 
 typedef struct strentry {
   GCINFO gc;
@@ -47,18 +39,9 @@ typedef struct strentry {
   };
 } strentry;
 
-typedef struct strblk {
-  usize id;
-  usize used_count;
-  usize next_free_index;
-  strentry entries[STRINGS_PER_BLOCK];
-} strblk;
-
 static const char *get_ptr_of_entry(strentry *e);
 static char *get_mutable_ptr_of_entry(strentry *e, usize length, hString *handle);
 static strentry *find_entry_for_handle(hString handle);
-static strblk *get_strblk(usize index);
-static strentry *get_entry(strblk *blk, usize index);
 static strentry *new_entry(usize length);
 static void place_str_at_offset(strentry *e, usize offset, const char *str, usize length);
 static int buffers_eq(const char *a_ptr, usize a_length, const char *b_ptr, usize b_length);
@@ -67,22 +50,17 @@ static flag is_tinystr(hString handle);
 static usize tinystr_length(hString handle);
 static void tinystr_into_buffer(char *buffer, hString handle, usize max_length);
 static hString tinystr_from_buffer(const char *buffer, usize length);
-static strblk *new_strblk();
 static void set_entry_size(strentry *e, usize length);
 
 void sbString_sys_init() {
-  g_free_block_index = 0;
-  sbBuffer_initialize(&g_string_blocks, sizeof(strblk*) * 8);
-
-  /* we need to have at least one strblk to start */
-  new_strblk();
+  sbPool_initialize(&g_string_pool, sizeof(strentry), STRINGS_PER_BLOCK);
 }
 
 void sbString_sys_deinit() {
   /* TODO free these i guess, or don't deinitialize this buffer, right now this
    * would just drop the pointers to no benefit. look, i'm going to write a
    * garbage collector, ok? give me a second */
-  //sbBuffer_deinitialize(&g_string_blocks);
+  //sbPool_deinitialize(&g_string_pool);
 }
 
 hString sbString_new(const char *value, usize length) {
@@ -265,8 +243,7 @@ static usize tinystr_length(hString handle) {
 
 static strentry *find_entry_for_handle(hString handle) {
   if (is_tinystr(handle)) return NULL;
-  strblk *blk = get_strblk((handle & ~FLAG_NONTINY) / STRINGS_PER_BLOCK);
-  return get_entry(blk, (handle & ~FLAG_NONTINY) % STRINGS_PER_BLOCK);
+  return sbPool_get_entry(&g_string_pool, handle & ~FLAG_NONTINY);
 }
 
 static strentry *duplicate_strentry(strentry *e, usize length) {
@@ -381,77 +358,6 @@ static hString tinystr_from_buffer(const char *buffer, usize length) {
   return new_handle;
 }
 
-static strblk *get_strblk(usize index) {
-  usize nblocks = g_string_blocks.size / sizeof(strblk*);
-  if (index >= nblocks) PANIC("attempt to get a string from a block (#%zu) that does not exist!", index);
-
-  return ((strblk**)g_string_blocks.data)[index];
-}
-
-static strentry *get_entry(strblk *blk, usize index) {
-  if (index >= STRINGS_PER_BLOCK) PANIC("attempt to get a string (#%zu) that does not exist in block (#%zu)!", index, blk->id);
-
-  return &blk->entries[index];
-}
-
-static strblk *new_strblk() {
-  usize nblocks = g_string_blocks.size / sizeof(strblk*);
-  strblk *new_block = calloc(1, sizeof(strblk));
-  new_block->id = nblocks;
-  strblk **where_to_put = sbBuffer_expand(&g_string_blocks, sizeof(strblk*));
-  *where_to_put = new_block;
-  return new_block;
-}
-
-static strentry *alloc_entry(strblk *blk) {
-  if (blk->used_count == STRINGS_PER_BLOCK) PANIC("attempt to allocate from a full strblk (#%zu)!", blk->id);
-
-  blk->used_count ++;
-
-  usize free_index = blk->next_free_index;
-
-  if (blk->used_count < STRINGS_PER_BLOCK) {
-    usize next_free_index = blk->next_free_index;
-    do {
-      next_free_index ++;
-      next_free_index %= STRINGS_PER_BLOCK;
-    } while (blk->entries[next_free_index].allocated && next_free_index != blk->next_free_index);
-
-    if (next_free_index == blk->next_free_index) {
-      PANIC("free count lied! for strblk (#%zu)", blk->id);
-    }
-
-    blk->next_free_index = next_free_index;
-  }
-
-  blk->entries[free_index].allocated = TRUE;
-
-  return &blk->entries[free_index];
-}
-
-static strblk *find_free_block() {
-  strblk *block_with_free = get_strblk(g_free_block_index);
-
-  if (block_with_free->used_count == STRINGS_PER_BLOCK) {
-    usize nblocks = g_string_blocks.size / sizeof(strblk*);
-    usize start_index = g_free_block_index;
-
-    do {
-      g_free_block_index ++;
-      g_free_block_index %= nblocks;
-      block_with_free = get_strblk(g_free_block_index);
-    } while (g_free_block_index != start_index && block_with_free->used_count == STRINGS_PER_BLOCK);
-
-    if (g_free_block_index == start_index) {
-      /* Where did that bring you? Back to me. */
-      block_with_free = new_strblk();
-      g_free_block_index = nblocks;
-    }
-  }
-
-  return block_with_free;
-}
-
 static void set_entry_size(strentry *e, usize length) {
   if (e->is_external) {
     sbBuffer_set_size(&e->somewhere_else_value, length + 1);
@@ -460,7 +366,7 @@ static void set_entry_size(strentry *e, usize length) {
     e->somewhere_else_value.data[length] = '\0';
   } else {
     if (length > INLINE_BUFFER_SIZE - 1) {
-      /* new length is too big for inline, so we need to copy to the heap */
+      /* new length is too big for inline, so we need to copy to the external heap */
       sbBuffer new_buf = {0};
       /* length + 1 for NUL terminator. even though we track the length, it's
        * easier to just preserve NUL terminator for talking to C lib functions */
@@ -499,9 +405,9 @@ static void place_str_at_offset(strentry *e, usize offset, const char *str, usiz
 }
 
 static strentry *new_entry(usize length) {
-  strblk *new_block = find_free_block();
-  strentry *new_entry = alloc_entry(new_block);
-  new_entry->handle = ((new_block->id * STRINGS_PER_BLOCK + (new_entry - new_block->entries)) | FLAG_NONTINY);
+  usize index;
+  strentry *new_entry = sbPool_alloc(&g_string_pool, &index);
+  new_entry->handle = (index | FLAG_NONTINY);
   set_entry_size(new_entry, length);
   return new_entry;
 }
index 439e060..5ab763f 100644 (file)
@@ -15,19 +15,19 @@ typedef u64 hSymbol;
 typedef i64 hInteger;
 
 enum intrinsic_type {
-  IT_NOTHING,     // sentinel for "no value here"
-  IT_NIL,         // nil ("there is a value here, but it's nil")
-  IT_BOOLEAN,     // true / false
-  IT_STRING,      // `abcdefg`
-  IT_SYMBOL,      // :hello
-  IT_INTEGER,     // 324892
-  IT_FLOAT,       // 0.0345
-  IT_DATETIME,    // unix timestamp
-  IT_REF,         // pointer \abc
-  IT_LIST,        // list [1, 3, 5, 7]
-  IT_HASH       // hash {a: 1, b: 2}
-  IT_FUNCTION   // function => a, b { a + b }
-  ITX_TOMBSTONE // <hashtable_tombstone>
+  IT_NOTHING,          // sentinel for "no value here"
+  IT_NIL = -1,         // nil ("there is a value here, but it's nil")
+  IT_BOOLEAN = -2,     // true / false
+  IT_STRING = -3,      // `abcdefg`
+  IT_SYMBOL = -4,      // :hello
+  IT_INTEGER = -5,     // 324892
+  IT_FLOAT = -6,       // 0.0345
+  IT_DATETIME = -7,    // unix timestamp
+  IT_REF = -8,         // pointer \abc
+  IT_LIST = -9,        // list [1, 3, 5, 7]
+  IT_HASH = -10,       // hash {a: 1, b: 2}
+  IT_FUNCTION = -11,   // function => a, b { a + b }
+  ITX_TOMBSTONE = -12, // <hashtable_tombstone>
 };
 
 typedef struct hV {
index 1498f62..a37e553 100644 (file)
@@ -2,8 +2,10 @@
 #define __SB_MEM_H__
 
 #include "global.h"
+
 #include "arena.h"
 #include "buffer.h"
 #include "cstrutil.h"
+#include "pool.h"
 
 #endif
diff --git a/src/mem/pool.c b/src/mem/pool.c
new file mode 100644 (file)
index 0000000..515fc38
--- /dev/null
@@ -0,0 +1,102 @@
+#include "mem/pool.h"
+
+/* this could be faster: computing offsets at runtime isn't
+ * strictly speaking optimal. but this would be insane to macro.
+ * "if only c had templates!" <- sentences dreamed up by the
+ * utterly deranged.  maybe i should manually code-generate these
+ * later. */
+
+// sorry
+// BLOCK_ALLOCATION_FLAGS(pool, block_index) gives the array of flags of length `block_size` for the given block
+// BLOCK_ALLOCATED_DATA(pool, block_index) gives a pointer to the start of the actual slot array (of size `elem_size * block_size`)
+#define BLOCK_ALLOCATION_FLAGS(pool, blk) ((flag*)(((Block*)(pool->block_ptrs[blk]))->data))
+#define BLOCK_ALLOCATED_DATA(pool, blk) ((char*)(&((Block*)(pool->block_ptrs[blk]))->data[pool->block_size]))
+
+/*
+typedef struct sbPool {
+  usize elem_size;
+  usize block_size;
+  usize num_blocks;
+  usize block_ptr_capacity;
+  usize next_block_id;
+  void **block_ptrs;
+  u16 *used_counts;
+} sbPool;
+*/
+
+typedef struct Block {
+  usize last_index;
+  char data[];
+} Block;
+
+void alloc_new_block(hPool pl);
+
+void sbPool_initialize(hPool pl, usize elem_size, usize block_size) {
+  *pl = (sbPool) {0};
+  pl->elem_size = elem_size;
+  pl->block_size = block_size;
+  alloc_new_block(pl);
+}
+
+void sbPool_deinitialize(hPool pl) {
+  for (usize i = 0; i < pl->num_blocks; i++) {
+    free(pl->block_ptrs[i]);
+  }
+  *pl = (sbPool) {0};
+}
+
+void *sbPool_alloc(hPool pl, usize *id_out) {
+  usize start_index = pl->next_block_id;
+  /* if current block is full, look for one that's empty */
+  while (pl->used_counts[pl->next_block_id] == pl->block_size) {
+    pl->next_block_id ++;
+    pl->next_block_id %= pl->num_blocks;
+    if (pl->next_block_id == start_index) {
+      /* Where did that bring you? Back to me. */
+      pl->next_block_id = pl->num_blocks;
+      alloc_new_block(pl);
+      break;
+    }
+  }
+
+  Block *block = pl->block_ptrs[pl->next_block_id];
+  while (BLOCK_ALLOCATION_FLAGS(pl, pl->next_block_id)[block->last_index]) {
+    block->last_index ++;
+    block->last_index %= pl->block_size;
+  }
+  BLOCK_ALLOCATION_FLAGS(pl, pl->next_block_id)[block->last_index] = 1;
+  pl->used_counts[pl->next_block_id] ++;
+
+  if (id_out) *id_out = pl->next_block_id * pl->block_size + block->last_index;
+
+  return &BLOCK_ALLOCATED_DATA(pl, pl->next_block_id)[block->last_index * pl->elem_size];
+}
+
+void *sbPool_get_entry(hPool pl, usize index) {
+  usize block_id = index / pl->block_size;
+  usize elem_id = index % pl->block_size;
+  return &BLOCK_ALLOCATED_DATA(pl, block_id)[elem_id * pl->elem_size];
+}
+
+/* this 'alloc' function gives us an unallocated slot. there is no 'free' on individual items
+ * (we only have a pointer to the individual item anyway), but items should be reclaimed
+ * by the garbage collector. once we write the garbage collector, at least...) */
+
+/* --- */
+
+void alloc_new_block(hPool pl) {
+  /* allocate block header + 'block_size' bytes for tracking allocation + enough space
+   * to hold all the elements */
+  void *new_block = calloc(1, sizeof(Block) + pl->block_size + pl->elem_size * pl->block_size);
+  if (pl->num_blocks >= pl->block_ptr_capacity) {
+    usize new_capacity = pl->block_ptr_capacity * 2;
+    void **new_data = realloc(pl->block_ptrs, new_capacity);
+    if (!new_data) {
+      PANIC("This should probably trigger the garbage collector or something!");
+    }
+    pl->block_ptrs = new_data;
+  }
+
+  pl->block_ptrs[pl->num_blocks] = new_block;
+  pl->num_blocks ++;
+}
diff --git a/src/mem/pool.h b/src/mem/pool.h
new file mode 100644 (file)
index 0000000..321dffd
--- /dev/null
@@ -0,0 +1,26 @@
+#ifndef __SARABANDE_POOL_H__
+#define __SARABANDE_POOL_H__
+
+#include "common.h"
+
+typedef struct sbPool {
+  usize elem_size;
+  usize block_size;
+  usize num_blocks;
+  usize block_ptr_capacity;
+  usize next_block_id;
+  void **block_ptrs;
+  u16 *used_counts;
+} sbPool;
+
+typedef sbPool *hPool;
+
+void sbPool_initialize(hPool pl, usize elem_size, usize block_size);
+
+void sbPool_deinitialize(hPool pl);
+
+void *sbPool_alloc(hPool pl, usize *id_out);
+
+void *sbPool_get_entry(hPool pl, usize index);
+
+#endif