189 lines
4.7 KiB
Text
189 lines
4.7 KiB
Text
// Dead simple key-value pair type (aka. hash table or hash map)
|
|
Table :: 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
|
|
}
|
|
|
|
get :: (t: *Table, key: t.Key) -> t.Value, bool {
|
|
slot, ok := find_slot(t, get_hash(t, key));
|
|
if !ok {
|
|
return mem.zero_of(t.Value), false;
|
|
}
|
|
|
|
return slot.value, true;
|
|
}
|
|
|
|
set :: (t: *Table, key: t.Key, value: t.Value) {
|
|
hash := get_hash(t, key);
|
|
slot, exists := find_slot(t, hash);
|
|
if !exists {
|
|
slot = create_or_reuse_slot(t);
|
|
slot.hash = hash;
|
|
}
|
|
|
|
slot.key = key;
|
|
slot.value = value;
|
|
}
|
|
|
|
exists :: (t: *Table, key: t.Key) -> bool {
|
|
_, exists := find_slot(t, get_hash(t, key));
|
|
return exists;
|
|
}
|
|
|
|
// @note(judah): we use 'delete' instead of 'remove' because it's a keyword...
|
|
delete :: (t: *Table, key: t.Key) -> t.Value, bool {
|
|
slot, ok, idx := find_slot(t, get_hash(t, key));
|
|
if !ok return mem.zero_of(t.Value), false;
|
|
|
|
last_value := slot.value;
|
|
mark_slot_for_reuse(t, idx);
|
|
|
|
return last_value, true;
|
|
}
|
|
|
|
reset :: (t: *Table) {
|
|
t.count = 0;
|
|
t.slots.count = 0;
|
|
t.free_slots.count = 0;
|
|
}
|
|
|
|
for_expansion :: (t: *Table, body: Code, flags: For_Flags) #expand {
|
|
#assert (flags & .POINTER == 0) "cannot iterate by pointer";
|
|
for <=(flags & .REVERSE == .REVERSE) slot: t.slots if slot.hash != t.invalid_hash {
|
|
`it := slot.value;
|
|
`it_index := slot.key;
|
|
#insert,scope(body)(break = break slot) body;
|
|
}
|
|
}
|
|
|
|
|
|
#scope_file;
|
|
|
|
get_hash :: inline (t: *Table, key: t.Key) -> u32 {
|
|
hash := t.hash_proc(key);
|
|
basic.assert(hash != t.invalid_hash, "key % collided with invalid hash marker (%)", key, t.invalid_hash);
|
|
return hash;
|
|
}
|
|
|
|
find_slot :: (t: *Table, hash: u32) -> *t.Slot, bool, int {
|
|
for * t.slots if it.hash == hash {
|
|
return it, true, it_index;
|
|
}
|
|
|
|
return null, false, -1;
|
|
}
|
|
|
|
create_or_reuse_slot :: (t: *Table) -> *t.Slot {
|
|
inline try_lazy_init(t);
|
|
|
|
if t.free_slots.count > 0 {
|
|
slot_idx := t.free_slots[t.free_slots.count - 1];
|
|
t.free_slots.count -= 1;
|
|
return *t.slots[slot_idx];
|
|
}
|
|
|
|
if t.slots.allocated == 0 {
|
|
array.resize(*t.slots, t.number_of_items_to_allocate_initially);
|
|
}
|
|
else if t.slots.count >= t.slots.allocated {
|
|
array.resize(*t.slots, mem.next_power_of_two(t.slots.allocated));
|
|
}
|
|
|
|
slot := array.append(*t.slots);
|
|
t.count = t.slots.count;
|
|
return slot;
|
|
}
|
|
|
|
mark_slot_for_reuse :: (t: *Table, index: int) {
|
|
inline try_lazy_init(t);
|
|
|
|
t.count -= 1;
|
|
t.slots[index] = .{ hash = t.invalid_hash };
|
|
|
|
array.append(*t.free_slots, index);
|
|
}
|
|
|
|
try_lazy_init :: inline (t: *Table) {
|
|
mem.lazy_set_allocator(t);
|
|
mem.lazy_set_allocator(*t.slots);
|
|
mem.lazy_set_allocator(*t.free_slots);
|
|
}
|
|
|
|
mem :: #import "jc/memory";
|
|
array :: #import "jc/array";
|
|
hash :: #import "jc/hash";
|
|
|
|
basic :: #import "Basic"; // @future
|
|
|
|
|
|
// ----------------------------------------------------------
|
|
// TESTS
|
|
// ----------------------------------------------------------
|
|
|
|
#if #exists(RUN_TESTS) #run {
|
|
test :: #import "jc/meta/test";
|
|
|
|
test.run("basic operations", t => {
|
|
ITERATIONS :: 64;
|
|
|
|
values: Table(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 := delete(*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: Table(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);
|
|
|
|
// deleting something that doesn't exist should do nothing
|
|
_, ok := delete(*values, 0);
|
|
test.expect(t, !ok);
|
|
test.expect(t, values.count == 3);
|
|
|
|
delete(*values, 2);
|
|
test.expect(t, values.count == 2);
|
|
});
|
|
|
|
test.run("iteration", t => {
|
|
values: Table(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);
|
|
});
|
|
}
|