jc/+internal/map.jai
2025-09-07 15:25:57 -06:00

176 lines
4 KiB
Text

// Dead simple key-value pair type (aka. hash table or hash map)
Map :: struct(Key: Type, Value: Type) {
allocator: Allocator;
slots: [..]Slot;
free_slots: [..]int;
count: int;
Slot :: struct {
hash: u32 = InvalidHash;
key: Key = ---;
value: Value = ---;
}
HashProc :: Murmur32;
InvalidHash :: (0x8000_dead).(u32); // @note(judah): I'm curious what values would hit this hash on accident
AllocatedItemsAtStart :: 16;
}
MapGet :: (r: *Map, key: r.Key) -> r.Value, bool {
slot, ok := FindSlot(r, GetHash(r, key));
if !ok
{ return zero_of(r.Value), false; }
return slot.value, true;
}
MapSet :: (r: *Map, key: r.Key, value: r.Value) {
hash := GetHash(r, key);
slot, exists := FindSlot(r, hash);
if !exists {
slot = CreateOrReuseSlot(r);
slot.hash = hash;
}
slot.key = key;
slot.value = value;
}
MapHas :: (r: *Map, key: r.Key) -> bool {
_, exists := FindSlot(r, GetHash(r, key));
return exists;
}
MapRemove :: (r: *Map, key: r.Key) -> bool, r.Value {
slot, ok, idx := FindSlot(r, GetHash(r, key));
if !ok
{ return false, zero_of(r.Value); }
last_value := slot.value;
MarkSlotForReuse(r, idx);
return true, last_value;
}
MapReset :: (t: *Map) {
t.count = 0;
t.slots.count = 0;
t.free_slots.count = 0;
}
for_expansion :: (r: *Map, body: Code, flags: For_Flags) #expand {
#assert (flags & .POINTER == 0) "cannot iterate by pointer";
for <=(flags & .REVERSE == .REVERSE) slot: r.slots if slot.hash != r.InvalidHash {
`it := slot.value;
`it_index := slot.key;
#insert,scope(body)(break = break slot) body;
}
}
#scope_file
GetHash :: inline (r: *Map, key: r.Key) -> u32 {
hash := r.HashProc(key);
Assert(hash != r.InvalidHash, "key collided with invalid hash");
return hash;
}
FindSlot :: (r: *Map, hash: u32) -> *r.Slot, bool, int {
for * r.slots if it.hash == hash {
return it, true, it_index;
}
return null, false, -1;
}
CreateOrReuseSlot :: (r: *Map) -> *r.Slot {
inline LazyInit(r);
if r.free_slots.count > 0 {
slot_idx := r.free_slots[r.free_slots.count - 1];
r.free_slots.count -= 1;
return *r.slots[slot_idx];
}
if r.slots.allocated == 0 {
ArrayGrow(*r.slots, r.AllocatedItemsAtStart);
}
else if r.slots.count >= r.slots.allocated {
ArrayGrow(*r.slots, NextPowerOfTwo(r.slots.allocated));
}
slot := ArrayAppend(*r.slots);
r.count = r.slots.count;
return slot;
}
MarkSlotForReuse :: (t: *Map, index: int) {
inline LazyInit(t);
t.count -= 1;
t.slots[index] = .{ hash = t.InvalidHash };
ArrayAppend(*t.free_slots, index);
}
LazyInit :: inline (t: *Map) {
TrySetAllocator(t);
TrySetAllocator(*t.slots);
TrySetAllocator(*t.free_slots);
}
#if RunTests #run {
Test("map:basic operations", t => {
ITERATIONS :: 64;
values: Map(int, int);
for 0..ITERATIONS {
MapSet(*values, it, it * it);
}
for 0..ITERATIONS {
v, ok := MapGet(*values, it);
Expect(v == it * it);
}
for 0..ITERATIONS if it % 2 == 0 {
ok := MapRemove(*values, it);
Expect(ok);
}
for 0..ITERATIONS if it % 2 == 0 {
_, ok := MapGet(*values, it);
Expect(!ok);
}
});
Test("map:free slots", t => {
values: Map(int, int);
MapSet(*values, 1, 100);
MapSet(*values, 2, 200);
MapSet(*values, 3, 300);
Expect(values.count == 3);
Expect(values.slots.allocated == values.AllocatedItemsAtStart);
// deleting something that doesn't exist should do nothing
ok := MapRemove(*values, 0);
Expect(!ok);
Expect(values.count == 3);
MapRemove(*values, 2);
Expect(values.count == 2);
});
Test("map:iteration", t => {
values: Map(int, int);
for 0..10 MapSet(*values, it, it * it);
Expect(values.count == 11);
for v, k: values Expect(v == k * k);
for < v, k: values Expect(v == k * k);
});
}