nova-nix-0.6.0.0: cbits/nn_symbol.c
/*
* nn_symbol.c - Interned string symbol table.
*
* Implementation: open-addressed hash table with linear probing.
* String data is stored in a contiguous arena (bulk allocation,
* no per-string malloc/free). Symbols are 1-indexed uint32_t
* values; 0 is the invalid sentinel.
*
* The table doubles when load exceeds 75%. Deletion is not
* supported - symbols live for the entire evaluation lifetime.
*/
#include "nn_symbol.h"
#include "nn_assert.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* --- Configuration --- */
#define NN_SYMBOL_DEFAULT_CAPACITY 4096
#define NN_SYMBOL_LOAD_PERCENT 75
#define NN_SYMBOL_ARENA_INITIAL (256 * 1024) /* 256 KB */
/* --- Internal types --- */
/* Per-symbol metadata stored in a flat array indexed by symbol ID. */
typedef struct {
uint32_t offset; /* byte offset into string arena */
uint32_t len; /* byte length of the string */
uint32_t hash; /* cached FNV-1a hash */
} nn_symbol_entry_t;
/* Hash table slot: maps hash -> symbol ID. Empty slots have id == 0. */
typedef struct {
uint32_t hash;
uint32_t id; /* 1-based symbol ID, 0 = empty */
} nn_slot_t;
/* Global symbol table state. */
static struct {
/* Symbol metadata array (1-indexed; index 0 unused). */
nn_symbol_entry_t *entries;
uint32_t count; /* number of interned symbols */
uint32_t entries_cap; /* allocated entry slots */
/* Hash table (open addressing, linear probing). */
nn_slot_t *slots;
uint32_t slots_cap; /* always a power of two */
uint32_t slots_mask; /* slots_cap - 1 */
/* Contiguous string arena. */
char *arena;
size_t arena_used;
size_t arena_cap;
} g_sym;
/* --- FNV-1a hash --- */
static uint32_t fnv1a(const char *data, size_t len)
{
uint32_t h = 2166136261u;
size_t i;
for (i = 0; i < len; i++) {
h ^= (uint8_t)data[i];
h *= 16777619u;
}
return h;
}
/* --- Arena --- */
/* Ensure the arena has room for `needed` more bytes. */
static void arena_ensure(size_t needed)
{
if (g_sym.arena_used + needed <= g_sym.arena_cap) return;
size_t new_cap = g_sym.arena_cap;
while (new_cap < g_sym.arena_used + needed) {
new_cap *= 2;
}
char *new_arena = (char *)realloc(g_sym.arena, new_cap);
if (!new_arena) {
fprintf(stderr, "nn_symbol: arena realloc failed (requested %zu bytes)\n", new_cap);
abort();
}
g_sym.arena = new_arena;
g_sym.arena_cap = new_cap;
}
/* Copy `len` bytes into the arena, returning the start offset. */
static uint32_t arena_push(const char *str, size_t len)
{
arena_ensure(len + 1); /* +1 for null terminator */
if (g_sym.arena_used > (size_t)UINT32_MAX) {
fprintf(stderr, "nn_symbol: arena offset exceeds uint32_t range\n");
abort();
}
uint32_t offset = (uint32_t)g_sym.arena_used;
memcpy(g_sym.arena + offset, str, len);
g_sym.arena[offset + len] = '\0';
g_sym.arena_used += len + 1;
return offset;
}
/* --- Entry array --- */
/* Ensure the entry array has room for one more symbol. */
static void entries_ensure(void)
{
/* count is 0-based count, entries are 1-indexed, so we need count+1 < cap */
if (g_sym.count + 1 < g_sym.entries_cap) return;
uint32_t new_cap = g_sym.entries_cap * 2;
nn_symbol_entry_t *new_entries = (nn_symbol_entry_t *)realloc(
g_sym.entries, (size_t)new_cap * sizeof(nn_symbol_entry_t));
if (!new_entries) {
fprintf(stderr, "nn_symbol: entries realloc failed\n");
abort();
}
g_sym.entries = new_entries;
g_sym.entries_cap = new_cap;
}
/* --- Hash table --- */
/* Rebuild the hash table at double capacity. */
static void slots_grow(void)
{
uint32_t new_cap = g_sym.slots_cap * 2;
uint32_t new_mask = new_cap - 1;
nn_slot_t *new_slots = (nn_slot_t *)calloc((size_t)new_cap, sizeof(nn_slot_t));
if (!new_slots) {
fprintf(stderr, "nn_symbol: slots calloc failed\n");
abort();
}
uint32_t i;
for (i = 0; i < g_sym.slots_cap; i++) {
if (g_sym.slots[i].id == 0) continue;
uint32_t idx = g_sym.slots[i].hash & new_mask;
while (new_slots[idx].id != 0) {
idx = (idx + 1) & new_mask;
}
new_slots[idx] = g_sym.slots[i];
}
free(g_sym.slots);
g_sym.slots = new_slots;
g_sym.slots_cap = new_cap;
g_sym.slots_mask = new_mask;
}
/* --- Public API --- */
void nn_symbol_init(uint32_t initial_capacity)
{
uint32_t cap = NN_SYMBOL_DEFAULT_CAPACITY;
if (initial_capacity > cap) {
/* Round up to power of two. */
cap = 1;
while (cap < initial_capacity) cap *= 2;
}
memset(&g_sym, 0, sizeof(g_sym));
/* Entry array (1-indexed, so allocate cap+1 but we just use cap). */
g_sym.entries_cap = cap;
g_sym.entries = (nn_symbol_entry_t *)calloc(
(size_t)cap, sizeof(nn_symbol_entry_t));
if (!g_sym.entries) { fprintf(stderr, "nn_symbol_init: entries alloc failed\n"); abort(); }
g_sym.count = 0;
/* Hash table at 2x entries for low load factor. */
g_sym.slots_cap = cap * 2;
g_sym.slots_mask = g_sym.slots_cap - 1;
g_sym.slots = (nn_slot_t *)calloc(
(size_t)g_sym.slots_cap, sizeof(nn_slot_t));
if (!g_sym.slots) { fprintf(stderr, "nn_symbol_init: slots alloc failed\n"); abort(); }
/* String arena. */
g_sym.arena_cap = NN_SYMBOL_ARENA_INITIAL;
g_sym.arena = (char *)malloc(g_sym.arena_cap);
if (!g_sym.arena) { fprintf(stderr, "nn_symbol_init: arena alloc failed\n"); abort(); }
g_sym.arena_used = 0;
}
void nn_symbol_destroy(void)
{
free(g_sym.entries);
free(g_sym.slots);
free(g_sym.arena);
memset(&g_sym, 0, sizeof(g_sym));
}
nn_symbol_t nn_symbol_intern(const char *str, size_t len)
{
uint32_t hash = fnv1a(str, len);
uint32_t idx = hash & g_sym.slots_mask;
/* Probe for existing entry. */
for (;;) {
uint32_t id = g_sym.slots[idx].id;
if (id == 0) break; /* empty slot - not found */
if (g_sym.slots[idx].hash == hash) {
nn_symbol_entry_t *e = &g_sym.entries[id];
if (e->len == (uint32_t)len &&
memcmp(g_sym.arena + e->offset, str, len) == 0) {
return (nn_symbol_t)id;
}
}
idx = (idx + 1) & g_sym.slots_mask;
}
/* Not found - insert new symbol. */
entries_ensure();
g_sym.count++;
uint32_t new_id = g_sym.count; /* 1-based */
uint32_t offset = arena_push(str, len);
g_sym.entries[new_id].offset = offset;
g_sym.entries[new_id].len = (uint32_t)len;
g_sym.entries[new_id].hash = hash;
g_sym.slots[idx].hash = hash;
g_sym.slots[idx].id = new_id;
/* Grow hash table if load exceeds threshold. */
if ((uint64_t)g_sym.count * 100 >= (uint64_t)g_sym.slots_cap * NN_SYMBOL_LOAD_PERCENT) {
slots_grow();
}
return (nn_symbol_t)new_id;
}
const char *nn_symbol_text(nn_symbol_t sym)
{
if (sym == NN_SYMBOL_INVALID || sym > g_sym.count) return NULL;
return g_sym.arena + g_sym.entries[sym].offset;
}
size_t nn_symbol_len(nn_symbol_t sym)
{
if (sym == NN_SYMBOL_INVALID || sym > g_sym.count) return 0;
return (size_t)g_sym.entries[sym].len;
}
uint32_t nn_symbol_count(void)
{
return g_sym.count;
}