diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/terminal/hash_map.zig | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/hash_map.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/hash_map.zig | 1540 |
1 files changed, 1540 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/terminal/hash_map.zig b/vendor/libghostty-vt/src/terminal/hash_map.zig new file mode 100644 index 00000000..5a38cc17 --- /dev/null +++ b/vendor/libghostty-vt/src/terminal/hash_map.zig @@ -0,0 +1,1540 @@ +//! 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); +} |
