hash table + symbol deduplication
authorcassowarii <cassowary@cassowary.me>
Thu, 25 Jun 2026 23:17:30 +0000 (16:17 -0700)
committercassowarii <cassowary@cassowary.me>
Thu, 25 Jun 2026 23:17:30 +0000 (16:17 -0700)
15 files changed:
src/data/handle.c [new file with mode: 0644]
src/data/handle.h [new file with mode: 0644]
src/data/hashtable.c [new file with mode: 0644]
src/data/hashtable.h [new file with mode: 0644]
src/data/string.h
src/data/symbol.c [new file with mode: 0644]
src/data/symbol.h [new file with mode: 0644]
src/main.c
src/mem/arena.c
src/mem/arena.h
src/mem/buffer.c
src/mem/buffer.h
src/parse/scanner.c
src/parse/scanner.h
src/parse/token.h

diff --git a/src/data/handle.c b/src/data/handle.c
new file mode 100644 (file)
index 0000000..93dd728
--- /dev/null
@@ -0,0 +1,51 @@
+#include "data/handle.h"
+
+#include "data/string.h"
+#include "data/hashtable.h"
+
+#define FLAG_NONINTRINSIC (1ULL << 63)
+
+hV sbV_nil() {
+  return (hV) {0};
+}
+
+hV sbV_string(hString str) {
+  return (hV) {
+    .type = IT_STRING,
+    .string = str,
+  };
+}
+
+flag sbV_eq(hV *a, hV *b) {
+  if (a->type != b->type) return FALSE;
+  if (a->type == ITX_TOMBSTONE || b->type == ITX_TOMBSTONE) return FALSE;
+  if (a->type == IT_NOTHING || b->type == IT_NOTHING) return FALSE;
+
+  if (a->type == IT_STRING) {
+    return sbString_eq(a->string, b->string);
+  } else {
+    return a == b;
+  }
+}
+
+void sbV_retain(hV *a) {
+  /* for some classes (e.g. nil, float), we don't need to retain them at all
+   * because they are trivially copiable. some classes (integer, string) need
+   * to sometimes be retained but sometimes not, so we delegate to them to
+   * figure out if they need to or not. */
+  if (a->type == IT_STRING) {
+    sbString_clone(a->string);
+  }
+}
+
+void sbV_release(hV *a) {
+  if (a->type == IT_STRING) {
+    sbString_release(a->string);
+  }
+}
+
+/* --- */
+
+flag val_is_intrinsic(hV val) {
+  return (val.type & FLAG_NONINTRINSIC) != 0;
+}
diff --git a/src/data/handle.h b/src/data/handle.h
new file mode 100644 (file)
index 0000000..8886c41
--- /dev/null
@@ -0,0 +1,44 @@
+#ifndef __SARABANDE_HANDLE_H__
+#define __SARABANDE_HANDLE_H__
+
+#include "common.h"
+
+typedef u64 hHash;
+typedef u64 hString;
+typedef u64 hSymbol;
+
+typedef 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 }
+  N_INTRINSIC_TYPES,
+  ITX_TOMBSTONE,  // <hashtable_tombstone>
+} intrinsic_type;
+
+typedef struct hV {
+  u64 type;
+  union {
+    hString string;
+    hSymbol symbol;
+    hHash hash;
+  };
+} hV;
+
+hV sbV_nil();
+hV sbV_string(hString str);
+
+flag sbV_eq(hV *a, hV *b);
+
+void sbV_retain(hV *a);
+void sbV_release(hV *a);
+
+#endif
diff --git a/src/data/hashtable.c b/src/data/hashtable.c
new file mode 100644 (file)
index 0000000..beb1cc3
--- /dev/null
@@ -0,0 +1,286 @@
+#include "data/hashtable.h"
+
+#include "data/string.h"
+#include "data/handle.h"
+
+#define HASHES_PER_BLOCK 256
+#define INLINE_TABLE_LENGTH 64
+
+/* TODO eliminate globals */
+sbBuffer g_hashtable_blocks = {0};
+
+typedef struct hashentry {
+  hV key;
+  hV value;
+} hashentry;
+
+typedef struct hashtbl {
+  usize size;
+  usize used;
+  hHash handle;
+  flag allocated;
+  flag is_inline;
+  union {
+    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);
+static hashentry *set_key(hashtbl *t, hV *key, hV *value);
+static hashentry delete_key(hashtbl *t, hV *key);
+static void set_hashtbl_size(hashtbl *t, usize new_size, flag rehash_all);
+static hashentry *set_key_in_array(hashentry *entries, usize length, hV *key, hV *value);
+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();
+}
+
+void sbHash_sys_deinit() {
+  //sbBuffer_deinitialize(&g_hashtable_blocks);
+}
+
+/* https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function */
+#define FNV_OFFSET_BASIS 0xcbf29ce484222325ULL
+#define FNV_PRIME 0x100000001b3ULL
+sbHashValue sbHash_hash_bytes(const char *bytes, usize length) {
+  sbHashValue result = FNV_OFFSET_BASIS;
+  for (usize i = 0; i < length; i++) {
+    result ^= bytes[i];
+    result *= FNV_PRIME;
+  }
+  return result;
+}
+
+sbHashValue sbHash_hash_obj(hV *obj) {
+  if (obj->type == IT_NIL) {
+    char zero = 0;
+    return sbHash_hash_bytes(&zero, 1);
+  } else if (obj->type == IT_STRING) {
+    char scratch[8];
+    usize length;
+    const char *strval = sbString_get_value(obj->string, scratch, &length);
+    return sbHash_hash_bytes(strval, length);
+  } else {
+    PANIC("don't know how to hash object of type %lld", obj->type);
+  }
+}
+
+hHash sbHash_create(usize initial_size) {
+  hashtbl *t = new_tbl(initial_size);
+  return t->handle;
+}
+
+void sbHash_insert(hHash h, hV *key, hV *value) {
+  hashtbl *t = find_tbl_for_handle(h);
+  set_key(t, key, value);
+}
+
+hV sbHash_find(hHash h, hV *key) {
+  hashtbl *t = find_tbl_for_handle(h);
+  hashentry *e = find_entry_by_key(t, key);
+  if (e->key.type == IT_NOTHING) {
+    return (hV) { .type = IT_NOTHING };
+  } else {
+    return e->value;
+  }
+}
+
+hV sbHash_find_or_insert(hHash h, hV *key, hV *value) {
+  hashtbl *t = find_tbl_for_handle(h);
+  hashentry *e = find_entry_by_key(t, key);
+  if (e->key.type == IT_NOTHING) {
+    e->value = *value;
+  }
+  return e->value;
+}
+
+void sbHash_delete(hHash h, hV *key, hV *value) {
+  hashtbl *t = find_tbl_for_handle(h);
+  delete_key(t, key);
+}
+
+/* --- */
+
+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");
+
+  if (new_size == INLINE_TABLE_LENGTH) {
+    t->is_inline = 1;
+  } else {
+    hashentry *new_data = calloc(new_size, sizeof(hashentry));
+    if (rehash_all) {
+      hashentry *current_data = get_entry_ptr_for_tbl(t, NULL);
+      for (usize i = 0; i < t->size; i++) {
+        if (current_data[i].key.type != IT_NOTHING && current_data[i].key.type != ITX_TOMBSTONE) {
+          set_key_in_array(new_data, new_size, &current_data[i].key, &current_data[i].value);
+          /* adding a new k/v pair will retain the values, so we need to release them
+           * from the previous allocation */
+          sbV_release(&current_data[i].key);
+          sbV_release(&current_data[i].value);
+        }
+      }
+      /* now that we've migrated all our things to the new version, we can free the old
+       * version (unless it was not alloc'd to begin with) */
+      if (!t->is_inline) free(current_data);
+    }
+    t->external_entries = new_data;
+    t->is_inline = 0;
+  }
+  t->size = new_size;
+}
+
+static hashentry *get_entry_ptr_for_tbl(hashtbl *t, usize *length_out) {
+  if (length_out) *length_out = t->size;
+  if (t->is_inline) {
+    return t->inline_entries;
+  } else {
+    return t->external_entries;
+  }
+}
+
+static hashentry *find_entry_in_array(hashentry *entries, usize length, hV *key) {
+  sbHashValue hash = sbHash_hash_obj(key);
+  u32 start = (hash & 0xFFFFFFFF);
+  u32 move = (hash >> 32);
+  while (length % move == 0) {
+    move ++;
+  }
+  usize index = start % length;
+  while (!sbV_eq(&entries[index].key, key) && entries[index].key.type != IT_NOTHING) {
+    index += move;
+    index %= length;
+  }
+  return &entries[index];
+}
+
+static hashentry *find_entry_by_key(hashtbl *t, hV *key) {
+  return find_entry_in_array(get_entry_ptr_for_tbl(t, NULL), t->size, key);
+}
+
+static hashentry *set_key_in_array(hashentry *entries, usize length, hV *key, hV *value) {
+  hashentry *location = find_entry_in_array(entries, length, key);
+
+  /* now, we either found the current entry for this key,
+   * or we found an empty slot that fits this key. */
+  if (location->key.type == IT_NOTHING) {
+    /* key was not here before, so we need to retain the key
+     * as well so it doesn't change out from under us */
+    sbV_retain(key);
+    location->key = *key;
+  } else {
+    /* replacing something that already exists. we can keep
+     * the key the same, but need to release the previous value. */
+    sbV_release(&location->value);
+  }
+
+  sbV_retain(value);
+  location->value = *value;
+
+  return location;
+}
+
+static hashentry *set_key(hashtbl *t, hV *key, hV *value) {
+  hashentry *location = find_entry_by_key(t, key);
+
+  /* now, we either found the current entry for this key,
+   * or we found an empty slot that fits this key. */
+  if (location->key.type == IT_NOTHING) {
+    /* key was not here before, so we need to retain the key
+     * as well so it doesn't change out from under us */
+    sbV_retain(key);
+    location->key = *key;
+    t->used ++;
+    if (t->used >= t->size * 3 / 4) {
+      set_hashtbl_size(t, t->size * 2, TRUE);
+    }
+  } else {
+    /* replacing something that already exists. we can keep
+     * the key the same, but need to release the previous value. */
+    sbV_release(&location->value);
+  }
+
+  sbV_retain(value);
+  location->value = *value;
+
+  return location;
+}
+
+static hashentry delete_key(hashtbl *t, hV *key) {
+  hashentry *location = find_entry_by_key(t, key);
+  hashentry to_return = *location;
+  location->key.type = ITX_TOMBSTONE;
+  return to_return;
+}
+
+static hashtbl *find_tbl_for_handle(hHash handle) {
+  hashblk *block = get_block(handle / HASHES_PER_BLOCK);
+  return &block->tables[handle % HASHES_PER_BLOCK];
+}
+
+static hashtbl *new_tbl(usize initial_size) {
+  hashtbl *t = find_free_entry(find_free_block());
+  set_hashtbl_size(t, initial_size, FALSE);
+  return t;
+}
diff --git a/src/data/hashtable.h b/src/data/hashtable.h
new file mode 100644 (file)
index 0000000..56748cd
--- /dev/null
@@ -0,0 +1,25 @@
+#include "common.h"
+
+#include "data/handle.h"
+
+/* Open-hashing hash table of object handle keys -> object handle values. */
+
+typedef u64 sbHashValue;
+
+void sbHash_sys_init();
+
+void sbHash_sys_deinit();
+
+sbHashValue sbHash_hash_bytes(const char *bytes, usize length);
+
+sbHashValue sbHash_hash_obj(hV *obj);
+
+hHash sbHash_create(usize initial_size);
+
+hV sbHash_find(hHash h, hV *key);
+
+void sbHash_insert(hHash h, hV *key, hV *value);
+
+hV sbHash_find_or_insert(hHash h, hV *key, hV *value);
+
+void sbHash_delete(hHash h, hV *key, hV *value);
index 6132014..5f916c9 100644 (file)
@@ -2,7 +2,7 @@
 
 #define OBJSL(value) (sbString_new(value, sizeof(value) - 1))
 
