303 lines
8.3 KiB
Zig
303 lines
8.3 KiB
Zig
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());
|
|
}
|