#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); 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.count -= 1; 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); }); } }