From 9cd865eaeef7aabd4959de8c31aac09a8a60564a Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Tue, 16 Sep 2025 20:11:50 +0800 Subject: [PATCH] Let the user specify the fallback for map_{fnv,siphash} --- ds/map/map_fnv/del.ha | 17 +++++------------ ds/map/map_fnv/finish.ha | 5 +++-- ds/map/map_fnv/get.ha | 14 +++++--------- ds/map/map_fnv/map.ha | 4 ++-- ds/map/map_fnv/new.ha | 41 ++++++++++++++++++++++++++++++++--------- ds/map/map_fnv/set.ha | 16 +++++----------- ds/map/map_fnv/test.ha | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++-- ds/map/map_siphash/del.ha | 18 ++++++------------ ds/map/map_siphash/finish.ha | 5 +++-- ds/map/map_siphash/get.ha | 15 ++++++--------- ds/map/map_siphash/map.ha | 4 ++-- ds/map/map_siphash/new.ha | 40 +++++++++++++++++++++++++++++++++------- ds/map/map_siphash/set.ha | 17 ++++++----------- ds/map/map_siphash/test.ha | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++-- diff --git a/ds/map/map_fnv/del.ha b/ds/map/map_fnv/del.ha index de03d94d0a54b35dd25e8686b7b12dcf4780971c..95cad53e907a3a38cfd7e254df10d360b8c95ee8 100644 --- a/ds/map/map_fnv/del.ha +++ b/ds/map/map_fnv/del.ha @@ -2,21 +2,14 @@ // SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2024 Drew DeVault // SPDX-FileCopyrightText: 2025 Runxi Yu -use bytes; use hash; use hash::fnv; +use ds::map; // Deletes an item from a [[map]]. export fn del(m: *map, key: []u8) (*opaque | void) = { - let hash = fnv::fnv64a(); - hash::write(&hash, key); - let bucket = &m.buckets[fnv::sum64(&hash): size % m.n]; - for (let i = 0z; i < len(bucket); i += 1) { - if (bytes::equal(bucket[i].0, key)) { - let item = bucket[i]; - delete(bucket[i]); - return item.1; - }; - }; + let h = fnv::fnv64a(); + hash::write(&h, key); + let b = m.buckets[fnv::sum64(&h): size % m.n]; + return map::del(b, key); }; - diff --git a/ds/map/map_fnv/finish.ha b/ds/map/map_fnv/finish.ha index 7573e1ffce717596a14e6514b6b80339a2c98a8d..42cb8681b9270e2554ad41e6214f6fe5ed0317a4 100644 --- a/ds/map/map_fnv/finish.ha +++ b/ds/map/map_fnv/finish.ha @@ -2,12 +2,13 @@ // 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) { - free(m.buckets[i]); + map::finish(m.buckets[i]); }; free(m.buckets); free(m); }; - diff --git a/ds/map/map_fnv/get.ha b/ds/map/map_fnv/get.ha index b935ea1704e10fabe147aa6f7e9be661da8e60f2..9437e7126f440821d7fcf8ca16f109618d028f41 100644 --- a/ds/map/map_fnv/get.ha +++ b/ds/map/map_fnv/get.ha @@ -2,18 +2,14 @@ // SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2024 Drew DeVault // SPDX-FileCopyrightText: 2025 Runxi Yu -use bytes; use hash; use hash::fnv; +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 hash = fnv::fnv64a(); - hash::write(&hash, key); - let bucket = &m.buckets[fnv::sum64(&hash): size % m.n]; - for (let i = 0z; i < len(bucket); i += 1) { - if (bytes::equal(bucket[i].0, key)) { - return bucket[i].1; - }; - }; + let h = fnv::fnv64a(); + hash::write(&h, key); + let b = m.buckets[fnv::sum64(&h): size % m.n]; + return map::get(b, key); }; diff --git a/ds/map/map_fnv/map.ha b/ds/map/map_fnv/map.ha index bcc83725e9989fbf07987a8a1de5dfd2afe839f8..d41a174ecd7e22a64fbcb0de9aa4fca5a2c7b95a 100644 --- a/ds/map/map_fnv/map.ha +++ b/ds/map/map_fnv/map.ha @@ -5,13 +5,13 @@ use ds::map; // A simple hash map from byte strings to opaque pointers, using SipHash for -// hashing and a basic linear search through a slice to resolve collisions. +// 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: [][]([]u8, *opaque), + buckets: []*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 695489ae0052cdddced9b19ece78753d661aab15..32cac7d8d74fc1d15d996f561e7f41b7e6700b4c 100644 --- a/ds/map/map_fnv/new.ha +++ b/ds/map/map_fnv/new.ha @@ -2,25 +2,48 @@ // SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2024 Drew DeVault // SPDX-FileCopyrightText: 2025 Runxi Yu -use bytes; use errors; -use hash; -use hash::fnv; +use ds::map; // Creates a new [[map]] with the given number of buckets. -export fn new(n: size) (*map | errors::invalid | nomem) = { +// 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) { return errors::invalid; }; - let empty_bucket: []([]u8, *opaque) = []; - let buckets: [][]([]u8, *opaque) = alloc([empty_bucket...], n)?; + 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 = alloc(map { + let m = match (alloc(map { vt = &_vt, n = n, buckets = buckets, - })?; - + })) { + 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_fnv/set.ha b/ds/map/map_fnv/set.ha index e03337ae9de7fabf1165cf5b09875a5d1109d3d3..918db1f9de7e831d8dc6a8d0d3b490d534313505 100644 --- a/ds/map/map_fnv/set.ha +++ b/ds/map/map_fnv/set.ha @@ -2,20 +2,14 @@ // SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2024 Drew DeVault // SPDX-FileCopyrightText: 2025 Runxi Yu -use bytes; use hash; use hash::fnv; +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 hash = fnv::fnv64a(); - hash::write(&hash, key); - let bucket = &m.buckets[fnv::sum64(&hash): size % m.n]; - for (let i = 0z; i < len(bucket); i += 1) { - if (bytes::equal(bucket[i].0, key)) { - bucket[i].1 = value; - return; - }; - }; - append(bucket, (key, value))?; + let h = fnv::fnv64a(); + hash::write(&h, key); + let b = m.buckets[fnv::sum64(&h): size % m.n]; + return map::set(b, key, value); }; diff --git a/ds/map/map_fnv/test.ha b/ds/map/map_fnv/test.ha index 3676ea7597df9a105e806f8cc03f81d6ac07278d..55fc410d9fe9d4f3eca17cc4dc1387acb159546a 100644 --- a/ds/map/map_fnv/test.ha +++ b/ds/map/map_fnv/test.ha @@ -1,9 +1,42 @@ use errors; use strings; use ds::map; +use ds::map::map_slice_basic; +use ds::map::map_slice_sorted; +use ds::map::map_btree; +use ds::map::map_rbtree; -@test fn roundtrip() void = { - let m: *map = match (new(16)) { +fn mk_slice_basic() (*map::map | nomem) = { + match (map_slice_basic::new()) { + case let p: *map_slice_basic::map => return (p: *map::map); + case nomem => return nomem; + }; +}; + +fn mk_slice_sorted() (*map::map | nomem) = { + match (map_slice_sorted::new()) { + case let p: *map_slice_sorted::map => return (p: *map::map); + case nomem => return nomem; + }; +}; + +fn mk_btree() (*map::map | nomem) = { + match (map_btree::new(2)) { + case let p: *map_btree::map => return (p: *map::map); + case errors::invalid => abort("map_btree::new(2) invalid (unexpected)"); + case nomem => return nomem; + }; +}; + +fn mk_rbtree() (*map::map | nomem) = { + match (map_rbtree::new()) { + case let p: *map_rbtree::map => return (p: *map::map); + case nomem => return nomem; + }; +}; + +fn run_case(make_fallback: *fn() (*map::map | nomem)) void = { + let m: *map = match (new(make_fallback, 16)) { case let p: *map => yield p; case errors::invalid => abort("unexpected errors::invalid"); case nomem => abort("unexpected nomem"); @@ -65,3 +98,19 @@ case *opaque => abort("del(k2) must be void after prior delete"); }; }; + +@test fn roundtrip_with_slice_basic() void = { + run_case(&mk_slice_basic); +}; + +@test fn roundtrip_with_slice_sorted() void = { + run_case(&mk_slice_sorted); +}; + +@test fn roundtrip_with_btree() void = { + run_case(&mk_btree); +}; + +@test fn roundtrip_with_rbtree() void = { + run_case(&mk_rbtree); +}; diff --git a/ds/map/map_siphash/del.ha b/ds/map/map_siphash/del.ha index 863e9020ceb9d7559d28218f7c26b3db1e8b9f57..4847c67dadfb714a6d185760c3aa6266515f868f 100644 --- a/ds/map/map_siphash/del.ha +++ b/ds/map/map_siphash/del.ha @@ -5,19 +5,13 @@ 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 hash = siphash::siphash(2, 4, &m.siphash_key); - defer hash::close(&hash); - hash::write(&hash, key); - let bucket = &m.buckets[siphash::sum(&hash): size % m.n]; - for (let i = 0z; i < len(bucket); i += 1) { - if (bytes::equal(bucket[i].0, key)) { - let item = bucket[i]; - delete(bucket[i]); - return item.1; - }; - }; + let h = siphash::siphash(2, 4, &m.siphash_key); + defer hash::close(&h); + hash::write(&h, key); + let b = m.buckets[siphash::sum(&h): size % m.n]; + return map::del(b, key); }; - diff --git a/ds/map/map_siphash/finish.ha b/ds/map/map_siphash/finish.ha index 7573e1ffce717596a14e6514b6b80339a2c98a8d..42cb8681b9270e2554ad41e6214f6fe5ed0317a4 100644 --- a/ds/map/map_siphash/finish.ha +++ b/ds/map/map_siphash/finish.ha @@ -2,12 +2,13 @@ // 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) { - free(m.buckets[i]); + map::finish(m.buckets[i]); }; free(m.buckets); free(m); }; - diff --git a/ds/map/map_siphash/get.ha b/ds/map/map_siphash/get.ha index ecedcb04e8e88d5e61f8ea6741000ae0ba50897d..ad306bdf69ec900456135204442966c39cda975c 100644 --- a/ds/map/map_siphash/get.ha +++ b/ds/map/map_siphash/get.ha @@ -5,16 +5,13 @@ 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 hash = siphash::siphash(2, 4, &m.siphash_key); - defer hash::close(&hash); - hash::write(&hash, key); - let bucket = &m.buckets[siphash::sum(&hash): size % m.n]; - for (let i = 0z; i < len(bucket); i += 1) { - if (bytes::equal(bucket[i].0, key)) { - return bucket[i].1; - }; - }; + let h = siphash::siphash(2, 4, &m.siphash_key); + defer hash::close(&h); + hash::write(&h, key); + let b = m.buckets[siphash::sum(&h): size % m.n]; + return map::get(b, key); }; diff --git a/ds/map/map_siphash/map.ha b/ds/map/map_siphash/map.ha index 6e54ded4a4cf22a78f767e2f4b01348d0c6ec17b..0b230a0100cea61a2a63415a5863ae5cddcf12af 100644 --- a/ds/map/map_siphash/map.ha +++ b/ds/map/map_siphash/map.ha @@ -5,14 +5,14 @@ use ds::map; // A simple hash map from byte strings to opaque pointers, using SipHash for -// hashing and a basic linear search through a slice to resolve collisions. +// hashing and fallback maps for collision resolution. // // You are advised to create these with [[new]]. export type map = struct { vt: map::map, n: size, siphash_key: [16]u8, - buckets: [][]([]u8, *opaque), + buckets: []*map::map, }; const _vt: map::vtable = map::vtable { diff --git a/ds/map/map_siphash/new.ha b/ds/map/map_siphash/new.ha index c3c0349b5047b97e12a83bce8428ecfabaf70761..1baaa390ede8aa963efa7538f3590f5c9edfce37 100644 --- a/ds/map/map_siphash/new.ha +++ b/ds/map/map_siphash/new.ha @@ -6,22 +6,48 @@ use bytes; use errors; use hash; use hash::siphash; +use ds::map; -// Creates a new [[map]] with the given number of buckets and SipHash key. -export fn new(n: size, siphash_key: [16]u8) (*map | errors::invalid | nomem) = { +// 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; }; - let empty_bucket: []([]u8, *opaque) = []; - let buckets: [][]([]u8, *opaque) = alloc([empty_bucket...], n)?; + 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 = alloc(map { + let m = match (alloc(map { vt = &_vt, n = n, siphash_key = siphash_key, buckets = buckets, - })?; - + })) { + 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_siphash/set.ha b/ds/map/map_siphash/set.ha index f53f64a5af6e1334d3db4ec241d665a2687ecd77..9b5bc767fe67f338df318db1ac6a1e14833ef703 100644 --- a/ds/map/map_siphash/set.ha +++ b/ds/map/map_siphash/set.ha @@ -5,18 +5,13 @@ 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 hash = siphash::siphash(2, 4, &m.siphash_key); - defer hash::close(&hash); - hash::write(&hash, key); - let bucket = &m.buckets[siphash::sum(&hash): size % m.n]; - for (let i = 0z; i < len(bucket); i += 1) { - if (bytes::equal(bucket[i].0, key)) { - bucket[i].1 = value; - return; - }; - }; - append(bucket, (key, value))?; + let h = siphash::siphash(2, 4, &m.siphash_key); + defer hash::close(&h); + hash::write(&h, key); + let b = m.buckets[siphash::sum(&h): size % m.n]; + return map::set(b, key, value); }; diff --git a/ds/map/map_siphash/test.ha b/ds/map/map_siphash/test.ha index f9e97e4cb24be8cd10c837f519768673df92cdf1..da9a92c8295ba61a10ea9de1199e8069c05b2ce8 100644 --- a/ds/map/map_siphash/test.ha +++ b/ds/map/map_siphash/test.ha @@ -1,11 +1,46 @@ +use crypto::random; use errors; use strings; use ds::map; +use ds::map::map_slice_basic; +use ds::map::map_slice_sorted; +use ds::map::map_btree; +use ds::map::map_rbtree; -@test fn roundtrip() void = { +fn mk_slice_basic() (*map::map | nomem) = { + match (map_slice_basic::new()) { + case let p: *map_slice_basic::map => return (p: *map::map); + case nomem => return nomem; + }; +}; + +fn mk_slice_sorted() (*map::map | nomem) = { + match (map_slice_sorted::new()) { + case let p: *map_slice_sorted::map => return (p: *map::map); + case nomem => return nomem; + }; +}; + +fn mk_btree() (*map::map | nomem) = { + match (map_btree::new(2)) { + case let p: *map_btree::map => return (p: *map::map); + case errors::invalid => abort("map_btree::new(2) invalid (unexpected)"); + case nomem => return nomem; + }; +}; + +fn mk_rbtree() (*map::map | nomem) = { + match (map_rbtree::new()) { + case let p: *map_rbtree::map => return (p: *map::map); + case nomem => return nomem; + }; +}; + +fn run_case(make_fallback: *fn() (*map::map | nomem)) void = { const key: [16]u8 = [0...]; + random::buffer(&key); - let m: *map = match (new(16, key)) { + let m: *map = match (new(make_fallback, 16, key)) { case let p: *map => yield p; case errors::invalid => abort("unexpected errors::invalid"); case nomem => abort("unexpected nomem"); @@ -67,3 +102,19 @@ case *opaque => abort("del(k2) must be void after prior delete"); }; }; + +@test fn roundtrip_with_slice_basic() void = { + run_case(&mk_slice_basic); +}; + +@test fn roundtrip_with_slice_sorted() void = { + run_case(&mk_slice_sorted); +}; + +@test fn roundtrip_with_btree() void = { + run_case(&mk_btree); +}; + +@test fn roundtrip_with_rbtree() void = { + run_case(&mk_rbtree); +}; -- 2.48.1