From 77ae783dbb898676bf5fd304ab42f17a9a5408ef Mon Sep 17 00:00:00 2001 From: Judah Caruso Date: Tue, 27 May 2025 00:04:35 -0600 Subject: [PATCH] initial kv implementation' --- _run_all_tests.jai | 1 + array/dynamic_array.jai | 11 ++- kv/module.jai | 185 ++++++++++++++++++++++++++++++++++++++++ memory/module.jai | 14 +++ 4 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 kv/module.jai diff --git a/_run_all_tests.jai b/_run_all_tests.jai index 3d1179b..0a2110d 100644 --- a/_run_all_tests.jai +++ b/_run_all_tests.jai @@ -11,6 +11,7 @@ #import,file "./memory/module.jai"(RUN_TESTS = true); #import,file "./meta/module.jai"(RUN_TESTS = true); #import,file "./platform/module.jai"(RUN_TESTS = true); + #import,file "./kv/module.jai"(RUN_TESTS = true); rmath :: #import,file "./math/module.jai"(.radians, RUN_TESTS = true); dmath :: #import,file "./math/module.jai"(.degrees, RUN_TESTS = true); diff --git a/array/dynamic_array.jai b/array/dynamic_array.jai index 7d8758c..1f26563 100644 --- a/array/dynamic_array.jai +++ b/array/dynamic_array.jai @@ -1,19 +1,24 @@ // @todo(judah): replace array_add append :: inline (arr: *[..]$T, value: T) -> *T { - ptr := basic.array_add(arr); + ptr := basic.array_add(arr,, allocator = arr.allocator); ptr.* = value; return ptr; } append :: inline (arr: *[..]$T, values: ..T) -> *T { count := arr.count; - basic.array_add(arr, ..values); + basic.array_add(arr, ..values,, allocator = arr.allocator); return *arr.data[count]; } append :: inline (arr: *[..]$T) -> *T { - return basic.array_add(arr); + return basic.array_add(arr,, allocator = arr.allocator); +} + +resize :: inline (arr: *[..]$T, new_size: int) { + if new_size <= arr.allocated return; + basic.array_reserve(arr, new_size,, allocator = arr.allocator); } reset :: inline (arr: *[..]$T, $keep_memory := true) { diff --git a/kv/module.jai b/kv/module.jai new file mode 100644 index 0000000..ebe64fe --- /dev/null +++ b/kv/module.jai @@ -0,0 +1,185 @@ +#module_parameters(RUN_TESTS := false); + +// Dead simple key-value pair type (aka. hash table or hash map) +Kv :: struct(Key: Type, Value: Type) { + allocator: Allocator; + slots: [..]Slot; + free_slots: [..]int; + count: int; + + Slot :: struct { + hash: u32 = invalid_hash; + key: Key = ---; + value: Value = ---; + } + + hash_proc :: hash.murmur32; + invalid_hash :: (0x8000_dead).(u32); // @note(judah): I'm curious what values would hit this hash on accident + number_of_items_to_allocate_initially :: 16; // @note(judah): must be a power of two +} + +init :: (kv: *Kv, allocator: Allocator) { + kv.allocator = allocator; + kv.slots.allocator = allocator; + kv.free_slots.allocator = allocator; +} + +get :: (kv: *Kv, key: kv.Key) -> kv.Value, bool { + slot, ok := find_slot(kv, kv.hash_proc(key)); + if !ok { + return mem.zero_of(kv.Value), false; + } + + return slot.value, true; +} + +set :: (kv: *Kv, key: kv.Key, value: kv.Value) { + hash := kv.hash_proc(key); + slot, exists := find_slot(kv, hash); + if !exists { + slot = create_or_reuse_slot(kv); + slot.hash = hash; + } + + slot.key = key; + slot.value = value; +} + +// @note(judah): we use 'evict' instead of 'remove' because it's a keyword... +evict :: (kv: *Kv, key: kv.Key) -> kv.Value, bool { + slot, ok, idx := find_slot(kv, kv.hash_proc(key)); + if !ok return mem.zero_of(kv.Value), false; + + last_value := slot.value; + mark_slot_for_reuse(kv, idx); + kv.count -= 1; + + return last_value, true; +} + +reset :: (kv: *Kv) { + kv.count = 0; + kv.slots.count = 0; + kv.free_slots.count = 0; +} + +for_expansion :: (kv: *Kv, body: Code, flags: For_Flags) #expand { + #assert (flags & .POINTER == 0) "cannot iterate by pointer"; + for <=(flags & .REVERSE == .REVERSE) slot: kv.slots if slot.hash != kv.invalid_hash { + `it := slot.value; + `it_index := slot.key; + #insert,scope(body)(break = break slot) body; + } +} + + +#scope_file; + +find_slot :: (kv: *Kv, hash: u32) -> *kv.Slot, bool, int { + for * kv.slots if it.hash == hash { + return it, true, it_index; + } + + return null, false, -1; +} + +create_or_reuse_slot :: (kv: *Kv) -> *kv.Slot { + inline try_lazy_init(kv); + + if kv.free_slots.count > 0 { + slot_idx := kv.free_slots[kv.free_slots.count - 1]; + kv.free_slots.count -= 1; + return *kv.slots[slot_idx]; + } + + if kv.slots.allocated == 0 { + array.resize(*kv.slots, kv.number_of_items_to_allocate_initially); + } + else if kv.slots.count >= kv.slots.allocated { + array.resize(*kv.slots, mem.next_power_of_two(kv.slots.allocated)); + } + + slot := array.append(*kv.slots); + kv.count = kv.slots.count; + return slot; +} + +mark_slot_for_reuse :: (kv: *Kv, index: int) { + inline try_lazy_init(kv); + + kv.slots[index] = .{ hash = kv.invalid_hash }; + array.append(*kv.free_slots, index); +} + +try_lazy_init :: inline (kv: *Kv) { + if kv.allocator.proc == null { + init(kv, context.allocator); + } +} + +mem :: #import "jc/memory"; +array :: #import "jc/array"; +hash :: #import "jc/hash"; + + +// ---------------------------------------------------------- +// TESTS +// ---------------------------------------------------------- + +#if RUN_TESTS { + test :: #import "jc/test"; + + #run { + test.run("basic operations", t => { + ITERATIONS :: 64; + + values: Kv(int, int); + for 0..ITERATIONS { + set(*values, it, it * it); + } + + for 0..ITERATIONS { + v, ok := get(*values, it); + test.expect(t, v == it * it); + } + + for 0..ITERATIONS if it % 2 == 0 { + _, ok := evict(*values, it); + test.expect(t, ok); + } + + for 0..ITERATIONS if it % 2 == 0 { + _, ok := get(*values, it); + test.expect(t, !ok); + } + }); + + test.run("free slots", t => { + values: Kv(int, int); + + set(*values, 1, 100); + set(*values, 2, 200); + set(*values, 3, 300); + test.expect(t, values.count == 3); + test.expect(t, values.slots.allocated == values.number_of_items_to_allocate_initially); + + // evicting something that doesn't exist should do nothing + _, ok := evict(*values, 0); + test.expect(t, !ok); + test.expect(t, values.count == 3); + + evict(*values, 2); + test.expect(t, values.count == 2); + }); + + test.run("iteration", t => { + values: Kv(int, int); + + for 0..10 set(*values, it, it * it); + test.expect(t, values.count == 11); + + for v, k: values test.expect(t, v == k * k); + for < v, k: values test.expect(t, v == k * k); + }); + } +} diff --git a/memory/module.jai b/memory/module.jai index 0317b37..a488f0d 100644 --- a/memory/module.jai +++ b/memory/module.jai @@ -34,6 +34,20 @@ power_of_two :: (x: int) -> bool { return x & (x - 1) == 0; } +next_power_of_two :: (x: int) -> int #no_aoc { + basic.assert(power_of_two(x), "value (%) must be a power of two", x); + + // Bit twiddling hacks next power of two + x |= x >> 1; + x |= x >> 2; + x |= x >> 4; + x |= x >> 8; + x |= x >> 16; + x |= x >> 32; + + return x + 1; +} + align_to :: (ptr: int, align: int = Default_Align) -> int { basic.assert(power_of_two(align), "alignment must be a power of two");