Something went wrong. Try again.
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541//! This file contains a fork of the Zig stdlib HashMap implementation tuned//! for use with our terminal page representation.//!//! The main goal we need to achieve that wasn't possible with the stdlib//! HashMap is to utilize offsets rather than full pointers so that we can//! copy around the entire backing memory and keep the hash map working.//!//! Additionally, for serialization/deserialization purposes, we need to be//! able to create a HashMap instance and manually set the offsets up. The//! stdlib HashMap does not export Metadata so this isn't possible.//!//! Also, I want to be able to understand possible capacity for a given K,V//! type and fixed memory amount. The stdlib HashMap doesn't publish its//! internal allocation size calculation.//!//! Finally, I removed many of the APIs that we'll never require for our//! usage just so that this file is smaller, easier to understand, and has//! less opportunity for bugs.//!//! Besides these shortcomings, the stdlib HashMap has some great qualities//! that we want to keep, namely the fact that it is backed by a single large//! allocation rather than pointers to separate allocations. This is important//! because our terminal page representation is backed by a single large//! allocation so we can give the HashMap a slice of memory to operate in.//!//! I haven't carefully benchmarked this implementation against other hash//! map implementations. It's possible using some of the newer variants out//! there would be better. However, I trust the built-in version is pretty good//! and its more important to get the terminal page representation working//! first then we can measure and improve this later if we find it to be a//! bottleneck.
const std = @import("std");const assert = @import("../quirks.zig").inlineAssert;const autoHash = std.hash.autoHash;const math = std.math;const mem = std.mem;const Allocator = mem.Allocator;const Wyhash = std.hash.Wyhash;
const Offset = @import("size.zig").Offset;const OffsetBuf = @import("size.zig").OffsetBuf;const getOffset = @import("size.zig").getOffset;
pub fn AutoOffsetHashMap(comptime K: type, comptime V: type) type { return OffsetHashMap(K, V, AutoContext(K));}
fn AutoHashMapUnmanaged(comptime K: type, comptime V: type) type { return HashMapUnmanaged(K, V, AutoContext(K));}
fn AutoContext(comptime K: type) type { return struct { pub const hash = std.hash_map.getAutoHashFn(K, @This()); pub const eql = std.hash_map.getAutoEqlFn(K, @This()); };}
/// A HashMap type that uses offsets rather than pointers, making it/// possible to efficiently move around the backing memory without/// invalidating the HashMap.pub fn OffsetHashMap( comptime K: type, comptime V: type, comptime Context: type,) type { return struct { const Self = @This();
/// This is the pointer-based map that we're wrapping. pub const Unmanaged = HashMapUnmanaged(K, V, Context); pub const Layout = Unmanaged.Layout;
/// This is the alignment that the base pointer must have. pub const base_align = Unmanaged.base_align;
metadata: Offset(Unmanaged.Metadata) = .{},
/// Returns the total size of the backing memory required for a /// HashMap with the given capacity. The base ptr must also be /// aligned to base_align. pub fn layout(cap: Unmanaged.Size) Layout { return Unmanaged.layoutForCapacity(cap); }
/// Initialize a new HashMap with the given capacity and backing /// memory. The backing memory must be aligned to base_align. pub fn init(buf: OffsetBuf, l: Layout) Self { assert(base_align.check(@intFromPtr(buf.start())));
const m = Unmanaged.init(buf, l); return .{ .metadata = getOffset( Unmanaged.Metadata, buf, @ptrCast(m.metadata.?), ) }; }
/// Returns the pointer-based map from a base pointer. pub fn map(self: Self, base: anytype) Unmanaged { return .{ .metadata = self.metadata.ptr(base) }; } };}
/// Fork of stdlib.HashMap as of Zig 0.12 modified to use offsets for/// the key/values pointer. The metadata is still a pointer to limit/// the amount of arithmetic required to access it. See the file comment/// for full details.fn HashMapUnmanaged( comptime K: type, comptime V: type, comptime Context: type,) type { return struct { const Self = @This();
comptime { assert(@alignOf(Metadata) == 1); }
const header_align = @alignOf(Header); const key_align = if (@sizeOf(K) == 0) 1 else @alignOf(K); const val_align = if (@sizeOf(V) == 0) 1 else @alignOf(V); const base_align: mem.Alignment = .fromByteUnits(@max( header_align, key_align, val_align, ));
// This is actually a midway pointer to the single buffer containing // a `Header` field, the `Metadata`s and `Entry`s. // At `-@sizeOf(Header)` is the Header field. // At `sizeOf(Metadata) * capacity + offset`, which is pointed to by // self.header().entries, is the array of entries. // This means that the hashmap only holds one live allocation, to // reduce memory fragmentation and struct size. /// Pointer to the metadata. metadata: ?[*]Metadata = null,
// This is purely empirical and not a /very smart magic constant™/. /// Capacity of the first grow when bootstrapping the hashmap. const minimal_capacity = 8;
// This hashmap is specially designed for sizes that fit in a u32. pub const Size = u32;
// u64 hashes guarantee us that the fingerprint bits will never be used // to compute the index of a slot, maximizing the use of entropy. pub const Hash = u64;
pub const Entry = struct { key_ptr: *K, value_ptr: *V, };
pub const KV = struct { key: K, value: V, };
const Header = struct { /// The keys/values offset are relative to the metadata values: Offset(V), keys: Offset(K), capacity: Size, size: Size, };
/// Metadata for a slot. It can be in three states: empty, used or /// tombstone. Tombstones indicate that an entry was previously used, /// they are a simple way to handle removal. /// To this state, we add 7 bits from the slot's key hash. These are /// used as a fast way to disambiguate between entries without /// having to use the equality function. If two fingerprints are /// different, we know that we don't have to compare the keys at all. /// The 7 bits are the highest ones from a 64 bit hash. This way, not /// only we use the `log2(capacity)` lowest bits from the hash to determine /// a slot index, but we use 7 more bits to quickly resolve collisions /// when multiple elements with different hashes end up wanting to be in the same slot. /// Not using the equality function means we don't have to read into /// the entries array, likely avoiding a cache miss and a potentially /// costly function call. const Metadata = packed struct { const FingerPrint = u7;
const free: FingerPrint = 0; const tombstone: FingerPrint = 1;
fingerprint: FingerPrint = free, used: u1 = 0,
const slot_free = @as(u8, @bitCast(Metadata{ .fingerprint = free })); const slot_tombstone = @as(u8, @bitCast(Metadata{ .fingerprint = tombstone }));
pub fn isUsed(self: Metadata) bool { return self.used == 1; }
pub fn isTombstone(self: Metadata) bool { return @as(u8, @bitCast(self)) == slot_tombstone; }
pub fn isFree(self: Metadata) bool { return @as(u8, @bitCast(self)) == slot_free; }
pub fn takeFingerprint(hash: Hash) FingerPrint { const hash_bits = @typeInfo(Hash).int.bits; const fp_bits = @typeInfo(FingerPrint).int.bits; return @as(FingerPrint, @truncate(hash >> (hash_bits - fp_bits))); }
pub fn fill(self: *Metadata, fp: FingerPrint) void { self.used = 1; self.fingerprint = fp; }
pub fn remove(self: *Metadata) void { self.used = 0; self.fingerprint = tombstone; } };
comptime { assert(@sizeOf(Metadata) == 1); assert(@alignOf(Metadata) == 1); }
pub const Iterator = struct { hm: *const Self, index: Size = 0,
pub fn next(it: *Iterator) ?Entry { assert(it.index <= it.hm.capacity()); if (it.hm.header().size == 0) return null;
const cap = it.hm.capacity(); const end = it.hm.metadata.? + cap; var metadata = it.hm.metadata.? + it.index;
while (metadata != end) : ({ metadata += 1; it.index += 1; }) { if (metadata[0].isUsed()) { const key = &it.hm.keys()[it.index]; const value = &it.hm.values()[it.index]; it.index += 1; return Entry{ .key_ptr = key, .value_ptr = value }; } }
return null; } };
pub const KeyIterator = FieldIterator(K); pub const ValueIterator = FieldIterator(V);
fn FieldIterator(comptime T: type) type { return struct { len: usize, metadata: [*]const Metadata, items: [*]T,
pub fn next(self: *@This()) ?*T { while (self.len > 0) { self.len -= 1; const used = self.metadata[0].isUsed(); const item = &self.items[0]; self.metadata += 1; self.items += 1; if (used) { return item; } } return null; } }; }
pub const GetOrPutResult = struct { key_ptr: *K, value_ptr: *V, found_existing: bool, };
/// Initialize a hash map with a given capacity and a buffer. The /// buffer must fit within the size defined by `layoutForCapacity`. pub fn init(buf: OffsetBuf, layout: Layout) Self { assert(base_align.check(@intFromPtr(buf.start())));
// Get all our main pointers const metadata_buf = buf.rebase(@sizeOf(Header)); const metadata_ptr: [*]Metadata = @ptrCast(metadata_buf.start());
// Build our map var map: Self = .{ .metadata = metadata_ptr }; const hdr = map.header(); hdr.capacity = layout.capacity; hdr.size = 0; if (@sizeOf([*]K) != 0) hdr.keys = metadata_buf.member(K, layout.keys_start); if (@sizeOf([*]V) != 0) hdr.values = metadata_buf.member(V, layout.vals_start); map.initMetadatas();
return map; }
pub fn ensureTotalCapacity(self: *Self, new_size: Size) Allocator.Error!void { if (new_size > self.header().size) { try self.growIfNeeded(new_size - self.header().size); } }
pub fn ensureUnusedCapacity(self: *Self, additional_size: Size) Allocator.Error!void { return ensureTotalCapacity(self, self.count() + additional_size); }
pub fn clearRetainingCapacity(self: *Self) void { if (self.metadata) |_| { self.initMetadatas(); self.header().size = 0; } }
pub fn count(self: *const Self) Size { return self.header().size; }
fn header(self: *const Self) *Header { return @ptrCast(@as([*]Header, @ptrCast(@alignCast(self.metadata.?))) - 1); }
fn keys(self: *const Self) [*]K { return self.header().keys.ptr(self.metadata.?); }
fn values(self: *const Self) [*]V { return self.header().values.ptr(self.metadata.?); }
pub fn capacity(self: *const Self) Size { if (self.metadata == null) return 0;
return self.header().capacity; }
pub fn iterator(self: *const Self) Iterator { return .{ .hm = self }; }
pub fn keyIterator(self: *const Self) KeyIterator { if (self.metadata) |metadata| { return .{ .len = self.capacity(), .metadata = metadata, .items = self.keys(), }; } else { return .{ .len = 0, .metadata = undefined, .items = undefined, }; } }
pub fn valueIterator(self: *const Self) ValueIterator { if (self.metadata) |metadata| { return .{ .len = self.capacity(), .metadata = metadata, .items = self.values(), }; } else { return .{ .len = 0, .metadata = undefined, .items = undefined, }; } }
/// Insert an entry in the map. Assumes it is not already present. pub fn putNoClobber(self: *Self, key: K, value: V) Allocator.Error!void { if (@sizeOf(Context) != 0) @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call putNoClobberContext instead."); return self.putNoClobberContext(key, value, undefined); } pub fn putNoClobberContext(self: *Self, key: K, value: V, ctx: Context) Allocator.Error!void { assert(!self.containsContext(key, ctx)); try self.growIfNeeded(1);
self.putAssumeCapacityNoClobberContext(key, value, ctx); }
/// Asserts there is enough capacity to store the new key-value pair. /// Clobbers any existing data. To detect if a put would clobber /// existing data, see `getOrPutAssumeCapacity`. pub fn putAssumeCapacity(self: *Self, key: K, value: V) void { if (@sizeOf(Context) != 0) @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call putAssumeCapacityContext instead."); return self.putAssumeCapacityContext(key, value, undefined); } pub fn putAssumeCapacityContext(self: *Self, key: K, value: V, ctx: Context) void { const gop = self.getOrPutAssumeCapacityContext(key, ctx); gop.value_ptr.* = value; }
/// Insert an entry in the map. Assumes it is not already present, /// and that no allocation is needed. pub fn putAssumeCapacityNoClobber(self: *Self, key: K, value: V) void { if (@sizeOf(Context) != 0) @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call putAssumeCapacityNoClobberContext instead."); return self.putAssumeCapacityNoClobberContext(key, value, undefined); } pub fn putAssumeCapacityNoClobberContext(self: *Self, key: K, value: V, ctx: Context) void { assert(!self.containsContext(key, ctx));
const hash = ctx.hash(key); const mask = self.capacity() - 1; var idx = @as(usize, @truncate(hash & mask));
var metadata = self.metadata.? + idx; while (metadata[0].isUsed()) { idx = (idx + 1) & mask; metadata = self.metadata.? + idx; }
const fingerprint = Metadata.takeFingerprint(hash); metadata[0].fill(fingerprint); self.keys()[idx] = key; self.values()[idx] = value; self.header().size += 1; }
/// Inserts a new `Entry` into the hash map, returning the previous one, if any. pub fn fetchPut(self: *Self, key: K, value: V) Allocator.Error!?KV { if (@sizeOf(Context) != 0) @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call fetchPutContext instead."); return self.fetchPutContext(key, value, undefined); } pub fn fetchPutContext(self: *Self, key: K, value: V, ctx: Context) Allocator.Error!?KV { const gop = try self.getOrPutContext(key, ctx); var result: ?KV = null; if (gop.found_existing) { result = KV{ .key = gop.key_ptr.*, .value = gop.value_ptr.*, }; } gop.value_ptr.* = value; return result; }
/// Inserts a new `Entry` into the hash map, returning the previous one, if any. /// If insertion happens, asserts there is enough capacity without allocating. pub fn fetchPutAssumeCapacity(self: *Self, key: K, value: V) ?KV { if (@sizeOf(Context) != 0) @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call fetchPutAssumeCapacityContext instead."); return self.fetchPutAssumeCapacityContext(key, value, undefined); } pub fn fetchPutAssumeCapacityContext(self: *Self, key: K, value: V, ctx: Context) ?KV { const gop = self.getOrPutAssumeCapacityContext(key, ctx); var result: ?KV = null; if (gop.found_existing) { result = KV{ .key = gop.key_ptr.*, .value = gop.value_ptr.*, }; } gop.value_ptr.* = value; return result; }
/// If there is an `Entry` with a matching key, it is deleted from /// the hash map, and then returned from this function. pub fn fetchRemove(self: *Self, key: K) ?KV { if (@sizeOf(Context) != 0) @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call fetchRemoveContext instead."); return self.fetchRemoveContext(key, undefined); } pub fn fetchRemoveContext(self: *Self, key: K, ctx: Context) ?KV { return self.fetchRemoveAdapted(key, ctx); } pub fn fetchRemoveAdapted(self: *Self, key: anytype, ctx: anytype) ?KV { if (self.getIndex(key, ctx)) |idx| { const old_key = &self.keys()[idx]; const old_val = &self.values()[idx]; const result = KV{ .key = old_key.*, .value = old_val.*, }; self.metadata.?[idx].remove(); old_key.* = undefined; old_val.* = undefined; self.header().size -= 1; return result; }
return null; }
/// Find the index containing the data for the given key. /// Whether this function returns null is almost always /// branched on after this function returns, and this function /// returns null/not null from separate code paths. We /// want the optimizer to remove that branch and instead directly /// fuse the basic blocks after the branch to the basic blocks /// from this function. To encourage that, this function is /// marked as inline. inline fn getIndex(self: Self, key: anytype, ctx: anytype) ?usize { if (self.header().size == 0) { return null; }
// If you get a compile error on this line, it means that your generic hash // function is invalid for these parameters. const hash = ctx.hash(key); if (@TypeOf(hash) != Hash) { @compileError("Context " ++ @typeName(@TypeOf(ctx)) ++ " has a generic hash function that returns the wrong type! " ++ @typeName(Hash) ++ " was expected, but found " ++ @typeName(@TypeOf(hash))); } const mask = self.capacity() - 1; const fingerprint = Metadata.takeFingerprint(hash); // Don't loop indefinitely when there are no empty slots. var limit = self.capacity(); var idx = @as(usize, @truncate(hash & mask));
var metadata = self.metadata.? + idx; while (!metadata[0].isFree() and limit != 0) { if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) { const test_key = &self.keys()[idx]; // If you get a compile error on this line, it means that your generic eql // function is invalid for these parameters. const eql = ctx.eql(key, test_key.*); // verifyContext can't verify the return type of generic eql functions, // so we need to double-check it here. if (@TypeOf(eql) != bool) { @compileError("Context " ++ @typeName(@TypeOf(ctx)) ++ " has a generic eql function that returns the wrong type! bool was expected, but found " ++ @typeName(@TypeOf(eql))); } if (eql) { return idx; } }
limit -= 1; idx = (idx + 1) & mask; metadata = self.metadata.? + idx; }
return null; }
pub fn getEntry(self: Self, key: K) ?Entry { if (@sizeOf(Context) != 0) @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call getEntryContext instead."); return self.getEntryContext(key, undefined); } pub fn getEntryContext(self: Self, key: K, ctx: Context) ?Entry { return self.getEntryAdapted(key, ctx); } pub fn getEntryAdapted(self: Self, key: anytype, ctx: anytype) ?Entry { if (self.getIndex(key, ctx)) |idx| { return Entry{ .key_ptr = &self.keys()[idx], .value_ptr = &self.values()[idx], }; } return null; }
/// Insert an entry if the associated key is not already present, otherwise update preexisting value. pub fn put(self: *Self, key: K, value: V) Allocator.Error!void { if (@sizeOf(Context) != 0) @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call putContext instead."); return self.putContext(key, value, undefined); } pub fn putContext(self: *Self, key: K, value: V, ctx: Context) Allocator.Error!void { const result = try self.getOrPutContext(key, ctx); result.value_ptr.* = value; }
/// Get an optional pointer to the actual key associated with adapted key, if present. pub fn getKeyPtr(self: Self, key: K) ?*K { if (@sizeOf(Context) != 0) @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call getKeyPtrContext instead."); return self.getKeyPtrContext(key, undefined); } pub fn getKeyPtrContext(self: Self, key: K, ctx: Context) ?*K { return self.getKeyPtrAdapted(key, ctx); } pub fn getKeyPtrAdapted(self: Self, key: anytype, ctx: anytype) ?*K { if (self.getIndex(key, ctx)) |idx| { return &self.keys()[idx]; } return null; }
/// Get a copy of the actual key associated with adapted key, if present. pub fn getKey(self: Self, key: K) ?K { if (@sizeOf(Context) != 0) @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call getKeyContext instead."); return self.getKeyContext(key, undefined); } pub fn getKeyContext(self: Self, key: K, ctx: Context) ?K { return self.getKeyAdapted(key, ctx); } pub fn getKeyAdapted(self: Self, key: anytype, ctx: anytype) ?K { if (self.getIndex(key, ctx)) |idx| { return self.keys()[idx]; } return null; }
/// Get an optional pointer to the value associated with key, if present. pub fn getPtr(self: Self, key: K) ?*V { if (@sizeOf(Context) != 0) @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call getPtrContext instead."); return self.getPtrContext(key, undefined); } pub fn getPtrContext(self: Self, key: K, ctx: Context) ?*V { return self.getPtrAdapted(key, ctx); } pub fn getPtrAdapted(self: Self, key: anytype, ctx: anytype) ?*V { if (self.getIndex(key, ctx)) |idx| { return &self.values()[idx]; } return null; }
/// Get a copy of the value associated with key, if present. pub fn get(self: Self, key: K) ?V { if (@sizeOf(Context) != 0) @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call getContext instead."); return self.getContext(key, undefined); } pub fn getContext(self: Self, key: K, ctx: Context) ?V { return self.getAdapted(key, ctx); } pub fn getAdapted(self: Self, key: anytype, ctx: anytype) ?V { if (self.getIndex(key, ctx)) |idx| { return self.values()[idx]; } return null; }
pub fn getOrPut(self: *Self, key: K) Allocator.Error!GetOrPutResult { if (@sizeOf(Context) != 0) @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call getOrPutContext instead."); return self.getOrPutContext(key, undefined); } pub fn getOrPutContext(self: *Self, key: K, ctx: Context) Allocator.Error!GetOrPutResult { const gop = try self.getOrPutContextAdapted(key, ctx); if (!gop.found_existing) { gop.key_ptr.* = key; } return gop; } pub fn getOrPutAdapted(self: *Self, key: anytype, key_ctx: anytype) Allocator.Error!GetOrPutResult { if (@sizeOf(Context) != 0) @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call getOrPutContextAdapted instead."); return self.getOrPutContextAdapted(key, key_ctx); } pub fn getOrPutContextAdapted(self: *Self, key: anytype, key_ctx: anytype) Allocator.Error!GetOrPutResult { self.growIfNeeded(1) catch |err| { // If allocation fails, try to do the lookup anyway. // If we find an existing item, we can return it. // Otherwise return the error, we could not add another. const index = self.getIndex(key, key_ctx) orelse return err; return GetOrPutResult{ .key_ptr = &self.keys()[index], .value_ptr = &self.values()[index], .found_existing = true, }; }; return self.getOrPutAssumeCapacityAdapted(key, key_ctx); }
pub fn getOrPutAssumeCapacity(self: *Self, key: K) GetOrPutResult { if (@sizeOf(Context) != 0) @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call getOrPutAssumeCapacityContext instead."); return self.getOrPutAssumeCapacityContext(key, undefined); } pub fn getOrPutAssumeCapacityContext(self: *Self, key: K, ctx: Context) GetOrPutResult { const result = self.getOrPutAssumeCapacityAdapted(key, ctx); if (!result.found_existing) { result.key_ptr.* = key; } return result; } pub fn getOrPutAssumeCapacityAdapted(self: *Self, key: anytype, ctx: anytype) GetOrPutResult { // If you get a compile error on this line, it means that your generic hash // function is invalid for these parameters. const hash = ctx.hash(key); // verifyContext can't verify the return type of generic hash functions, // so we need to double-check it here. if (@TypeOf(hash) != Hash) { @compileError("Context " ++ @typeName(@TypeOf(ctx)) ++ " has a generic hash function that returns the wrong type! " ++ @typeName(Hash) ++ " was expected, but found " ++ @typeName(@TypeOf(hash))); } const mask = self.capacity() - 1; const fingerprint = Metadata.takeFingerprint(hash); var limit = self.capacity(); var idx = @as(usize, @truncate(hash & mask));
var first_tombstone_idx: usize = self.capacity(); // invalid index var metadata = self.metadata.? + idx; while (!metadata[0].isFree() and limit != 0) { if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) { const test_key = &self.keys()[idx]; // If you get a compile error on this line, it means that your generic eql // function is invalid for these parameters. const eql = ctx.eql(key, test_key.*); // verifyContext can't verify the return type of generic eql functions, // so we need to double-check it here. if (@TypeOf(eql) != bool) { @compileError("Context " ++ @typeName(@TypeOf(ctx)) ++ " has a generic eql function that returns the wrong type! bool was expected, but found " ++ @typeName(@TypeOf(eql))); } if (eql) { return GetOrPutResult{ .key_ptr = test_key, .value_ptr = &self.values()[idx], .found_existing = true, }; } } else if (first_tombstone_idx == self.capacity() and metadata[0].isTombstone()) { first_tombstone_idx = idx; }
limit -= 1; idx = (idx + 1) & mask; metadata = self.metadata.? + idx; }
if (first_tombstone_idx < self.capacity()) { // Cheap try to lower probing lengths after deletions. Recycle a tombstone. idx = first_tombstone_idx; metadata = self.metadata.? + idx; }
metadata[0].fill(fingerprint); const new_key = &self.keys()[idx]; const new_value = &self.values()[idx]; new_key.* = undefined; new_value.* = undefined; self.header().size += 1;
return GetOrPutResult{ .key_ptr = new_key, .value_ptr = new_value, .found_existing = false, }; }
pub fn getOrPutValue(self: *Self, key: K, value: V) Allocator.Error!Entry { if (@sizeOf(Context) != 0) @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call getOrPutValueContext instead."); return self.getOrPutValueContext(key, value, undefined); } pub fn getOrPutValueContext(self: *Self, key: K, value: V, ctx: Context) Allocator.Error!Entry { const res = try self.getOrPutAdapted(key, ctx); if (!res.found_existing) { res.key_ptr.* = key; res.value_ptr.* = value; } return Entry{ .key_ptr = res.key_ptr, .value_ptr = res.value_ptr }; }
/// Return true if there is a value associated with key in the map. pub fn contains(self: *const Self, key: K) bool { if (@sizeOf(Context) != 0) @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call containsContext instead."); return self.containsContext(key, undefined); } pub fn containsContext(self: *const Self, key: K, ctx: Context) bool { return self.containsAdapted(key, ctx); } pub fn containsAdapted(self: *const Self, key: anytype, ctx: anytype) bool { return self.getIndex(key, ctx) != null; }
fn removeByIndex(self: *Self, idx: usize) void { self.metadata.?[idx].remove(); self.keys()[idx] = undefined; self.values()[idx] = undefined; self.header().size -= 1; }
/// If there is an `Entry` with a matching key, it is deleted from /// the hash map, and this function returns true. Otherwise this /// function returns false. pub fn remove(self: *Self, key: K) bool { if (@sizeOf(Context) != 0) @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call removeContext instead."); return self.removeContext(key, undefined); } pub fn removeContext(self: *Self, key: K, ctx: Context) bool { return self.removeAdapted(key, ctx); } pub fn removeAdapted(self: *Self, key: anytype, ctx: anytype) bool { if (self.getIndex(key, ctx)) |idx| { self.removeByIndex(idx); return true; }
return false; }
/// Delete the entry with key pointed to by key_ptr from the hash map. /// key_ptr is assumed to be a valid pointer to a key that is present /// in the hash map. pub fn removeByPtr(self: *Self, key_ptr: *K) void { // TODO: replace with pointer subtraction once supported by zig // if @sizeOf(K) == 0 then there is at most one item in the hash // map, which is assumed to exist as key_ptr must be valid. This // item must be at index 0. const idx = if (@sizeOf(K) > 0) (@intFromPtr(key_ptr) - @intFromPtr(self.keys())) / @sizeOf(K) else 0;
self.removeByIndex(idx); }
fn initMetadatas(self: *Self) void { @memset(@as([*]u8, @ptrCast(self.metadata.?))[0 .. @sizeOf(Metadata) * self.capacity()], 0); }
fn growIfNeeded(self: *Self, new_count: Size) Allocator.Error!void { const available = self.capacity() - self.header().size; if (new_count > available) return error.OutOfMemory; }
/// The memory layout for the underlying buffer for a given capacity. const Layout = struct { /// The total size of the buffer required. The buffer is expected /// to be aligned to `base_align`. total_size: usize,
/// The offset to the start of the keys data. keys_start: usize,
/// The offset to the start of the values data. vals_start: usize,
/// The capacity that was used to calculate this layout. capacity: Size, };
/// Returns the memory layout for the buffer for a given capacity. /// The actual size may be able to fit more than the given capacity /// because capacity is rounded up to the next power of two. This is /// a design requirement for this hash map implementation. pub fn layoutForCapacity(new_capacity: Size) Layout { assert(new_capacity == 0 or std.math.isPowerOfTwo(new_capacity));
// Cast to usize to prevent overflow in size calculations. // See: https://github.com/ziglang/zig/pull/19048 const cap: usize = new_capacity;
// Pack our metadata, keys, and values. const meta_start = @sizeOf(Header); const meta_end = @sizeOf(Header) + cap * @sizeOf(Metadata); const keys_start = std.mem.alignForward(usize, meta_end, key_align); const keys_end = keys_start + cap * @sizeOf(K); const vals_start = std.mem.alignForward(usize, keys_end, val_align); const vals_end = vals_start + cap * @sizeOf(V);
// Our total memory size required is the end of our values // aligned to the base required alignment. const total_size = std.mem.alignForward( usize, vals_end, base_align.toByteUnits(), );
// The offsets we actually store in the map are from the // metadata pointer so that we can use self.metadata as // the base. const keys_offset = keys_start - meta_start; const vals_offset = vals_start - meta_start;
return .{ .total_size = total_size, .keys_start = keys_offset, .vals_start = vals_offset, .capacity = new_capacity, }; } };}
const testing = std.testing;const expect = std.testing.expect;const expectEqual = std.testing.expectEqual;
test "HashMap basic usage" { const Map = AutoHashMapUnmanaged(u32, u32);
const alloc = testing.allocator; const cap = 16; const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf);
var map = Map.init(.init(buf), layout);
const count = 5; var i: u32 = 0; var total: u32 = 0; while (i < count) : (i += 1) { try map.put(i, i); total += i; }
var sum: u32 = 0; var it = map.iterator(); while (it.next()) |kv| { sum += kv.key_ptr.*; } try expectEqual(total, sum);
i = 0; sum = 0; while (i < count) : (i += 1) { try expectEqual(i, map.get(i).?); sum += map.get(i).?; } try expectEqual(total, sum);}
test "HashMap ensureTotalCapacity" { const Map = AutoHashMapUnmanaged(i32, i32); const cap = 32;
const alloc = testing.allocator; const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); var map = Map.init(.init(buf), layout);
const initial_capacity = map.capacity(); try testing.expect(initial_capacity >= 20); var i: i32 = 0; while (i < 20) : (i += 1) { try testing.expect(map.fetchPutAssumeCapacity(i, i + 10) == null); } // shouldn't resize from putAssumeCapacity try testing.expect(initial_capacity == map.capacity());}
test "HashMap ensureUnusedCapacity with tombstones" { const Map = AutoHashMapUnmanaged(i32, i32); const cap = 32;
const alloc = testing.allocator; const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); var map = Map.init(.init(buf), layout);
var i: i32 = 0; while (i < 100) : (i += 1) { try map.ensureUnusedCapacity(1); map.putAssumeCapacity(i, i); _ = map.remove(i); }}
test "HashMap clearRetainingCapacity" { const Map = AutoHashMapUnmanaged(u32, u32); const cap = 16;
const alloc = testing.allocator; const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); var map = Map.init(.init(buf), layout);
map.clearRetainingCapacity();
try map.put(1, 1); try expectEqual(map.get(1).?, 1); try expectEqual(map.count(), 1);
map.clearRetainingCapacity(); map.putAssumeCapacity(1, 1); try expectEqual(map.get(1).?, 1); try expectEqual(map.count(), 1);
const actual_cap = map.capacity(); try expect(actual_cap > 0);
map.clearRetainingCapacity(); map.clearRetainingCapacity(); try expectEqual(map.count(), 0); try expectEqual(map.capacity(), actual_cap); try expect(!map.contains(1));}
test "HashMap ensureTotalCapacity with existing elements" { const Map = AutoHashMapUnmanaged(u32, u32); const cap = Map.minimal_capacity;
const alloc = testing.allocator; const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); var map = Map.init(.init(buf), layout);
try map.put(0, 0); try expectEqual(map.count(), 1); try expectEqual(map.capacity(), Map.minimal_capacity);
try testing.expectError(error.OutOfMemory, map.ensureTotalCapacity(65)); try expectEqual(map.count(), 1); try expectEqual(map.capacity(), Map.minimal_capacity);}
test "HashMap remove" { const Map = AutoHashMapUnmanaged(u32, u32); const cap = 32;
const alloc = testing.allocator; const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); var map = Map.init(.init(buf), layout);
var i: u32 = 0; while (i < 16) : (i += 1) { try map.put(i, i); }
i = 0; while (i < 16) : (i += 1) { if (i % 3 == 0) { _ = map.remove(i); } } try expectEqual(map.count(), 10); var it = map.iterator(); while (it.next()) |kv| { try expectEqual(kv.key_ptr.*, kv.value_ptr.*); try expect(kv.key_ptr.* % 3 != 0); }
i = 0; while (i < 16) : (i += 1) { if (i % 3 == 0) { try expect(!map.contains(i)); } else { try expectEqual(map.get(i).?, i); } }}
test "HashMap reverse removes" { const Map = AutoHashMapUnmanaged(u32, u32); const cap = 32;
const alloc = testing.allocator; const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); var map = Map.init(.init(buf), layout);
var i: u32 = 0; while (i < 16) : (i += 1) { try map.putNoClobber(i, i); }
i = 16; while (i > 0) : (i -= 1) { _ = map.remove(i - 1); try expect(!map.contains(i - 1)); var j: u32 = 0; while (j < i - 1) : (j += 1) { try expectEqual(map.get(j).?, j); } }
try expectEqual(map.count(), 0);}
test "HashMap multiple removes on same metadata" { const Map = AutoHashMapUnmanaged(u32, u32); const cap = 32;
const alloc = testing.allocator; const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); var map = Map.init(.init(buf), layout);
var i: u32 = 0; while (i < 16) : (i += 1) { try map.put(i, i); }
_ = map.remove(7); _ = map.remove(15); _ = map.remove(14); _ = map.remove(13); try expect(!map.contains(7)); try expect(!map.contains(15)); try expect(!map.contains(14)); try expect(!map.contains(13));
i = 0; while (i < 13) : (i += 1) { if (i == 7) { try expect(!map.contains(i)); } else { try expectEqual(map.get(i).?, i); } }
try map.put(15, 15); try map.put(13, 13); try map.put(14, 14); try map.put(7, 7); i = 0; while (i < 16) : (i += 1) { try expectEqual(map.get(i).?, i); }}
test "HashMap put and remove loop in random order" { const Map = AutoHashMapUnmanaged(u32, u32); const cap = 64;
const alloc = testing.allocator; const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); var map = Map.init(.init(buf), layout);
var keys: std.ArrayList(u32) = .empty; defer keys.deinit(alloc);
const size = 32; const iterations = 100;
var i: u32 = 0; while (i < size) : (i += 1) { try keys.append(alloc, i); } var prng = std.Random.DefaultPrng.init(0); const random = prng.random();
while (i < iterations) : (i += 1) { random.shuffle(u32, keys.items);
for (keys.items) |key| { try map.put(key, key); } try expectEqual(map.count(), size);
for (keys.items) |key| { _ = map.remove(key); } try expectEqual(map.count(), 0); }}
test "HashMap put" { const Map = AutoHashMapUnmanaged(u32, u32); const cap = 32;
const alloc = testing.allocator; const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); var map = Map.init(.init(buf), layout);
var i: u32 = 0; while (i < 16) : (i += 1) { try map.put(i, i); }
i = 0; while (i < 16) : (i += 1) { try expectEqual(map.get(i).?, i); }
i = 0; while (i < 16) : (i += 1) { try map.put(i, i * 16 + 1); }
i = 0; while (i < 16) : (i += 1) { try expectEqual(map.get(i).?, i * 16 + 1); }}
test "HashMap put full load" { const Map = AutoHashMapUnmanaged(usize, usize); const cap = 16;
const alloc = testing.allocator; const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); var map = Map.init(.init(buf), layout);
for (0..cap) |i| try map.put(i, i); for (0..cap) |i| try expectEqual(map.get(i).?, i);
try testing.expectError(error.OutOfMemory, map.put(cap, cap));}
test "HashMap putAssumeCapacity" { const Map = AutoHashMapUnmanaged(u32, u32); const cap = 32;
const alloc = testing.allocator; const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); var map = Map.init(.init(buf), layout);
var i: u32 = 0; while (i < 20) : (i += 1) { map.putAssumeCapacityNoClobber(i, i); }
i = 0; var sum = i; while (i < 20) : (i += 1) { sum += map.getPtr(i).?.*; } try expectEqual(sum, 190);
i = 0; while (i < 20) : (i += 1) { map.putAssumeCapacity(i, 1); }
i = 0; sum = i; while (i < 20) : (i += 1) { sum += map.get(i).?; } try expectEqual(sum, 20);}
test "HashMap repeat putAssumeCapacity/remove" { const Map = AutoHashMapUnmanaged(u32, u32); const cap = 32;
const alloc = testing.allocator; const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); var map = Map.init(.init(buf), layout);
const limit = cap;
var i: u32 = 0; while (i < limit) : (i += 1) { map.putAssumeCapacityNoClobber(i, i); }
// Repeatedly delete/insert an entry without resizing the map. // Put to different keys so entries don't land in the just-freed slot. i = 0; while (i < 10 * limit) : (i += 1) { try testing.expect(map.remove(i)); if (i % 2 == 0) { map.putAssumeCapacityNoClobber(limit + i, i); } else { map.putAssumeCapacity(limit + i, i); } }
i = 9 * limit; while (i < 10 * limit) : (i += 1) { try expectEqual(map.get(limit + i), i); } try expectEqual(map.count(), limit);}
test "HashMap getOrPut" { const Map = AutoHashMapUnmanaged(u32, u32); const cap = 32;
const alloc = testing.allocator; const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); var map = Map.init(.init(buf), layout);
var i: u32 = 0; while (i < 10) : (i += 1) { try map.put(i * 2, 2); }
i = 0; while (i < 20) : (i += 1) { _ = try map.getOrPutValue(i, 1); }
i = 0; var sum = i; while (i < 20) : (i += 1) { sum += map.get(i).?; }
try expectEqual(sum, 30);}
test "HashMap basic hash map usage" { const Map = AutoHashMapUnmanaged(i32, i32); const cap = 32;
const alloc = testing.allocator; const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); var map = Map.init(.init(buf), layout);
try testing.expect((try map.fetchPut(1, 11)) == null); try testing.expect((try map.fetchPut(2, 22)) == null); try testing.expect((try map.fetchPut(3, 33)) == null); try testing.expect((try map.fetchPut(4, 44)) == null);
try map.putNoClobber(5, 55); try testing.expect((try map.fetchPut(5, 66)).?.value == 55); try testing.expect((try map.fetchPut(5, 55)).?.value == 66);
const gop1 = try map.getOrPut(5); try testing.expect(gop1.found_existing == true); try testing.expect(gop1.value_ptr.* == 55); gop1.value_ptr.* = 77; try testing.expect(map.getEntry(5).?.value_ptr.* == 77);
const gop2 = try map.getOrPut(99); try testing.expect(gop2.found_existing == false); gop2.value_ptr.* = 42; try testing.expect(map.getEntry(99).?.value_ptr.* == 42);
const gop3 = try map.getOrPutValue(5, 5); try testing.expect(gop3.value_ptr.* == 77);
const gop4 = try map.getOrPutValue(100, 41); try testing.expect(gop4.value_ptr.* == 41);
try testing.expect(map.contains(2)); try testing.expect(map.getEntry(2).?.value_ptr.* == 22); try testing.expect(map.get(2).? == 22);
const rmv1 = map.fetchRemove(2); try testing.expect(rmv1.?.key == 2); try testing.expect(rmv1.?.value == 22); try testing.expect(map.fetchRemove(2) == null); try testing.expect(map.remove(2) == false); try testing.expect(map.getEntry(2) == null); try testing.expect(map.get(2) == null);
try testing.expect(map.remove(3) == true);}
test "HashMap ensureUnusedCapacity" { const Map = AutoHashMapUnmanaged(u64, u64); const cap = 64;
const alloc = testing.allocator; const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); var map = Map.init(.init(buf), layout);
try map.ensureUnusedCapacity(32); try testing.expectError(error.OutOfMemory, map.ensureUnusedCapacity(cap + 1));}
test "HashMap removeByPtr" { const Map = AutoHashMapUnmanaged(i32, u64); const cap = 64;
const alloc = testing.allocator; const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); var map = Map.init(.init(buf), layout);
var i: i32 = undefined; i = 0; while (i < 10) : (i += 1) { try map.put(i, 0); }
try testing.expect(map.count() == 10);
i = 0; while (i < 10) : (i += 1) { const key_ptr = map.getKeyPtr(i); try testing.expect(key_ptr != null);
if (key_ptr) |ptr| { map.removeByPtr(ptr); } }
try testing.expect(map.count() == 0);}
test "HashMap removeByPtr 0 sized key" { const Map = AutoHashMapUnmanaged(i32, u64); const cap = 64;
const alloc = testing.allocator; const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); var map = Map.init(.init(buf), layout);
try map.put(0, 0);
try testing.expect(map.count() == 1);
const key_ptr = map.getKeyPtr(0); try testing.expect(key_ptr != null);
if (key_ptr) |ptr| { map.removeByPtr(ptr); }
try testing.expect(map.count() == 0);}
test "HashMap repeat fetchRemove" { const Map = AutoHashMapUnmanaged(u64, void); const cap = 64;
const alloc = testing.allocator; const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); var map = Map.init(.init(buf), layout);
map.putAssumeCapacity(0, {}); map.putAssumeCapacity(1, {}); map.putAssumeCapacity(2, {}); map.putAssumeCapacity(3, {});
// fetchRemove() should make slots available. var i: usize = 0; while (i < 10) : (i += 1) { try testing.expect(map.fetchRemove(3) != null); map.putAssumeCapacity(3, {}); }
try testing.expect(map.get(0) != null); try testing.expect(map.get(1) != null); try testing.expect(map.get(2) != null); try testing.expect(map.get(3) != null);}
test "OffsetHashMap basic usage" { const OffsetMap = AutoOffsetHashMap(u32, u32); const cap = 16;
const alloc = testing.allocator; const layout = OffsetMap.layout(cap); const buf = try alloc.alignedAlloc(u8, OffsetMap.base_align, layout.total_size); defer alloc.free(buf); var offset_map = OffsetMap.init(.init(buf), layout); var map = offset_map.map(buf.ptr);
const count = 5; var i: u32 = 0; var total: u32 = 0; while (i < count) : (i += 1) { try map.put(i, i); total += i; }
var sum: u32 = 0; var it = map.iterator(); while (it.next()) |kv| { sum += kv.key_ptr.*; } try expectEqual(total, sum);
i = 0; sum = 0; while (i < count) : (i += 1) { try expectEqual(i, map.get(i).?); sum += map.get(i).?; } try expectEqual(total, sum);}
test "OffsetHashMap remake map" { const OffsetMap = AutoOffsetHashMap(u32, u32); const cap = 16;
const alloc = testing.allocator; const layout = OffsetMap.layout(cap); const buf = try alloc.alignedAlloc(u8, OffsetMap.base_align, layout.total_size); defer alloc.free(buf); var offset_map = OffsetMap.init(.init(buf), layout);
{ var map = offset_map.map(buf.ptr); try map.put(5, 5); }
{ var map = offset_map.map(buf.ptr); try expectEqual(5, map.get(5).?); }}
test "layoutForCapacity no overflow for large capacity" { // Test that layoutForCapacity correctly handles large capacities without overflow. // Prior to the fix, new_capacity (u32) was multiplied before widening to usize, // causing overflow when new_capacity * @sizeOf(K) exceeded 2^32. // See: https://github.com/ghostty-org/ghostty/issues/9862 const Map = AutoHashMapUnmanaged(u64, u64);
// Use 2^30 capacity - this would overflow in u32 when multiplied by @sizeOf(u64)=8 // 0x40000000 * 8 = 0x2_0000_0000 which wraps to 0 in u32 const large_cap: Map.Size = 1 << 30; const layout = Map.layoutForCapacity(large_cap);
// With the fix, total_size should be at least cap * (sizeof(K) + sizeof(V)) // = 2^30 * 16 = 2^34 bytes = 16 GiB // Without the fix, this would wrap and produce a much smaller value. const min_expected: usize = @as(usize, large_cap) * (@sizeOf(u64) + @sizeOf(u64)); try expect(layout.total_size >= min_expected);
// Also verify the individual offsets don't wrap try expect(layout.keys_start > 0); try expect(layout.vals_start > layout.keys_start);}