simple hashmap implementation
This commit is contained in:
@@ -4,6 +4,7 @@ import "@std/mem"
|
|||||||
import "@std/arraylist"
|
import "@std/arraylist"
|
||||||
|
|
||||||
test import "@std/arraylist"
|
test import "@std/arraylist"
|
||||||
|
test import "@std/hashmap"
|
||||||
|
|
||||||
program ::
|
program ::
|
||||||
`# these are immutable
|
`# these are immutable
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import "@std/mem"
|
||||||
|
|
||||||
|
PutError :: enum { key_exists }
|
||||||
|
|
||||||
|
Entry func($K, $V type) type {
|
||||||
|
return struct {
|
||||||
|
# hash = 0 means empty
|
||||||
|
hash usize = 0
|
||||||
|
key K
|
||||||
|
value V
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HashMap func(
|
||||||
|
$K, $V type,
|
||||||
|
$hash_key func(key K) usize,
|
||||||
|
$keys_eql func(a, b K) bool,
|
||||||
|
) type {
|
||||||
|
return struct {
|
||||||
|
entries []mut Entry(K, V)
|
||||||
|
count usize
|
||||||
|
allocator mem.Allocator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StringHashMap func($V type) type {
|
||||||
|
return HashMap([]u8, V, str_hash, str_eql)
|
||||||
|
}
|
||||||
|
|
||||||
|
init func(
|
||||||
|
$K, $V type,
|
||||||
|
$hash_key func(key K) usize,
|
||||||
|
$keys_eql func(a, b K) bool,
|
||||||
|
allocator mem.Allocator,
|
||||||
|
) HashMap(K, V, hash_key, keys_eql) {
|
||||||
|
return HashMap(K, V, hash_key, keys_eql){
|
||||||
|
entries = mem.empty(Entry(K, V)),
|
||||||
|
count = 0,
|
||||||
|
allocator = allocator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# frees the entries in the hash map and invalidates it.
|
||||||
|
deinit func(
|
||||||
|
$K, $V type,
|
||||||
|
$hash_key func(key K) usize,
|
||||||
|
$keys_eql func(a, b K) bool,
|
||||||
|
map @HashMap(K, V, hash_key, keys_eql),
|
||||||
|
) void { mem.free(map.allocator, map.entries) }
|
||||||
|
|
||||||
|
get func(
|
||||||
|
$K, $V type,
|
||||||
|
$hash_key func(key K) usize,
|
||||||
|
$keys_eql func(a, b K) bool,
|
||||||
|
map @HashMap(K, V, hash_key, keys_eql),
|
||||||
|
key K,
|
||||||
|
) ?V {
|
||||||
|
if (map.count == 0) return none
|
||||||
|
|
||||||
|
hash :: normalize(hash_key(key))
|
||||||
|
idx usize = hash & (map.entries.len - 1)
|
||||||
|
|
||||||
|
while true {
|
||||||
|
entry :: map.entries[idx]
|
||||||
|
if (entry.hash == 0) return none
|
||||||
|
|
||||||
|
if (entry.hash == hash and keys_eql(entry.key, key)) {
|
||||||
|
return entry.value
|
||||||
|
}
|
||||||
|
|
||||||
|
idx = (idx + 1) & (map.entries.len - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
put func(
|
||||||
|
$K, $V type,
|
||||||
|
$hash_key func(key K) usize,
|
||||||
|
$keys_eql func(a, b K) bool,
|
||||||
|
map @mut HashMap(K, V, hash_key, keys_eql),
|
||||||
|
key K,
|
||||||
|
value V,
|
||||||
|
) void ! (PutError | mem.AllocError) {
|
||||||
|
threshold :: map.entries.len - divtrunc!(map.entries.len, 4)
|
||||||
|
if (map.entries.len == 0 or map.count + 1 > threshold) {
|
||||||
|
# grow entries array
|
||||||
|
old_entries :: map.entries
|
||||||
|
new_size :: if (old_entries.len > 0) old_entries.len * 2 else 8
|
||||||
|
new_entries :: try mem.alloc(Entry(K, V), map.allocator, new_size)
|
||||||
|
|
||||||
|
# zero new entries
|
||||||
|
for 0..new_entries.len |i| {
|
||||||
|
new_entries[i].hash = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# move old entries
|
||||||
|
for old_entries |entry| {
|
||||||
|
if (entry.hash == 0) continue
|
||||||
|
|
||||||
|
# find an empty slot
|
||||||
|
idx usize = entry.hash & (new_entries.len - 1)
|
||||||
|
while new_entries[idx].hash != 0 {
|
||||||
|
idx = (idx + 1) & (new_entries.len - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
new_entries[idx] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
map.entries = new_entries
|
||||||
|
mem.free(map.allocator, old_entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
hash :: normalize(hash_key(key))
|
||||||
|
idx usize = hash & (map.entries.len - 1)
|
||||||
|
|
||||||
|
while true {
|
||||||
|
entry :: map.entries[idx]
|
||||||
|
if entry.hash == 0 {
|
||||||
|
map.entries[idx] = Entry(K, V){
|
||||||
|
hash = hash,
|
||||||
|
key = key,
|
||||||
|
value = value,
|
||||||
|
}
|
||||||
|
map.count += 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.hash == hash and keys_eql(entry.key, key)) {
|
||||||
|
return .key_exists
|
||||||
|
}
|
||||||
|
|
||||||
|
idx = (idx + 1) & (map.entries.len - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hide normalize func(hash usize) usize {
|
||||||
|
# mapping both 0 and 1 to 1 is safe because equality resolves
|
||||||
|
# collisions (since hash and key must both be equal).
|
||||||
|
if (hash == 0) return 1
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
|
#! FNV-1a hash implementation.
|
||||||
|
#! note: vulnerable to collision attacks.
|
||||||
|
hide str_hash func(key []u8) usize {
|
||||||
|
hash u32 = 2166136261 # offset basis
|
||||||
|
prime u32 = 16777619
|
||||||
|
|
||||||
|
for key |byte| {
|
||||||
|
product u64 :: u64(hash xor u32(byte)) * prime
|
||||||
|
hash = u32(product & u64(maxval!(u32)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return usize(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
hide str_eql func(a, b []u8) bool {
|
||||||
|
return mem.eql(a, b)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import "@std/mem"
|
||||||
|
import "@std/testing"
|
||||||
|
|
||||||
|
handles_put_and_get test {
|
||||||
|
map StringHashMap(u32) = init(mem.c_allocator)
|
||||||
|
defer deinit(&map)
|
||||||
|
|
||||||
|
try put(&map, "key", 42)
|
||||||
|
value :: get(&map, "key")
|
||||||
|
|
||||||
|
try testing.expect_type(?u32, value)
|
||||||
|
try testing.expect_equal(42, value?)
|
||||||
|
}
|
||||||
+21
-11
@@ -41,17 +41,7 @@ eql func($T type, left, right []T) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
hide empty_storage [1]mut u64 = [0]
|
# allocate memory for a slice of type `T` with `count` elements.
|
||||||
|
|
||||||
hide empty_slice func($T type, count usize) []mut T {
|
|
||||||
pointer *mut T :: ptrcast!(T, (&empty_storage).ptr)
|
|
||||||
return pointer[..count]
|
|
||||||
}
|
|
||||||
|
|
||||||
empty func($T type) []mut T {
|
|
||||||
return empty_slice(T, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
alloc func($T type, allocator Allocator, count usize) []mut T ! AllocError {
|
alloc func($T type, allocator Allocator, count usize) []mut T ! AllocError {
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
return empty_slice(T, 0)
|
return empty_slice(T, 0)
|
||||||
@@ -73,10 +63,14 @@ alloc func($T type, allocator Allocator, count usize) []mut T ! AllocError {
|
|||||||
return .out_of_memory
|
return .out_of_memory
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# reallocate memory for a slice of type `T` with `new_count` elements.
|
||||||
|
# reallocating with `new_count == 0` will free the memory and return an empty slice.
|
||||||
|
# note: memory must be reallocated with the same allocator that was used to allocate it.
|
||||||
realloc func($T type, allocator Allocator, memory []mut T, new_count usize) []mut T ! AllocError {
|
realloc func($T type, allocator Allocator, memory []mut T, new_count usize) []mut T ! AllocError {
|
||||||
if new_count == memory.len {
|
if new_count == memory.len {
|
||||||
return memory
|
return memory
|
||||||
}
|
}
|
||||||
|
|
||||||
if new_count == 0 {
|
if new_count == 0 {
|
||||||
free(allocator, memory)
|
free(allocator, memory)
|
||||||
return empty_slice(T, 0)
|
return empty_slice(T, 0)
|
||||||
@@ -107,15 +101,31 @@ realloc func($T type, allocator Allocator, memory []mut T, new_count usize) []mu
|
|||||||
pointer *mut T :: ptrcast!(T, bytes)
|
pointer *mut T :: ptrcast!(T, bytes)
|
||||||
return pointer[..new_count]
|
return pointer[..new_count]
|
||||||
}
|
}
|
||||||
|
|
||||||
return .out_of_memory
|
return .out_of_memory
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# free memory allocated for a slice of type `T`.
|
||||||
|
# note: memory must be freed with the same allocator that was used to allocate it.
|
||||||
free func($T type, allocator Allocator, memory []mut T) void {
|
free func($T type, allocator Allocator, memory []mut T) void {
|
||||||
if memory.len != 0 and sizeof!(T) != 0 {
|
if memory.len != 0 and sizeof!(T) != 0 {
|
||||||
raw_free(allocator, ptrcast!(u8, memory.ptr), memory.len * sizeof!(T), alignof!(T))
|
raw_free(allocator, ptrcast!(u8, memory.ptr), memory.len * sizeof!(T), alignof!(T))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# get an empty slice of type `T` with `count` elements.
|
||||||
|
empty_slice func($T type, count usize) []mut T {
|
||||||
|
pointer *mut T :: ptrcast!(T, (&empty_storage).ptr)
|
||||||
|
return pointer[..count]
|
||||||
|
}
|
||||||
|
|
||||||
|
# get an empty slice of type `T` with 0 elements.
|
||||||
|
empty func($T type) []mut T {
|
||||||
|
return empty_slice(T, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
hide empty_storage [1]mut u64 = [0]
|
||||||
|
|
||||||
hide malloc_alignment usize :: 16 # ponytail: aarch64-macos libc malloc alignment assumption.
|
hide malloc_alignment usize :: 16 # ponytail: aarch64-macos libc malloc alignment assumption.
|
||||||
|
|
||||||
hide power_of_two func(value usize) bool {
|
hide power_of_two func(value usize) bool {
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ expect_equal func($T type, expected, actual T, location SourceLocation) void ! E
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expect_type func($Expected, $Actual type, _ Actual, location SourceLocation) void ! Error {
|
||||||
|
try expect($(Expected == Actual), location)
|
||||||
|
}
|
||||||
|
|
||||||
run func(name []u8, callback *func() void ! Error) bool {
|
run func(name []u8, callback *func() void ! Error) bool {
|
||||||
callback() catch |_| {
|
callback() catch |_| {
|
||||||
debug.print("{s} [failed]\n", {name,})
|
debug.print("{s} [failed]\n", {name,})
|
||||||
|
|||||||
Reference in New Issue
Block a user