-typedef u64 hString;
+#include "data/handle.h"
 
 /* create hString:
  *
diff --git a/src/data/symbol.c b/src/data/symbol.c
new file mode 100644 (file)
index 0000000..ab29e8c
--- /dev/null
@@ -0,0 +1,68 @@
+#include "symbol.h"
+
+#include "data/string.h"
+#include "data/hashtable.h"
+#include <string.h>
+#include "mem/mem.h"
+
+#define NAME_BLOCK_SIZE 4096
+
+/* TODO globals. though, this might be ok? */
+hHash symbol_table;
+u64 next_symbol_id = 0;
+sbArena symbol_text_arena;
+
+hSymbol new_symbol(const char *name, usize length);
+
+hSymbol sbSymbol_from_bytes(const char *text, usize length) {
+  hV symkey = {
+    .type = IT_STRING,
+    .string = sbString_new(text, length),
+  };
+
+  hV result = sbHash_find(symbol_table, &symkey);
+
+  if (result.type == IT_NOTHING) {
+    /* add new symbol */
+    hV symval = {
+      .type = IT_SYMBOL,
+      .symbol = new_symbol(text, length),
+    };
+
+    sbHash_insert(symbol_table, &symkey, &symval);
+    return symval.symbol;
+  } else if (result.type == IT_SYMBOL) {
+    /* return already existing symbol */
+    return result.symbol;
+  } else {
+    PANIC("Only symbol values are allowed in the symbol table!");
+  }
+}
+
+hSymbol sbSymbol_from_string(hString str) {
+  char scratch[8];
+  usize length;
+  const char *data = sbString_get_value(str, scratch, &length);
+  return sbSymbol_from_bytes(data, length);
+}
+
+void sbSymbol_sys_init() {
+  symbol_table = sbHash_create(256);
+  sbArena_initialize(&symbol_text_arena, NAME_BLOCK_SIZE);
+  /* create symbol 0 as empty symbol */
+  sbSymbol_from_bytes("", 0);
+}
+
+char *sbSymbol_name(hSymbol sym) {
+  return (char*)sym;
+}
+
+/* --- */
+
+hSymbol new_symbol(const char *name, usize length) {
+  char *name_loc = sbArena_alloc(&symbol_text_arena, length + 1);
+  memcpy(name_loc, name, length);
+  name_loc[length] = '\0';
+  return (hSymbol)name_loc;
+}
+
diff --git a/src/data/symbol.h b/src/data/symbol.h
new file mode 100644 (file)
index 0000000..d400e07
--- /dev/null
@@ -0,0 +1,11 @@
+#include "common.h"
+
+#include "handle.h"
+
+void sbSymbol_sys_init();
+
+hSymbol sbSymbol_from_string(hString str);
+
+hSymbol sbSymbol_from_bytes(const char *text, usize length);
+
+char *sbSymbol_name(hSymbol sym);
index 2ce7429..e1c0d78 100644 (file)
@@ -3,6 +3,8 @@
 #include "parse/filereader.h"
 #include "parse/lexer.h"
 #include "data/string.h"
