Lindenii Project Forge
Let the user specify the fallback for map_{fnv,siphash}
// SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2024 Drew DeVault <drew@ddevault.org> // SPDX-FileCopyrightText: 2025 Runxi Yu <me@runxiyu.org>
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);
};
// SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2024 Drew DeVault <drew@ddevault.org> // SPDX-FileCopyrightText: 2025 Runxi Yu <me@runxiyu.org>
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); };
// SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2024 Drew DeVault <drew@ddevault.org> // SPDX-FileCopyrightText: 2025 Runxi Yu <me@runxiyu.org>
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);
};
// SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2024 Drew DeVault <drew@ddevault.org> // SPDX-FileCopyrightText: 2025 Runxi Yu <me@runxiyu.org> 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 { 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);
// SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2024 Drew DeVault <drew@ddevault.org> // SPDX-FileCopyrightText: 2025 Runxi Yu <me@runxiyu.org>
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; };
// SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2024 Drew DeVault <drew@ddevault.org> // SPDX-FileCopyrightText: 2025 Runxi Yu <me@runxiyu.org>
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);
};
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"); }; defer finish(m); let v1 = 1, v2 = 2, v3 = 3; let p1: *opaque = (&v1: *opaque); let p2: *opaque = (&v2: *opaque); let p3: *opaque = (&v3: *opaque); let k1 = strings::toutf8("alpha"); let k2 = strings::toutf8("beta"); let k3 = strings::toutf8("gamma"); match (map::set(m, k1, p1)) { case void => yield; case nomem => abort("unexpected nomem in set(k1,p1)"); }; match (map::get(m, k1)) { case let got: *opaque => assert(got == p1, "get(k1) must return p1"); case void => abort("get(k1) unexpectedly void"); }; match (map::set(m, k1, p2)) { case void => yield; case nomem => abort("unexpected nomem in replace"); }; match (map::get(m, k1)) { case let got: *opaque => assert(got == p2, "replace must overwrite prior value"); case void => abort("get(k1) void after replace"); }; match (map::set(m, k2, p3)) { case void => yield; case nomem => abort("unexpected nomem in set(k2,p3)"); }; match (map::get(m, k3)) { case void => yield; case *opaque => abort("get(k3) must be void for missing key"); }; match (map::del(m, k2)) { case let got: *opaque => assert(got == p3, "del(k2) must return stored value"); case void => abort("del(k2) unexpectedly void"); }; match (map::del(m, k2)) { case void => yield; 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); };
// SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2024 Drew DeVault <drew@ddevault.org> // SPDX-FileCopyrightText: 2025 Runxi Yu <me@runxiyu.org> 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);
};
// SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2024 Drew DeVault <drew@ddevault.org> // SPDX-FileCopyrightText: 2025 Runxi Yu <me@runxiyu.org>
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); };
// SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2024 Drew DeVault <drew@ddevault.org> // SPDX-FileCopyrightText: 2025 Runxi Yu <me@runxiyu.org> 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);
};
// SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2024 Drew DeVault <drew@ddevault.org> // SPDX-FileCopyrightText: 2025 Runxi Yu <me@runxiyu.org> 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 { 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);
// SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2024 Drew DeVault <drew@ddevault.org> // SPDX-FileCopyrightText: 2025 Runxi Yu <me@runxiyu.org> 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; };
// SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2024 Drew DeVault <drew@ddevault.org> // SPDX-FileCopyrightText: 2025 Runxi Yu <me@runxiyu.org> 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);
};
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"); }; defer finish(m); let v1 = 1, v2 = 2, v3 = 3; let p1: *opaque = (&v1: *opaque); let p2: *opaque = (&v2: *opaque); let p3: *opaque = (&v3: *opaque); let k1 = strings::toutf8("alpha"); let k2 = strings::toutf8("beta"); let k3 = strings::toutf8("gamma"); match (map::set(m, k1, p1)) { case void => yield; case nomem => abort("unexpected nomem in set(k1,p1)"); }; match (map::get(m, k1)) { case let got: *opaque => assert(got == p1, "get(k1) must return p1"); case void => abort("get(k1) unexpectedly void"); }; match (map::set(m, k1, p2)) { case void => yield; case nomem => abort("unexpected nomem in replace"); }; match (map::get(m, k1)) { case let got: *opaque => assert(got == p2, "replace must overwrite prior value"); case void => abort("get(k1) void after replace"); }; match (map::set(m, k2, p3)) { case void => yield; case nomem => abort("unexpected nomem in set(k2,p3)"); }; match (map::get(m, k3)) { case void => yield; case *opaque => abort("get(k3) must be void for missing key"); }; match (map::del(m, k2)) { case let got: *opaque => assert(got == p3, "del(k2) must return stored value"); case void => abort("del(k2) unexpectedly void"); }; match (map::del(m, k2)) { case void => yield; 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); };