diff options
Diffstat (limited to 'lib/std/hash_map.zig')
| -rw-r--r-- | lib/std/hash_map.zig | 1097 |
1 files changed, 872 insertions, 225 deletions
diff --git a/lib/std/hash_map.zig b/lib/std/hash_map.zig index 62f577de1a..3a3c30bdf8 100644 --- a/lib/std/hash_map.zig +++ b/lib/std/hash_map.zig @@ -15,7 +15,7 @@ const trait = meta.trait; const Allocator = mem.Allocator; const Wyhash = std.hash.Wyhash; -pub fn getAutoHashFn(comptime K: type) (fn (K) u64) { +pub fn getAutoHashFn(comptime K: type, comptime Context: type) (fn (Context, K) u64) { comptime { assert(@hasDecl(std, "StringHashMap")); // detect when the following message needs updated if (K == []const u8) { @@ -28,7 +28,7 @@ pub fn getAutoHashFn(comptime K: type) (fn (K) u64) { } return struct { - fn hash(key: K) u64 { + fn hash(ctx: Context, key: K) u64 { if (comptime trait.hasUniqueRepresentation(K)) { return Wyhash.hash(0, std.mem.asBytes(&key)); } else { @@ -40,31 +40,51 @@ pub fn getAutoHashFn(comptime K: type) (fn (K) u64) { }.hash; } -pub fn getAutoEqlFn(comptime K: type) (fn (K, K) bool) { +pub fn getAutoEqlFn(comptime K: type, comptime Context: type) (fn (Context, K, K) bool) { return struct { - fn eql(a: K, b: K) bool { + fn eql(ctx: Context, a: K, b: K) bool { return meta.eql(a, b); } }.eql; } pub fn AutoHashMap(comptime K: type, comptime V: type) type { - return HashMap(K, V, getAutoHashFn(K), getAutoEqlFn(K), default_max_load_percentage); + return HashMap(K, V, AutoContext(K), default_max_load_percentage); } pub fn AutoHashMapUnmanaged(comptime K: type, comptime V: type) type { - return HashMapUnmanaged(K, V, getAutoHashFn(K), getAutoEqlFn(K), default_max_load_percentage); + return HashMapUnmanaged(K, V, AutoContext(K), default_max_load_percentage); +} + +pub fn AutoContext(comptime K: type) type { + return struct { + pub const hash = getAutoHashFn(K, @This()); + pub const eql = getAutoEqlFn(K, @This()); + }; } /// Builtin hashmap for strings as keys. +/// Key memory is managed by the caller. Keys and values +/// will not automatically be freed. pub fn StringHashMap(comptime V: type) type { - return HashMap([]const u8, V, hashString, eqlString, default_max_load_percentage); + return HashMap([]const u8, V, StringContext, default_max_load_percentage); } +/// Key memory is managed by the caller. Keys and values +/// will not automatically be freed. pub fn StringHashMapUnmanaged(comptime V: type) type { - return HashMapUnmanaged([]const u8, V, hashString, eqlString, default_max_load_percentage); + return HashMapUnmanaged([]const u8, V, StringContext, default_max_load_percentage); } +pub const StringContext = struct { + pub fn hash(self: @This(), s: []const u8) u64 { + return hashString(s); + } + pub fn eql(self: @This(), a: []const u8, b: []const u8) bool { + return eqlString(a, b); + } +}; + pub fn eqlString(a: []const u8, b: []const u8) bool { return mem.eql(u8, a, b); } @@ -78,6 +98,222 @@ pub const DefaultMaxLoadPercentage = default_max_load_percentage; pub const default_max_load_percentage = 80; +/// This function issues a compile error with a helpful message if there +/// is a problem with the provided context type. A context must have the following +/// member functions: +/// - hash(self, PseudoKey) Hash +/// - eql(self, PseudoKey, Key) bool +/// If you are passing a context to a *Adapted function, PseudoKey is the type +/// of the key parameter. Otherwise, when creating a HashMap or HashMapUnmanaged +/// type, PseudoKey = Key = K. +pub fn verifyContext(comptime RawContext: type, comptime PseudoKey: type, comptime Key: type, comptime Hash: type) void { + comptime { + var allow_const_ptr = false; + var allow_mutable_ptr = false; + // Context is the actual namespace type. RawContext may be a pointer to Context. + var Context = RawContext; + // Make sure the context is a namespace type which may have member functions + switch (@typeInfo(Context)) { + .Struct, .Union, .Enum => {}, + // Special-case .Opaque for a better error message + .Opaque => @compileError("Hash context must be a type with hash and eql member functions. Cannot use "++@typeName(Context)++" because it is opaque. Use a pointer instead."), + .Pointer => |ptr| { + if (ptr.size != .One) { + @compileError("Hash context must be a type with hash and eql member functions. Cannot use "++@typeName(Context)++" because it is not a single pointer."); + } + Context = ptr.child; + allow_const_ptr = true; + allow_mutable_ptr = !ptr.is_const; + switch (@typeInfo(Context)) { + .Struct, .Union, .Enum, .Opaque => {}, + else => @compileError("Hash context must be a type with hash and eql member functions. Cannot use "++@typeName(Context)), + } + }, + else => @compileError("Hash context must be a type with hash and eql member functions. Cannot use "++@typeName(Context)), + } + + // Keep track of multiple errors so we can report them all. + var errors: []const u8 = ""; + + // Put common errors here, they will only be evaluated + // if the error is actually triggered. + const lazy = struct { + const prefix = "\n "; + const deep_prefix = prefix ++ " "; + const hash_signature = "fn (self, "++@typeName(PseudoKey)++") "++@typeName(Hash); + const eql_signature = "fn (self, "++@typeName(PseudoKey)++", "++@typeName(Key)++") bool"; + const err_invalid_hash_signature = prefix ++ @typeName(Context) ++ ".hash must be " ++ hash_signature ++ + deep_prefix ++ "but is actually " ++ @typeName(@TypeOf(Context.hash)); + const err_invalid_eql_signature = prefix ++ @typeName(Context) ++ ".eql must be " ++ eql_signature ++ + deep_prefix ++ "but is actually " ++ @typeName(@TypeOf(Context.eql)); + }; + + // Verify Context.hash(self, PseudoKey) => Hash + if (@hasDecl(Context, "hash")) { + const hash = Context.hash; + const info = @typeInfo(@TypeOf(hash)); + if (info == .Fn) { + const func = info.Fn; + if (func.args.len != 2) { + errors = errors ++ lazy.err_invalid_hash_signature; + } else { + var emitted_signature = false; + if (func.args[0].arg_type) |Self| { + if (Self == Context) { + // pass, this is always fine. + } else if (Self == *const Context) { + if (!allow_const_ptr) { + if (!emitted_signature) { + errors = errors ++ lazy.err_invalid_hash_signature; + emitted_signature = true; + } + errors = errors ++ lazy.deep_prefix ++ "First parameter must be "++@typeName(Context)++", but is "++@typeName(Self); + errors = errors ++ lazy.deep_prefix ++ "Note: Cannot be a pointer because it is passed by value."; + } + } else if (Self == *Context) { + if (!allow_mutable_ptr) { + if (!emitted_signature) { + errors = errors ++ lazy.err_invalid_hash_signature; + emitted_signature = true; + } + if (!allow_const_ptr) { + errors = errors ++ lazy.deep_prefix ++ "First parameter must be "++@typeName(Context)++", but is "++@typeName(Self); + errors = errors ++ lazy.deep_prefix ++ "Note: Cannot be a pointer because it is passed by value."; + } else { + errors = errors ++ lazy.deep_prefix ++ "First parameter must be "++@typeName(Context)++" or "++@typeName(*const Context)++", but is "++@typeName(Self); + errors = errors ++ lazy.deep_prefix ++ "Note: Cannot be non-const because it is passed by const pointer."; + } + } + } else { + if (!emitted_signature) { + errors = errors ++ lazy.err_invalid_hash_signature; + emitted_signature = true; + } + errors = errors ++ lazy.deep_prefix ++ "First parameter must be "++@typeName(Context); + if (allow_const_ptr) { + errors = errors++" or "++@typeName(*const Context); + if (allow_mutable_ptr) { + errors = errors++" or "++@typeName(*Context); + } + } + errors = errors++", but is "++@typeName(Self); + } + } + if (func.args[1].arg_type != null and func.args[1].arg_type.? != PseudoKey) { + if (!emitted_signature) { + errors = errors ++ lazy.err_invalid_hash_signature; + emitted_signature = true; + } + errors = errors ++ lazy.deep_prefix ++ "Second parameter must be "++@typeName(PseudoKey)++", but is "++@typeName(func.args[1].arg_type.?); + } + if (func.return_type != null and func.return_type.? != Hash) { + if (!emitted_signature) { + errors = errors ++ lazy.err_invalid_hash_signature; + emitted_signature = true; + } + errors = errors ++ lazy.deep_prefix ++ "Return type must be "++@typeName(Hash)++", but was "++@typeName(func.return_type.?); + } + // If any of these are generic (null), we cannot verify them. + // The call sites check the return type, but cannot check the + // parameters. This may cause compile errors with generic hash/eql functions. + } + } else { + errors = errors ++ lazy.err_invalid_hash_signature; + } + } else { + errors = errors ++ lazy.prefix ++ @typeName(Context) ++ " must declare a hash function with signature " ++ lazy.hash_signature; + } + + // Verify Context.eql(self, PseudoKey, Key) => bool + if (@hasDecl(Context, "eql")) { + const eql = Context.eql; + const info = @typeInfo(@TypeOf(eql)); + if (info == .Fn) { + const func = info.Fn; + if (func.args.len != 3) { + errors = errors ++ lazy.err_invalid_eql_signature; + } else { + var emitted_signature = false; + if (func.args[0].arg_type) |Self| { + if (Self == Context) { + // pass, this is always fine. + } else if (Self == *const Context) { + if (!allow_const_ptr) { + if (!emitted_signature) { + errors = errors ++ lazy.err_invalid_eql_signature; + emitted_signature = true; + } + errors = errors ++ lazy.deep_prefix ++ "First parameter must be "++@typeName(Context)++", but is "++@typeName(Self); + errors = errors ++ lazy.deep_prefix ++ "Note: Cannot be a pointer because it is passed by value."; + } + } else if (Self == *Context) { + if (!allow_mutable_ptr) { + if (!emitted_signature) { + errors = errors ++ lazy.err_invalid_eql_signature; + emitted_signature = true; + } + if (!allow_const_ptr) { + errors = errors ++ lazy.deep_prefix ++ "First parameter must be "++@typeName(Context)++", but is "++@typeName(Self); + errors = errors ++ lazy.deep_prefix ++ "Note: Cannot be a pointer because it is passed by value."; + } else { + errors = errors ++ lazy.deep_prefix ++ "First parameter must be "++@typeName(Context)++" or "++@typeName(*const Context)++", but is "++@typeName(Self); + errors = errors ++ lazy.deep_prefix ++ "Note: Cannot be non-const because it is passed by const pointer."; + } + } + } else { + if (!emitted_signature) { + errors = errors ++ lazy.err_invalid_eql_signature; + emitted_signature = true; + } + errors = errors ++ lazy.deep_prefix ++ "First parameter must be "++@typeName(Context); + if (allow_const_ptr) { + errors = errors++" or "++@typeName(*const Context); + if (allow_mutable_ptr) { + errors = errors++" or "++@typeName(*Context); + } + } + errors = errors++", but is "++@typeName(Self); + } + } + if (func.args[1].arg_type.? != PseudoKey) { + if (!emitted_signature) { + errors = errors ++ lazy.err_invalid_eql_signature; + emitted_signature = true; + } + errors = errors ++ lazy.deep_prefix ++ "Second parameter must be "++@typeName(PseudoKey)++", but is "++@typeName(func.args[1].arg_type.?); + } + if (func.args[2].arg_type.? != Key) { + if (!emitted_signature) { + errors = errors ++ lazy.err_invalid_eql_signature; + emitted_signature = true; + } + errors = errors ++ lazy.deep_prefix ++ "Third parameter must be "++@typeName(Key)++", but is "++@typeName(func.args[2].arg_type.?); + } + if (func.return_type.? != bool) { + if (!emitted_signature) { + errors = errors ++ lazy.err_invalid_eql_signature; + emitted_signature = true; + } + errors = errors ++ lazy.deep_prefix ++ "Return type must be bool, but was "++@typeName(func.return_type.?); + } + // If any of these are generic (null), we cannot verify them. + // The call sites check the return type, but cannot check the + // parameters. This may cause compile errors with generic hash/eql functions. + } + } else { + errors = errors ++ lazy.err_invalid_eql_signature; + } + } else { + errors = errors ++ lazy.prefix ++ @typeName(Context) ++ " must declare a eql function with signature " ++ lazy.eql_signature; + } + + if (errors.len != 0) { + // errors begins with a newline (from lazy.prefix) + @compileError("Problems found with hash context type "++@typeName(Context)++":"++errors); + } + } +} + /// General purpose hash table. /// No order is guaranteed and any modification invalidates live iterators. /// It provides fast operations (lookup, insertion, deletion) with quite high @@ -86,83 +322,167 @@ pub const default_max_load_percentage = 80; /// field, see `HashMapUnmanaged`. /// If iterating over the table entries is a strong usecase and needs to be fast, /// prefer the alternative `std.ArrayHashMap`. +/// Context must be a struct type with two member functions: +/// hash(self, K) u64 +/// eql(self, K, K) bool +/// Adapted variants of many functions are provided. These variants +/// take a pseudo key instead of a key. Their context must have the functions: +/// hash(self, PseudoKey) u64 +/// eql(self, PseudoKey, K) bool pub fn HashMap( comptime K: type, comptime V: type, - comptime hashFn: fn (key: K) u64, - comptime eqlFn: fn (a: K, b: K) bool, + comptime Context: type, comptime max_load_percentage: u64, ) type { + comptime verifyContext(Context, K, K, u64); return struct { unmanaged: Unmanaged, allocator: *Allocator, + ctx: Context, - pub const Unmanaged = HashMapUnmanaged(K, V, hashFn, eqlFn, max_load_percentage); + /// The type of the unmanaged hash map underlying this wrapper + pub const Unmanaged = HashMapUnmanaged(K, V, Context, max_load_percentage); + /// An entry, containing pointers to a key and value stored in the map pub const Entry = Unmanaged.Entry; + /// A copy of a key and value which are no longer in the map + pub const KV = Unmanaged.KV; + /// The integer type that is the result of hashing pub const Hash = Unmanaged.Hash; + /// The iterator type returned by iterator() pub const Iterator = Unmanaged.Iterator; + + pub const KeyIterator = Unmanaged.KeyIterator; + pub const ValueIterator = Unmanaged.ValueIterator; + + /// The integer type used to store the size of the map pub const Size = Unmanaged.Size; + /// The type returned from getOrPut and variants pub const GetOrPutResult = Unmanaged.GetOrPutResult; const Self = @This(); + /// Create a managed hash map with an empty context. + /// If the context is not zero-sized, you must use + /// initContext(allocator, ctx) instead. pub fn init(allocator: *Allocator) Self { + if (@sizeOf(Context) != 0) { + @compileError("Context must be specified! Call initContext(allocator, ctx) instead."); + } + return .{ + .unmanaged = .{}, + .allocator = allocator, + .ctx = undefined, // ctx is zero-sized so this is safe. + }; + } + + /// Create a managed hash map with a context + pub fn initContext(allocator: *Allocator, ctx: Context) Self { return .{ .unmanaged = .{}, .allocator = allocator, + .ctx = ctx, }; } + /// Release the backing array and invalidate this map. + /// This does *not* deinit keys, values, or the context! + /// If your keys or values need to be released, ensure + /// that that is done before calling this function. pub fn deinit(self: *Self) void { self.unmanaged.deinit(self.allocator); self.* = undefined; } + /// Empty the map, but keep the backing allocation for future use. + /// This does *not* free keys or values! Be sure to + /// release them if they need deinitialization before + /// calling this function. pub fn clearRetainingCapacity(self: *Self) void { return self.unmanaged.clearRetainingCapacity(); } + /// Empty the map and release the backing allocation. + /// This does *not* free keys or values! Be sure to + /// release them if they need deinitialization before + /// calling this function. pub fn clearAndFree(self: *Self) void { return self.unmanaged.clearAndFree(self.allocator); } + /// Return the number of items in the map. pub fn count(self: Self) Size { return self.unmanaged.count(); } + /// Create an iterator over the entries in the map. + /// The iterator is invalidated if the map is modified. pub fn iterator(self: *const Self) Iterator { return self.unmanaged.iterator(); } + /// Create an iterator over the keys in the map. + /// The iterator is invalidated if the map is modified. + pub fn keyIterator(self: *const Self) KeyIterator { + return self.unmanaged.keyIterator(); + } + + /// Create an iterator over the values in the map. + /// The iterator is invalidated if the map is modified. + pub fn valueIterator(self: *const Self) ValueIterator { + return self.unmanaged.valueIterator(); + } + /// If key exists this function cannot fail. /// If there is an existing item with `key`, then the result - /// `Entry` pointer points to it, and found_existing is true. + /// `Entry` pointers point to it, and found_existing is true. /// Otherwise, puts a new item with undefined value, and - /// the `Entry` pointer points to it. Caller should then initialize + /// the `Entry` pointers point to it. Caller should then initialize /// the value (but not the key). pub fn getOrPut(self: *Self, key: K) !GetOrPutResult { - return self.unmanaged.getOrPut(self.allocator, key); + return self.unmanaged.getOrPutContext(self.allocator, key, self.ctx); + } + + /// If key exists this function cannot fail. + /// If there is an existing item with `key`, then the result + /// `Entry` pointers point to it, and found_existing is true. + /// Otherwise, puts a new item with undefined key and value, and + /// the `Entry` pointers point to it. Caller must then initialize + /// the key and value. + pub fn getOrPutAdapted(self: *Self, key: anytype, ctx: anytype) !GetOrPutResult { + return self.unmanaged.getOrPutContextAdapted(self.allocator, key, ctx, self.ctx); } /// If there is an existing item with `key`, then the result - /// `Entry` pointer points to it, and found_existing is true. + /// `Entry` pointers point to it, and found_existing is true. /// Otherwise, puts a new item with undefined value, and - /// the `Entry` pointer points to it. Caller should then initialize + /// the `Entry` pointers point to it. Caller should then initialize /// the value (but not the key). /// If a new entry needs to be stored, this function asserts there /// is enough capacity to store it. pub fn getOrPutAssumeCapacity(self: *Self, key: K) GetOrPutResult { - return self.unmanaged.getOrPutAssumeCapacity(key); + return self.unmanaged.getOrPutAssumeCapacityContext(key, self.ctx); + } + + /// If there is an existing item with `key`, then the result + /// `Entry` pointers point to it, and found_existing is true. + /// Otherwise, puts a new item with undefined value, and + /// the `Entry` pointers point to it. Caller must then initialize + /// the key and value. + /// If a new entry needs to be stored, this function asserts there + /// is enough capacity to store it. + pub fn getOrPutAssumeCapacityAdapted(self: *Self, key: anytype, ctx: anytype) GetOrPutResult { + return self.unmanaged.getOrPutAssumeCapacityAdapted(self.allocator, key, ctx); } - pub fn getOrPutValue(self: *Self, key: K, value: V) !*Entry { - return self.unmanaged.getOrPutValue(self.allocator, key, value); + pub fn getOrPutValue(self: *Self, key: K, value: V) !Entry { + return self.unmanaged.getOrPutValueContext(self.allocator, key, value, self.ctx); } /// Increases capacity, guaranteeing that insertions up until the /// `expected_count` will not cause an allocation, and therefore cannot fail. pub fn ensureCapacity(self: *Self, expected_count: Size) !void { - return self.unmanaged.ensureCapacity(self.allocator, expected_count); + return self.unmanaged.ensureCapacityContext(self.allocator, expected_count, self.ctx); } /// Returns the number of total elements which may be present before it is @@ -174,67 +494,114 @@ pub fn HashMap( /// Clobbers any existing data. To detect if a put would clobber /// existing data, see `getOrPut`. pub fn put(self: *Self, key: K, value: V) !void { - return self.unmanaged.put(self.allocator, key, value); + return self.unmanaged.putContext(self.allocator, key, value, self.ctx); } /// Inserts a key-value pair into the hash map, asserting that no previous /// entry with the same key is already present pub fn putNoClobber(self: *Self, key: K, value: V) !void { - return self.unmanaged.putNoClobber(self.allocator, key, value); + return self.unmanaged.putNoClobberContext(self.allocator, key, value, self.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 { - return self.unmanaged.putAssumeCapacity(key, value); + return self.unmanaged.putAssumeCapacityContext(key, value, self.ctx); } /// Asserts there is enough capacity to store the new key-value pair. /// Asserts that it does not clobber any existing data. /// To detect if a put would clobber existing data, see `getOrPutAssumeCapacity`. pub fn putAssumeCapacityNoClobber(self: *Self, key: K, value: V) void { - return self.unmanaged.putAssumeCapacityNoClobber(key, value); + return self.unmanaged.putAssumeCapacityNoClobberContext(key, value, self.ctx); } /// Inserts a new `Entry` into the hash map, returning the previous one, if any. - pub fn fetchPut(self: *Self, key: K, value: V) !?Entry { - return self.unmanaged.fetchPut(self.allocator, key, value); + pub fn fetchPut(self: *Self, key: K, value: V) !?KV { + return self.unmanaged.fetchPutContext(self.allocator, key, value, self.ctx); } /// Inserts a new `Entry` into the hash map, returning the previous one, if any. /// If insertion happuns, asserts there is enough capacity without allocating. - pub fn fetchPutAssumeCapacity(self: *Self, key: K, value: V) ?Entry { - return self.unmanaged.fetchPutAssumeCapacity(key, value); + pub fn fetchPutAssumeCapacity(self: *Self, key: K, value: V) ?KV { + return self.unmanaged.fetchPutAssumeCapacityContext(key, value, self.ctx); } + /// Removes a value from the map and returns the removed kv pair. + pub fn fetchRemove(self: *Self, key: K) ?KV { + return self.unmanaged.fetchRemoveContext(key, self.ctx); + } + + pub fn fetchRemoveAdapted(self: *Self, key: anytype, ctx: anytype) ?KV { + return self.unmanaged.fetchRemoveAdapted(key, ctx); + } + + /// Finds the value associated with a key in the map pub fn get(self: Self, key: K) ?V { - return self.unmanaged.get(key); + return self.unmanaged.getContext(key, self.ctx); + } + pub fn getAdapted(self: Self, key: anytype, ctx: anytype) ?V { + return self.unmanaged.getAdapted(key, ctx); + } + + pub fn getPtr(self: Self, key: K) ?*V { + return self.unmanaged.getPtrContext(key, self.ctx); + } + pub fn getPtrAdapted(self: Self, key: anytype, ctx: anytype) ?*V { + return self.unmanaged.getPtrAdapted(key, self.ctx); + } + + /// Finds the key and value associated with a key in the map + pub fn getEntry(self: Self, key: K) ?Entry { + return self.unmanaged.getEntryContext(key, self.ctx); } - pub fn getEntry(self: Self, key: K) ?*Entry { - return self.unmanaged.getEntry(key); + pub fn getEntryAdapted(self: Self, key: anytype, ctx: anytype) ?Entry { + return self.unmanaged.getEntryAdapted(key, ctx); } + /// Check if the map contains a key pub fn contains(self: Self, key: K) bool { - return self.unmanaged.contains(key); + return self.unmanaged.containsContext(key, self.ctx); + } + + pub fn containsAdapted(self: Self, key: anytype, ctx: anytype) bool { + return self.unmanaged.containsAdapted(key, ctx); } /// If there is an `Entry` with a matching key, it is deleted from /// the hash map, and then returned from this function. - pub fn remove(self: *Self, key: K) ?Entry { - return self.unmanaged.remove(key); + pub fn remove(self: *Self, key: K) bool { + return self.unmanaged.removeContext(key, self.ctx); } - /// Asserts there is an `Entry` with matching key, deletes it from the hash map, - /// and discards it. - pub fn removeAssertDiscard(self: *Self, key: K) void { - return self.unmanaged.removeAssertDiscard(key); + pub fn removeAdapted(self: *Self, key: anytype, ctx: anytype) bool { + return self.unmanaged.removeAdapted(key, ctx); } + /// Creates a copy of this map, using the same allocator pub fn clone(self: Self) !Self { - var other = try self.unmanaged.clone(self.allocator); - return other.promote(self.allocator); + var other = try self.unmanaged.cloneContext(self.allocator, self.ctx); + return other.promoteContext(self.allocator, self.ctx); + } + + /// Creates a copy of this map, using a specified allocator + pub fn cloneWithAllocator(self: Self, new_allocator: *Allocator) !Self { + var other = try self.unmanaged.cloneContext(new_allocator, self.ctx); + return other.promoteContext(new_allocator, self.ctx); + } + + /// Creates a copy of this map, using a specified context + pub fn cloneWithContext(self: Self, new_ctx: anytype) !HashMap(K, V, @TypeOf(new_ctx), max_load_percentage) { + var other = try self.unmanaged.cloneContext(self.allocator, new_ctx); + return other.promoteContext(self.allocator, new_ctx); + } + + /// Creates a copy of this map, using a specified allocator and context + pub fn cloneWithAllocatorAndContext(new_allocator: *Allocator, new_ctx: anytype) !HashMap(K, V, @TypeOf(new_ctx), max_load_percentage) { + var other = try self.unmanaged.cloneContext(new_allocator, new_ctx); + return other.promoteContext(new_allocator, new_ctx); } }; } @@ -251,11 +618,12 @@ pub fn HashMap( pub fn HashMapUnmanaged( comptime K: type, comptime V: type, - hashFn: fn (key: K) u64, - eqlFn: fn (a: K, b: K) bool, + comptime Context: type, comptime max_load_percentage: u64, ) type { - comptime assert(max_load_percentage > 0 and max_load_percentage < 100); + if (max_load_percentage <= 0 or max_load_percentage >= 100) + @compileError("max_load_percentage must be between 0 and 100."); + comptime verifyContext(Context, K, K, u64); return struct { const Self = @This(); @@ -284,19 +652,25 @@ pub fn HashMapUnmanaged( const minimal_capacity = 8; // This hashmap is specially designed for sizes that fit in a u32. - const Size = 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. - const Hash = u64; + pub const Hash = u64; pub const Entry = struct { + key_ptr: *K, + value_ptr: *V, + }; + + pub const KV = struct { key: K, value: V, }; const Header = packed struct { - entries: [*]Entry, + values: [*]V, + keys: [*]K, capacity: Size, }; @@ -353,11 +727,11 @@ pub fn HashMapUnmanaged( assert(@alignOf(Metadata) == 1); } - const Iterator = struct { + pub const Iterator = struct { hm: *const Self, index: Size = 0, - pub fn next(it: *Iterator) ?*Entry { + pub fn next(it: *Iterator) ?Entry { assert(it.index <= it.hm.capacity()); if (it.hm.size == 0) return null; @@ -370,9 +744,10 @@ pub fn HashMapUnmanaged( it.index += 1; }) { if (metadata[0].isUsed()) { - const entry = &it.hm.entries()[it.index]; + const key = &it.hm.keys()[it.index]; + const value = &it.hm.values()[it.index]; it.index += 1; - return entry; + return Entry{ .key_ptr = key, .value_ptr = value }; } } @@ -380,17 +755,50 @@ pub fn HashMapUnmanaged( } }; + 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 { - entry: *Entry, + key_ptr: *K, + value_ptr: *V, found_existing: bool, }; - pub const Managed = HashMap(K, V, hashFn, eqlFn, max_load_percentage); + pub const Managed = HashMap(K, V, Context, max_load_percentage); pub fn promote(self: Self, allocator: *Allocator) Managed { + if (@sizeOf(Context) != 0) + @compileError("Cannot infer context "++@typeName(Context)++", call promoteContext instead."); + return promoteContext(self, allocator, undefined); + } + + pub fn promoteContext(self: Self, allocator: *Allocator, ctx: Context) Managed { return .{ .unmanaged = self, .allocator = allocator, + .ctx = ctx, }; } @@ -403,26 +811,6 @@ pub fn HashMapUnmanaged( self.* = undefined; } - fn deallocate(self: *Self, allocator: *Allocator) void { - if (self.metadata == null) return; - - const cap = self.capacity(); - const meta_size = @sizeOf(Header) + cap * @sizeOf(Metadata); - - const alignment = @alignOf(Entry) - 1; - const entries_size = @as(usize, cap) * @sizeOf(Entry) + alignment; - - const total_size = meta_size + entries_size; - - var slice: []u8 = undefined; - slice.ptr = @intToPtr([*]u8, @ptrToInt(self.header())); - slice.len = total_size; - allocator.free(slice); - - self.metadata = null; - self.available = 0; - } - fn capacityForSize(size: Size) Size { var new_cap = @truncate(u32, (@as(u64, size) * 100) / max_load_percentage + 1); new_cap = math.ceilPowerOfTwo(u32, new_cap) catch unreachable; @@ -430,8 +818,13 @@ pub fn HashMapUnmanaged( } pub fn ensureCapacity(self: *Self, allocator: *Allocator, new_size: Size) !void { + if (@sizeOf(Context) != 0) + @compileError("Cannot infer context "++@typeName(Context)++", call ensureCapacityContext instead."); + return ensureCapacityContext(self, allocator, new_size, undefined); + } + pub fn ensureCapacityContext(self: *Self, allocator: *Allocator, new_size: Size, ctx: Context) !void { if (new_size > self.size) - try self.growIfNeeded(allocator, new_size - self.size); + try self.growIfNeeded(allocator, new_size - self.size, ctx); } pub fn clearRetainingCapacity(self: *Self) void { @@ -456,8 +849,12 @@ pub fn HashMapUnmanaged( return @ptrCast(*Header, @ptrCast([*]Header, self.metadata.?) - 1); } - fn entries(self: *const Self) [*]Entry { - return self.header().entries; + fn keys(self: *const Self) [*]K { + return self.header().keys; + } + + fn values(self: *const Self) [*]V { + return self.header().values; } pub fn capacity(self: *const Self) Size { @@ -470,28 +867,75 @@ pub fn HashMapUnmanaged( 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, allocator: *Allocator, key: K, value: V) !void { - assert(!self.contains(key)); - try self.growIfNeeded(allocator, 1); + if (@sizeOf(Context) != 0) + @compileError("Cannot infer context "++@typeName(Context)++", call putNoClobberContext instead."); + return self.putNoClobberContext(allocator, key, value, undefined); + } + pub fn putNoClobberContext(self: *Self, allocator: *Allocator, key: K, value: V, ctx: Context) !void { + assert(!self.containsContext(key, ctx)); + try self.growIfNeeded(allocator, 1, ctx); - self.putAssumeCapacityNoClobber(key, value); + 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 { - const gop = self.getOrPutAssumeCapacity(key); - gop.entry.value = value; + 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 { - assert(!self.contains(key)); + 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 = hashFn(key); + const hash = ctx.hash(key); const mask = self.capacity() - 1; var idx = @truncate(usize, hash & mask); @@ -508,40 +952,102 @@ pub fn HashMapUnmanaged( const fingerprint = Metadata.takeFingerprint(hash); metadata[0].fill(fingerprint); - self.entries()[idx] = Entry{ .key = key, .value = value }; + self.keys()[idx] = key; + self.values()[idx] = value; self.size += 1; } /// Inserts a new `Entry` into the hash map, returning the previous one, if any. - pub fn fetchPut(self: *Self, allocator: *Allocator, key: K, value: V) !?Entry { - const gop = try self.getOrPut(allocator, key); - var result: ?Entry = null; + pub fn fetchPut(self: *Self, allocator: *Allocator, key: K, value: V) !?KV { + if (@sizeOf(Context) != 0) + @compileError("Cannot infer context "++@typeName(Context)++", call fetchPutContext instead."); + return self.fetchPutContext(allocator, key, value, undefined); + } + pub fn fetchPutContext(self: *Self, allocator: *Allocator, key: K, value: V, ctx: Context) !?KV { + const gop = try self.getOrPutContext(allocator, key, ctx); + var result: ?KV = null; if (gop.found_existing) { - result = gop.entry.*; + result = KV{ + .key = gop.key_ptr.*, + .value = gop.value_ptr.*, + }; } - gop.entry.value = value; + 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) ?Entry { - const gop = self.getOrPutAssumeCapacity(key); - var result: ?Entry = null; + 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 = gop.entry.*; + result = KV{ + .key = gop.key_ptr.*, + .value = gop.value_ptr.*, + }; } - gop.entry.value = value; + gop.value_ptr.* = value; return result; } - pub fn getEntry(self: Self, key: K) ?*Entry { + /// 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.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. + fn getIndex(self: Self, key: anytype, ctx: anytype) callconv(.Inline) ?usize { + comptime verifyContext(@TypeOf(ctx), @TypeOf(key), K, Hash); + if (self.size == 0) { return null; } - const hash = hashFn(key); + // 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 idx = @truncate(usize, hash & mask); @@ -549,11 +1055,20 @@ pub fn HashMapUnmanaged( var metadata = self.metadata.? + idx; while (metadata[0].isUsed() or metadata[0].isTombstone()) { if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) { - const entry = &self.entries()[idx]; - if (eqlFn(entry.key, key)) { - return entry; + 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; } } + idx = (idx + 1) & mask; metadata = self.metadata.? + idx; } @@ -561,46 +1076,122 @@ pub fn HashMapUnmanaged( 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, allocator: *Allocator, key: K, value: V) !void { - const result = try self.getOrPut(allocator, key); - result.entry.value = value; + if (@sizeOf(Context) != 0) + @compileError("Cannot infer context "++@typeName(Context)++", call putContext instead."); + return self.putContext(allocator, key, value, undefined); + } + pub fn putContext(self: *Self, allocator: *Allocator, key: K, value: V, ctx: Context) !void { + const result = try self.getOrPutContext(allocator, key, ctx); + result.value_ptr.* = value; } /// Get an optional pointer to the value associated with key, if present. - pub fn get(self: Self, key: K) ?V { - if (self.size == 0) { - return null; + 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; + } - const hash = hashFn(key); - const mask = self.capacity() - 1; - const fingerprint = Metadata.takeFingerprint(hash); - var idx = @truncate(usize, hash & mask); - - var metadata = self.metadata.? + idx; - while (metadata[0].isUsed() or metadata[0].isTombstone()) { - if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) { - const entry = &self.entries()[idx]; - if (eqlFn(entry.key, key)) { - return entry.value; - } - } - idx = (idx + 1) & mask; - metadata = self.metadata.? + idx; + /// 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, allocator: *Allocator, key: K) !GetOrPutResult { - try self.growIfNeeded(allocator, 1); - - return self.getOrPutAssumeCapacity(key); + if (@sizeOf(Context) != 0) + @compileError("Cannot infer context "++@typeName(Context)++", call getOrPutContext instead."); + return self.getOrPutContext(allocator, key, undefined); + } + pub fn getOrPutContext(self: *Self, allocator: *Allocator, key: K, ctx: Context) !GetOrPutResult { + const gop = try self.getOrPutContextAdapted(allocator, key, ctx, ctx); + if (!gop.found_existing) { + gop.key_ptr.* = key; + } + return gop; + } + pub fn getOrPutAdapted(self: *Self, allocator: *Allocator, key: anytype, key_ctx: anytype) !GetOrPutResult { + if (@sizeOf(Context) != 0) + @compileError("Cannot infer context "++@typeName(Context)++", call getOrPutContextAdapted instead."); + return self.getOrPutContextAdapted(allocator, key, key_ctx, undefined); + } + pub fn getOrPutContextAdapted(self: *Self, allocator: *Allocator, key: anytype, key_ctx: anytype, ctx: Context) !GetOrPutResult { + self.growIfNeeded(allocator, 1, ctx) 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 { - const hash = hashFn(key); + 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 { + comptime verifyContext(@TypeOf(ctx), @TypeOf(key), K, Hash); + + // 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 idx = @truncate(usize, hash & mask); @@ -609,9 +1200,21 @@ pub fn HashMapUnmanaged( var metadata = self.metadata.? + idx; while (metadata[0].isUsed() or metadata[0].isTombstone()) { if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) { - const entry = &self.entries()[idx]; - if (eqlFn(entry.key, key)) { - return GetOrPutResult{ .entry = entry, .found_existing = true }; + 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; @@ -631,79 +1234,67 @@ pub fn HashMapUnmanaged( } metadata[0].fill(fingerprint); - const entry = &self.entries()[idx]; - entry.* = .{ .key = key, .value = undefined }; + const new_key = &self.keys()[idx]; + const new_value = &self.values()[idx]; + new_key.* = key; + new_value.* = undefined; self.size += 1; - return GetOrPutResult{ .entry = entry, .found_existing = false }; + return GetOrPutResult{ + .key_ptr = new_key, + .value_ptr = new_value, + .found_existing = false, + }; } - pub fn getOrPutValue(self: *Self, allocator: *Allocator, key: K, value: V) !*Entry { - const res = try self.getOrPut(allocator, key); - if (!res.found_existing) res.entry.value = value; - return res.entry; + pub fn getOrPutValue(self: *Self, allocator: *Allocator, key: K, value: V) !Entry { + if (@sizeOf(Context) != 0) + @compileError("Cannot infer context "++@typeName(Context)++", call getOrPutValueContext instead."); + return self.getOrPutValueContext(allocator, key, value, undefined); + } + pub fn getOrPutValueContext(self: *Self, allocator: *Allocator, key: K, value: V, ctx: Context) !Entry { + const res = try self.getOrPutAdapted(allocator, 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 { - return self.get(key) != null; + if (@sizeOf(Context) != 0) + @compileError("Cannot infer context "++@typeName(Context)++", call containsContext instead."); + return self.containsContext(key, undefined); } - - /// If there is an `Entry` with a matching key, it is deleted from - /// the hash map, and then returned from this function. - pub fn remove(self: *Self, key: K) ?Entry { - if (self.size == 0) return null; - - const hash = hashFn(key); - const mask = self.capacity() - 1; - const fingerprint = Metadata.takeFingerprint(hash); - var idx = @truncate(usize, hash & mask); - - var metadata = self.metadata.? + idx; - while (metadata[0].isUsed() or metadata[0].isTombstone()) { - if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) { - const entry = &self.entries()[idx]; - if (eqlFn(entry.key, key)) { - const removed_entry = entry.*; - metadata[0].remove(); - entry.* = undefined; - self.size -= 1; - return removed_entry; - } - } - idx = (idx + 1) & mask; - metadata = self.metadata.? + idx; - } - - return null; + 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; } - /// Asserts there is an `Entry` with matching key, deletes it from the hash map, - /// and discards it. - pub fn removeAssertDiscard(self: *Self, key: K) void { - assert(self.contains(key)); - - const hash = hashFn(key); - const mask = self.capacity() - 1; - const fingerprint = Metadata.takeFingerprint(hash); - var idx = @truncate(usize, hash & mask); - - var metadata = self.metadata.? + idx; - while (metadata[0].isUsed() or metadata[0].isTombstone()) { - if (metadata[0].isUsed() and metadata[0].fingerprint == fingerprint) { - const entry = &self.entries()[idx]; - if (eqlFn(entry.key, key)) { - metadata[0].remove(); - entry.* = undefined; - self.size -= 1; - return; - } - } - idx = (idx + 1) & mask; - metadata = self.metadata.? + idx; + /// 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.metadata.?[idx].remove(); + self.keys()[idx] = undefined; + self.values()[idx] = undefined; + self.size -= 1; + return true; } - unreachable; + return false; } fn initMetadatas(self: *Self) void { @@ -718,14 +1309,19 @@ pub fn HashMapUnmanaged( return @truncate(Size, max_load - self.available); } - fn growIfNeeded(self: *Self, allocator: *Allocator, new_count: Size) !void { + fn growIfNeeded(self: *Self, allocator: *Allocator, new_count: Size, ctx: Context) !void { if (new_count > self.available) { - try self.grow(allocator, capacityForSize(self.load() + new_count)); + try self.grow(allocator, capacityForSize(self.load() + new_count), ctx); } } pub fn clone(self: Self, allocator: *Allocator) !Self { - var other = Self{}; + if (@sizeOf(Context) != 0) + @compileError("Cannot infer context "++@typeName(Context)++", call cloneContext instead."); + return self.cloneContext(allocator, @as(Context, undefined)); + } + pub fn cloneContext(self: Self, allocator: *Allocator, new_ctx: anytype) !HashMapUnmanaged(K, V, @TypeOf(new_ctx), max_load_percentage) { + var other = HashMapUnmanaged(K, V, @TypeOf(new_ctx), max_load_percentage){}; if (self.size == 0) return other; @@ -736,11 +1332,11 @@ pub fn HashMapUnmanaged( var i: Size = 0; var metadata = self.metadata.?; - var entr = self.entries(); + var keys_ptr = self.keys(); + var values_ptr = self.values(); while (i < self.capacity()) : (i += 1) { if (metadata[i].isUsed()) { - const entry = &entr[i]; - other.putAssumeCapacityNoClobber(entry.key, entry.value); + other.putAssumeCapacityNoClobberContext(keys_ptr[i], values_ptr[i], new_ctx); if (other.size == self.size) break; } @@ -749,7 +1345,8 @@ pub fn HashMapUnmanaged( return other; } - fn grow(self: *Self, allocator: *Allocator, new_capacity: Size) !void { + fn grow(self: *Self, allocator: *Allocator, new_capacity: Size, ctx: Context) !void { + @setCold(true); const new_cap = std.math.max(new_capacity, minimal_capacity); assert(new_cap > self.capacity()); assert(std.math.isPowerOfTwo(new_cap)); @@ -764,11 +1361,11 @@ pub fn HashMapUnmanaged( const old_capacity = self.capacity(); var i: Size = 0; var metadata = self.metadata.?; - var entr = self.entries(); + var keys_ptr = self.keys(); + var values_ptr = self.values(); while (i < old_capacity) : (i += 1) { if (metadata[i].isUsed()) { - const entry = &entr[i]; - map.putAssumeCapacityNoClobber(entry.key, entry.value); + map.putAssumeCapacityNoClobberContext(keys_ptr[i], values_ptr[i], ctx); if (map.size == self.size) break; } @@ -780,26 +1377,64 @@ pub fn HashMapUnmanaged( } fn allocate(self: *Self, allocator: *Allocator, new_capacity: Size) !void { + 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 max_align = comptime math.max3(header_align, key_align, val_align); + const meta_size = @sizeOf(Header) + new_capacity * @sizeOf(Metadata); + comptime assert(@alignOf(Metadata) == 1); + + const keys_start = std.mem.alignForward(meta_size, key_align); + const keys_end = keys_start + new_capacity * @sizeOf(K); - const alignment = @alignOf(Entry) - 1; - const entries_size = @as(usize, new_capacity) * @sizeOf(Entry) + alignment; + const vals_start = std.mem.alignForward(keys_end, val_align); + const vals_end = vals_start + new_capacity * @sizeOf(V); - const total_size = meta_size + entries_size; + const total_size = std.mem.alignForward(vals_end, max_align); - const slice = try allocator.alignedAlloc(u8, @alignOf(Header), total_size); + const slice = try allocator.alignedAlloc(u8, max_align, total_size); const ptr = @ptrToInt(slice.ptr); const metadata = ptr + @sizeOf(Header); - var entry_ptr = ptr + meta_size; - entry_ptr = (entry_ptr + alignment) & ~@as(usize, alignment); - assert(entry_ptr + @as(usize, new_capacity) * @sizeOf(Entry) <= ptr + total_size); const hdr = @intToPtr(*Header, ptr); - hdr.entries = @intToPtr([*]Entry, entry_ptr); + if (@sizeOf([*]V) != 0) { + hdr.values = @intToPtr([*]V, ptr + vals_start); + } + if (@sizeOf([*]K) != 0) { + hdr.keys = @intToPtr([*]K, ptr + keys_start); + } hdr.capacity = new_capacity; self.metadata = @intToPtr([*]Metadata, metadata); } + + fn deallocate(self: *Self, allocator: *Allocator) void { + if (self.metadata == null) return; + + 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 max_align = comptime math.max3(header_align, key_align, val_align); + + const cap = self.capacity(); + const meta_size = @sizeOf(Header) + cap * @sizeOf(Metadata); + comptime assert(@alignOf(Metadata) == 1); + + const keys_start = std.mem.alignForward(meta_size, key_align); + const keys_end = keys_start + cap * @sizeOf(K); + + const vals_start = std.mem.alignForward(keys_end, val_align); + const vals_end = vals_start + cap * @sizeOf(V); + + const total_size = std.mem.alignForward(vals_end, max_align); + + const slice = @intToPtr([*]align(max_align) u8, @ptrToInt(self.header()))[0..total_size]; + allocator.free(slice); + + self.metadata = null; + self.available = 0; + } }; } @@ -822,14 +1457,14 @@ test "std.hash_map basic usage" { var sum: u32 = 0; var it = map.iterator(); while (it.next()) |kv| { - sum += kv.key; + sum += kv.key_ptr.*; } - try expect(sum == total); + try expectEqual(total, sum); i = 0; sum = 0; while (i < count) : (i += 1) { - try expectEqual(map.get(i).?, i); + try expectEqual(i, map.get(i).?); sum += map.get(i).?; } try expectEqual(total, sum); @@ -903,7 +1538,7 @@ test "std.hash_map grow" { i = 0; var it = map.iterator(); while (it.next()) |kv| { - try expectEqual(kv.key, kv.value); + try expectEqual(kv.key_ptr.*, kv.value_ptr.*); i += 1; } try expectEqual(i, growTo); @@ -931,9 +1566,9 @@ test "std.hash_map clone" { defer b.deinit(); try expectEqual(b.count(), 3); - try expectEqual(b.get(1), 1); - try expectEqual(b.get(2), 2); - try expectEqual(b.get(3), 3); + try expectEqual(b.get(1).?, 1); + try expectEqual(b.get(2).?, 2); + try expectEqual(b.get(3).?, 3); } test "std.hash_map ensureCapacity with existing elements" { @@ -975,8 +1610,8 @@ test "std.hash_map remove" { try expectEqual(map.count(), 10); var it = map.iterator(); while (it.next()) |kv| { - try expectEqual(kv.key, kv.value); - try expect(kv.key % 3 != 0); + try expectEqual(kv.key_ptr.*, kv.value_ptr.*); + try expect(kv.key_ptr.* % 3 != 0); } i = 0; @@ -1146,7 +1781,7 @@ test "std.hash_map putAssumeCapacity" { i = 0; var sum = i; while (i < 20) : (i += 1) { - sum += map.get(i).?; + sum += map.getPtr(i).?.*; } try expectEqual(sum, 190); @@ -1201,33 +1836,34 @@ test "std.hash_map basic hash map usage" { const gop1 = try map.getOrPut(5); try testing.expect(gop1.found_existing == true); - try testing.expect(gop1.entry.value == 55); - gop1.entry.value = 77; - try testing.expect(map.getEntry(5).?.value == 77); + 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.entry.value = 42; - try testing.expect(map.getEntry(99).?.value == 42); + 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 == 77); + try testing.expect(gop3.value_ptr.* == 77); const gop4 = try map.getOrPutValue(100, 41); - try testing.expect(gop4.value == 41); + try testing.expect(gop4.value_ptr.* == 41); try testing.expect(map.contains(2)); - try testing.expect(map.getEntry(2).?.value == 22); + try testing.expect(map.getEntry(2).?.value_ptr.* == 22); try testing.expect(map.get(2).? == 22); - const rmv1 = map.remove(2); + const rmv1 = map.fetchRemove(2); try testing.expect(rmv1.?.key == 2); try testing.expect(rmv1.?.value == 22); - try testing.expect(map.remove(2) == null); + 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); - map.removeAssertDiscard(3); + try testing.expect(map.remove(3) == true); } test "std.hash_map clone" { @@ -1247,3 +1883,14 @@ test "std.hash_map clone" { try testing.expect(copy.get(i).? == i * 10); } } + +test "compile everything" { + std.testing.refAllDecls(AutoHashMap(i32, i32)); + std.testing.refAllDecls(StringHashMap([]const u8)); + std.testing.refAllDecls(AutoHashMap(i32, void)); + std.testing.refAllDecls(StringHashMap(u0)); + std.testing.refAllDecls(AutoHashMapUnmanaged(i32, i32)); + std.testing.refAllDecls(StringHashMapUnmanaged([]const u8)); + std.testing.refAllDecls(AutoHashMapUnmanaged(i32, void)); + std.testing.refAllDecls(StringHashMapUnmanaged(u0)); +} |
