const std = @import("std"); const debug = std.debug; const Allocator = std.mem.Allocator; const Obj = @import("./object.zig").Obj; const Value = @import("./values.zig").Value; const grow_capacity = @import("./utils.zig").grow_capacity; const compute_hash = @import("./utils.zig").compute_hash; const TABLE_MAX_LOAD = 0.75; const Entry = struct { key: ?*Obj.String, value: Value, }; pub const Table = struct { allocator: Allocator, count: usize, capacity: usize, entries: []Entry, pub fn new(allocator: Allocator) Table { return Table{ .allocator = allocator, .count = 0, .capacity = 0, .entries = &.{}, }; } pub fn deinit(self: *Table) void { if (self.capacity == 0) { return; } self.allocator.free(self.entries); } pub fn set(self: *Table, key: *Obj.String, value: Value) bool { const current_count: f32 = @floatFromInt(self.count + 1); const current_capacity: f32 = @floatFromInt(self.capacity); if (current_count > current_capacity * TABLE_MAX_LOAD) { const capacity = grow_capacity(self.capacity); self.adjust_capacity(capacity); } const entry = Table.find_entry(self.entries, key); const is_new = entry.?.key == null; if (is_new and entry.?.value.is_nil()) { self.count += 1; } entry.?.key = key; entry.?.value = value; return is_new; } pub fn find_entry(entries: []Entry, key: *Obj.String) ?*Entry { var tombstone: ?*Entry = null; var index = key.hash % entries.len; while (true) { const entry = &entries[index]; if (entry.key == null) { if (entry.value.is_nil()) { // Empty entry. if (tombstone != null) { return tombstone; } else { return entry; } } else { // We found a tombestone if (tombstone == null) { tombstone = entry; } } } else if (entry.key == key) { // We found the key return entry; } index = (index + 1) % entries.len; } } pub fn adjust_capacity(self: *Table, capacity: usize) void { var entries = self.allocator.alloc(Entry, capacity) catch unreachable; for (0..entries.len) |idx| { entries[idx].key = null; entries[idx].value = Value.nil_val(); } self.count = 0; for (0..self.capacity) |idx| { const entry = self.entries[idx]; if (entry.key == null) { continue; } const dest_entry = Table.find_entry(entries, entry.key.?); dest_entry.?.key = entry.key; dest_entry.?.value = entry.value; self.count += 1; } self.capacity = capacity; if (entries.len > 0) { self.allocator.free(self.entries); } self.entries = entries; } pub fn dump(self: Table) void { std.debug.print("== Hash table count:{} capacity:{} ==\n", .{ self.count, self.capacity }); for (self.entries, 0..) |entry, idx| { if (entry.key != null) { std.debug.print("{d} ({d}) - {s}: ", .{ idx, entry.key.?.hash, entry.key.?.chars }); entry.value.type_print(); std.debug.print("\n", .{}); } if (entry.key == null and entry.value.as_bool()) { std.debug.print("{d} - tombstone\n", .{idx}); } } std.debug.print("== End of hash table ==\n\n", .{}); } pub fn add_all(self: *Table, from: Table) void { for (from.entries) |entry| { if (entry.key == null) { continue; } _ = self.set(entry.key.?, entry.value); } } pub fn get(self: Table, key: *Obj.String, value: *Value) bool { if (self.count == 0) { return false; } const entry = Table.find_entry(self.entries, key); if (entry.?.key == null) { return false; } value.* = entry.?.value; return true; } pub fn del(self: *Table, key: *Obj.String) bool { if (self.count == 0) { return false; } // Find the entry const entry = Table.find_entry(self.entries, key); if (entry.?.key == null) { return false; } // Place a tombstone in the entry entry.?.key = null; entry.?.value = Value.bool_val(true); return true; } pub fn find_string(self: *Table, chars: []const u8, hash: u32) ?*Obj.String { if (self.count == 0) { return null; } var index = hash % self.capacity; while (true) { const entry = &self.entries[index]; if (entry.key == null) { // Stop if we find an empty non-tombstone entry. if (entry.value.is_nil()) { return null; } } else if (entry.key.?.chars.len == chars.len and entry.key.?.hash == hash and std.mem.eql(u8, chars, entry.key.?.chars)) { return entry.key; } index = (index + 1) % self.capacity; } } }; test "initialize an hash table" { const allocator = std.testing.allocator; var table = Table.new(allocator); defer table.deinit(); try std.testing.expectEqual(0, table.count); try std.testing.expectEqual(0, table.capacity); } test "adding values" { const allocator = std.testing.allocator; const key = Obj.String.new(allocator, "hello world"); defer key.destroy(); var table = Table.new(allocator); defer table.deinit(); var res = table.set(key, Value.nil_val()); try std.testing.expectEqual(true, res); try std.testing.expectEqual(1, table.count); try std.testing.expectEqual(8, table.capacity); res = table.set(key, Value.nil_val()); try std.testing.expectEqual(false, res); try std.testing.expectEqual(1, table.count); try std.testing.expectEqual(8, table.capacity); } test "adding tables" { const allocator = std.testing.allocator; const key = Obj.String.new(allocator, "hello world"); defer key.destroy(); var table = Table.new(allocator); defer table.deinit(); const res = table.set(key, Value.nil_val()); try std.testing.expectEqual(true, res); try std.testing.expectEqual(8, table.capacity); try std.testing.expectEqual(1, table.count); var table2 = Table.new(allocator); defer table2.deinit(); try std.testing.expectEqual(0, table2.capacity); try std.testing.expectEqual(0, table2.count); table2.add_all(table); try std.testing.expectEqual(8, table2.capacity); try std.testing.expectEqual(1, table2.count); } test "deleting from table" { const allocator = std.testing.allocator; const key = Obj.String.new(allocator, "hello world"); defer key.destroy(); var table = Table.new(allocator); defer table.deinit(); var res = table.set(key, Value.nil_val()); try std.testing.expectEqual(true, res); // table.dump(); res = table.del(key); try std.testing.expectEqual(true, res); // table.dump(); } test "find" { const allocator = std.testing.allocator; const key = Obj.String.new(allocator, "hello world"); defer key.destroy(); var table = Table.new(allocator); defer table.deinit(); const value = Value.number_val(42.0); var res = table.set(key, value); try std.testing.expectEqual(true, res); var entry = table.find_string("bye world", compute_hash("bye world")); try std.testing.expectEqual(entry, null); entry = table.find_string("hello world", compute_hash("hello world")); // std.debug.print("{any}\n", .{entry}); try std.testing.expect(entry != null); var value_obj = Value.nil_val(); res = table.get(entry.?, &value_obj); try std.testing.expect(res); // std.debug.print("{any}\n", .{value_obj}); try std.testing.expectEqual(value_obj.as_number(), value.as_number()); }