From 396e6d0e882aebafedddc97f39eae3efd6561cce Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Tue, 16 Sep 2025 23:40:58 +0800 Subject: [PATCH] Add a generic hashmap implementation --- ds/map/map_fnv/del.ha | 3 +-- ds/map/map_fnv/finish.ha | 5 +---- ds/map/map_fnv/get.ha | 3 +-- ds/map/map_fnv/internal.ha | 2 +- ds/map/map_fnv/map.ha | 5 ++--- ds/map/map_fnv/new.ha | 43 ++++++++++++++----------------------------- ds/map/map_fnv/set.ha | 3 +-- ds/map/map_hashmap/README | 5 +++++ ds/map/map_hashmap/del.ha | 14 ++++++++++++++ ds/map/map_hashmap/finish.ha | 14 ++++++++++++++ ds/map/map_hashmap/get.ha | 14 ++++++++++++++ ds/map/map_hashmap/map.ha | 29 +++++++++++++++++++++++++++++ ds/map/map_hashmap/new.ha | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++ ds/map/map_hashmap/set.ha | 14 ++++++++++++++ ds/map/map_siphash/del.ha | 6 +----- ds/map/map_siphash/finish.ha | 6 ++---- ds/map/map_siphash/get.ha | 6 +----- ds/map/map_siphash/internal.ha | 15 +++++++++++++-- ds/map/map_siphash/map.ha | 5 ++--- ds/map/map_siphash/new.ha | 57 +++++++++++++++++++++++------------------------------ ds/map/map_siphash/set.ha | 6 +----- diff --git a/ds/map/map_fnv/del.ha b/ds/map/map_fnv/del.ha index fca01b176fca8d080c9bad231482c2c2eaecce04..b51ed1ad268bd33aa21d68873a13084d2bbfac34 100644 --- a/ds/map/map_fnv/del.ha +++ b/ds/map/map_fnv/del.ha @@ -6,6 +6,5 @@ use ds::map; // Deletes an item from a [[map]]. export fn del(m: *map, key: []u8) (*opaque | void) = { - let b = m.buckets[hash64(m, key) % m.n]; - return map::del(b, key); + return map::del(m.inner, key); }; diff --git a/ds/map/map_fnv/finish.ha b/ds/map/map_fnv/finish.ha index 42cb8681b9270e2554ad41e6214f6fe5ed0317a4..b4c9af8dca99f4d089d1833c08cd17e44d127343 100644 --- a/ds/map/map_fnv/finish.ha +++ b/ds/map/map_fnv/finish.ha @@ -6,9 +6,6 @@ use ds::map; // Frees resources associated with a [[map]]. export fn finish(m: *map) void = { - for (let i = 0z; i < m.n; i += 1) { - map::finish(m.buckets[i]); - }; - free(m.buckets); + map::finish(m.inner); free(m); }; diff --git a/ds/map/map_fnv/get.ha b/ds/map/map_fnv/get.ha index 097c3b764a6223e9d27b8716ac4d73a4a7c7e897..b205c35f1f2457745f859ad7a89523dfafb3ef15 100644 --- a/ds/map/map_fnv/get.ha +++ b/ds/map/map_fnv/get.ha @@ -6,6 +6,5 @@ use ds::map; // Gets an item from a [[map]] by key, returning void if not found. export fn get(m: *map, key: []u8) (*opaque | void) = { - let b = m.buckets[hash64(m, key) % m.n]; - return map::get(b, key); + return map::get(m.inner, key); }; diff --git a/ds/map/map_fnv/internal.ha b/ds/map/map_fnv/internal.ha index aff1fd56de99c7878a66500db903da55ff7fb0ac..662b45ca523f974cc1a45f7ec8935de4d3f75397 100644 --- a/ds/map/map_fnv/internal.ha +++ b/ds/map/map_fnv/internal.ha @@ -1,7 +1,7 @@ use hash; use hash::fnv; -fn hash64(m: *map, key: []u8) size = { +fn hash64(_params: nullable *opaque, key: []u8) size = { let h = fnv::fnv64a(); hash::write(&h, key); return fnv::sum64(&h): size; diff --git a/ds/map/map_fnv/map.ha b/ds/map/map_fnv/map.ha index d41a174ecd7e22a64fbcb0de9aa4fca5a2c7b95a..b8f595affaebacaa7895c6170229e2e2c713e3f0 100644 --- a/ds/map/map_fnv/map.ha +++ b/ds/map/map_fnv/map.ha @@ -4,14 +4,13 @@ // SPDX-FileCopyrightText: 2025 Runxi Yu use ds::map; -// A simple hash map from byte strings to opaque pointers, using SipHash for +// A simple hash map from byte strings to opaque pointers, using FNV for // hashing and fallback maps for collision resolution. // // You are advised to create these with [[new]]. export type map = struct { vt: map::map, - n: size, - buckets: []*map::map, + inner: *map::map, }; const _vt: map::vtable = map::vtable { diff --git a/ds/map/map_fnv/new.ha b/ds/map/map_fnv/new.ha index 32cac7d8d74fc1d15d996f561e7f41b7e6700b4c..44b3882e20ed60858438905d3e80454902a327b2 100644 --- a/ds/map/map_fnv/new.ha +++ b/ds/map/map_fnv/new.ha @@ -4,45 +4,30 @@ // SPDX-FileCopyrightText: 2025 Runxi Yu use errors; use ds::map; +use ds::map::map_hashmap; // Creates a new [[map]] with the given number of buckets. // make_fallback is a function that creates per-bucket fallback maps. -export fn new(make_fallback: *fn() (*map::map | nomem), n: size) (*map | errors::invalid | nomem) = { - if (n == 0) { +export fn new( + make_fallback: *fn() (*map::map | nomem), + n: size, +) (*map | errors::invalid | nomem) = { + let inner = match (map_hashmap::new(make_fallback, n, &hash64, null)) { + case let hm: *map_hashmap::map => + yield (hm: *map::map); + case errors::invalid => return errors::invalid; - }; - - let buckets: []*map::map = []; - for (let i = 0z; i < n; i += 1) { - let fb = match (make_fallback()) { - case let p: *map::map => yield p; - case nomem => - for (let j = 0z; j < len(buckets); j += 1) { - map::finish(buckets[j]); - }; - return nomem; - }; - match (append(buckets, fb)) { - case void => yield; - case nomem => - for (let j = 0z; j < len(buckets); j += 1) { - map::finish(buckets[j]); - }; - return nomem; - }; + case nomem => + return nomem; }; let m = match (alloc(map { vt = &_vt, - n = n, - buckets = buckets, + inner = inner, })) { - case let pm: *map => yield pm; + case let p: *map => yield p; case nomem => - for (let j = 0z; j < len(buckets); j += 1) { - map::finish(buckets[j]); - }; - free(buckets); + map::finish(inner); return nomem; }; return m; diff --git a/ds/map/map_fnv/set.ha b/ds/map/map_fnv/set.ha index 1c4dcd57fe87ee18b82a3a06a6b2315aa59c56ad..b41293d89189d73b53f6c2e6d501b0f9d9b2f67c 100644 --- a/ds/map/map_fnv/set.ha +++ b/ds/map/map_fnv/set.ha @@ -6,6 +6,5 @@ use ds::map; // Sets an item in a [[map]], replacing any existing item with the same key. export fn set(m: *map, key: []u8, value: *opaque) (void | nomem) = { - let b = m.buckets[hash64(m, key) % m.n]; - return map::set(b, key, value); + return map::set(m.inner, key, value); }; diff --git a/ds/map/map_hashmap/README b/ds/map/map_hashmap/README new file mode 100644 index 0000000000000000000000000000000000000000..eeca73d2cf4fd1688ff2e8e78e2ad31d75f6ffbc --- /dev/null +++ b/ds/map/map_hashmap/README @@ -0,0 +1,5 @@ +map_hashmap: key-value map implemented as a generic hashmap + +This module provides a simple implementation of a hashmap from []u8 to *opaque. +The hash algorithm and fallback collision resolution strategy are both +configurable. diff --git a/ds/map/map_hashmap/del.ha b/ds/map/map_hashmap/del.ha new file mode 100644 index 0000000000000000000000000000000000000000..98e2468295ad05ec00d69cfc7236683aaf0c3816 --- /dev/null +++ b/ds/map/map_hashmap/del.ha @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2024 Drew DeVault +// SPDX-FileCopyrightText: 2025 Runxi Yu + +use bytes; +use hash; +use hash::siphash; +use ds::map; + +// Deletes an item from a [[map]]. +export fn del(m: *map, key: []u8) (*opaque | void) = { + let b = m.buckets[m.hash64(m.hash_params, key) % m.n]; + return map::del(b, key); +}; diff --git a/ds/map/map_hashmap/finish.ha b/ds/map/map_hashmap/finish.ha new file mode 100644 index 0000000000000000000000000000000000000000..42cb8681b9270e2554ad41e6214f6fe5ed0317a4 --- /dev/null +++ b/ds/map/map_hashmap/finish.ha @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2024 Drew DeVault +// SPDX-FileCopyrightText: 2025 Runxi Yu + +use ds::map; + +// Frees resources associated with a [[map]]. +export fn finish(m: *map) void = { + for (let i = 0z; i < m.n; i += 1) { + map::finish(m.buckets[i]); + }; + free(m.buckets); + free(m); +}; diff --git a/ds/map/map_hashmap/get.ha b/ds/map/map_hashmap/get.ha new file mode 100644 index 0000000000000000000000000000000000000000..bdaa785dc283cc927ebabd08fc05d2fcca68fa61 --- /dev/null +++ b/ds/map/map_hashmap/get.ha @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2024 Drew DeVault +// SPDX-FileCopyrightText: 2025 Runxi Yu + +use bytes; +use hash; +use hash::siphash; +use ds::map; + +// Gets an item from a [[map]] by key, returning void if not found. +export fn get(m: *map, key: []u8) (*opaque | void) = { + let b = m.buckets[m.hash64(m.hash_params, key) % m.n]; + return map::get(b, key); +}; diff --git a/ds/map/map_hashmap/map.ha b/ds/map/map_hashmap/map.ha new file mode 100644 index 0000000000000000000000000000000000000000..8ae987150b183301740fe63a067f9e2afd7ecee3 --- /dev/null +++ b/ds/map/map_hashmap/map.ha @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2024 Drew DeVault +// SPDX-FileCopyrightText: 2025 Runxi Yu + +use ds::map; + +// A simple hash map from []u8 to *opaque pointers, using the user-provided hash +// and collision resolution function. +// +// You are advised to create these with [[new]]. +export type map = struct { + vt: map::map, + n: size, + buckets: []*map::map, + hash64: *fn(hash_params: nullable *opaque, key: []u8) size, + hash_params: nullable *opaque, +}; + +const _vt: map::vtable = map::vtable { + getter = &vt_get, + setter = &vt_set, + deleter = &vt_del, + finisher = &vt_finish, +}; + +fn vt_get(m: *map::map, key: []u8) (*opaque | void) = get(m: *map, key); +fn vt_set(m: *map::map, key: []u8, v: *opaque) (void | nomem) = set(m: *map, key, v); +fn vt_del(m: *map::map, key: []u8) (*opaque | void) = del(m: *map, key); +fn vt_finish(m: *map::map) void = finish(m: *map); diff --git a/ds/map/map_hashmap/new.ha b/ds/map/map_hashmap/new.ha new file mode 100644 index 0000000000000000000000000000000000000000..17df04134be34b10b2e6169384fd762f5a512899 --- /dev/null +++ b/ds/map/map_hashmap/new.ha @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2024 Drew DeVault +// SPDX-FileCopyrightText: 2025 Runxi Yu + +use bytes; +use errors; +use ds::map; + +// Creates a new [[map]] with a function that creates fallback maps, the number +// of buckets, and the hash function and parameters. +export fn new( + make_fallback: *fn() (*map::map | nomem), + n: size, + hash64: *fn(hash_params: nullable *opaque, key: []u8) size, + hash_params: nullable *opaque, +) (*map | errors::invalid | nomem) = { + if (n == 0) { + return errors::invalid; + }; + + let buckets: []*map::map = []; + for (let i = 0z; i < n; i += 1) { + let fb = match (make_fallback()) { + case let p: *map::map => yield p; + case nomem => + for (let j = 0z; j < len(buckets); j += 1) { + map::finish(buckets[j]); + }; + return nomem; + }; + match (append(buckets, fb)) { + case void => yield; + case nomem => + for (let j = 0z; j < len(buckets); j += 1) { + map::finish(buckets[j]); + }; + return nomem; + }; + }; + + let m = match (alloc(map { + vt = &_vt, + n = n, + buckets = buckets, + hash64 = hash64, + hash_params = hash_params, + })) { + case let pm: *map => yield pm; + case nomem => + for (let j = 0z; j < len(buckets); j += 1) { + map::finish(buckets[j]); + }; + free(buckets); + return nomem; + }; + return m; +}; diff --git a/ds/map/map_hashmap/set.ha b/ds/map/map_hashmap/set.ha new file mode 100644 index 0000000000000000000000000000000000000000..40ead2a7f9967712ed2f614fcba9a7e7f315725a --- /dev/null +++ b/ds/map/map_hashmap/set.ha @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2024 Drew DeVault +// SPDX-FileCopyrightText: 2025 Runxi Yu + +use bytes; +use hash; +use hash::siphash; +use ds::map; + +// Sets an item in a [[map]], replacing any existing item with the same key. +export fn set(m: *map, key: []u8, value: *opaque) (void | nomem) = { + let b = m.buckets[m.hash64(m.hash_params, key) % m.n]; + return map::set(b, key, value); +}; diff --git a/ds/map/map_siphash/del.ha b/ds/map/map_siphash/del.ha index b3c24b78ded3a6ff5af0b9b8cc50dddb20b3c1d3..b51ed1ad268bd33aa21d68873a13084d2bbfac34 100644 --- a/ds/map/map_siphash/del.ha +++ b/ds/map/map_siphash/del.ha @@ -2,13 +2,9 @@ // SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2024 Drew DeVault // SPDX-FileCopyrightText: 2025 Runxi Yu -use bytes; -use hash; -use hash::siphash; use ds::map; // Deletes an item from a [[map]]. export fn del(m: *map, key: []u8) (*opaque | void) = { - let b = m.buckets[hash64(m, key) % m.n]; - return map::del(b, key); + return map::del(m.inner, key); }; diff --git a/ds/map/map_siphash/finish.ha b/ds/map/map_siphash/finish.ha index 42cb8681b9270e2554ad41e6214f6fe5ed0317a4..49e27e82fb187b2a7c85e5fad9e5cbec845c7467 100644 --- a/ds/map/map_siphash/finish.ha +++ b/ds/map/map_siphash/finish.ha @@ -6,9 +6,7 @@ use ds::map; // Frees resources associated with a [[map]]. export fn finish(m: *map) void = { - for (let i = 0z; i < m.n; i += 1) { - map::finish(m.buckets[i]); - }; - free(m.buckets); + map::finish(m.inner); + free(m.key); free(m); }; diff --git a/ds/map/map_siphash/get.ha b/ds/map/map_siphash/get.ha index 4a554b20720ee1fcbed21c8eae003b9bba5a3423..b205c35f1f2457745f859ad7a89523dfafb3ef15 100644 --- a/ds/map/map_siphash/get.ha +++ b/ds/map/map_siphash/get.ha @@ -2,13 +2,9 @@ // SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2024 Drew DeVault // SPDX-FileCopyrightText: 2025 Runxi Yu -use bytes; -use hash; -use hash::siphash; use ds::map; // Gets an item from a [[map]] by key, returning void if not found. export fn get(m: *map, key: []u8) (*opaque | void) = { - let b = m.buckets[hash64(m, key) % m.n]; - return map::get(b, key); + return map::get(m.inner, key); }; diff --git a/ds/map/map_siphash/internal.ha b/ds/map/map_siphash/internal.ha index 63b4bc83156c1c1c982473770f28a5bef8271793..95f8078320cd68e89bfa68cd394f42c33859cbbf 100644 --- a/ds/map/map_siphash/internal.ha +++ b/ds/map/map_siphash/internal.ha @@ -1,8 +1,19 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2024 Drew DeVault +// SPDX-FileCopyrightText: 2025 Runxi Yu + use hash; use hash::siphash; -fn hash64(m: *map, key: []u8) size = { - let h = siphash::siphash(2, 4, &m.siphash_key); +fn hash64(params: nullable *opaque, key: []u8) size = { + let keyptr = match (params) { + case null => + abort("map_siphash: missing key"); + case let p: *opaque => + yield (p: *[16]u8); + }; + + let h = siphash::siphash(2, 4, keyptr); defer hash::close(&h); hash::write(&h, key); return siphash::sum(&h): size; diff --git a/ds/map/map_siphash/map.ha b/ds/map/map_siphash/map.ha index 0b230a0100cea61a2a63415a5863ae5cddcf12af..72ced6b2f5dcfa9b832fceee659b4b0c96abd90b 100644 --- a/ds/map/map_siphash/map.ha +++ b/ds/map/map_siphash/map.ha @@ -10,9 +10,8 @@ // // You are advised to create these with [[new]]. export type map = struct { vt: map::map, - n: size, - siphash_key: [16]u8, - buckets: []*map::map, + inner: *map::map, + key: *[16]u8, }; const _vt: map::vtable = map::vtable { diff --git a/ds/map/map_siphash/new.ha b/ds/map/map_siphash/new.ha index 1baaa390ede8aa963efa7538f3590f5c9edfce37..3624b100fe925fa5a9c02bb3b81ff3a49c4336aa 100644 --- a/ds/map/map_siphash/new.ha +++ b/ds/map/map_siphash/new.ha @@ -2,51 +2,44 @@ // SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2024 Drew DeVault // SPDX-FileCopyrightText: 2025 Runxi Yu -use bytes; use errors; -use hash; -use hash::siphash; use ds::map; +use ds::map::map_hashmap; // Creates a new [[map]] with a function that creates fallback maps, the number // of buckets, and the SipHash key. -export fn new(make_fallback: *fn() (*map::map | nomem), n: size, siphash_key: [16]u8) (*map | errors::invalid | nomem) = { - if (n == 0) { - return errors::invalid; +export fn new( + make_fallback: *fn() (*map::map | nomem), + n: size, + siphash_key: [16]u8, +) (*map | errors::invalid | nomem) = { + let keybox = match (alloc(siphash_key)) { + case let kp: *[16]u8 => yield kp; + case nomem => return nomem; }; - let buckets: []*map::map = []; - for (let i = 0z; i < n; i += 1) { - let fb = match (make_fallback()) { - case let p: *map::map => yield p; - case nomem => - for (let j = 0z; j < len(buckets); j += 1) { - map::finish(buckets[j]); - }; - return nomem; - }; - match (append(buckets, fb)) { - case void => yield; - case nomem => - for (let j = 0z; j < len(buckets); j += 1) { - map::finish(buckets[j]); - }; - return nomem; - }; + let inner = match (map_hashmap::new( + make_fallback, n, &hash64, (keybox: *opaque), + )) { + case let hm: *map_hashmap::map => + yield (hm: *map::map); + case errors::invalid => + free(keybox); + return errors::invalid; + case nomem => + free(keybox); + return nomem; }; let m = match (alloc(map { vt = &_vt, - n = n, - siphash_key = siphash_key, - buckets = buckets, + inner = inner, + key = keybox, })) { - case let pm: *map => yield pm; + case let p: *map => yield p; case nomem => - for (let j = 0z; j < len(buckets); j += 1) { - map::finish(buckets[j]); - }; - free(buckets); + map::finish(inner); + free(keybox); return nomem; }; return m; diff --git a/ds/map/map_siphash/set.ha b/ds/map/map_siphash/set.ha index 903f2869a72df1869273236e9044065a226b3a79..b41293d89189d73b53f6c2e6d501b0f9d9b2f67c 100644 --- a/ds/map/map_siphash/set.ha +++ b/ds/map/map_siphash/set.ha @@ -2,13 +2,9 @@ // SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2024 Drew DeVault // SPDX-FileCopyrightText: 2025 Runxi Yu -use bytes; -use hash; -use hash::siphash; use ds::map; // Sets an item in a [[map]], replacing any existing item with the same key. export fn set(m: *map, key: []u8, value: *opaque) (void | nomem) = { - let b = m.buckets[hash64(m, key) % m.n]; - return map::set(b, key, value); + return map::set(m.inner, key, value); }; -- 2.48.1