Lindenii Project Forge
Login

hare-ds

Data structures for Hare
Commit info
ID
9cd865eaeef7aabd4959de8c31aac09a8a60564a
Author
Runxi Yu <me@runxiyu.org>
Author date
Tue, 16 Sep 2025 20:11:50 +0800
Committer
Runxi Yu <me@runxiyu.org>
Committer date
Tue, 16 Sep 2025 20:51:09 +0800
Actions
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);
};