+#include "data/hashtable.h"
+#include "data/symbol.h"
 
 int main(int argc, char **argv) {
     if (argc == 2) {
@@ -17,6 +19,8 @@ int main(int argc, char **argv) {
         sbLexer_initialize(&lx, fr);
 
         sbString_sys_init();
+        sbHash_sys_init();
+        sbSymbol_sys_init();
 
         sbLexToken t;
         char scratch[8];
index 993ed90..d62e236 100644 (file)
@@ -11,15 +11,7 @@ struct block {
     char data[];
 };
 
-struct sbArena {
-    struct block *first;
-    struct block *current;
-    struct block *last;
-};
-
-hArena sbArena_create(usize initial_size) {
-    hArena arena = malloc(sizeof(struct sbArena));
-
+void sbArena_initialize(hArena arena, usize initial_size) {
     while (initial_size % ALIGN != 0) initial_size++;
 
     struct block *block = calloc(sizeof(struct block) + initial_size, 1);
@@ -30,8 +22,6 @@ hArena sbArena_create(usize initial_size) {
     arena->first = block;
     arena->current = block;
     arena->last = block;
-
-    return arena;
 }
 
 void *sbArena_alloc(hArena arena, usize size) {
@@ -88,5 +78,5 @@ void sbArena_destroy(hArena arena) {
         blk = blk_next;
     } while (blk);
 
-    free(arena);
+    *arena = (sbArena) {0};
 }
index abb934b..270b15a 100644 (file)
@@ -7,9 +7,15 @@
  * For a flexible-length contiguous buffer that may need to grow in length, use sbBuffer instead.
  * (however, sbBuffer doesn't have the 'pointer validity' guarantee.) */
 
+typedef struct sbArena {
+    struct block *first;
+    struct block *current;
+    struct block *last;
+} sbArena;
+
 typedef struct sbArena *hArena;
 
-hArena sbArena_create(usize initial_size);
+void sbArena_initialize(hArena arena, usize initial_size);
 
 void *sbArena_alloc(hArena arena, usize size);
 
index faa7d4a..b844c76 100644 (file)
@@ -72,7 +72,7 @@ void sbBuffer_set_size(hBuffer buf, usize new_size) {
   }
 }
 
-void sbBuffer_append(hBuffer buf, const char *data, usize data_length) {
+void sbBuffer_append(hBuffer buf, const void *data, usize data_length) {
     if (data_length == 0) return;
 
     char *put = sbBuffer_expand(buf, data_length);
index 949b94e..2540a00 100644 (file)
@@ -23,7 +23,7 @@ void *sbBuffer_shrink(hBuffer buf, usize shrink_size);
 
 void sbBuffer_set_size(hBuffer buf, usize new_size);
 
-void sbBuffer_append(hBuffer buf, const char *data, usize data_length);
+void sbBuffer_append(hBuffer buf, const void *data, usize data_length);
 
 void sbBuffer_reset(hBuffer buf);
 
index 34a6724..c297bed 100644 (file)
@@ -1,5 +1,8 @@
 #include "scanner.h"
 
+#include "data/string.h"
+#include "data/symbol.h"
+
 /* This is like, the pre lexing stage. I mean, this does a lot of the tokenizing,
  * but it includes stuff like spaces and newlines, too. Think of this as 'normalizing'
  * the input. The lexer in the second stage will use some additional context and state
@@ -46,7 +49,7 @@ static void check_if_start(hScanner sc);
 static sbLexToken compute_next_token(hScanner sc);
 
 void sbScanner_initialize(hScanner sc, hFileReader fr) {
-    sc->arena = sbArena_create(MEM_BLOCK_SIZE);
+    sbArena_initialize(&sc->arena, MEM_BLOCK_SIZE);
     sc->file_reader = fr;
     sc->next_token = (sbLexToken) {0};
 
@@ -66,7 +69,7 @@ sbLexToken sbScanner_next(hScanner sc) {
 }
 
 void sbScanner_deinitialize(hScanner sc) {
-    sbArena_destroy(sc->arena);
+    sbArena_destroy(&sc->arena);
     sbBuffer_deinitialize(&sc->dynamic_buffer);
 }
 
@@ -153,12 +156,6 @@ static void read_char_into_buffer(hScanner sc, char c) {
     }
 }
 
-static void *save_buffer(hScanner sc) {
-    char *storage = sbArena_alloc(sc->arena, sc->dynamic_buffer.size);
-    sbstrncpy(storage, sc->dynamic_buffer.data, sc->dynamic_buffer.size);
-    return storage;
-}
-
 static usize read_identifier(hScanner sc) {
     usize token_size = 0;
     char ch = PEEK;
@@ -380,9 +377,8 @@ static sbLexToken compute_next_token(hScanner sc) {
             new_token.type = T_SYMBOL;
 
             token_size = read_symbol(sc);
-            char *storage = save_buffer(sc);
-
-            new_token.cstr = storage;
+            new_token.symb = sbSymbol_from_bytes(sc->dynamic_buffer.data, sc->dynamic_buffer.size - 1);
+            printf("%s -> %p\n", (char*)new_token.symb, (void*)new_token.symb);
             new_token.size = token_size;
         } else {
             /* : */
@@ -450,9 +446,8 @@ static sbLexToken compute_next_token(hScanner sc) {
         new_token.type = T_IDENTIFIER;
 
         token_size = read_identifier(sc);
-        char *storage = save_buffer(sc);
-
-        new_token.cstr = storage;
+        new_token.symb = sbSymbol_from_bytes(sc->dynamic_buffer.data, sc->dynamic_buffer.size - 1);
+        printf("%s -> %p\n", (char*)new_token.symb, (void*)new_token.symb);
         new_token.size = token_size;
     } else if (is_digit(ch)) {
         new_token.type = T_INTEGER;
index c016cd8..e3aabd3 100644 (file)
@@ -7,7 +7,7 @@
 typedef struct sbScanner {
     sbLexToken next_token;
     hFileReader file_reader;
-    hArena arena;
+    sbArena arena;
     sbBuffer dynamic_buffer;
 } sbScanner;
 
index 66c8864..4f3b94d 100644 (file)
@@ -3,7 +3,7 @@
 
 #include "common.h"
 
-#include "data/string.h"
+#include "data/handle.h"
 
 typedef enum sbTokenType {
     T_NULL                  = 0,
@@ -92,6 +92,7 @@ typedef struct sbLexToken {
     union {
         char *cstr;
         hString hstr;
+        hSymbol symb;
         float fl;
         int i;
     };