105 lines
3.8 KiB
Zig
105 lines
3.8 KiB
Zig
//! Stores null-terminated strings and maps them to unique 32-bit indices.
|
|
//!
|
|
//! Lookups are omnidirectional: both (string -> index) and (index -> string) are supported
|
|
//! operations.
|
|
//!
|
|
//! The implementation is based on:
|
|
//! https://zig.news/andrewrk/how-to-use-hash-map-contexts-to-save-memory-when-doing-a-string-table-3l33
|
|
|
|
const std = @import("std");
|
|
|
|
const StringTable = @This();
|
|
|
|
string_bytes: std.ArrayListUnmanaged(u8) = .{},
|
|
|
|
/// Key is string_bytes index.
|
|
string_table: std.HashMapUnmanaged(u32, void, IndexContext, std.hash_map.default_max_load_percentage) = .{},
|
|
|
|
pub const Index = u32;
|
|
|
|
/// Returns the index of a string key, if it exists.
|
|
pub fn index(table: *StringTable, key: []const u8) ?Index {
|
|
const slice_context: SliceAdapter = .{ .string_bytes = &table.string_bytes };
|
|
const found_entry = table.string_table.getEntryAdapted(key, slice_context);
|
|
if (found_entry) |e| return e.key_ptr.*;
|
|
return null;
|
|
}
|
|
|
|
/// Returns the index of a string key, inserting if not exists.
|
|
pub fn indexOrPut(table: *StringTable, allocator: std.mem.Allocator, key: []const u8) !Index {
|
|
const slice_context: SliceAdapter = .{ .string_bytes = &table.string_bytes };
|
|
const index_context: IndexContext = .{ .string_bytes = &table.string_bytes };
|
|
const entry = try table.string_table.getOrPutContextAdapted(allocator, key, slice_context, index_context);
|
|
if (!entry.found_existing) {
|
|
entry.key_ptr.* = @intCast(table.string_bytes.items.len);
|
|
try table.string_bytes.appendSlice(allocator, key);
|
|
try table.string_bytes.append(allocator, '\x00');
|
|
}
|
|
return entry.key_ptr.*;
|
|
}
|
|
|
|
/// Returns a null-terminated string given the index
|
|
pub fn string(table: *StringTable, idx: Index) [:0]const u8 {
|
|
return std.mem.span(@as([*:0]const u8, @ptrCast(table.string_bytes.items.ptr)) + idx);
|
|
}
|
|
|
|
pub fn deinit(table: *StringTable, allocator: std.mem.Allocator) void {
|
|
table.string_bytes.deinit(allocator);
|
|
table.string_table.deinit(allocator);
|
|
}
|
|
|
|
const IndexContext = struct {
|
|
string_bytes: *std.ArrayListUnmanaged(u8),
|
|
|
|
pub fn eql(ctx: IndexContext, a: u32, b: u32) bool {
|
|
_ = ctx;
|
|
return a == b;
|
|
}
|
|
|
|
pub fn hash(ctx: IndexContext, x: u32) u64 {
|
|
const x_slice = std.mem.span(@as([*:0]const u8, @ptrCast(ctx.string_bytes.items.ptr)) + x);
|
|
return std.hash_map.hashString(x_slice);
|
|
}
|
|
};
|
|
|
|
const SliceAdapter = struct {
|
|
string_bytes: *std.ArrayListUnmanaged(u8),
|
|
|
|
pub fn eql(adapter: SliceAdapter, a_slice: []const u8, b: u32) bool {
|
|
const b_slice = std.mem.span(@as([*:0]const u8, @ptrCast(adapter.string_bytes.items.ptr)) + b);
|
|
return std.mem.eql(u8, a_slice, b_slice);
|
|
}
|
|
|
|
pub fn hash(adapter: SliceAdapter, adapted_key: []const u8) u64 {
|
|
_ = adapter;
|
|
return std.hash_map.hashString(adapted_key);
|
|
}
|
|
};
|
|
|
|
test {
|
|
const gpa = std.testing.allocator;
|
|
|
|
var table: StringTable = .{};
|
|
defer table.deinit(gpa);
|
|
|
|
const index_context: IndexContext = .{ .string_bytes = &table.string_bytes };
|
|
_ = index_context;
|
|
|
|
// "hello" -> index 0
|
|
const hello_index = try table.indexOrPut(gpa, "hello");
|
|
try std.testing.expectEqual(@as(Index, 0), hello_index);
|
|
|
|
try std.testing.expectEqual(@as(Index, 6), try table.indexOrPut(gpa, "world"));
|
|
try std.testing.expectEqual(@as(Index, 12), try table.indexOrPut(gpa, "foo"));
|
|
try std.testing.expectEqual(@as(Index, 16), try table.indexOrPut(gpa, "bar"));
|
|
try std.testing.expectEqual(@as(Index, 20), try table.indexOrPut(gpa, "baz"));
|
|
|
|
// index 0 -> "hello"
|
|
try std.testing.expectEqualStrings("hello", table.string(hello_index));
|
|
|
|
// Lookup "hello" -> index 0
|
|
try std.testing.expectEqual(hello_index, table.index("hello").?);
|
|
|
|
// Lookup "foobar" -> null
|
|
try std.testing.expectEqual(@as(?Index, null), table.index("foobar"));
|
|
}
|