refactor: address interning review feedback

This commit is contained in:
2026-10-25 00:30:39 +02:00
parent 675fa5049f
commit 678bb2583b
4 changed files with 209 additions and 35 deletions
+112 -16
View File
@@ -1,4 +1,5 @@
import "@std/mem"
import "@std/meta"
PutError :: enum { key_exists }
@@ -13,10 +14,11 @@ Entry proc($K, $V type) type {
#! owns entry storage and retains its allocator; release storage with deinit.
#! keys and values are stored by value; any resources they reference remain caller-owned.
#! null callbacks select automatic operations; equal keys must always produce equal hashes.
HashMap proc(
$K, $V type,
$hash_key proc(key K) usize,
$keys_eql proc(a, b K) bool,
$hash_key ?proc(key K) usize,
$keys_eql ?proc(a, b K) bool,
) type {
return struct {
entries []mut Entry(K, V)
@@ -29,11 +31,18 @@ StringHashMap proc($V type) type {
return HashMap([]u8, V, str_hash, str_eql)
}
#! automatic value hashing for integers, booleans, enums, distinct types, arrays and records.
#! slices and floating-point keys require explicit hash/equality operations through HashMap.
#! record padding is ignored; pointer, union and optional keys are not supported.
AutoHashMap proc($K, $V type) type {
return HashMap(K, V, null, null)
}
#! retain `allocator` for later growth and cleanup; this call does not allocate.
init proc(
$K, $V type,
$hash_key proc(key K) usize,
$keys_eql proc(a, b K) bool,
$hash_key ?proc(key K) usize,
$keys_eql ?proc(a, b K) bool,
allocator mem.Allocator,
) HashMap(K, V, hash_key, keys_eql) {
return HashMap(K, V, hash_key, keys_eql){
@@ -47,29 +56,38 @@ init proc(
#! note: this operation invalidates the map.
deinit proc(
$K, $V type,
$hash_key proc(key K) usize,
$keys_eql proc(a, b K) bool,
$hash_key ?proc(key K) usize,
$keys_eql ?proc(a, b K) bool,
map @HashMap(K, V, hash_key, keys_eql),
) void { mem.free(map.allocator, map.entries) }
get proc(
$K, $V type,
$hash_key proc(key K) usize,
$keys_eql proc(a, b K) bool,
$hash_key ?proc(key K) usize,
$keys_eql ?proc(a, b K) bool,
map @HashMap(K, V, hash_key, keys_eql),
key K,
) ?V {
if (map.count == 0) return null
hash :: normalize(hash_key(key))
hash usize := 0
if $(hash_key == null) {
hash = normalize(auto_hash(K, key))
} else {
hash = normalize(call_hash(K, hash_key?, key))
}
idx := hash & (map.entries.len - 1)
while true {
entry :: map.entries[idx]
if (entry.hash == 0) return null
if (entry.hash == hash and keys_eql(entry.key, key)) {
return entry.value
if (entry.hash == hash) {
if $(keys_eql == null) {
if (auto_eql(K, entry.key, key)) return entry.value
} else {
if (call_eql(K, keys_eql?, entry.key, key)) return entry.value
}
}
idx = (idx + 1) & (map.entries.len - 1)
@@ -78,8 +96,8 @@ get proc(
put proc(
$K, $V type,
$hash_key proc(key K) usize,
$keys_eql proc(a, b K) bool,
$hash_key ?proc(key K) usize,
$keys_eql ?proc(a, b K) bool,
map @mut HashMap(K, V, hash_key, keys_eql),
key K,
value V,
@@ -107,7 +125,12 @@ put proc(
mem.free(map.allocator, old_entries)
}
hash :: normalize(hash_key(key))
hash usize := 0
if $(hash_key == null) {
hash = normalize(auto_hash(K, key))
} else {
hash = normalize(call_hash(K, hash_key?, key))
}
idx := hash & (map.entries.len - 1)
while true {
@@ -122,14 +145,29 @@ put proc(
return
}
if entry.hash == hash and keys_eql(entry.key, key) {
return .key_exists
if (entry.hash == hash) {
if $(keys_eql == null) {
if (auto_eql(K, entry.key, key)) return .key_exists
} else {
if (call_eql(K, keys_eql?, entry.key, key)) return .key_exists
}
}
idx = (idx + 1) & (map.entries.len - 1)
}
}
# concrete comptime callbacks use named calls so their specializations are registered.
@hide:file
call_hash proc($K type, $callback proc(key K) usize, key K) usize {
return callback(key)
}
@hide:file
call_eql proc($K type, $callback proc(a, b K) bool, a, b K) bool {
return callback(a, b)
}
@hide:file
normalize proc(hash usize) usize {
# mapping both 0 and 1 to 1 is safe because equality resolves
@@ -155,3 +193,61 @@ str_hash proc(key []u8) usize {
@hide:file
str_eql proc(a, b []u8) bool { return mem.eql(a, b) }
@hide:file
auto_hash proc($K type, key K) usize {
match typeinfo!(K) {
.integer: {
bytes @[sizeof!(K)]u8 :: ptrcast!(&key)
return str_hash(bytes[..])
}
.bool: return if (key) 1 else 0
.enum |info|: {
inline for (info.fields) |field| {
if (key == field!(K, field.name)) return field.index
}
unreachable
}
.distinct |backing|: return auto_hash(ptrcast!(@backing, &key)^)
.array: {
hash usize := 0
for (key) |element| hash = combine_hash(hash, auto_hash(element))
return hash
}
.record |info|: {
hash usize := 0
inline for (info.fields) |field| {
hash = combine_hash(hash, auto_hash(field!(key, field.name)))
}
return hash
}
else: compile_error!("AutoHashMap key requires integers, booleans, enums, distinct types, arrays or records; use HashMap for custom hashing")
}
}
@hide:file
combine_hash proc(left, right usize) usize {
# rotate before mixing so ordered fields do not cancel like a plain xor.
return ((left << 5) xor (left >> (sizeof!(usize) * 8 - 5))) xor right
}
@hide:file
auto_eql proc($K type, left, right K) bool {
match typeinfo!(K) {
.integer, .bool, .enum: return left == right
.distinct |backing|: return auto_eql(ptrcast!(@backing, &left)^, ptrcast!(@backing, &right)^)
.array: {
for (left) |element, index| {
if (!auto_eql(element, right[index])) return false
}
return true
}
.record |info|: {
inline for (info.fields) |field| {
if (!auto_eql(field!(left, field.name), field!(right, field.name))) return false
}
return true
}
else: compile_error!("AutoHashMap key requires integers, booleans, enums, distinct types, arrays or records; use HashMap for custom equality")
}
}
+75
View File
@@ -11,3 +11,78 @@ handles_put_and_get test {
try testing.expect_type(?u32, value)
try testing.expect_equal(42, value?)
}
@hide:file
collision_hash proc(_ u32) usize { return 0 }
@hide:file
collision_eql proc(left, right u32) bool {
return rem!(left, 100) == rem!(right, 100)
}
custom_callbacks_preserve_collisions_and_equivalent_keys test {
map HashMap(u32, u32, collision_hash, collision_eql) := init(mem.c_allocator)
defer deinit(&map)
for (u32(1)..u32(32)) |key| try put(&map, key, key * 2)
for (u32(1)..u32(32)) |key| {
try testing.expect_equal(key * 2, get(&map, key + 100)?)
}
try testing.expect_equal(null, get(&map, 99))
duplicate := false
put(&map, 101, 999) catch |error| { duplicate = error == .key_exists }
try testing.expect(duplicate)
try testing.expect_equal(u32(31), u32(map.count))
try testing.expect_equal(u32(2), get(&map, 1)?)
}
auto_integer_keys_preserve_boundaries_and_rehashing test {
map AutoHashMap(i64, usize) := init(mem.c_allocator)
defer deinit(&map)
keys [_]i64 :: [minval!(i64), -1, 0, 1, maxval!(i64)]
for (keys) |key, index| try put(&map, key, index)
for (i64(2)..i64(100)) |key| try put(&map, key, usize(key))
for (keys) |key, index| try testing.expect_equal(index, get(&map, key)?)
for (i64(2)..i64(100)) |key| try testing.expect_equal(usize(key), get(&map, key)?)
try testing.expect_equal(null, get(&map, -2))
duplicate := false
put(&map, -1, 999) catch |error| { duplicate = error == .key_exists }
try testing.expect(duplicate)
try testing.expect_equal(usize(1), get(&map, -1)?)
wide AutoHashMap(u64, bool) := init(mem.c_allocator)
defer deinit(&wide)
try put(&wide, maxval!(u64), true)
try testing.expect_equal(true, get(&wide, maxval!(u64))?)
}
@hide:file
TestKeyKind :: enum(u8) { first = 3, second = 91 }
@hide:file
TestKeyId :: distinct u32
@hide:file
TestKey :: struct {
enabled bool
id TestKeyId
kind TestKeyKind
indices [2]u16
}
auto_record_keys_use_field_values_and_array_order test {
map AutoHashMap(TestKey, i32) := init(mem.c_allocator)
defer deinit(&map)
first :: TestKey{ enabled = true, id = TestKeyId(17), kind = .first, indices = [2, 9] }
reversed :: TestKey{ enabled = true, id = TestKeyId(17), kind = .first, indices = [9, 2] }
second :: TestKey{ enabled = false, id = TestKeyId(18), kind = .second, indices = [2, 9] }
try put(&map, first, 42)
try put(&map, reversed, 51)
try put(&map, second, 63)
try testing.expect_equal(i32(42), get(&map, TestKey{
enabled = true, id = TestKeyId(17), kind = .first, indices = [2, 9],
})?)
try testing.expect_equal(i32(51), get(&map, reversed)?)
try testing.expect_equal(i32(63), get(&map, second)?)
try testing.expect_equal(null, get(&map, TestKey{
enabled = true, id = TestKeyId(18), kind = .first, indices = [2, 9],
}))
}