From: cassowarii Date: Thu, 25 Jun 2026 23:17:30 +0000 (-0700) Subject: hash table + symbol deduplication X-Git-Url: https://www.git.cassowary.me/gitweb.cgi?a=commitdiff_plain;h=1335caeeac75b0257afdad1d4628d89b38225c2f;p=sarabande.git hash table + symbol deduplication --- diff --git a/src/data/handle.c b/src/data/handle.c new file mode 100644 index 0000000..93dd728 --- /dev/null +++ b/src/data/handle.c @@ -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 index 0000000..8886c41 --- /dev/null +++ b/src/data/handle.h @@ -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, // +} 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 index 0000000..beb1cc3 --- /dev/null +++ b/src/data/hashtable.c @@ -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, ¤t_data[i].key, ¤t_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(¤t_data[i].key); + sbV_release(¤t_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 index 0000000..56748cd --- /dev/null +++ b/src/data/hashtable.h @@ -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); diff --git a/src/data/string.h b/src/data/string.h index 6132014..5f916c9 100644 --- a/src/data/string.h +++ b/src/data/string.h @@ -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 index 0000000..ab29e8c --- /dev/null +++ b/src/data/symbol.c @@ -0,0 +1,68 @@ +#include "symbol.h" + +#include "data/string.h" +#include "data/hashtable.h" +#include +#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 index 0000000..d400e07 --- /dev/null +++ b/src/data/symbol.h @@ -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); diff --git a/src/main.c b/src/main.c index 2ce7429..e1c0d78 100644 --- a/src/main.c +++ b/src/main.c @@ -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]; diff --git a/src/mem/arena.c b/src/mem/arena.c index 993ed90..d62e236 100644 --- a/src/mem/arena.c +++ b/src/mem/arena.c @@ -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}; } diff --git a/src/mem/arena.h b/src/mem/arena.h index abb934b..270b15a 100644 --- a/src/mem/arena.h +++ b/src/mem/arena.h @@ -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); diff --git a/src/mem/buffer.c b/src/mem/buffer.c index faa7d4a..b844c76 100644 --- a/src/mem/buffer.c +++ b/src/mem/buffer.c @@ -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); diff --git a/src/mem/buffer.h b/src/mem/buffer.h index 949b94e..2540a00 100644 --- a/src/mem/buffer.h +++ b/src/mem/buffer.h @@ -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); diff --git a/src/parse/scanner.c b/src/parse/scanner.c index 34a6724..c297bed 100644 --- a/src/parse/scanner.c +++ b/src/parse/scanner.c @@ -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; diff --git a/src/parse/scanner.h b/src/parse/scanner.h index c016cd8..e3aabd3 100644 --- a/src/parse/scanner.h +++ b/src/parse/scanner.h @@ -7,7 +7,7 @@ typedef struct sbScanner { sbLexToken next_token; hFileReader file_reader; - hArena arena; + sbArena arena; sbBuffer dynamic_buffer; } sbScanner; diff --git a/src/parse/token.h b/src/parse/token.h index 66c8864..4f3b94d 100644 --- a/src/parse/token.h +++ b/src/parse/token.h @@ -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; };